Skip to content

Commit bb3199c

Browse files
committed
WIP: Add sampler example
1 parent dca90f0 commit bb3199c

3 files changed

Lines changed: 170 additions & 2 deletions

File tree

README.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ Planned features:
4444

4545
* loopback tests to verify correct operation and accurate latency values
4646

47+
* fade in/out?
48+
49+
* loop?
50+
4751
* playlist/queue?
4852

4953
Out of scope:
@@ -59,8 +63,6 @@ Out of scope:
5963

6064
* resampling (apart from what PortAudio does)
6165

62-
* fade in/out
63-
6466
* fast forward/rewind
6567

6668
* panning/balance

examples/sampler.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#!/usr/bin/env python3
2+
"""A minimalistic sampler with an even more minimalistic GUI."""
3+
4+
import collections
5+
import math
6+
try:
7+
import tkinter as tk
8+
except ImportError:
9+
# Python 2.x
10+
import Tkinter as tk
11+
12+
import rtmixer
13+
from tkhelper import TkKeyEventDebouncer
14+
15+
16+
HELPTEXT = 'Hold uppercase key for recording,\nlowercase for playback'
17+
REC_OFF = '#600'
18+
REC_ON = '#e00'
19+
BUFFER_DURATION = 0.1 # seconds
20+
21+
22+
class Sample(object):
23+
24+
def __init__(self):
25+
elementsize = stream.samplesize[0]
26+
size = BUFFER_DURATION * stream.samplerate
27+
# Python 2.x doesn't have math.log2(), and it needs int():
28+
size = 2**int(math.ceil(math.log(size, 2)))
29+
self.ringbuffer = rtmixer.RingBuffer(elementsize, size)
30+
self.buffer = bytearray()
31+
self.action = None
32+
33+
34+
class MiniSampler(tk.Tk, TkKeyEventDebouncer):
35+
36+
def __init__(self, *args, **kwargs):
37+
tk.Tk.__init__(self, *args, **kwargs)
38+
TkKeyEventDebouncer.__init__(self)
39+
self.title('MiniSampler')
40+
tk.Label(self, text=HELPTEXT).pack(ipadx=20, ipady=20)
41+
self.rec_display = tk.Label(self, text='recording')
42+
self.rec_counter = tk.IntVar()
43+
self.rec_counter.trace(mode='w', callback=self.update_rec_display)
44+
self.rec_counter.set(0)
45+
self.rec_display.pack(padx=10, pady=10, ipadx=5, ipady=5)
46+
self.samples = collections.defaultdict(Sample)
47+
48+
def update_rec_display(self, *args):
49+
if self.rec_counter.get() == 0:
50+
self.rec_display['bg'] = REC_OFF
51+
else:
52+
self.rec_display['bg'] = REC_ON
53+
54+
def on_key_press(self, event):
55+
ch = event.char
56+
if ch.isupper():
57+
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)
61+
del sample.buffer[:]
62+
self.rec_counter.set(self.rec_counter.get() + 1)
63+
self.poll_ringbuffer(sample)
64+
elif ch in self.samples:
65+
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
80+
81+
def on_key_release(self, event):
82+
# NB: State of "shift" button may change between key press and release!
83+
ch = event.char.lower()
84+
if ch not in self.samples:
85+
return
86+
sample = self.samples[ch]
87+
# TODO: fade out (both recording and playback)?
88+
# TODO: is it possible that there is no action?
89+
assert sample.action
90+
# TODO: create a public API for that?
91+
if sample.action.type == rtmixer._lib.RECORD_RINGBUFFER:
92+
# TODO: check for errors/xruns? check for rinbuffer overflow?
93+
# Stop recording
94+
sample.action = stream.cancel(sample.action)
95+
# A CANCEL action is returned which is checked by poll_ringbuffer()
96+
elif sample.action.type == rtmixer._lib.PLAY_BUFFER:
97+
# TODO: check for errors/xruns?
98+
# Stop playback
99+
sample.action = stream.cancel(sample.action)
100+
# TODO: do something with sample.action?
101+
elif sample.action.type == rtmixer._lib.CANCEL:
102+
print('key {!r} released during CANCEL'.format(event.char))
103+
else:
104+
print(event.char, sample.action)
105+
assert False
106+
107+
def poll_ringbuffer(self, sample):
108+
# TODO: check for errors? is everything still working OK?
109+
assert sample.action, sample.action
110+
assert sample.action.type in (rtmixer._lib.RECORD_RINGBUFFER,
111+
rtmixer._lib.CANCEL), sample.action.type
112+
chunk = sample.ringbuffer.read()
113+
if chunk:
114+
sample.buffer.extend(chunk)
115+
116+
if (sample.action.type == rtmixer._lib.CANCEL and
117+
sample.action not in stream.actions):
118+
# TODO: check for errors in CANCEL action?
119+
sample.action = None
120+
self.rec_counter.set(self.rec_counter.get() - 1)
121+
else:
122+
# Set polling rate based on input latency (which may change!):
123+
self.after(int(stream.latency[0] * 1000),
124+
self.poll_ringbuffer, sample)
125+
126+
127+
app = MiniSampler()
128+
with rtmixer.MixerAndRecorder(channels=1) as stream:
129+
app.mainloop()

examples/tkhelper.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Key event debouncer for tkinter."""
2+
3+
4+
class TkKeyEventDebouncer(object):
5+
"""Swallow repeated keyboard events if key is pressed and held.
6+
7+
See https://gist.github.com/vtsatskin/8e3c0c636339b2228138
8+
9+
"""
10+
11+
_deferred_key_release = None
12+
13+
def __init__(self, root=None, on_key_press=None, on_key_release=None):
14+
if root is None:
15+
root = self
16+
if on_key_press is not None:
17+
self.on_key_press = on_key_press
18+
if on_key_release is not None:
19+
self.on_key_release = on_key_release
20+
self.root = root
21+
self.root.bind('<KeyPress>', self._on_key_press)
22+
self.root.bind('<KeyRelease>', self._on_key_release)
23+
24+
def _on_key_press(self, event):
25+
if self._deferred_key_release:
26+
self.root.after_cancel(self._deferred_key_release)
27+
self._deferred_key_release = None
28+
else:
29+
self.on_key_press(event)
30+
31+
def _on_key_release(self, event):
32+
self._deferred_key_release = self.root.after_idle(
33+
self._on_key_release2, event)
34+
35+
def _on_key_release2(self, event):
36+
self._deferred_key_release = None
37+
self.on_key_release(event)

0 commit comments

Comments
 (0)