#!/usr/bin/python """A grammar of melodies. 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 math import random 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]