157 lines
5.1 KiB
Python
157 lines
5.1 KiB
Python
# adapted from Pythoven
|
|
# https://github.com/phiresky/pythoven
|
|
|
|
from random import randint
|
|
from math import sin, log, pi
|
|
from array import array
|
|
|
|
from pygame import version
|
|
from pygame.mixer import Sound, get_init
|
|
|
|
class Samples(Sound):
|
|
|
|
def __init__(self):
|
|
self.set_amplitude()
|
|
if version.vernum < (1, 9, 2):
|
|
Sound.__init__(self, self.build())
|
|
else:
|
|
Sound.__init__(self, buffer=self.build())
|
|
|
|
def set_amplitude(self):
|
|
self.amplitude = (1 << (self.get_sample_width() * 8 - 1)) - 1
|
|
|
|
def get_sample_width(self):
|
|
return abs(int(get_init()[1] / 8))
|
|
|
|
def build(self):
|
|
pass
|
|
|
|
def get_empty_array(self, length):
|
|
return array(self.get_array_typecode(), [0] * length)
|
|
|
|
def get_array_typecode(self):
|
|
return [None, "b", "h"][self.get_sample_width()]
|
|
|
|
|
|
class Note(Samples):
|
|
|
|
base_frequency = 440.0
|
|
base_octave = 4
|
|
base_name = "A"
|
|
names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
|
|
SQUARE, TRIANGLE, SAW, SINE, DIRTY = range(5)
|
|
|
|
def __init__(self, name=None, octave=4, frequency=None, shape=SQUARE, volume=1.0):
|
|
names = self.names
|
|
self.shape = shape
|
|
if frequency is None:
|
|
self.name = name
|
|
self.octave = octave
|
|
self.set_frequency()
|
|
elif name is None:
|
|
self.frequency = float(frequency)
|
|
self.set_name_and_octave()
|
|
Samples.__init__(self)
|
|
self.set_volume(volume)
|
|
|
|
def set_frequency(self):
|
|
name, octave = self.name, self.octave
|
|
names = self.names
|
|
octave_length = len(names)
|
|
offset = (octave - self.base_octave) * octave_length + \
|
|
names.index(name) - names.index(self.base_name)
|
|
self.frequency = self.base_frequency * 2 ** (offset / float(octave_length))
|
|
|
|
def set_name_and_octave(self):
|
|
names = self.names
|
|
octave_length = len(names)
|
|
offset = int(round(log(self.frequency / self.base_frequency, 2) * \
|
|
octave_length)) + names.index(self.base_name)
|
|
self.octave = self.base_octave + offset / octave_length
|
|
self.name = names[offset % octave_length]
|
|
|
|
def __repr__(self):
|
|
return "%s%i %.2f" % (self.name, self.octave, self.frequency)
|
|
|
|
def build(self):
|
|
period = int(round(get_init()[0] / self.frequency))
|
|
samples = self.get_empty_array(period)
|
|
shape = self.shape
|
|
if shape == self.TRIANGLE:
|
|
self.store_triangle_wave(samples, period)
|
|
elif shape == self.SAW:
|
|
self.store_saw_wave(samples, period)
|
|
elif shape == self.SINE:
|
|
self.store_sine_wave(samples, period)
|
|
elif shape == self.DIRTY:
|
|
self.store_dirty_wave(samples)
|
|
else:
|
|
self.store_square_wave(samples, period)
|
|
return samples
|
|
|
|
def store_triangle_wave(self, samples, period):
|
|
amplitude = self.amplitude
|
|
if period > 1:
|
|
coefficient = 4 * amplitude / float(period - 1)
|
|
for time in range(int(round(period / 2.0))):
|
|
y = int((coefficient * time) - amplitude)
|
|
samples[time] = y
|
|
samples[-time - 1] = y
|
|
|
|
def store_saw_wave(self, samples, period):
|
|
amplitude = self.amplitude
|
|
if period > 1:
|
|
for time in range(period):
|
|
samples[time] = int(2 * amplitude / float(period - 1) * time - \
|
|
amplitude)
|
|
|
|
def store_sine_wave(self, samples, period):
|
|
amplitude = self.amplitude
|
|
for time in range(period):
|
|
samples[time] = int(round(sin(time / (period / pi / 2)) * \
|
|
amplitude))
|
|
|
|
def store_dirty_wave(self, samples):
|
|
amplitude = self.amplitude
|
|
for time in range(len(samples)):
|
|
samples[time] = randint(-amplitude, amplitude)
|
|
|
|
def store_square_wave(self, samples, period):
|
|
amplitude = self.amplitude
|
|
for time in range(period):
|
|
if time < period / 2:
|
|
samples[time] = amplitude
|
|
else:
|
|
samples[time] = -amplitude
|
|
|
|
def play(self, maxtime=0, fadeout=None, panning=None, fade_in=0, position=None):
|
|
channel = Samples.play(self, -1, maxtime, fade_in)
|
|
if fadeout:
|
|
self.fadeout(fadeout)
|
|
if position is not None:
|
|
panning = self.get_panning(position)
|
|
if channel and panning:
|
|
channel.set_volume(*panning)
|
|
return channel
|
|
|
|
def get_panning(self, position):
|
|
return 1 - max(0, ((position - .5) * 2)), 1 + min(0, ((position - .5) * 2))
|
|
|
|
|
|
class Chord:
|
|
|
|
def __init__(self, *args):
|
|
self.notes = args
|
|
|
|
def play(self, maxtime=0, fadeout=[None], panning=None, fade_in=[0], position=None):
|
|
if isinstance(fadeout, int):
|
|
fadeout = [fadeout]
|
|
if isinstance(fade_in, int):
|
|
fade_in = [fade_in]
|
|
for ii, note in enumerate(self.notes):
|
|
note.play(maxtime, fadeout[ii % len(fadeout)], panning, fade_in[ii % len(fade_in)], position)
|
|
|
|
def stop(self):
|
|
for note in self.notes:
|
|
note.stop()
|