Source code for codex.songplayer

"""songplayer module

Very high-level text-based music player
Both songs and voices are specified as text strings

Uses a TrackPlayer for the actual playback.
The focus in this module is on translating from a text-based format
into the simple format used by TrackPlayer, and on doing as much of the
setup as possible for the user.

    * song_player = `SongPlayer`


"""

from soundlib import *

from trackplayer import *

NOTES = {
  'C1'  : 261.63 / 8,
  'C#1' : 277.18 / 8,
  'Db1' : 277.18 / 8,
  'D1'  : 293.66 / 8,
  'D#1' : 311.13 / 8,
  'Eb1' : 311.13 / 8,
  'E1'  : 329.63 / 8,
  'F1'  : 349.23 / 8,
  'F#1' : 369.99 / 8,
  'Gb1' : 369.99 / 8,
  'G1'  : 392.00 / 8,
  'G#1' : 415.30 / 8,
  'Ab1' : 415.30 / 8,
  'A1'  : 440.00 / 8,
  'A#1' : 466.16 / 8,
  'B1'  : 493.88 / 8,
  'C2'  : 261.63 / 4,
  'C#2' : 277.18 / 4,
  'Db2' : 277.18 / 4,
  'D2'  : 293.66 / 4,
  'D#2' : 311.13 / 4,
  'Eb2' : 311.13 / 4,
  'E2'  : 329.63 / 4,
  'F2'  : 349.23 / 4,
  'F#2' : 369.99 / 4,
  'Gb2' : 369.99 / 4,
  'G2'  : 392.00 / 4,
  'G#2' : 415.30 / 4,
  'Ab2' : 415.30 / 4,
  'A2'  : 440.00 / 4,
  'A#2' : 466.16 / 4,
  'B2'  : 493.88 / 4,
  'C3'  : 261.63 / 2,
  'C#3' : 277.18 / 2,
  'Db3' : 277.18 / 2,
  'D3'  : 293.66 / 2,
  'D#3' : 311.13 / 2,
  'Eb3' : 311.13 / 2,
  'E3'  : 329.63 / 2,
  'F3'  : 349.23 / 2,
  'F#3' : 369.99 / 2,
  'Gb3' : 369.99 / 2,
  'G3'  : 392.00 / 2,
  'G#3' : 415.30 / 2,
  'Ab3' : 415.30 / 2,
  'A3'  : 440.00 / 2,
  'A#3' : 466.16 / 2,
  'B3'  : 493.88 / 2,
  'C4'  : 261.63,
  'C#4' : 277.18,
  'Db4' : 277.18,
  'D4'  : 293.66,
  'D#4' : 311.13,
  'Eb4' : 311.13,
  'E4'  : 329.63,
  'F4'  : 349.23,
  'F#4' : 369.99,
  'Gb4' : 369.99,
  'G4'  : 392.00,
  'G#4' : 415.30,
  'Ab4' : 415.30,
  'A4'  : 440.00,
  'A#4' : 466.16,
  'Bb4' : 466.16,
  'B4'  : 493.88,
  'C5'  : 261.63 * 2,
  'C#5' : 277.18 * 2,
  'Db5' : 277.18 * 2,
  'D5'  : 293.66 * 2,
  'D#5' : 311.13 * 2,
  'Eb5' : 311.13 * 2,
  'E5'  : 329.63 * 2,
  'F5'  : 349.23 * 2,
  'F#5' : 369.99 * 2,
  'Gb5' : 369.99 * 2,
  'G5'  : 392.00 * 2,
  'G#5' : 415.30 * 2,
  'Ab5' : 415.30 * 2,
  'A5'  : 440.00 * 2,
  'A#5' : 466.16 * 2,
  'Bb5' : 466.16 * 2,
  'B5'  : 493.88 * 2,
  'C6'  : 261.63 * 4,
  'C#6' : 277.18 * 4,
  'Db6' : 277.18 * 4,
  'D6'  : 293.66 * 4,
  'D#6' : 311.13 * 4,
  'Eb6' : 311.13 * 4,
  'E6'  : 329.63 * 4,
  'F6'  : 349.23 * 4,
  'F#6' : 369.99 * 4,
  'Gb6' : 369.99 * 4,
  'G6'  : 392.00 * 4,
  'G#6' : 415.30 * 4,
  'Ab6' : 415.30 * 4,
  'A6'  : 440.00 * 4,
  'A#6' : 466.16 * 4,
  'Bb6' : 466.16 * 4,
  'B6'  : 493.88 * 4,
  'C7'  : 261.63 * 8,
  'C#7' : 277.18 * 8,
  'Db7' : 277.18 * 8,
  'D7'  : 293.66 * 8,
  'D#7' : 311.13 * 8,
  'Eb7' : 311.13 * 8,
  'E7'  : 329.63 * 8,
  'F7'  : 349.23 * 8,
  'F#7' : 369.99 * 8,
  'Gb7' : 369.99 * 8,
  'G7'  : 392.00 * 8,
  'G#7' : 415.30 * 8,
  'Ab7' : 415.30 * 8,
  'A7'  : 440.00 * 8,
  'A#7' : 466.16 * 8,
  'Bb7' : 466.16 * 8,
  'B7'  : 493.88 * 8,
  'C8'  : 261.63 * 16,
  'C#8' : 277.18 * 16,
  'Db8' : 277.18 * 16,
  'D8'  : 293.66 * 16,
  'D#8' : 311.13 * 16,
  'Eb8' : 311.13 * 16,
  'E8'  : 329.63 * 16,
  'F8'  : 349.23 * 16,
  'F#8' : 369.99 * 16,
  'Gb8' : 369.99 * 16,
  'G8'  : 392.00 * 16,
  'G#8' : 415.30 * 16,
  'Ab8' : 415.30 * 16,
  'A8'  : 440.00 * 16,
  'A#8' : 466.16 * 16,
  'Bb8' : 466.16 * 16,
  'B8'  : 493.88 * 16,
  'C9'  : 261.63 * 32,
}

