3 var playSynthDef, makeGui, setCycle, setYZoom, setIndex, setNumChannels,
4 setRate, setStyle, updateColors;
7 var <window, <view, <scopeView, cycleSlider, yZoomSlider,
8 idxNumBox, chNumBox, styleMenu, rateMenu;
10 // static (immutable runtime environment)
12 var scopeBuffer, maxBufSize;
13 var aBusSpec, cBusSpec, cycleSpec, yZoomSpec;
14 var <>smallSize, <>largeSize;
16 // runtime (mutable at runtime)
17 var <bus; // partly immutable; can't change numChannels at runtime
18 var busSpec; // either aBusSpec or cBusSpec, depending on bus rate
20 var synth, synthWatcher, defName;
25 arg server, numChannels = 2, index = 0, bufsize = 4096,
26 cycle = 1024, rate = \audio, view;
28 var bus = Bus(rate, index, numChannels, server);
29 if(server.isNil) {server = Server.default};
31 ^super.new.initQStethoscope( server, view, bus, bufsize, cycle );
34 initQStethoscope { arg server_, parent, bus_, bufsize_, cycle_;
39 maxBufSize = max(bufsize_, 128);
42 singleBus = bus.class === Bus;
44 aBusSpec = ControlSpec(0, server.options.numAudioBusChannels, step:1);
45 cBusSpec = ControlSpec(0, server.options.numControlBusChannels, step:1);
47 busSpec = if(bus.rate===\audio){aBusSpec}{cBusSpec};
50 cycleSpec = ControlSpec( 64, maxBufSize, \exponential );
51 yZoomSpec = ControlSpec( 0.125, 16, \exponential );
52 cycle = cycleSpec.constrain(cycle_);
53 yZoom = yZoomSpec.unmap(1.0);
55 smallSize = Size(250,250);
56 largeSize = Size(500,500);
58 makeGui = { arg parent;
61 // WINDOW, WRAPPER VIEW
63 if( window.notNil ) {window.close};
66 view = window = QWindow(
67 bounds: (smallSize).asRect.center_(QWindow.availableBounds.center)
68 ).name_("Stethoscope");
70 view = QView( parent, Rect(0,0,250,250) );
76 scopeView = QScope2();
77 scopeView.server = server;
78 scopeView.canFocus = true;
80 cycleSlider = QSlider().orientation_(\horizontal).value_(cycleSpec.unmap(cycle));
81 yZoomSlider = QSlider().orientation_(\vertical).value_(yZoom);
83 rateMenu = QPopUpMenu().items_(["AUDIO","CONTROL"]).enabled_(singleBus);
84 idxNumBox = QNumberBox().decimals_(0).step_(1).scroll_step_(1).enabled_(singleBus);
85 chNumBox = QNumberBox().decimals_(0).step_(1).scroll_step_(1)
86 .clipLo_(1).clipHi_(128).enabled_(singleBus);
89 rateMenu.value_(if(bus.rate===\audio){0}{1});
90 idxNumBox.clipLo_(busSpec.minval).clipHi_(busSpec.maxval).value_(bus.index);
91 chNumBox.value_(bus.numChannels);
94 styleMenu = QPopUpMenu().items_(["TRACKS","MIX","X/Y"]);
98 gizmo = idxNumBox.minSizeHint.width * 2;
99 idxNumBox.minWidth = gizmo;
100 idxNumBox.maxWidth = gizmo;
101 chNumBox.minWidth = gizmo;
102 chNumBox.maxWidth = gizmo;
104 view.layout = QVLayout(
109 idxNumBox.minWidth_(35),
110 chNumBox.minWidth_(35),
113 ).margins_(0).spacing_(1), 0, 0
116 .add(yZoomSlider,1,1)
117 .add(cycleSlider,2,0)
118 .margins_(0).spacing_(0)
119 ).margins_([5,0,0,0]).spacing_(1);
123 cycleSlider.action = { |me| setCycle.value(cycleSpec.map(me.value)) };
124 yZoomSlider.action = { |me| setYZoom.value(me.value) };
125 idxNumBox.action = { |me| setIndex.value(me.value) };
126 chNumBox.action = { |me| setNumChannels.value(me.value) };
127 rateMenu.action = { |me| setRate.value(me.value) };
128 styleMenu.action = { |me| setStyle.value(me.value) };
129 view.asView.keyDownAction = { |v, char| this.keyDown(char) };
130 view.onClose = { view = nil; this.quit; };
135 if( window.notNil ) { window.front };
138 setCycle = { arg val;
140 if( synth.notNil ) { synth.set(\frames, val) }
143 setYZoom = { arg val;
145 scopeView.yZoom = yZoomSpec.map(val);
148 // NOTE: assuming a single Bus
150 bus = Bus(bus.rate, i, bus.numChannels, bus.server);
151 if(synth.notNil) { synth.set(\in, i) };
154 // NOTE: assuming a single Bus
155 setNumChannels = { arg n;
156 // we have to restart the whole thing:
158 bus = Bus(bus.rate, bus.index, n, bus.server);
162 // NOTE: assuming a single Bus
166 bus = Bus(\audio, bus.index, bus.numChannels, bus.server);
168 if(synth.notNil) { synth.set(\switch, 0) };
171 bus = Bus(\control, bus.index, bus.numChannels, bus.server);
173 if(synth.notNil) { synth.set(\switch, 1) };
176 idxNumBox.clipLo_(busSpec.minval).clipHi_(busSpec.maxval).value_(bus.index);
177 this.index = bus.index; // ensure conformance with busSpec;
181 setStyle = { arg val;
182 if(this.numChannels < 2 and: { val == 2 }) {
183 "QStethoscope: x/y scoping with one channel only; y will be a constant 0".warn;
185 scopeView.style = val;
191 var c = if(b.rate === \audio){Color.new255(255, 218, 000)}{Color.new255(125, 255, 205)};
192 colors = colors ++ Array.fill(b.numChannels, c);
194 scopeView.waveColors = colors;
197 playSynthDef = { arg def, args;
198 if( synthWatcher.notNil ) {synthWatcher.stop};
199 synthWatcher = fork {
202 synth = Synth.tail(RootNode(server), def.name, args);
204 {if(view.notNil){updateColors.value; scopeView.start}}.defer;
208 makeGui.value(parent);
210 ServerBoot.add(this, server);
211 ServerQuit.add(this, server);
228 CmdPeriod.remove(this);
234 if(running || server.serverRunning.not) {^this};
236 if(scopeBuffer.isNil){
237 scopeBuffer = ScopeBuffer.alloc(server);
238 scopeView.bufnum = scopeBuffer.index;
239 defName = "stethoscope" ++ scopeBuffer.index.asString;
242 n_chan = this.numChannels.asInteger;
244 if( bus.class === Bus ) {
246 SynthDef(defName, { arg in, switch, frames;
248 z = Select.ar(switch, [
250 K2A.ar(In.kr(in, n_chan))]
252 ScopeOut2.ar(z, scopeBuffer.index, maxBufSize, frames );
254 [\in, bus.index, \switch, if('audio' === bus.rate){0}{1}, \frames, cycle]
258 SynthDef(defName, { arg frames;
259 var z = Array(n_chan);
260 bus.do { |b| z = z ++ b.ar };
261 ScopeOut2.ar(z, scopeBuffer.index, maxBufSize, frames);
271 if( view.notNil ) { {scopeView.stop}.defer };
273 if( synthWatcher.notNil ) { synthWatcher.stop };
286 ServerBoot.remove(this, server);
287 ServerQuit.remove(this, server);
288 CmdPeriod.remove(this);
289 if(scopeBuffer.notNil) {scopeBuffer.free; scopeBuffer=nil};
290 if(window.notNil) { win = window; window = nil; { win.close }.defer; };
293 setProperties { arg numChannels, index, bufsize=4096, zoom, rate;
295 var isRunning = running;
297 if (isRunning) {this.stop};
301 if(index.notNil || numChannels.notNil || rate.notNil) {
302 bus = if(bus.class === Bus) {
306 numChannels ? bus.numChannels,
318 if(bufsize.notNil) { maxBufSize = max(bufsize, 128) };
319 if(zoom.notNil) { yZoom = yZoomSpec.constrain(zoom) };
321 // set other vars related to args
323 busSpec = if(bus.rate === \audio) {aBusSpec} {cBusSpec};
324 cycleSpec = ControlSpec( 64, maxBufSize, \exponential );
325 cycle = cycleSpec.constrain(cycle);
329 yZoomSlider.value = yZoom;
330 cycleSlider.value = cycleSpec.unmap(cycle);
331 rateMenu.value_(if(bus.rate === \audio){0}{1}).enabled_(true);
332 idxNumBox.clipLo_(busSpec.minval).clipHi_(busSpec.maxval).value_(bus.index).enabled_(true);
333 chNumBox.value_(bus.numChannels).enabled_(true);
334 scopeView.yZoom = yZoomSpec.map(yZoom);
336 if (isRunning) {this.run};
339 bufsize { ^maxBufSize }
342 var isSingle = b.class === Bus;
343 var isRunning = running;
345 if (isRunning) {this.stop};
350 busSpec = if(bus.rate === \audio) {aBusSpec} {cBusSpec};
351 rateMenu.value = if(b.rate===\audio){0}{1};
352 idxNumBox.clipLo_(busSpec.minval).clipHi_(busSpec.maxval).value_(bus.index);
353 chNumBox.value = b.numChannels;
356 rateMenu.value = nil;
357 idxNumBox.string = "-";
358 chNumBox.string = "-";
360 rateMenu.enabled = isSingle;
361 idxNumBox.enabled = isSingle;
362 chNumBox.enabled = isSingle;
364 if (isRunning) {this.run};
369 if( bus.class === Bus ) {
372 num = 0; bus.do { |b| num = num + b.numChannels };
377 // will always be clipped between 0 and the amount of channels
378 // at the beginning of the current run
379 numChannels_ { arg n;
380 if( (bus.class === Bus).not ) { ^this };
381 setNumChannels.value(n);
386 if( bus.class === Bus ) { ^bus.index } { nil }
390 if( (bus.class === Bus).not ) { ^this };
391 setIndex.value( busSpec.constrain(i) );
396 if( bus.class === Bus ) { ^bus.rate } { nil }
399 rate_ { arg argRate=\audio;
401 if( (bus.class === Bus).not ) { ^this };
402 val = if(argRate===\audio){0}{1};
404 rateMenu.value = val;
408 if( bus.class === Bus ) {
409 this.rate = if(bus.rate === \control) {\audio} {\control};
413 // [0, 1] -> [64, 8192] frames
415 setCycle.value( cycleSpec.constrain(val) );
416 cycleSlider.value = cycleSpec.unmap(val);
419 // [0, 1] -> [0.125, 16] y scaling factor
422 yZoomSlider.value = val;
425 zoom_ { arg val; this.xZoom_(val ? 1) }
429 styleMenu.value = val;
433 var sz = value.asSize;
434 if( window.notNil ) { window.setInnerExtent(sz.width,sz.height) };
439 sizeToggle = sizeToggle.not;
441 { this.size = largeSize }
442 { this.size = smallSize };
447 var i = server.options.numOutputBusChannels;
448 var c = server.options.numInputBusChannels;
449 this.bus = Bus(\audio, i, c, server);
453 var c = server.options.numOutputBusChannels;
454 this.bus = Bus(\audio, 0, c, server);
459 { char === $i }, { this.toInputBus },
460 { char === $o }, { this.toOutputBus },
461 { char === $ }, { this.run },
462 { char === $s }, { this.style = (scopeView.style + 1).wrap(0,2) },
463 { char === $S }, { this.style = 2 },
464 { char === $j }, { if(this.index.notNil) {this.index = this.index - 1} },
465 { char === $k }, { this.switchRate; },
466 { char === $l }, { if(this.index.notNil) {this.index = this.index + 1} },
467 { char === $- }, { cycleSlider.increment; cycleSlider.doAction },
468 { char === $+ }, { cycleSlider.decrement; cycleSlider.doAction },
469 { char === $* }, { yZoomSlider.increment; yZoomSlider.doAction },
470 { char === $_ }, { yZoomSlider.decrement; yZoomSlider.doAction },
471 { char === $m }, { this.toggleSize },
472 { char === $.}, { this.stop },