Skip to content

Commit d11dec8

Browse files
committed
tbish: pulled out simple step sequencer to its own class, modeled after synth_tools.step_sequencer
1 parent 9d57fa7 commit d11dec8

File tree

5 files changed

+138
-69
lines changed

5 files changed

+138
-69
lines changed

circuitpython/tbish/code.py

Lines changed: 35 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
14 May 2025 - @todbot / Tod Kurt
1010
1111
"""
12+
# this reduces some of per-step latency from 10ms down to 6ms
13+
import microcontroller
14+
microcontroller.cpu.frequency = 200_000_000
1215

1316
import time, random
1417
import ulab.numpy as np
@@ -19,32 +22,28 @@
1922
from param_set import ParamSet, Param
2023

2124
from tbish_synth import TBishSynth
25+
from tbish_sequencer import TBishSequencer
2226
from tbish_ui import TBishUI
2327

2428
display = setup_display()
2529
touches = setup_touch()
2630

27-
playing = True
28-
bpm = 120
29-
steps_per_beat = 2
30-
secs_per_step = 60 / bpm / steps_per_beat
31-
glide_time = 0.25
3231
seqs = [
3332
[[36, 36, 48, 36, 48, 48+7, 36, 48], # notes, 0 = rest
34-
[127, 80, 80, 80, 127, 1, 30, 1]], # vels, 1=slide, 127=accent
33+
[127, 80, 80, 80, 127, 1, 30, 1]], # vels, 1=slide, 127=accent
34+
3535
[[34, 36, 34, 36, 48, 48, 36, 48], # notes, 0 = rest
36-
[127, 80, 120, 80, 127, 11, 127, 80]], # vels 127=accent
37-
[[36, 36+12, 36, 36+12, 36, 0, 36, 0], # notes, 0 = rest
38-
[127, 80, 120, 80, 127, 80, 127, 80]], # vels 127=accent
36+
[127, 80, 120, 80, 127, 11, 127, 80]], # vels 127=accent
37+
38+
[[36, 36-12, 36, 36+12, 36, 0, 36, 0], # notes, 0 = rest
39+
[127, 1, 1, 80, 127, 80, 127, 80]], # vels 127=accent
3940
]
40-
seq_num = 0
41-
42-
midi_note = seqs[seq_num][0][0]
43-
next_midi_note = 0
44-
gate_off_time = 0
45-
gate_amount = 0.75 # TB-303 historical gate lengnth
46-
transpose = 0
47-
i=0
41+
# future path
42+
seqs_steps = [
43+
[[ ], # notes,
44+
[ ], # slide,
45+
[ ],], # accent,
46+
]
4847

4948
params = [
5049
Param("cutoff", 4000, 200, 6000, "%4d", 'cutoff'),
@@ -70,6 +69,8 @@
7069
tb_audio = tb.add_audioeffects()
7170
mixer.voice[0].play(tb_audio)
7271

72+
sequencer = TBishSequencer(tb, seqs)
73+
7374
param_set = ParamSet(params, num_knobs=2)
7475
param_set.apply_params(tb) # set up synth with param set
7576

@@ -78,19 +79,22 @@
7879
print("="*80)
7980
print("tbish synth! press button to play/pause")
8081
print("="*80)
81-
print("secs_per_step:%.3f" % secs_per_step)
82+
print("secs_per_step:%.3f" % sequencer._secs_per_step)
8283
import gc
8384
print("mem_free:", gc.mem_free())
8485

8586
last_ui_time = time.monotonic()
8687
def update_ui():
87-
global last_ui_time, gate_amount, bpm, secs_per_step
88+
global last_ui_time
8889
ki = tb_disp.curr_param_pair # shorthand
8990
if time.monotonic() - last_ui_time > 0.05:
9091
last_ui_time = time.monotonic()
9192

93+
# bit-reduction filtering then normalize
94+
#knobvals = (knobA.value, knobB.value)
95+
#knobvals = [((k>>8)<<8)/65535 for k in knobvals]
96+
# just normalized
9297
knobvals = (knobA.value/65535, knobB.value/65535)
93-
#knobvals = [int(k)>>8<<8 for k in knobvals]
9498
param_set.update_knobs(knobvals)
9599

96100
# set synth with params
@@ -99,21 +103,23 @@ def update_ui():
99103
# for non-tb params
100104
#gate_amount = param_set.param_for_name('gate').val
101105
bpm = param_set.param_for_name('bpm').val
102-
secs_per_step = 60 / bpm / steps_per_beat
103-
106+
sequencer.bpm = bpm
107+
108+
seq_num = int(param_set.param_for_name('seq').val)
109+
sequencer.seq_num = seq_num
110+
104111
tb_disp.update_param_pairs()
105112

106-
next_step_time = time.monotonic()
113+
sequencer.start()
107114

108115
while True:
109116

110117
if key := keys.events.get():
111118
if key.pressed:
112-
playing = not playing
113-
if playing:
114-
next_step_time = time.monotonic()
119+
if sequencer.playing:
120+
sequencer.stop()
115121
else:
116-
tb.note_off(midi_note)
122+
sequencer.start()
117123

118124
if touch_events := check_touch():
119125
for touch in touch_events:
@@ -124,39 +130,10 @@ def update_ui():
124130
param_set.idx = tb_disp.curr_param_pair
125131
if touch.key_number in touchpad_to_transpose:
126132
transpose = touch.key_number # chromatic
133+
sequencer.transpose = transpose
127134

128135
update_ui()
129136

130-
if not playing:
131-
continue
132-
133-
now = time.monotonic()
134-
if gate_off_time and gate_off_time - now <= 0:
135-
gate_off_time = 0
136-
#if next_midi_note != 0:
137-
tb.note_off(midi_note)
138-
139-
dt = (next_step_time - now)
140-
if dt <= 0:
141-
next_step_time = now + secs_per_step + dt
142-
# add dt delta to attempt to make up for display hosing us
143-
144-
midi_note = seqs[seq_num][0][i]
145-
vel = seqs[seq_num][1][i]
146-
if midi_note != 0:
147-
midi_note += transpose
148-
vel = vel # + random.randint(-30,0)
149-
tb.secs_per_step = secs_per_step * 1.0
150-
tb.note_on(midi_note, vel)
151-
#tb.note_on(midi_note, True, False)
152-
gate_off_time = time.monotonic() + secs_per_step * gate_amount
153-
154-
seq_num = int(param_set.param_for_name('seq').val)
155-
i = (i+1) % len(seqs[seq_num][0])
156-
#next_midi_note = midi_notes[i]
157-
print(i,"new: %d old: %d glide_time: %.2f vel:%3d" %
158-
(midi_note, tb.glider.midi_note, tb.glider.glide_time, vel),
159-
tb.filt_env.offset, tb.filt_env.scale)
160-
137+
sequencer.update()
161138

162139

circuitpython/tbish/param_set.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class ParamSet:
6464
especially for the case when there are fewer knobs than Params.
6565
"""
6666