REST_INDICATOR = 'R'
REST = 1 # Using a very low frequency tone versus stopping() audio playback completely to reduce "clicking"

# Design Decision - to support varying the tempo on-the-fly,
# we are going to standardize on the following durations
# but then adjust the actual playback speed at runtime
DURATIONS = {
    '1' : 240 / DEFAULT_BPM,
    '2' : 120 / DEFAULT_BPM,
    '4' : 60 / DEFAULT_BPM,
    '8' : 30 / DEFAULT_BPM,
    '16' : 15 / DEFAULT_BPM,

    '1.' : 360 / DEFAULT_BPM,
    '2.' : 180 / DEFAULT_BPM,
    '4.' : 90 / DEFAULT_BPM,
    '8.' : 45 / DEFAULT_BPM,
    '16.' : 22.5 / DEFAULT_BPM,

    '4t' : 40 / DEFAULT_BPM,
    '8t' : 20 / DEFAULT_BPM,
    '16t' : 10 / DEFAULT_BPM,
}

[docs]class SongPlayer(): """Compile and play text-based songs""" def __init__(self, waveform_name = "flute"): if hasattr(self, '__doc__'): return # Autodoc stop here # Note that we use the soundmaker instance auto-created by soundlib self.tone = soundmaker.get_tone(waveform_name) self.track_player = TrackPlayer(self.tone) def set_voice(self, waveform_name): self.tone = soundmaker.get_tone(waveform_name) self.track_player.set_tone(self.tone) def set_bpm(self, bpm): self.track_player.set_bpm(bpm) def compile_error(self, track): print("Unsupported song_text format! Maybe expand your track compiler?") return track def compile_snippet(self, note_name, note_duration): if note_name == REST_INDICATOR: note_pitch = REST elif note_name in NOTES: note_pitch = NOTES[note_name] else: print("Unsupported note name! Maybe add " + note_name + " to your NOTES dictionary?") return None if note_duration in DURATIONS: duration = DURATIONS[note_duration] else: print("Unsupported note (or rest) duration! Maybe add " + note_duration + " to your DURATIONS dictionary?") return None return (note_pitch, duration) def compile_piece(self, piece): # The only thing that is not optional is a note name (or R for rest) found = False while not found: for note_name in 'ABCDEFGR': index = piece.find(note_name) if index != -1: found = True break if not found: return None if index == 0: # No left-hand side note_duration = self.default_duration else: note_duration = piece[0:index] self.default_duration = note_duration if index == len(piece)-1: note_name = piece[index] + self.default_octave else: partial_note_name = piece[index] remainder = piece[index+1:] if (remainder[0] == '#') or (remainder[0] == 'b'): partial_note_name += remainder[0] remainder = remainder[1:] if remainder == '': note_name = partial_note_name + self.default_octave elif remainder[0] in "123456789": note_name = partial_note_name + remainder[0] self.default_octave = remainder[0] else: temp_octave = int(self.default_octave) while len(remainder): if remainder[0] == '+': temp_octave += 1 elif remainder[0] == '-': temp_octave -= 1 remainder = remainder[1:] if temp_octave < 1: temp_octave = 1 if temp_octave > 9: temp_octave = 9 self.default_octave = str(temp_octave) note_name = partial_note_name + self.default_octave # Rests don't have octaves if note_name[0] == 'R': note_name = 'R' snippet = self.compile_snippet(note_name, note_duration) return snippet
[docs] def compile_song(self, song_text): """Convert a song from an ASCII representation to a list of frequency, duration pairs""" self.default_duration = '4' self.default_octave = '4' track = [] source_list = song_text.split() for piece in source_list: snippet = self.compile_piece(piece) if snippet is not None: track.append(snippet) else: print("Unsupported song_text format! Maybe track compiler needs expanding?") return track print("song_text compiled into track format successfuly") return track
[docs] def play_compiled_song(self, track): """Play a list of frequency, duration pairs (play a 'track')""" self.track_player.set_track(track) self.track_player.play()
[docs] def play_song(self, song_text): """Play a song based on an ASCII representation (compiles it, then plays it)""" track = self.compile_song(song_text) self.play_compiled_song(track) return track # in case you want to play it again without recompiling it (ex. after tempo or voice changes)
# Create the global SongPlayer instance song_player = SongPlayer()