Audiosynthese mit Python

Code-Stücke können hier veröffentlicht werden.
Antworten
jerch
User
Beiträge: 1630
Registriert: Mittwoch 4. März 2009, 14:19

Dienstag 27. Mai 2014, 00:02

Angeregt durch diesen Thread, hab ich mal ein bisschen mit Audiosynthese rumgespielt. Funktioniert nur unter Linux (braucht /dev/dsp) und gibt auch nur AM, additive und FM-Synthese. Viel Spass damit :P

Code: Alles auswählen

import math
from operator import methodcaller
from itertools import imap, islice, cycle
from ossaudiodev import (SNDCTL_DSP_SPEED, SNDCTL_DSP_CHANNELS,
                         SNDCTL_DSP_RESET, SNDCTL_DSP_SETFMT, AFMT_S16_LE)
from fcntl import ioctl
from struct import pack

WAVES = {
    'sin': math.sin,
    'sq': lambda x: -1 if int(x/math.pi) % 2 else 1,
    'saw': lambda x: 1 - (x/math.pi) % 2
}


class Oscillator(object):
    """
    Oscillator with 'sin' (sinus), 'sq' (square) and 'saw' (sawtooth)
    wave forms.
    """
    def __init__(self, freq=440.0, wave='sin', rate=44100, offset=0.0,
                 amplitude=1.0):
        self.amplitude = (1 if amplitude > 1 else
                          (0 if amplitude < 0 else amplitude))
        period = int(rate / freq)
        self.table = [WAVES[wave](2 * math.pi / period * i) * self.amplitude
                      for i in xrange(period)][::-1]
        self.wave = wave
        self.length = len(self.table)
        self.pos = 0
        self._offset = 0.0
        self.offset = offset

    @property
    def offset(self):
        return self._offset

    @offset.setter
    def offset(self, value):
        # float: start offset relative to period
        if isinstance(value, float):
            if 0.0 <= value < 1.0:
                self._offset = value
                self.pos = self.length - int(self.length * self._offset)
            return
        # int: start offset at pos
        if isinstance(value, int):
            self._offset = value
            self.pos = self.length - self._offset % self.length

    def next(self):
        if not self.pos:
            self.pos = self.length
        self.pos -= 1
        return self.table[self.pos]

    def __iter__(self):
        return self


def additive(a, b):
    """
    Simple additive synthesis.
    """
    while True:
        yield (a.next() + b.next()) / 2


def am(a, b):
    """
    Amplitude modulation.
    """
    while True:
        yield a.next() * (1 + b.next()) / (1 + b.amplitude)


def fm(carrier, modulator, beta):
    """
    Frequency modulation.
    """
    # TODO: Kinda broken since not stackable - allow samples as carrier with interpolation?
    c_x = [2*math.pi / carrier.length * i for i in xrange(carrier.length)][::-1]
    c_x = c_x[carrier.pos:] + c_x[:carrier.pos]
    c = cycle(c_x)
    while True:
        yield WAVES[carrier.wave](c.next() + beta * modulator.next())


class SoundEnvironment(object):
    """
    /dev/dsp only atm. (hardcoded with 44100, 2 channels, signed 16bit LE)
    """
    def __init__(self, duration=1.0):
        self.duration = duration
        self._oscillators = []
        self.channels = {'left': [], 'right': []}
        self.rate = 44100
        self.f = None
        self.reset()

    def reset(self):
        if self.f:
            self.f.close()
        self.f = open('/dev/dsp', 'w')
        ioctl(self.f.fileno(), SNDCTL_DSP_CHANNELS, pack('<l', 2))
        ioctl(self.f.fileno(), SNDCTL_DSP_SETFMT, pack('<l', AFMT_S16_LE))
        ioctl(self.f.fileno(), SNDCTL_DSP_SPEED, pack('<l', self.rate))
        #ioctl(self.f.fileno(), SNDCTL_DSP_RESET, pack('<l', 0))

    def add_oscillator(self, oscillator, channel='left'):
        self.channels[channel].append(oscillator)

    def clear_oscillators(self, channel='left'):
        self.channels[channel] = []

    def play(self, left, right, duration=1.0):
        frames = int(duration * self.rate * self.duration)
        left += self.channels['left']
        right += self.channels['right']
        len_left = len(left) + 1
        len_right = len(right) + 1

        for _ in xrange(frames):
            v_left = sum(imap(methodcaller('next'), left))
            v_right = sum(imap(methodcaller('next'), right))
            self.f.write(pack('<h', int(v_right * 32767 / len_right)) +
                         pack('<h', int(v_left * 32767 / len_left)))


def plot(it, *args):
    """
    Plot oscillator or generator with plot(seq, [start,] stop [, step]).
    """
    #import matplotlib
    #matplotlib.use('QT4Agg')
    from matplotlib import pyplot
    s = slice(*args)
    r = range(s.start or 0, s.stop, s.step or 1)
    l = list(islice(it, *args))
    pyplot.plot(r, l)
    pyplot.plot(r, l, 'rx')
    pyplot.show()


