#!/usr/bin/python """A grammar of melodies. Some results of various versions of this program are in . Run it with the names of one or more sample .wav files (8-bit, mono) on the command line, all of the same pitch, ideally 110Hz. It will take an absurdly long time to generate an output .wav file that is probably almost listenable. This is an effort to randomly generate listenable scores. The idea is to use an encoding in which listenable music has lower entropy than unlistenable music. One of the appealing things about bytebeat (see ) is how easy it is to accidentally stumble upon listenable things. This is because there are a lot of listenable things, which in part is because of the similarity in the underlying mechanisms of binary arithmetic and traditional Western music. The idea of this grammar is to encode a little bit more music theory into the 'language', making listenable music more frequent. A 'score' lasts some power-of-2 number of beats and reduces ultimately to a sequence of notes, each of which consists of: - an instrument identifier, - a pitch at which to play a note on that instrument, measured in semitones above or below the base pitch, A2 (110 Hz); - a time at which to start playing it (duration is omitted), and - a volume at which to play it; coupled with a sound font that defines the instruments, this produces a waveform you can listen to. A 'score' is really a directed acyclic graph of scores. The primitive scores, the leaf nodes in the DAG, are: - a Rest, which represents one beat of silence, and - a NoteScore, which represents a sampled sound playing at full volume and the default pitch, for one beat. There is one possible NoteScore for each instrument. The other kinds of scores are different ways of combining or modifying scores: - Transpose: playing a score lowered by a perfect fifth; - Louder: playing a score slightly louder (3dB); - Sequence: playing one score followed by another at slightly diminished volume (2dB) and slightly altered timing, with one or the other repeated so that they each take equal time, if necessary; - Parallel: playing two scores concurrently, one raised by an octave, with one or the other repeated so that they take equal time, if necessary; - and I may add others, like time-reversal, inversion, simple repetition, slowing, speeding, and so on. """ from __future__ import division import cgitb import math import os import random import sys import wave ### Melody generation class Note: def __init__(self, instrument, start_time, pitch, volume): self.instrument = instrument self.start_time = start_time self.pitch = pitch self.volume = volume def __str__(self): return "%s %s %s %s" % (self.instrument, self.start_time, self.pitch, self.volume) def __repr__(self): return 'Note(%r, %r, %r, %r)' % (self.instrument, self.start_time, self.pitch, self.volume) def __cmp__(self, other): return cmp((float(str(self.start_time)), self.instrument, self.pitch, self.volume), (float(str(other.start_time)), other.instrument, other.pitch, other.volume)) def copy(self): return Note(self.instrument, self.start_time, self.pitch, self.volume) class Rest: def render2(self): return () def beats(self): return 1 def __str__(self): return '.' def peak_volume(self): return -200 def tracks(self): return 1 def depth(self): return 1 class NoteScore: def __init__(self, instrument): self.instrument = instrument def render2(self): yield Note(self.instrument, 0, 0, 0) def beats(self): return 1 def __str__(self): return str(self.instrument) def peak_volume(self): return 0 def tracks(self): return 1 def depth(self): return 1 class Transpose: def __init__(self, score): self.score = score def render2(self): for note in self.score.render2(): note.pitch -= 7 yield note def beats(self): return self.score.beats() def __str__(self): return '_%s' % self.score def peak_volume(self): return self.score.peak_volume() def tracks(self): return self.score.tracks() def depth(self): return 1 + self.score.depth() class Louder: def __init__(self, score): self.score = score def render2(self): for note in self.score.render2(): note.volume += 3 yield note def beats(self): return self.score.beats() def __str__(self): return '+%s' % self.score def peak_volume(self): return self.score.peak_volume() + 3 def tracks(self): return self.score.tracks() def depth(self): return 1 + self.score.depth() class RepeatFor: def __init__(self, score, beats): self.score = score self._beats = beats def render2(self): for note in self.score.render2(): while note.start_time < self._beats: new_note = note.copy() yield note note = new_note note.start_time += self.score.beats() class Sequence: def __init__(self, left, right): self.left = left self.right = right self._beats = 2 * max(left.beats(), right.beats()) self._pv = max(self.left.peak_volume(), self.right.peak_volume() - 2) self._tracks = max(self.left.tracks(), self.right.tracks()) self._depth = 1 + max(self.left.depth(), self.right.depth()) def render2(self): b2 = self.beats()/2 for note in RepeatFor(self.left, b2).render2(): yield note right_start = b2 + 0.1 for note in RepeatFor(self.right, b2).render2(): note.start_time += right_start note.volume -= 2 yield note def beats(self): return self._beats def __str__(self): return '(%s %s)' % (self.left, self.right) def peak_volume(self): return self._pv def tracks(self): return self._tracks def depth(self): return self._depth class Parallel: def __init__(self, low, high): self.low = low self.high = high self._beats = max(low.beats(), high.beats()) self._pv = max(low.peak_volume(), high.peak_volume()) self._tracks = low.tracks() + high.tracks() self._depth = 1 + max(self.low.depth(), self.high.depth()) def render2(self): for note in RepeatFor(self.low, self.beats()).render2(): yield note for note in RepeatFor(self.high, self.beats()).render2(): note.pitch += 12 yield note def beats(self): return self._beats def __str__(self): return '(%s ^ %s)' % (self.low, self.high) def peak_volume(self): return self._pv def tracks(self): return self._tracks def depth(self): return self._depth def _choose(lst): "Choose an item from lst with a bias toward later items." bias = 1.5 return lst[int(random.randrange(int(len(lst)**bias))**(1/bias))] def double(score): return Sequence(score, score) def random_score(instruments, complexity): nodes = ([NoteScore(instrument) for instrument in instruments] + [Rest()] * len(instruments)) while len(nodes) < complexity + len(instruments) + 1: node_type = random.choice([Transpose, Louder, Sequence, Parallel, Parallel, Sequence, Sequence, Sequence]) if node_type in [Transpose, Louder, double]: new_node = node_type(_choose(nodes)) else: new_node = node_type(_choose(nodes), _choose(nodes)) nodes.append(new_node) return nodes[-1] ### Sound mixing def resample_input(filename, rate): sample = wave.open(filename, 'r') assert sample.getnchannels() == 1 assert sample.getcomptype() == 'NONE' freq = sample.getframerate() assert sample.getsampwidth() == 1 # until I implement more contents = sample.readframes(sample.getnframes()) # A sum table. rv = [] total = 0 # Very crude resampling. for ii, sample_byte in enumerate(contents): while len(rv) < (ii+1) * rate / freq: total += ord(sample_byte) rv.append(total) return rv class DitherClipper: def __init__(self): self.err = 0 def dither(self, val): val += 128 val_int = int(val + 0.5) self.err += val_int - val if self.err < -0.55: self.err += 1 val_int += 1 elif self.err > 0.55: self.err -= 1 val_int -= 1 if val_int < 0: self.err += -val_int val_int = 0 elif val_int > 255: self.err -= val_int - 255 val_int = 255 return val_int class Sample: def __init__(self, sample, speed, volume): self.sample = sample self.speed = speed self.volume = volume self.fp = 0 def __iter__(self): return self def next(self): samp = self.sample fp = self.fp if fp == len(samp)-1: raise StopIteration nfp = min(fp + self.speed, len(samp)-1) infp, ifp = int(nfp), int(fp) assert infp != ifp, (nfp, fp, len(samp), self.speed) avg = (samp[infp] - samp[ifp]) / (infp - ifp) self.fp = nfp return (avg - 128) * self.volume class Mixer: def __init__(self): self.notes = [] def add(self, note): self.notes.append(note) def __iter__(self): return self def next(self): samples = [] ii = 0 while ii < len(self.notes): note = self.notes[ii] try: samples.append(note.next()) except StopIteration: self.notes.pop(ii) ii += 1 return sum(samples) def __len__(self): return len(self.notes) ### Main program def choose_outfile_name(): ii = 0 while True: filename = 'synthgramelodia-%d.wav' % ii if not os.path.exists(filename): return filename ii += 1 def main(argv): cgitb.enable(format='text') output_rate = 8000 oversampling = 16 instruments = [resample_input(filename, output_rate * oversampling) for filename in argv[1:]] bpm = 140 * 2 # * 32 # maximum notes per second on a track seconds_per_beat = 60 / bpm samples_per_beat = seconds_per_beat * output_rate instrument_names = 'abcdefghijklmnopqrstuvwxyz' score = random_score(instrument_names[:len(instruments)], 160) print score notes = list(score.render2()) notes.sort(reverse=True) for note in notes: print note outfile = wave.open(choose_outfile_name(), 'w') outfile.setnchannels(1) outfile.setsampwidth(1) outfile.setframerate(output_rate) # A sample I'm working with happens to be like 326Hz, and I want # to resample it to 220Hz. base_pitch_speed = oversampling * 220 / 326 mixer = Mixer() clipper = DitherClipper() sample_number = 0 peak_volume = score.peak_volume() print "total beats %s, peak volume %s, %d tracks, %d nodes deep" % (score.beats(), peak_volume, score.tracks(), score.depth()) print "planning %d samples" % (samples_per_beat * score.beats()) output_buffer = [] while sample_number < samples_per_beat * score.beats() or len(mixer): while notes and sample_number >= notes[-1].start_time * samples_per_beat: note = notes.pop() instrument = instruments[instrument_names.index(note.instrument)] pitch = max(note.pitch, -28) mixer.add(Sample(instrument, speed = base_pitch_speed * 2 ** (pitch/12), volume = 10 ** ((note.volume - peak_volume)/20))) print "now playing %d notes at once; latest: %s" % (len(mixer), note) output_buffer.append(chr(clipper.dither(mixer.next()))) if len(output_buffer) > 128: outfile.writeframesraw(''.join(output_buffer)) output_buffer[:] = [] sample_number += 1 print "%d samples in all; %d leftover notes!" % (sample_number, len(notes)) outfile.writeframes(''.join(output_buffer)) outfile.close() if __name__ == '__main__': main(sys.argv)