A sequencer synth in less than 50 lines of FoxDot
René Ghosh, 10/01/2019
One of the difficulties I find in live coding music is to make a melody come together while having to think separately about note pitch and duration in distinct lists. I’ve written about the stemel library for generating musical patterns from a score format in a step sequencer formalism. Here I’m going to present a simplified version of it, focusing more on a more FoxDot-y player function.
What I want to do is to have a multi-voice sequencer that takes a score as a string, a list of synths on which to play each voice, a list of players to pipe them to, and the duration of the sequencer step.
So, what I want is to be able to write:
play_sequencer("00000---/77*A5577/AA>>00<AA>00", [bass,sitar,feel], [b1,b2,b3], step=0.5)
In the function call, there’s the bass playing 00000---
, or 4 short notes and one long one,
the sitar playing 77*A5577
and the feel playing 00<AA>00
. The players used to
channel the sound will be players b1
,b2
and b3
.
In the score interpreter, I’ll use the following conventions:
0
,1
,2
,3
,4
,5
,6
,7
,8
,9
,A
,B
are notes 0 to 11,>
and<
are the up-octave and down-octave operators that affect all following notes,*
is a rest,-
prolongs the preceding note by one step,/
ends the current voice and starts a new voice.
I need to translate the score into lists of pitch and duration. Here’s the function to do so:
def generate_sequencer(stream, step):
notes = ['0','1','2','3','4','5','6','7','8','9','A','B'] #basic notes
octave = 0 #starting octave
score = [[]] #empty score, no voices
voice_cursor=0
note_cursor=-1
seq_length = 0
for c in stream:
if c=='/':
score.append([]) # add a new voice to the sequencer
voice_cursor += 1
note_cursor=-1
elif c=='*':
score[voice_cursor].append([-1,rest(step)]) #add a rest
elif c=='-':
score[voice_cursor][note_cursor][1]+=step #increment previous note's length
elif c=='>':
octave += 1 # up octave
elif c=='<':
octave = octave - 1 if octave >0 else 0 # down octave
elif c in notes:
score[voice_cursor].append([notes.index(c)+12*octave,step]) #add note
note_cursor+=1
if len(score[voice_cursor])>seq_length:
seq_length=len(score[voice_cursor])
transposed_score = []
for voice in score: #group pitch with pitch, duration with duration
transposed_voice = []
transposed_score.append(transposed_voice)
for i in range(0,2):
transposed_voice.append(list(map(lambda x: x[i],voice)))
return transposed_score
So, for example, if I run this function on "00-->7*"
, it gives me [[[0, 0, 19, -1], [1, 3, 1, <rest: 1>]]]
.
The next step is to send these musical patterns to synths. This is simply a question of taking the synths and players as parameters to a function that will apply a synth to each voice, on one player:
def play_sequencer(input, synths, players, step=1):
patterns = generate_sequencer(input, step)
for index, [pitches, durations] in enumerate(patterns):
players[index % len(players)] >> synths[index % len(synths)](pitches, dur=durations)
This is the sequencer player.
It wouldn’t be interesting if I couldn’t tweak the sound of each synth. In FoxDot, synths are functions, so to apply distinct effects to each synth, I have to partially evaluate it with the desired effects. In functional programming terms, this is known as currying. To do this I’ll import the python library that enables partial evaluation:
from functools import partial
msitar = partial(sitar, mix=0.3, room=0.4, echo=0.5)
mbass = partial(bass, mix=0.1, room=0.4)
mfeel = partial(feel, tremolo=4)
And now I can use my pythonic step sequencer:
Scale.default="chromatic"
play_sequencer("00000---/77*A5577/AA>>00<AA>00", [mbass, msitar,mfeel], [b1,b2,b3], 0.5)
Happy FoxDoting!