Created
December 14, 2018 19:23
-
-
Save cool-RR/282c52106213406449eba69cd1b06181 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import numbers | |
import struct | |
import pathlib | |
import math | |
import abc | |
import wave as wave_module | |
import itertools | |
import re | |
import sortedcontainers | |
CUTOFF_FACTOR = 8 | |
letter_map = {'c': 0, 'd': 2, 'e': 4, 'f': 5, 'g': 7, 'a': 9, 'b': 11} | |
for letter, number in dict(letter_map).items(): | |
letter_map[f'{letter}#'] = (number + 1) % 12 | |
letter_map[f'{letter}b'] = (number - 1) % 12 | |
def get_frequency_from_letter(letter): | |
match = re.match('^([abcdefg][b#]?)([0-9]?)', letter.lower()) | |
letter_, octave = match.groups() | |
octave = int(octave) if octave else 4 | |
return 16.352 * 2 ** (octave + (letter_map[letter_] / 12)) | |
class Timbre: | |
def __init__(self, overtones: dict): | |
self.overtones = dict(overtones) | |
class Instrument: | |
def __init__(self, timbre: Timbre, half_life: numbers.Rational = 0.2): | |
self.timbre = timbre | |
self.half_life = half_life | |
piano = Instrument(Timbre({i: 2 / i for i in range(1, 8)}), half_life=0.2) | |
class Listenable(metaclass=abc.ABCMeta): | |
@abc.abstractmethod | |
def get_pressure(self, time: numbers.Rational) -> numbers.Rational: | |
pass | |
@abc.abstractmethod | |
def get_length(self) -> numbers.Rational: | |
pass | |
def save_wav(self, path: pathlib.Path, hz=8000): | |
wav_file = wave_module.open(str(path), 'w') | |
nchannels = 1 | |
sampwidth = 2 | |
nframes = int(hz * self.get_length()) | |
comptype = 'NONE' | |
compname = 'not compressed' | |
wav_file.setparams((nchannels, sampwidth, hz, nframes, comptype, | |
compname)) | |
for i in range(nframes): | |
time = i / hz | |
wav_file.writeframes( | |
struct.pack('h', int(self.get_pressure(time) * 1000)) | |
) | |
wav_file.close() | |
class Note(Listenable): | |
def __init__(self, frequency, instrument: Instrument = piano, | |
volume: numbers.Rational = 1): | |
if isinstance(frequency, numbers.Number): | |
self.frequency = frequency | |
else: | |
self.frequency = get_frequency_from_letter(frequency) | |
self.instrument = instrument | |
self.volume = volume | |
def get_length(self): | |
try: | |
return self._length | |
except AttributeError: | |
self._length = self.instrument.half_life * CUTOFF_FACTOR | |
return self._length | |
def get_pressure(self, time: numbers.Rational) -> numbers.Rational: | |
if time < 0 or time > self.get_length(): | |
return 0 | |
result = 0 | |
for overtone, overtone_volume in \ | |
self.instrument.timbre.overtones.items(): | |
result += overtone_volume * math.sin( | |
time * self.frequency * overtone * (math.pi * 2) | |
) | |
result *= self.volume * 2 ** (- time / self.instrument.half_life) | |
return result | |
class Sequence(Listenable): | |
def __init__(self, members): | |
self.members = sortedcontainers.SortedListWithKey( | |
members, | |
key=lambda pair: pair[0] | |
) | |
def get_pressure(self, time: numbers.Rational) -> numbers.Rational: | |
return sum(member.get_pressure(time - time_) for time_, member in | |
self.members) | |
def get_length(self,): | |
return max( | |
time + member.get_length() for time, member in self.members | |
) | |
bpm = 50 | |
period = 1 / (bpm / 60) | |
foo = ( | |
(0 * period + period * 0 / 6, Note('c4')), | |
(0 * period + period * 2 / 6, Note('c4')), | |
(0 * period + period * 3 / 6, Note('e4')), | |
(0 * period + period * 5 / 6, Note('g4')), | |
(1 * period + period * 0 / 6, Note('a3')), | |
(1 * period + period * 2 / 6, Note('a3')), | |
(1 * period + period * 3 / 6, Note('c4')), | |
(1 * period + period * 5 / 6, Note('e4')), | |
(2 * period + period * 0 / 6, Note('f3')), | |
(2 * period + period * 2 / 6, Note('f3')), | |
(2 * period + period * 3 / 6, Note('a3')), | |
(2 * period + period * 5 / 6, Note('c4')), | |
(3 * period + period * 0 / 6, Note('g3')), | |
(3 * period + period * 2 / 6, Note('g3')), | |
(3 * period + period * 3 / 6, Note('b3')), | |
(3 * period + period * 5 / 6, Note('d4')), | |
) | |
foofoo = [] | |
for i, entry in itertools.product(range(12), foo): | |
foofoo.append( | |
(entry[0] + i * 4 * period, entry[1]) | |
) | |
sequence = Sequence(foofoo) | |
sequence.save_wav('output.wav') | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment