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:
Code: Alles auswählen
# -*- 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()
- 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