1""" 2tkSnack 3An interface to Kare Sjolander's Snack Tcl extension 4http://www.speech.kth.se/snack/index.html 5 6by Kevin Russell and Kare Sjolander 7last modified: Mar 28, 2003 8""" 9 10import Tkinter 11import types 12import string 13 14Tkroot = None 15audio = None 16mixer = None 17 18def initializeSnack(newroot): 19 global Tkroot, audio, mixer 20 Tkroot = newroot 21 Tkroot.tk.call('eval', 'package require snack') 22 Tkroot.tk.call('snack::createIcons') 23 Tkroot.tk.call('snack::setUseOldObjAPI') 24 audio = AudioControllerSingleton() 25 mixer = MixerControllerSingleton() 26 27 28def _cast(astring): 29 """This function tries to convert a string returned by a Tcl call 30 to a Python integer or float, if possible, and otherwise passes on the 31 raw string (or None instead of an empty string).""" 32 try: 33 return int(astring) 34 except ValueError: 35 try: 36 return float(astring) 37 except ValueError: 38 if astring: 39 return astring 40 else: 41 return None 42 43 44class NotImplementedException(Exception): 45 pass 46 47 48class TkObject: 49 """A mixin class for various Python/Tk communication functions, 50 such as reading and setting the object's configuration options. 51 We put them in a mixin class so we don't have to keep repeating 52 them for sounds, filters, and spectrograms. 53 These are mostly copied from the Tkinter.Misc class.""" 54 55 def _getboolean(self, astring): 56 if astring: 57 return self.tk.getboolean(astring) 58 59 def _getints(self, astring): 60 if astring: 61 return tuple(map(int, self.tk.splitlist(astring))) 62 63 def _getdoubles(self, astring): 64 if astring: 65 return tuple(map(float, self.tk.splitlist(astring))) 66 67 def _options(self, cnf, kw=None): 68 if kw: 69 cnf = Tkinter._cnfmerge((cnf, kw)) 70 else: 71 cnf = Tkinter._cnfmerge(cnf) 72 res = () 73 for k,v in cnf.items(): 74 if v is not None: 75 if k[-1] == '_': k = k[:-1] 76 #if callable(v): 77 # v = self._register(v) 78 res = res + ('-'+k, v) 79 return res 80 81 def configure(self, cnf=None, **kw): 82 self._configure(cnf, kw) 83 84 def _configure(self, cnf=None, kw={}): 85 if kw: 86 cnf = Tkinter._cnfmerge((cnf, kw)) 87 elif cnf: 88 cnf = Tkinter._cnfmerge(cnf) 89 if cnf is None: 90 cnf = {} 91 for x in self.tk.split( 92 self.tk.call(self.name, 'configure')): 93 cnf[x[0][1:]] = (x[0][1:],) + x[1:] 94 return cnf 95 if type(cnf) is types.StringType: 96 x = self.tk.split(self.tk.call(self.name, 'configure', '-'+cnf)) 97 return (x[0][1:],) + x[1:] 98 self.tk.call((self.name, 'configure') + self._options(cnf)) 99 config = configure 100 101 def cget(self, key): 102 return _cast(self.tk.call(self.name, 'cget' , '-'+key)) 103 104 # Set "cget" as the method to handle dictionary-like attribute access 105 __getitem__ = cget 106 107 def __setitem__(self, key, value): 108 self.configure({key: value}) 109 110 def keys(self): 111 return map(lambda x: x[0][1:], 112 self.tk.split(self.tk.call(self.name, 'configure'))) 113 114 def __str__(self): 115 return self.name 116 117 118 119class Sound (TkObject): 120 121 def __init__(self, name=None, master=None, **kw): 122 self.name = None 123 if not master: 124 if Tkroot: 125 master = Tkroot 126 else: 127 raise RuntimeError, \ 128 'Tk not intialized or not registered with Snack' 129 self.tk = master.tk 130 if not name: 131 self.name = self.tk.call(('sound',) + self._options(kw)) 132 else: 133 self.name = self.tk.call(('sound', name) + self._options(kw)) 134 #self._configure(cnf, kw) 135 136 def append(self, binarydata, **kw): 137 """Appends binary string data to the end of the sound.""" 138 self.tk.call((self.name, 'append', binarydata) + self._options(kw)) 139 140 def concatenate(self, othersound): 141 """Concatenates the sample data from othersound to the end of 142 this sound. Both sounds must be of the same type.""" 143 self.tk.call(self.name, 'concatenate', othersound.name) 144 145 def configure(self, **kw): 146 """The configure command is used to set options for a sound.""" 147 self.tk.call((self.name, 'configure') + self._options(kw)) 148 149 def copy(self, sound, **kw): 150 """Copies sample data from another sound into self.""" 151 self.tk.call((self.name, 'copy', sound.name) + self._options(kw)) 152 153 def changed(self, flag): 154 """This command is used to inform Snack that the sound object has been 155 modified. Normally Snack tracks changes to sound objects automatically, 156 but in a few cases this must be performed explicitly. For example, 157 if individual samples are changed using the sample command these 158 will not be tracked for performance reasons.""" 159 self.tk.call((self.name, 'changed', flag)) 160 161 def convert(self, **kw): 162 """Convert a sound to a different sample encoding, sample rate, 163 or number of channels.""" 164 self.tk.call((self.name, 'convert') + self._options(kw)) 165 166 def crop(self, start=1, end=None, **kw): 167 """Removes all samples outside of the range [start..end].""" 168 if end is None: 169 end = self.length() 170 self.tk.call((self.name, 'crop', start, end) + self._options(kw)) 171 172 def cut(self, start=1, end=None, **kw): 173 """Removes all samples inside the range [start..end].""" 174 if end is None: 175 end = self.length() 176 self.tk.call((self.name, 'cut', start, end) + self._options(kw)) 177 178 def data(self, binarydata=None, **kw): 179 """Loads sound data from, or writes to, a binary string.""" 180 if binarydata: # copy data to sound 181 self.tk.call((self.name, 'data', binarydata) + self._options(kw)) 182 else: # return sound data 183 return self.tk.call((self.name, 'data') + self._options(kw)) 184 185 def destroy(self): 186 """Removes the Tcl command for this sound and frees the storage 187 associated with it.""" 188 self.tk.call(self.name, 'destroy') 189 190 def dBPowerSpectrum(self, **kw): 191 """Computes the log FFT power spectrum of the sound (at the time 192 given by the start option) and returns a list of dB values.""" 193 result = self.tk.call((self.name, 'dBPowerSpectrum') 194 + self._options(kw)) 195 return self._getdoubles(result) 196 197 def powerSpectrum(self, **kw): 198 """Computes the FFT power spectrum of the sound (at the time 199 given by the start option) and returns a list of magnitude values.""" 200 result = self.tk.call((self.name, 'powerSpectrum') 201 + self._options(kw)) 202 return self._getdoubles(result) 203 204 def filter(self, filter, **kw): 205 """Applies the given filter to the sound.""" 206 return self.tk.call((self.name, 'filter', filter.name) + 207 self._options(kw)) 208 209 def formant(self, **kw): 210 """Returns a list of formant trajectories.""" 211 result = self.tk.call((self.name, 'formant') + self._options(kw)) 212 return map(self._getdoubles, self.tk.splitlist(result)) 213 214 def flush(self): 215 """Removes all audio data from the sound.""" 216 self.tk.call(self.name, 'flush') 217 218 def info(self, format='string'): 219 """Returns a list with information about the sound. The entries are 220 [length, rate, max, min, encoding, channels, fileFormat, headerSize] 221 """ 222 result = self.tk.call(self.name, 'info') 223 if format == 'list': 224 return map(self._cast, string.split(result)) 225 else: 226 return result 227 228 def insert(self, sound, position, **kw): 229 """Inserts sound at position.""" 230 self.tk.call((self.name, 'insert', sound.name, position) + self._options(kw)) 231 232 def length(self, n=None, **kw): 233 """Gets/sets the length of the sound in number of samples (default) 234 or seconds, as determined by the 'units' option.""" 235 if n is not None: 236 result = self.tk.call((self.name, 'length', n) + self._options(kw)) 237 else: 238 result = self.tk.call((self.name, 'length') + self._options(kw)) 239 return _cast(result) 240 241 def load(self, filename, **kw): 242 """Reads new sound data from a file. Synonym for "read".""" 243 self.tk.call((self.name, 'read', filename) + self._options(kw)) 244 245 def max(self, **kw): 246 """Returns the largest positive sample value of the sound.""" 247 return _cast(self.tk.call((self.name, 'max') + self._options(kw))) 248 249 def min(self, **kw): 250 """Returns the largest negative sample value of the sound.""" 251 return _cast(self.tk.call((self.name, 'min') + self._options(kw))) 252 253 def mix(self, sound, **kw): 254 """Mixes sample data from another sound into self.""" 255 self.tk.call((self.name, 'mix', sound.name) + self._options(kw)) 256 257 def pause(self): 258 """Pause current record/play operation. Next pause invocation 259 resumes play/record.""" 260 self.tk.call(self.name, 'pause') 261 262 def pitch(self, method=None, **kw): 263 """Returns a list of pitch values.""" 264 if method is None or method is "amdf" or method is "AMDF": 265 result = self.tk.call((self.name, 'pitch') + self._options(kw)) 266 return self._getdoubles(result) 267 else: 268 result = self.tk.call((self.name, 'pitch', '-method', method) + 269 self._options(kw)) 270 return map(self._getdoubles, self.tk.splitlist(result)) 271 272 def play(self, **kw): 273 """Plays the sound.""" 274 self.tk.call((self.name, 'play') + self._options(kw)) 275 276 def power(self, **kw): 277 """Computes the FFT power spectrum of the sound (at the time 278 given by the start option) and returns a list of power values.""" 279 result = self.tk.call((self.name, 'power') 280 + self._options(kw)) 281 return self._getdoubles(result) 282 283 def read(self, filename, **kw): 284 """Reads new sound data from a file.""" 285 self.tk.call((self.name, 'read', filename) + self._options(kw)) 286 287 def record(self, **kw): 288 """Starts recording data from the audio device into the sound object.""" 289 self.tk.call((self.name, 'record') + self._options(kw)) 290 291 def reverse(self, **kw): 292 """Reverses a sound.""" 293 self.tk.call((self.name, 'reverse') + self._options(kw)) 294 295 def sample(self, index, left=None, right=None): 296 """Without left/right, this gets the sample value at index. 297 With left/right, it sets the sample value at index in the left 298 and/or right channels.""" 299 if right is not None: 300 if left is None: 301 left = '?' 302 opts = (left, right) 303 elif left is not None: 304 opts = (left,) 305 else: 306 opts = () 307 return _cast(self.tk.call((self.name, 'sample', index) + opts)) 308 309 def stop(self): 310 """Stops current play or record operation.""" 311 self.tk.call(self.name, 'stop') 312 313 def stretch(self, **kw): 314 self.tk.call((self.name, 'stretch') + self._options(kw)) 315 316 def write(self, filename, **kw): 317 """Writes sound data to a file.""" 318 self.tk.call((self.name, 'write', filename) + self._options(kw)) 319 320 321class AudioControllerSingleton(TkObject): 322 """This class offers functions that control various aspects of the 323 audio devices. 324 It is written as a class instead of as a set of module-level functions 325 so that we can piggy-back on the Tcl-interface functions in TkObject, 326 and so that the user can invoke the functions in a way more similar to 327 how they're invoked in Tcl, e.g., snack.audio.rates(). 328 It is intended that there only be once instance of this class, the 329 one created in snack.initialize. 330 """ 331 332 def __init__(self): 333 self.tk = Tkroot.tk 334 335 def encodings(self): 336 """Returns a list of supported sample encoding formats for the 337 currently selected device.""" 338 result = self.tk.call('snack::audio', 'encodings') 339 return self.tk.splitlist(result) 340 341 def rates(self): 342 """Returns a list of supported sample rates for the currently 343 selected device.""" 344 result = self.tk.call('snack::audio', 'frequencies') 345 return self._getints(result) 346 347 def frequencies(self): 348 """Returns a list of supported sample rates for the currently 349 selected device.""" 350 result = self.tk.call('snack::audio', 'frequencies') 351 return self._getints(result) 352 353 def inputDevices(self): 354 """Returns a list of available audio input devices""" 355 result = self.tk.call('snack::audio', 'inputDevices') 356 return self.tk.splitlist(result) 357 358 def playLatency(self, latency=None): 359 """Sets/queries (in ms) how much sound will be queued up at any 360 time to the audio device to play back.""" 361 if latency is not None: 362 return _cast(self.tk.call('snack::audio', 'playLatency', latency)) 363 else: 364 return _cast(self.tk.call('snack::audio', 'playLatency')) 365 366 def pause(self): 367 """Toggles between play/pause for all playback on the audio device.""" 368 self.tk.call('snack::audio', 'pause') 369 370 def play(self): 371 """Resumes paused playback on the audio device.""" 372 self.tk.call('snack::audio', 'play') 373 374 def play_gain(self, gain=None): 375 """Returns/sets the current play gain. Valid values are integers 376 in the range 0-100.""" 377 if gain is not None: 378 return _cast(self.tk.call('snack::audio', 'play_gain', gain)) 379 else: 380 return _cast(self.tk.call('snack::audio', 'play_gain')) 381 382 def outputDevices(self): 383 """Returns a list of available audio output devices.""" 384 result = self.tk.call('snack::audio', 'outputDevices') 385 return self.tk.splitlist(result) 386 387 def selectOutput(self, device): 388 """Selects an audio output device to be used as default.""" 389 self.tk.call('snack::audio', 'selectOutput', device) 390 391 def selectInput(self, device): 392 """Selects an audio input device to be used as default.""" 393 self.tk.call('snack::audio', 'selectInput', device) 394 395 def stop(self): 396 """Stops all playback on the audio device.""" 397 self.tk.call('snack::audio', 'stop') 398 399 def elapsedTime(self): 400 """Return the time since the audio device started playback.""" 401 result = self.tk.call('snack::audio', 'elapsedTime') 402 return self.tk.getdouble(result) 403 404class Filter(TkObject): 405 406 def __init__(self, name, *args, **kw): 407 global Tkroot 408 self.name = None 409 if Tkroot: 410 master = Tkroot 411 else: 412 raise RuntimeError, \ 413 'Tk not intialized or not registered with Snack' 414 self.tk = master.tk 415 self.name = self.tk.call(('snack::filter', name) + args + 416 self._options(kw)) 417 418 def configure(self, *args): 419 """Configures the filter.""" 420 self.tk.call((self.name, 'configure') + args) 421 422 def destroy(self): 423 """Removes the Tcl command for the filter and frees its storage.""" 424 self.tk.call(self.name, 'destroy') 425 426 427class MixerControllerSingleton(TkObject): 428 429 """Like AudioControllerSingleton, this class is intended to have only 430 a single instance object, which will control various aspects of the 431 mixers.""" 432 433 def __init__(self): 434 self.tk = Tkroot.tk 435 436 def channels(self, line): 437 """Returns a list with the names of the channels for the 438 specified line.""" 439 result = self.tk.call('snack::mixer', 'channels', line) 440 return self.tk.splitlist(result) 441 442 def devices(self): 443 """Returns a list of the available mixer devices.""" 444 result = self.tk.call('snack::mixer', 'devices') 445 return self.tk.splitlist(result) 446 447 def input(self, jack=None, tclVar=None): 448 """Gets/sets the current input jack. Optionally link a boolean 449 Tcl variable.""" 450 opts = () 451 if jack is not None: 452 opts = opts + jack 453 if tclVar is not None: 454 opts = opts + tclVar 455 return self.tk.call(('snack::mixer', 'input') + opts) 456 457 def inputs(self): 458 """Returns a list of available input ports.""" 459 result = self.tk.call('snack::mixer', 'inputs') 460 return self.tk.splitlist(result) 461 462 def lines(self): 463 """Returns a list with the names of the lines of the mixer device.""" 464 result = self.tk.call('snack::mixer', 'lines') 465 return self.tk.splitlist(result) 466 467 def output(self, jack=None, tclVar=None): 468 """Gets/sets the current output jack. Optionally link a boolean 469 Tcl variable.""" 470 opts = () 471 if jack is not None: 472 opts = opts + jack 473 if tclVar is not None: 474 opts = opts + tclVar 475 return self.tk.call(('snack::mixer', 'output') + opts) 476 477 def outputs(self): 478 """Returns a list of available output ports.""" 479 result = self.tk.call('snack::mixer', 'outputs') 480 return self.tk.splitlist(result) 481 482 def update(self): 483 """Updates all linked variables to reflect the status of the 484 mixer device.""" 485 self.tk.call('snack::mixer', 'update') 486 487 def volume(self, line, leftVar=None, rightVar=None): 488 if self.channels(line)[0] == 'Mono': 489 return self.tk.call('snack::mixer', 'volume', line, rightVar) 490 else: 491 return self.tk.call('snack::mixer', 'volume', line, leftVar, rightVar) 492 493 def select(self, device): 494 """Selects a device to be used as default.""" 495 self.tk.call('snack::mixer', 'select', device) 496 497 498 499class SoundFrame(Tkinter.Frame): 500 501 """A simple "tape recorder" widget.""" 502 503 def __init__(self, parent=None, sound=None, *args, **kw): 504 Tkinter.Frame.__init__(self) 505 if sound: 506 self.sound = sound 507 else: 508 self.sound = Sound() 509 self.canvas = SnackCanvas(self, height=100) 510 kw['sound'] = self.sound.name 511 self.canvas.create_waveform(0, 0, kw) 512 self.canvas.pack(side='top') 513 bbar = Tkinter.Frame(self) 514 bbar.pack(side='left') 515 Tkinter.Button(bbar, image='snackOpen', command=self.load 516 ).pack(side='left') 517 Tkinter.Button(bbar, bitmap='snackPlay', command=self.play 518 ).pack(side='left') 519 Tkinter.Button(bbar, bitmap='snackRecord', fg='red', 520 command=self.record).pack(side='left') 521 Tkinter.Button(bbar, bitmap='snackStop', command=self.stop 522 ).pack(side='left') 523 Tkinter.Button(bbar, text='Info', command=self.info).pack(side='left') 524 525 526 def load(self): 527 file = Tkroot.tk.call('eval', 'snack::getOpenFile') 528 self.sound.read(file, progress='snack::progressCallback') 529 530 def play(self): 531 self.sound.play() 532 533 def stop(self): 534 self.sound.stop() 535 536 def record(self): 537 self.sound.record() 538 539 def info(self): 540 print self.sound.info() 541 542def createSpectrogram(canvas, *args, **kw): 543 """Draws a spectrogram of a sound on canvas.""" 544 return canvas._create('spectrogram', args, kw) 545 546def createSection(canvas, *args, **kw): 547 """Draws and FFT log power spectrum section on canvas.""" 548 return canvas._create('section', args, kw) 549 550def createWaveform(canvas, *args, **kw): 551 """Draws a waveform on canvas.""" 552 return canvas._create('waveform', args, kw) 553 554 555class SnackCanvas(Tkinter.Canvas): 556 557 def __init__(self, master=None, cnf={}, **kw): 558 Tkinter.Widget.__init__(self, master, 'canvas', cnf, kw) 559 560 def create_spectrogram(self, *args, **kw): 561 """Draws a spectrogram of a sound on the canvas.""" 562 return self._create('spectrogram', args, kw) 563 564 def create_section(self, *args, **kw): 565 """Draws an FFT log power spectrum section.""" 566 return self._create('section', args, kw) 567 568 def create_waveform(self, *args, **kw): 569 """Draws a waveform.""" 570 return self._create('waveform', args, kw) 571 572 573if __name__ == '__main__': 574 # Create a test SoundFrame if the module is called as the main program 575 root = Tkinter.Tk() 576 initializeSnack(root) 577 frame = SoundFrame(root) 578 frame.pack(expand=0) 579 root.mainloop() 580