67-
def __init__(self, params, num_knobs, min_knob_change=0.1, knob_smooth=0.5):
67+
def __init__(self, params, num_knobs, min_knob_change=0.05, knob_smooth=0.5):
6868
self.params = params
6969
self.nparams = len(params)
7070
self.nknobs = num_knobs
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import time
2+
3+
class TBishSequencer:
4+
def __init__(self, tb, seqs):
5+
self.tb = tb
6+
self.seqs = seqs
7+
self._steps_per_beat = 4 # 4 = 16th note, 2 = 8th note, 1 = quarter note
8+
self.bpm = 120
9+
self.gate_time = 0
10+
self.gate_amount = 0.75 # traiditional 303 gate length
11+
self.next_step_time = 0
12+
self.gate_off_time = 0
13+
self.midi_note = 0 # current note being played or 0 for none
14+
self.seqs = seqs
15+
self.seq_num = 0
16+
self.transpose = 0
17+
self.i = 0 # step number
18+
self.playing = True
19+
20+
@property
21+
def steps_per_beat(self):
22+
return self._steps_per_beat
23+
24+
@steps_per_beat.setter
25+
def steps_per_beat(self, s):
26+
self._steps_per_beat = s
27+
self._secs_per_step = 60 / self._bpm / self._steps_per_beat
28+
29+
@property
30+
def bpm(self):
31+
return self._bpm
32+
33+
@bpm.setter
34+
def bpm(self, b):
35+
self._bpm = b
36+
self._secs_per_step = 60 / self._bpm / self._steps_per_beat
37+
38+
def start(self):
39+
self.playing = True
40+
self.i = 0
41+
self.next_step_time = time.monotonic()
42+
43+
def stop(self):
44+
self.playing = False
45+
self.tb.note_off(self.midi_note)
46+
47+
def update(self):
48+
if not self.playing:
49+
return
50+
now = time.monotonic()
51+
52+
# turn off note (not really a TB-303 thing, but a MIDI thing)
53+
if self.gate_off_time and self.gate_off_time - now <= 0:
54+
self.gate_off_time = 0
55+
self.tb.note_off(self.midi_note)
56+
57+
# time for next step?
58+
dt = (self.next_step_time - now)
59+
if dt <= 0:
60+
self.next_step_time = now + self._secs_per_step + dt
61+
# add dt delta to attempt to make up for display hosing us
62+
63+
midi_note = self.seqs[self.seq_num][0][self.i]
64+
vel = self.seqs[self.seq_num][1][self.i]
65+
if midi_note != 0: # midi_note == 0 = rest
66+
midi_note += self.transpose
67+
vel = vel # + random.randint(-30,0)
68+
self.tb.secs_per_step = self._secs_per_step * 1.0
69+
self.tb.note_on(midi_note, vel)
70+
#self.tb.note_on(midi_note, True, False)
71+
self.gate_off_time = time.monotonic() + self._secs_per_step * self.gate_amount
72+
73+
#seq_num = int(param_set.param_for_name('seq').val)
74+
self.i = (self.i+1) % len(self.seqs[self.seq_num][0])
75+
self.midi_note = midi_note
76+
77+
print(self.i,"note: %d vel:%3d" % (midi_note, vel),
78+
int(self._secs_per_step*1000), int(dt*1000))
79+

