import math
import random
from . import SAMPLE_RATE
from .signal import Signal
__all__ = ('Sample', 'SineWave', 'SquareWave', 'SawtoothWave', 'TriangleWave', 'Noise', 'BrownNoise', 'Digitar', 'harmonics', 'wavetable')
[docs]
class Sample(Signal):
"""
An infinitely long sound generated by a simple algorithm
"""
def __init__(self, frequency):
self.frequency = float(frequency)
self.duration = float('inf')
@property
def period(self):
return 1/self.frequency * SAMPLE_RATE
[docs]
class SineWave(Sample):
"""
A sample that outputs a sine wave at the given frequency
"""
def amplitude(self, frame):
return math.sin(frame * 2 * math.pi / self.period)
[docs]
class SquareWave(Sample):
"""
A sample that outputs a square wave at the given frequency
Due to aliasing, you may want to use a series of sine wave harmonics if you want to
obtain a more pleasant sound.
"""
def __init__(self, frequency, split=0.5):
super(SquareWave, self).__init__(frequency)
self.split = split
def amplitude(self, frame):
return 1 if frame % self.period < self.period*self.split else -1
[docs]
class SawtoothWave(Sample):
"""
A sample that outputs a sawtooth wave at the given frequency
Due to aliasing, you may want to use a series of sine wave harmonics if you want to
obtain a more pleasant sound.
"""
def amplitude(self, frame):
return frame % self.period / self.period * 2 - 1
[docs]
class TriangleWave(Sample):
"""
A sample that outputs a triangle wave at the given frequency
Due to aliasing, you may want to use a series of sine wave harmonics if you want to
obtain a more pleasant sound.
"""
def amplitude(self, frame):
pframe = frame % self.period
hperiod = self.period/2
qperiod = hperiod/2
if pframe < qperiod:
return pframe / qperiod
pframe -= qperiod
if pframe < hperiod:
return pframe / -hperiod*2 + 1
pframe -= hperiod
return pframe / qperiod - 1
[docs]
class Noise(Sample):
"""
A sample that outputs white noise, random data uniformly distributed over [0,1].
"""
# I... guess this is technically pure?
def __init__(self):
super(Noise, self).__init__(0)
def amplitude(self, frame):
return random.random() * 2 - 1
[docs]
class BrownNoise(Sample):
"""
A sample that outputs brown noise, the integration of white noise.
It is technically pure, but output may more closely resemble white noise if sample impurely.
"""
def __init__(self, fac=0.5):
super(BrownNoise, self).__init__(0)
self.prev = 0.
self.fac = float(fac)
def amplitude(self, frame):
self.prev += (random.random() * 2 - 1) * self.fac
if self.prev > 1: self.prev = 1.
elif self.prev < -1: self.prev = -1.
return self.prev
def wavetable(table):
class WaveSample(Sample):
def __init__(self, freq):
Sample.__init__(self, freq)
basefreq = SAMPLE_RATE * 1./len(table)
self.phaseinc = freq / basefreq
def amplitude(self, frame):
realframe = frame * self.phaseinc
s1 = table[int(realframe) % len(table)]
s2 = table[int(realframe + 1) % len(table)]
interp = realframe - int(realframe)
return s2 * interp + s1 * (1 - interp)
return WaveSample
[docs]
class Digitar(Sample):
"""
A sample that implements the Karplus-Strong plucked string synthesis algorithm.
The basic idea is that an wavetable initially populated with random noise run though
a low-pass filter cyclically, gradually removing inharmonic components and smoothing
the waveform. The sound is tuned by keeping a separate "phase" counter which causes the
output to cycle through the wavetable at a different speed than the filter, effectively
adjusting the period of the signal. The decay rate is adjusted by changing the size of
the wavetable - the smaller the wavetable, the faster the filter adjusts the signal and
the faster that the sound decays. Keep in mind that if it decays very quickly, then
the noise doesn't have time to resolve into a tone, and the output will sound more
drum-like.
:param frequency: The desired output frequency
:param buffersize: The size of the wavetable.
Optional, defaults to a good plucked string sound.
:param wavesrc: A signal to sample to produce the initial wavetable.
Optional, defaults to white noise.
"""
def __init__(self, frequency, buffersize=256, wavesrc=None):
self.wavesrc = wavesrc if wavesrc is not None else Noise()
super(Digitar, self).__init__(frequency)
self.buffersize = buffersize
self.sample_window = []
self.cur_frame = 0
basefreq = SAMPLE_RATE * 1./self.buffersize
self.phaseinc = frequency / basefreq
self.phase = 0
self.new_buffer()
self.pure = False
def new_buffer(self):
self.sample_window = [self.wavesrc.amplitude(i) for i in range(self.buffersize)]
self.cur_frame = 0
self.phase = 0
def get_buffer(self, frame):
return self.sample_window[frame % self.buffersize]
def set_buffer(self, frame, value):
self.sample_window[frame % self.buffersize] = value
def tick(self):
self.set_buffer(self.cur_frame + 1, self.get_buffer(self.cur_frame) * 0.3 + self.get_buffer(self.cur_frame + 1) * 0.7)
self.cur_frame += 1
self.phase = (self.phase + self.phaseinc) % self.buffersize
def seek(self, frame):
if frame < self.cur_frame:
self.cur_frame = 0
while self.cur_frame < frame:
self.tick()
def amplitude(self, frame):
self.seek(frame)
s1 = self.get_buffer(int(self.phase))
s2 = self.get_buffer(int(self.phase) + 1)
interp = self.phase - int(self.phase)
return interp * s2 + (1-interp) * s1
[docs]
def harmonics(freq, ns=(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16), subsample=SineWave):
"""
Generate a number of harmonics of a given sample. The base tone is treated as the first harmonic.
:param freq: The base frequency to use
:param ns: A list of the harmonics to produce.
Optional, defauts to the first 16 harmonics.
:param subsample: The class of the sample to use.
Optional, defaults to a sine wave.
"""
return [subsample(freq*n) for n in ns]