def equal():
    """
    Equal intonation.
    """
    f = lambda x: 262 * 2 ** (float(x)/12)
    return {
        'h0': f(-1),
        'c1': f(0), 'd1': f(2), 'eb1': f(3), 'e1': f(4), 'f1': f(5),
        'g1': f(7), 'ab1': f(8), 'a1': f(9), 'hb1': f(10), 'h1': f(11),
        'c2': f(12), 'd2': f(14), 'eb2': f(15), 'e2': f(16), 'f2': f(17),
        'g2': f(19), 'ab2': f(20), 'a2': f(21), 'hb2': f(22), 'h2': f(23),
        'c3': f(24), 'f#1': f(6)}
EQUAL = equal()


# examples
def naturals(se, steps):
    def empty():
        while True:
            yield 1
    o = Oscillator(220.0)
    a = additive(o, empty())
    for _ in xrange(steps):
        se.play([a], [a])
        a = additive(a, o)


def maria(se):
    o = Oscillator
    t = EQUAL
    se.play([o(t['g1'], 'sq')], [o(t['g1'], 'saw')],  0.5)
    se.play([o(t['g1'], 'sq')], [o(t['f1'], 'saw')],  0.5)
    se.play([o(t['c2'], 'sq')], [o(t['eb1'], 'saw')], 1.5)
    se.play([o(t['d2'], 'sq')], [o(t['g1'], 'saw')],  0.5)
    se.play([o(t['eb2'], 'sq')], [o(t['c2'], 'saw')],  1.0)
    se.play([o(t['g2'], 'sq')], [o(t['eb2'], 'saw')], 1.0)
    se.play([o(t['eb2'], 'sq')], [o(t['c2'], 'saw')],  1.0)
    se.play([o(t['d2'], 'sq')], [o(t['c2'], 'saw')],  0.5)
    se.play([o(t['c2'], 'sq')], [o(t['c2'], 'saw')],  0.5)
    se.play([o(t['d2'], 'sq')], [o(t['g1'], 'saw')],  2.0)
    se.play([o(t['eb2'], 'sq')], [o(t['c2'], 'saw')],  1.0)
    se.play([o(t['eb2'], 'sq')], [o(t['c2'], 'saw')],  0.5)
    se.play([o(t['eb2'], 'sq')], [o(t['c2'], 'saw')],  0.5)
    se.play([o(t['f2'], 'sq')], [o(t['hb1'], 'saw')], 1.0)
    se.play([o(t['f2'], 'sq')], [o(t['hb1'], 'saw')], 0.5)
    se.play([o(t['f2'], 'sq')], [o(t['f1'], 'saw')],  0.5)
    se.play([o(t['g2'], 'sq')], [o(t['eb1'], 'saw')], 2.0)


def bach(se):
    o = Oscillator
    t = EQUAL
    for _ in xrange(2):
        se.add_oscillator(o(t['c1']))
        se.play([], [])
        se.add_oscillator(o(t['e1']))
        se.play([], [])
        se.play([], [o(t['g1'])])
        se.play([], [o(t['c2'])])
        se.play([], [o(t['e2'])])
        se.play([], [o(t['g1'])])
        se.play([], [o(t['c2'])])
        se.play([], [o(t['e2'])])
        se.clear_oscillators('left')


def synthi_example(se):
    for i in xrange(3):
        for f in xrange(200, 800, 5):
            se.play([fm(Oscillator(300.0), Oscillator(float(f)), .5)],
                    [fm(Oscillator(300.0), Oscillator(float(f), 'sq'), .5)], .01)
        for f in xrange(800, 200, -5):
            se.play([fm(Oscillator(300.0), Oscillator(float(f), 'sq'), .5)],
                    [fm(Oscillator(300.0), Oscillator(float(f)), .5)], .01)


def slow_leslie(se):
    t = EQUAL
    for _1, _2, _3 in [(t['e2'], t['c2'], t['g1']), (t['d2'], t['g1'], t['d1']),
                       (t['c2'], t['g1'], t['e1']), (t['a1'], t['f#1'], t['d1']),
                       (t['h1'], t['g1'], t['d1'])]:
        a1 = am(fm(Oscillator(_1), Oscillator(0.65, offset=.5, amplitude=.2), 20),
                Oscillator(1.3, amplitude=.4, offset=.5))
        a2 = am(fm(Oscillator(_2), Oscillator(1.3, offset=.5, amplitude=.1), 10),
                Oscillator(1.3, amplitude=.4, offset=.5))
        a3 = am(fm(Oscillator(_3), Oscillator(1.3, offset=.5, amplitude=.1), 4),
                Oscillator(1.3, amplitude=.4, offset=.5))
        b1 = am(fm(Oscillator(_1), Oscillator(0.65, amplitude=.2), 20),
                Oscillator(1.3, amplitude=.4))
        b2 = am(fm(Oscillator(_2), Oscillator(1.3, amplitude=.1), 10),
                Oscillator(1.3, amplitude=.4))
        b3 = am(fm(Oscillator(_3), Oscillator(1.3, amplitude=.1), 4),
                Oscillator(1.3, amplitude=.4))
        se.play([a1, a2, a3], [b1, b2, b3])


if __name__ == '__main__':
    se = SoundEnvironment()

    # natural intonation scale (weird additive synthesis)
    naturals(se, 15)

    # examples with 'notes' in equal intonation
    se.duration = .5
    maria(se)
    se.duration = .2
    bach(se)

    # synthi style with fm
    se.duration = 1
    synthi_example(se)

    # rotary speaker alike
    slow_leslie(se)
Antworten