22"""A minimalistic sampler with an even more minimalistic GUI."""
33
44import collections
5+ import functools
56import math
67try :
78 import tkinter as tk
89except ImportError :
9- # Python 2.x
10- import Tkinter as tk
10+ import Tkinter as tk # Python 2.x
1111
1212import rtmixer
13- from tkhelper import TkKeyEventDebouncer
13+ import tkhelper
1414
1515
1616HELPTEXT = 'Hold uppercase key for recording,\n lowercase for playback'
2121
2222class 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 ()
0 commit comments