Skip to content

Commit 3884910

Browse files
committed
Some improvements (arguably) to the sampler example
1 parent bb3199c commit 3884910

2 files changed

Lines changed: 65 additions & 52 deletions

File tree

examples/sampler.py

Lines changed: 47 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
"""A minimalistic sampler with an even more minimalistic GUI."""
33

44
import collections
5+
import functools
56
import math
67
try:
78
import tkinter as tk
89
except ImportError:
9-
# Python 2.x
10-
import Tkinter as tk
10+
import Tkinter as tk # Python 2.x
1111

1212
import rtmixer
13-
from tkhelper import TkKeyEventDebouncer
13+
import tkhelper
1414

1515

1616
HELPTEXT = 'Hold uppercase key for recording,\nlowercase for playback'
@@ -21,29 +21,29 @@
2121

2222
class Sample(object):
2323

24-
def __init__(self):
25-
elementsize = stream.samplesize[0]
26-
size = BUFFER_DURATION * stream.samplerate
24+
def __init__(self, elementsize, size):
2725
# Python 2.x doesn't have math.log2(), and it needs int():
2826
size = 2**int(math.ceil(math.log(size, 2)))
2927
self.ringbuffer = rtmixer.RingBuffer(elementsize, size)
3028
self.buffer = bytearray()
3129
self.action = None
3230

3331

34-
class MiniSampler(tk.Tk, TkKeyEventDebouncer):
32+
class MiniSampler(tk.Tk, tkhelper.KeyEventDebouncer):
3533

36-
def __init__(self, *args, **kwargs):
37-
tk.Tk.__init__(self, *args, **kwargs)
38-
TkKeyEventDebouncer.__init__(self)
34+
def __init__(self, stream, buffer_duration=BUFFER_DURATION):
35+
tk.Tk.__init__(self)
36+
tkhelper.KeyEventDebouncer.__init__(self)
3937
self.title('MiniSampler')
4038
tk.Label(self, text=HELPTEXT).pack(ipadx=20, ipady=20)
4139
self.rec_display = tk.Label(self, text='recording')
42-
self.rec_counter = tk.IntVar()
40+
self.rec_counter = tkhelper.IntVar()
4341
self.rec_counter.trace(mode='w', callback=self.update_rec_display)
4442
self.rec_counter.set(0)
4543
self.rec_display.pack(padx=10, pady=10, ipadx=5, ipady=5)
46-
self.samples = collections.defaultdict(Sample)
44+
self.samples = collections.defaultdict(functools.partial(
45+
Sample, stream.samplesize[0], buffer_duration * stream.samplerate))
46+
self.stream = stream
4747

4848
def update_rec_display(self, *args):
4949
if self.rec_counter.get() == 0:
@@ -55,28 +55,20 @@ def on_key_press(self, event):
5555
ch = event.char
5656
if ch.isupper():
5757
sample = self.samples[ch.lower()]
58-
# TODO: check if we are already recording? check action?
59-
sample.ringbuffer.flush()
60-
sample.action = stream.record_ringbuffer(sample.ringbuffer)
58+
if sample.action in self.stream.actions:
59+
return
60+
if sample.ringbuffer.read_available:
61+
return
6162
del sample.buffer[:]
62-
self.rec_counter.set(self.rec_counter.get() + 1)
63+
self.rec_counter += 1
64+
sample.action = self.stream.record_ringbuffer(sample.ringbuffer)
6365
self.poll_ringbuffer(sample)
6466
elif ch in self.samples:
6567
sample = self.samples[ch]
66-
# TODO: can it be still recording or already playing?
67-
if sample.action in stream.actions:
68-
# TODO: the CANCEL action might still be active, which
69-
# shouldn't be a problem, right?
70-
print(ch, 'action still running:', sample.action.type)
71-
if ((sample.action is not None and
72-
sample.action.type != rtmixer._lib.CANCEL) or
73-
not sample.buffer):
74-
print(sample.action.type, len(sample.buffer))
75-
raise RuntimeError('Unable to play')
76-
sample.action = stream.play_buffer(sample.buffer, channels=1)
77-
else:
78-
# TODO: handle special keys?
79-
pass
68+
if sample.action in self.stream.actions:
69+
# CANCEL action from last key release might still be active
70+
return
71+
sample.action = self.stream.play_buffer(sample.buffer, channels=1)
8072

