5 var playSynthDef, makeGui, setCycle, setYZoom, setIndex, setNumChannels,
6 setRate, setStyle, updateColors;
9 var <window, <view, <scopeView, cycleSlider, yZoomSlider,
10 idxNumBox, chNumBox, styleMenu, rateMenu;
12 // static (immutable runtime environment)
14 var scopeBuffer, maxBufSize;
15 var aBusSpec, cBusSpec, cycleSpec, yZoomSpec;
16 var <>smallSize, <>largeSize;
18 // runtime (mutable at runtime)
19 var <bus; // partly immutable; can't change numChannels at runtime
20 var busSpec; // either aBusSpec or cBusSpec, depending on bus rate
22 var synth, synthWatcher, defName;
26 *implementsClass {^'Stethoscope'}
28 *defaultServer { ^if( Server.default.isLocal, Server.default, Server.local ) }
30 *isValidServer { arg aServer; ^aServer.isLocal }
33 if (ugenScopes.isNil) { ugenScopes = Set.new };
38 var screenBounds = QWindow.availableBounds;
40 var x = (ugenScopes.size * (w + 10)) + 10;
42 var y = floor(right / screenBounds.width) * (h + 20) + 20;
43 if(right > screenBounds.width)
44 { x = floor(right % screenBounds.width / (w + 10)) * (w + 10) + 10 };
45 x = x + screenBounds.left;
50 arg server, numChannels = 2, index = 0, bufsize = 4096,
51 zoom = 1.0, rate = \audio, view, bufnum;
54 if(server.isNil) {server = this.defaultServer};
55 if(server.isLocal.not) {Error("Can not scope on remote server.").throw};
57 bus = Bus(rate, index, numChannels, server);
59 ^super.new.initQStethoscope( server, view, bus, bufsize, 1024 * zoom.asFloat.reciprocal );
62 initQStethoscope { arg server_, parent, bus_, bufsize_, cycle_;
67 maxBufSize = max(bufsize_, 128);
70 singleBus = bus.class === Bus;
72 aBusSpec = ControlSpec(0, server.options.numAudioBusChannels, step:1);
73 cBusSpec = ControlSpec(0, server.options.numControlBusChannels, step:1);
75 busSpec = if(bus.rate===\audio){aBusSpec}{cBusSpec};
78 cycleSpec = ControlSpec( 64, maxBufSize, \exponential );
79 yZoomSpec = ControlSpec( 0.125, 16, \exponential );
80 cycle = cycleSpec.constrain(cycle_);
83 smallSize = Size(250,250);
84 largeSize = Size(500,500);
86 makeGui = { arg parent;
89 // WINDOW, WRAPPER VIEW
91 if( window.notNil ) {window.close};
94 view = window = QWindow(
95 bounds: (smallSize).asRect.center_(QWindow.availableBounds.center)
96 ).name_("Stethoscope");
98 view = QView( parent, Rect(0,0,250,250) );
104 scopeView = QScope2();
105 scopeView.server = server;
106 scopeView.canFocus = true;
108 cycleSlider = QSlider().orientation_(\horizontal).value_(cycleSpec.unmap(cycle));
109 yZoomSlider = QSlider().orientation_(\vertical).value_(yZoomSpec.unmap(yZoom));
111 rateMenu = QPopUpMenu().items_(["Audio","Control"]).enabled_(singleBus);
112 idxNumBox = QNumberBox().decimals_(0).step_(1).scroll_step_(1).enabled_(singleBus);
113 chNumBox = QNumberBox().decimals_(0).step_(1).scroll_step_(1)
114 .clipLo_(1).clipHi_(128).enabled_(singleBus);
117 rateMenu.value_(if(bus.rate===\audio){0}{1});
118 idxNumBox.clipLo_(busSpec.minval).clipHi_(busSpec.maxval).value_(bus.index);
119 chNumBox.value_(bus.numChannels);
122 styleMenu = QPopUpMenu().items_(["Tracks","Overlay","X/Y"]);
126 gizmo = idxNumBox.minSizeHint.width * 2;
127 idxNumBox.minWidth = gizmo;
128 idxNumBox.maxWidth = gizmo;
129 chNumBox.minWidth = gizmo;
130 chNumBox.maxWidth = gizmo;
137 idxNumBox.minWidth_(35),
138 chNumBox.minWidth_(35),
141 ).margins_(0).spacing_(2), 0, 0
144 .add(yZoomSlider.maxWidth_(15), 1,1)
145 .add(cycleSlider.maxHeight_(15), 2,0)
146 .margins_(2).spacing_(2);
150 cycleSlider.action = { |me| setCycle.value(cycleSpec.map(me.value)) };
151 yZoomSlider.action = { |me| setYZoom.value(yZoomSpec.map(me.value)) };
152 idxNumBox.action = { |me| setIndex.value(me.value) };
153 chNumBox.action = { |me| setNumChannels.value(me.value) };
154 rateMenu.action = { |me| setRate.value(me.value) };
155 styleMenu.action = { |me| setStyle.value(me.value) };
156 view.asView.keyDownAction = { |v, char| this.keyDown(char) };
157 view.onClose = { view = nil; this.quit; };
162 if( window.notNil ) { window.front };
165 setCycle = { arg val;
167 if( synth.notNil ) { synth.set(\frames, val) }
170 setYZoom = { arg val;
172 scopeView.yZoom = val;
175 // NOTE: assuming a single Bus
177 bus = Bus(bus.rate, i, bus.numChannels, bus.server);
178 if(synth.notNil) { synth.set(\in, i) };
181 // NOTE: assuming a single Bus
182 setNumChannels = { arg n;
183 // we have to restart the whole thing:
185 bus = Bus(bus.rate, bus.index, n, bus.server);
189 // NOTE: assuming a single Bus
193 bus = Bus(\audio, bus.index, bus.numChannels, bus.server);
195 if(synth.notNil) { synth.set(\switch, 0) };
198 bus = Bus(\control, bus.index, bus.numChannels, bus.server);
200 if(synth.notNil) { synth.set(\switch, 1) };
203 idxNumBox.clipLo_(busSpec.minval).clipHi_(busSpec.maxval).value_(bus.index);
204 this.index = bus.index; // ensure conformance with busSpec;
208 setStyle = { arg val;
209 if(this.numChannels < 2 and: { val == 2 }) {
210 "QStethoscope: x/y scoping with one channel only; y will be a constant 0".warn;
212 scopeView.style = val;
218 var c = if(b.rate === \audio){Color.new255(255, 218, 000)}{Color.new255(125, 255, 205)};
219 colors = colors ++ Array.fill(b.numChannels, c);
221 scopeView.waveColors = colors;
224 playSynthDef = { arg def, args;
225 if( synthWatcher.notNil ) {synthWatcher.stop};
226 synthWatcher = fork {
229 synth = Synth.tail(RootNode(server), def.name, args);
231 {if(view.notNil){updateColors.value; scopeView.start}}.defer;
235 makeGui.value(parent);
237 ServerBoot.add(this, server);
238 ServerQuit.add(this, server);
255 CmdPeriod.remove(this);
261 if(running || server.serverRunning.not) {^this};
263 if(scopeBuffer.isNil){
264 scopeBuffer = ScopeBuffer.alloc(server);
265 scopeView.bufnum = scopeBuffer.index;
266 defName = "stethoscope" ++ scopeBuffer.index.asString;
269 n_chan = this.numChannels.asInteger;
271 if( bus.class === Bus ) {
273 SynthDef(defName, { arg in, switch, frames;
275 z = Select.ar(switch, [
277 K2A.ar(In.kr(in, n_chan))]
279 ScopeOut2.ar(z, scopeBuffer.index, maxBufSize, frames );
281 [\in, bus.index, \switch, if('audio' === bus.rate){0}{1}, \frames, cycle]
285 SynthDef(defName, { arg frames;
286 var z = Array(n_chan);
287 bus.do { |b| z = z ++ b.ar };
288 ScopeOut2.ar(z, scopeBuffer.index, maxBufSize, frames);
298 if( view.notNil ) { {scopeView.stop}.defer };
300 if( synthWatcher.notNil ) { synthWatcher.stop };
313 ServerBoot.remove(this, server);
314 ServerQuit.remove(this, server);
315 CmdPeriod.remove(this);
316 if(scopeBuffer.notNil) {scopeBuffer.free; scopeBuffer=nil};
317 if(window.notNil) { win = window; window = nil; { win.close }.defer; };
320 setProperties { arg numChannels, index, bufsize, zoom, rate;
322 var isRunning = running;
324 if (isRunning) {this.stop};
328 if(index.notNil || numChannels.notNil || rate.notNil) {
329 bus = if(bus.class === Bus) {
333 numChannels ? bus.numChannels,
345 if(bufsize.notNil) { maxBufSize = max(bufsize, 128) };
347 // set other vars related to args
349 busSpec = if(bus.rate === \audio) {aBusSpec} {cBusSpec};
350 cycleSpec = ControlSpec( 64, maxBufSize, \exponential );
352 { cycle = cycleSpec.constrain( 1024 * zoom.asFloat.reciprocal ) };
356 cycleSlider.value = cycleSpec.unmap(cycle);
357 rateMenu.value_(if(bus.rate === \audio){0}{1}).enabled_(true);
358 idxNumBox.clipLo_(busSpec.minval).clipHi_(busSpec.maxval).value_(bus.index).enabled_(true);
359 chNumBox.value_(bus.numChannels).enabled_(true);
361 if (isRunning) {this.run};
364 bufsize { ^maxBufSize }
367 var isSingle = b.class === Bus;
368 var isRunning = running;
370 if (isRunning) {this.stop};
375 busSpec = if(bus.rate === \audio) {aBusSpec} {cBusSpec};
376 rateMenu.value = if(b.rate===\audio){0}{1};
377 idxNumBox.clipLo_(busSpec.minval).clipHi_(busSpec.maxval).value_(bus.index);
378 chNumBox.value = b.numChannels;
381 rateMenu.value = nil;
382 idxNumBox.string = "-";
383 chNumBox.string = "-";
385 rateMenu.enabled = isSingle;
386 idxNumBox.enabled = isSingle;
387 chNumBox.enabled = isSingle;
389 if (isRunning) {this.run};
394 if( bus.class === Bus ) {
397 num = 0; bus.do { |b| num = num + b.numChannels };
402 // will always be clipped between 0 and the amount of channels
403 // at the beginning of the current run
404 numChannels_ { arg n;
405 if( (bus.class === Bus).not ) { ^this };
406 setNumChannels.value(n);
411 if( bus.class === Bus ) { ^bus.index } { nil }
415 if( (bus.class === Bus).not ) { ^this };
416 setIndex.value( busSpec.constrain(i) );
421 if( bus.class === Bus ) { ^bus.rate } { nil }
424 rate_ { arg argRate=\audio;
426 if( (bus.class === Bus).not ) { ^this };
427 val = if(argRate===\audio){0}{1};
429 rateMenu.value = val;
433 if( bus.class === Bus ) {
434 this.rate = if(bus.rate === \control) {\audio} {\control};
438 // [0, 1] -> [64, 8192] frames
440 setCycle.value( cycleSpec.constrain(val) );
441 cycleSlider.value = cycleSpec.unmap(val);
444 xZoom_ { arg val; this.cycle = 1024 * val.asFloat.reciprocal }
445 xZoom { ^(1024 * cycle.reciprocal) }
448 zoom_ { arg val; this.xZoom_(val ? 1) }
450 // [0, 1] -> [0.125, 16] y scaling factor
452 setYZoom.value( yZoomSpec.constrain(val) );
453 yZoomSlider.value = yZoomSpec.unmap(val);
458 styleMenu.value = val;
462 var sz = value.asSize;
463 if( window.notNil ) { window.setInnerExtent(sz.width,sz.height) };
468 sizeToggle = sizeToggle.not;
470 { this.size = largeSize }
471 { this.size = smallSize };
476 var i = server.options.numOutputBusChannels;
477 var c = server.options.numInputBusChannels;
478 this.bus = Bus(\audio, i, c, server);
482 var c = server.options.numOutputBusChannels;
483 this.bus = Bus(\audio, 0, c, server);
488 { char === $i }, { this.toInputBus },
489 { char === $o }, { this.toOutputBus },
490 { char === $ }, { this.run },
491 { char === $s }, { this.style = (scopeView.style + 1).wrap(0,2) },
492 { char === $S }, { this.style = 2 },
493 { char === $j }, { if(this.index.notNil) {this.index = this.index - 1} },
494 { char === $k }, { this.switchRate; },
495 { char === $l }, { if(this.index.notNil) {this.index = this.index + 1} },
496 { char === $- }, { cycleSlider.increment; cycleSlider.doAction },
497 { char === $+ }, { cycleSlider.decrement; cycleSlider.doAction },
498 { char === $* }, { yZoomSlider.increment; yZoomSlider.doAction },
499 { char === $_ }, { yZoomSlider.decrement; yZoomSlider.doAction },
500 { char === $m }, { this.toggleSize },
501 { char === $.}, { this.stop },