circuitpython/tbish/tbish_synth.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def __init__(self, sample_rate, channel_count):
6969
self.cutoff = 8000 # aka 'filter frequency'
7070
self.envmod = 0.5 # aka 'filter depth'
7171
self.envdecay = 0.01
72+
self._accent = 0.5
7273
self.autoslide = True # FIXME unused yet
7374
self.filt_env = synthio.LFO(rate=1, scale=self.cutoff, once=True,
7475
waveform=np.array((32767,0),dtype=np.int16))
@@ -108,14 +109,12 @@ def add_audioeffects(self):
108109
decay = 0.1,
109110
freq_shift = False,
110111
)
111-
print("delay_ms=", self.secs_per_step * 1000 * 4)
112-
#self.fx_delay = ... # FIXME: add tempo-sync'd delay
113112
self.fx_delay.play(self.fx_distortion)
114113
self.fx_distortion.play(self.fx_filter2) # plug 2nd filter into distortion
115114
self.fx_filter2.play(self.fx_filter1) # plug 1st filter into 2nd filter
116115
self.fx_filter1.play(self.synth) # plug synth into 1st filter
117-
#return self.fx_distortion # this "output" of this synth
118-
return self.fx_delay
116+
#return self.fx_distortion # the "output" of this synth
117+
return self.fx_delay # the "output" of this synth
119118

120119
def note_on_step(self, midi_note, slide=False, accent=False):
121120
print("note_on_step:", midi_note, slide, accent)
@@ -130,7 +129,7 @@ def note_on(self, midi_note, vel=127):
130129
self.note_off(midi_note) # just in case
131130

132131
#frate = 1 / self.secs_per_step
133-
envdecay = max(0.05, self.secs_per_step * self.envdecay)
132+
envdecay = max(0.05, self.secs_per_step * self.envdecay * 1.5) # 1.5 fudge
134133
frate = 1 / envdecay
135134
cutoff = self.cutoff * 1.3 if vel==127 else self.cutoff # FIXME verify
136135
envmod = self.envmod * 0.5 if vel==127 else self.envmod
@@ -167,6 +166,14 @@ def decay(self):
167166
def decay(self,t):
168167
self.envdecay = t
169168

169+
@property
170+
def accent(self):
171+
return self.accent
172+
173+
@accent.setter
174+
def accent(self, v):
175+
self.accent = v
176+
170177
@property
171178
def drive(self):
172179
return self.fx_distortion.pre_gain

circuitpython/tbish/tbish_ui.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,16 @@ def update_param_pairs(self):
4242
self.rect.x = 45 + 5*(self.curr_param_pair)
4343
paramL = self.params[self.curr_param_pair*2+0]
4444
paramR = self.params[self.curr_param_pair*2+1]
45-
self.labelA.text = paramL.name
46-
self.labelB.text = paramR.name
47-
self.textA.text = paramL.fmt % paramL.val
48-
self.textB.text = paramR.fmt % paramR.val
49-
#print("refreshing")
45+
textAnew = paramL.fmt % paramL.val
46+
textBnew = paramR.fmt % paramR.val
47+
# try to be smart and only update what's needed
48+
if paramL.name != self.labelA.text:
49+
self.labelA.text = paramL.name
50+
if paramR.name != self.labelB.text:
51+
self.labelB.text = paramR.name
52+
if self.textA.text != textAnew:
53+
self.textA.text = textAnew
54+
if self.textB.text != textBnew:
55+
self.textB.text = textBnew
5056
self.display.refresh()
5157

0 commit comments

Comments
 (0)