eigentlich komme ich aus der Perl-Welt. Es kann nicht schaden, dachte ich mir, auch mal über den Tellerrand zu schauen und etwas in Python zu programmieren. Zufällig zur gleichen Zeit interessiere ich mich auch für elektronische Klangerzeugung. Ich denke nicht, dass mein Interesse soweit reicht, dass ich mir perspektivisch eine zehntausende Euro teure Tonstudio-anlage in mein wohnzimmer stellen werde. Aber es reicht gerade eben aus, um mir – nach einem Eintagsgrundlagenstudium der Klangtheorie, Harmonik etc. – einen ganz einfachen Synthesizer zusammen basteln. Unwahrscheinlich ist, dass ich damit irgendeinen professionellen Toningenieur beeindrucken kann, aber ich bekomme wahrscheinlich ein gutes Gefühl dafür, warum entsprechende Gerätschaften so schweineteuer sind.
Natürlich weiß ich, dass es gute Open-Source-Synthesizer gibt. Sowas versuchen selbst zu programmieren ist spannender, als mit fertigen Programmen rumzuspielen.
Ich bin für ehrliche Kritik hinsichtlich Implementierung und Zweckeignung dieses Moduls immer zu haben.
Here we go:
- # -*- coding: utf-8 -*-
- import pyaudio
- import numpy as np
- import time
- """ This is a very simple synthesizer for single sound design
- It supports tones composed of fundamental and overtones. Amplitude and or frequency
- of each partial can be sine-modulated independently.
- Nobody expects that little exercise in python programming to be suitable for
- serious sound design, do they? Real instruments produce a wide spectrum of
- overtones. I am not sure python can handle these numbers with that approach,
- because I do not take care of any real-time provisions.
- Based on http://stackoverflow.com/questions/8299 ... -in-python
- """
- fs = 44100 # sampling rate, Hz, must be integer
- def main():
- play_melody(1, ["simple", 440, 3])
- time.sleep(1)
- play_melody(1, ['vibr',220.0, 60]) # a minute of sound with vibrato
- stream.stop_stream()
- # generate samples, note conversion to float32 array
- def get_samples (timbre, freq, duration):
- partial_samples = []
- divisor = 0
- iseq = np.arange(fs*duration) # integer sequence 1 .. fs*duration
- for partial_tone in timbre_inventory[timbre]:
- partial_samples.append(partial_tone.render_samples(freq, iseq))
- divisor += partial_tone.share
- # mix partials by their weights and return an numpy array ready to
- # play
- return (sum(partial_samples)/divisor).astype(np.float32)
- class Modulation:
- def __init__(self, frequency, base_share, mod_share):
- self.base_share = base_share
- self.mod_share = mod_share
- self.frequency = frequency
- def modulate(self, iseq):
- """ Caution: Not quite sure if that really does what it is supposed to.
- This was programmed by a python learner not fond of trying to
- tell professional sound architects about some new kid in town.
- It is just a very naive way to make a tone more dynamic, spawned
- from my intuition.
- [-------|-------|-------|-------] Frequency in intervals per second
- * * * * * T
- *** *** *** *** *** | ^ mod_share (3)
- ***** ***** ***** ***** ***** | = Modulation intensity in relation
- ******************************* – to ...
- ******************************* |
- ******************************* |
- ******************************* |
- ******************************* | ^ base_share (6)
- ******************************* _ = Minimum amplitude or frequency
- """
- b = self.base_share
- m = self.mod_share
- f = self.frequency
- return ( m * (np.sin(2*np.pi * iseq * f/fs) + 1) / 2 + b) / (m + b)
- class Partial:
- def __init__(self, nfactor=1, share=1, deviation=0, am=None, fm=None):
- self.nfactor = nfactor
- self.deviation = deviation
- self.share = share
- self.amplitude_modulation = am
- self.frequency_modulation = fm
- def render_samples(self, freq, iseq):
- n = self.nfactor
- d = self.deviation
- s = self.share
- if self.amplitude_modulation is not None:
- s *= self.amplitude_modulation.modulate(iseq)
- if self.frequency_modulation is not None:
- freq *= self.frequency_modulation.modulate(iseq)
- return np.sin( 2*np.pi * iseq * (n*freq+d) / fs ) * s
- P = Partial
- M = Modulation
- timbre_inventory = {
- 'simple': [P(1)],
- 'test': [P(1), P(2,0.13), P(3,0.05)],
- 'vibr': [P(1), P(2,0.03,fm=M(30,50,1)), P(3,0.15,am=M(5,350,75))],
- }
- p = pyaudio.PyAudio()
- stream = p.open(format=pyaudio.paFloat32,
- channels=1,
- rate=fs,
- output=True)
- def play_melody(volume, *sounds):
- for sound in sounds:
- stream.write(volume*get_samples(*sound))
- if __name__ == "__main__":
- main()
- stream.close()
- p.terminate()
Wenn ich dieses Programm laufen lasse, kommen mir vier Fragen in den Sinn. Kann sie mir jemand vielleicht beantworten?:
- Warum ist eine Sekunde gefühlt drei mal so kurz wie üblich?
- Warum ändert sich der zweite Ton mit der Zeit, tritt das Vibrato zunehmend in den Vordergrund und wird immer höher?
- Warum gibt es vor dem Abspielen des zweiten Tons einen buffer underrun? Also teilweise kann ich mir das schon erklären, das ist hier ja keine Echtzeitgeschichte. Aber warum nicht bereits vor dem ersten?
- Wie kann ich meine genialen Ergüsse der Klangerzeugung in ein WAV gießen, statt sie mir gleich über Lautsprecher auszugeben? Ah, mit dem Modul wave sollte das gehen, haben erste Recherchen ergeben. Was ich mich allerdings frage ist, wie ich den Framebuffer konfigurieren muss. Manche nehmen 1024 oder so. Ist der Wert beliebig?
Danke,
-- gotridofmyphone