8173
def on_key_release(self, event):
8274
# NB: State of "shift" button may change between key press and release!
@@ -85,45 +77,50 @@ def on_key_release(self, event):
8577
return
8678
sample = self.samples[ch]
8779
# TODO: fade out (both recording and playback)?
88-
# TODO: is it possible that there is no action?
89-
assert sample.action
80+
assert sample.action is not None
9081
# TODO: create a public API for that?
9182
if sample.action.type == rtmixer._lib.RECORD_RINGBUFFER:
9283
# TODO: check for errors/xruns? check for rinbuffer overflow?
9384
# Stop recording
94-
sample.action = stream.cancel(sample.action)
85+
sample.action = self.stream.cancel(sample.action)
9586
# A CANCEL action is returned which is checked by poll_ringbuffer()
9687
elif sample.action.type == rtmixer._lib.PLAY_BUFFER:
9788
# TODO: check for errors/xruns?
98-
# Stop playback
99-
sample.action = stream.cancel(sample.action)
100-
# TODO: do something with sample.action?
89+
# Stop playback (if still running)
90+
if sample.action in self.stream.actions:
91+
sample.action = self.stream.cancel(sample.action)
92+
# TODO: do something with sample.action?
10193
elif sample.action.type == rtmixer._lib.CANCEL:
102-
print('key {!r} released during CANCEL'.format(event.char))
94+
# We might end up here if on_key_press() exits early
95+
pass
10396
else:
104-
print(event.char, sample.action)
105-
assert False
97+
assert False, (event.char, sample.action)
10698

10799
def poll_ringbuffer(self, sample):
108-
# TODO: check for errors? is everything still working OK?
109-
assert sample.action, sample.action
100+
assert sample.action is not None
110101
assert sample.action.type in (rtmixer._lib.RECORD_RINGBUFFER,
111-
rtmixer._lib.CANCEL), sample.action.type
102+
rtmixer._lib.CANCEL)
103+
# TODO: check for errors? is everything still working OK?
104+
# TODO: check for xruns?
112105
chunk = sample.ringbuffer.read()
113106
if chunk:
114107
sample.buffer.extend(chunk)
115108

116-
if (sample.action.type == rtmixer._lib.CANCEL and
117-
sample.action not in stream.actions):
109+
if sample.action not in self.stream.actions:
110+
# Recording is finished
118111
# TODO: check for errors in CANCEL action?
119-
sample.action = None
120-
self.rec_counter.set(self.rec_counter.get() - 1)
112+
self.rec_counter -= 1
121113
else:
122114
# Set polling rate based on input latency (which may change!):
123-
self.after(int(stream.latency[0] * 1000),
115+
self.after(int(self.stream.latency[0] * 1000),
124116
self.poll_ringbuffer, sample)
125117

126118

127-
app = MiniSampler()
128-
with rtmixer.MixerAndRecorder(channels=1) as stream:
129-
app.mainloop()
119+
def main():
120+
with rtmixer.MixerAndRecorder(channels=1) as stream:
121+
app = MiniSampler(stream)
122+
app.mainloop()
123+
124+
125+
if __name__ == '__main__':
126+
main()

examples/tkhelper.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
1-
"""Key event debouncer for tkinter."""
1+
"""Some tools for tkinter."""
2+
try:
3+
import tkinter as tk
4+
except ImportError:
5+
import Tkinter as tk # Python 2.x
26

37

4-
class TkKeyEventDebouncer(object):
8+
class IntVar(tk.IntVar):
9+
"""IntVar with increment and decrement operators."""
10+
11+
def __iadd__(self, value):
12+
self._tk.eval('incr {0} {1:d}'.format(self._name, value))
13+
return self
14+
15+
def __isub__(self, value):
16+
self += -value
17+
return self
18+
19+
20+
class KeyEventDebouncer(object):
521
"""Swallow repeated keyboard events if key is pressed and held.
622
723
See https://gist.github.com/vtsatskin/8e3c0c636339b2228138

0 commit comments

Comments
 (0)