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","Overlay","X/Y"]);
98 gizmo = idxNumBox.minSizeHint.width * 2;
99 idxNumBox.minWidth = gizmo;
100 idxNumBox.maxWidth = gizmo;
101 chNumBox.minWidth = gizmo;
102 chNumBox.maxWidth = gizmo;
109 idxNumBox.minWidth_(35),
110 chNumBox.minWidth_(35),
113 ).margins_(0).spacing_(2), 0, 0
116 .add(yZoomSlider.maxWidth_(15), 1,1)
117 .add(cycleSlider.maxHeight_(15), 2,0)
118 .margins_(2).spacing_(2);
122 cycleSlider.action = { |me| setCycle.value(cycleSpec.map(me.value)) };
123 yZoomSlider.action = { |me| setYZoom.value(me.value) };
124 idxNumBox.action = { |me| setIndex.value(me.value) };
125 chNumBox.action = { |me| setNumChannels.value(me.value) };
126 rateMenu.action = { |me| setRate.value(me.value) };
127 styleMenu.action = { |me| setStyle.value(me.value) };
128 view.asView.keyDownAction = { |v, char| this.keyDown(char) };
129 view.onClose = { view = nil; this.quit; };
134 if( window.notNil ) { window.front };
137 setCycle = { arg val;
139 if( synth.notNil ) { synth.set(\frames, val) }
142 setYZoom = { arg val;
144 scopeView.yZoom = yZoomSpec.map(val);
147 // NOTE: assuming a single Bus
149 bus = Bus(bus.rate, i, bus.numChannels, bus.server);
150 if(synth.notNil) { synth.set(\in, i) };
153 // NOTE: assuming a single Bus
154 setNumChannels = { arg n;
155 // we have to restart the whole thing:
157 bus = Bus(bus.rate, bus.index, n, bus.server);
161 // NOTE: assuming a single Bus
165 bus = Bus(\audio, bus.index, bus.numChannels, bus.server);
167 if(synth.notNil) { synth.set(\switch, 0) };
170 bus = Bus(\control, bus.index, bus.numChannels, bus.server);
172 if(synth.notNil) { synth.set(\switch, 1) };
175 idxNumBox.clipLo_(busSpec.minval).clipHi_(busSpec.maxval).value_(bus.index);
176 this.index = bus.index; // ensure conformance with busSpec;
180 setStyle = { arg val;
181 if(this.numChannels < 2 and: { val == 2 }) {
182 "QStethoscope: x/y scoping with one channel only; y will be a constant 0".warn;
184 scopeView.style = val;
190 var c = if(b.rate === \audio){Color.new255(255, 218, 000)}{Color.new255(125, 255, 205)};
191 colors = colors ++ Array.fill(b.numChannels, c);
193 scopeView.waveColors = colors;
196 playSynthDef = { arg def, args;
197 if( synthWatcher.notNil ) {synthWatcher.stop};
198 synthWatcher = fork {
201 synth = Synth.tail(RootNode(server), def.name, args);
203 {if(view.notNil){updateColors.value; scopeView.start}}.defer;
207 makeGui.value(parent);
209 ServerBoot.add(this, server);
210 ServerQuit.add(this, server);
227 CmdPeriod.remove(this);
233 if(running || server.serverRunning.not) {^this};
235 if(scopeBuffer.isNil){
236 scopeBuffer = ScopeBuffer.alloc(server);
237 scopeView.bufnum = scopeBuffer.index;
238 defName = "stethoscope" ++ scopeBuffer.index.asString;
241 n_chan = this.numChannels.asInteger;
243 if( bus.class === Bus ) {
245 SynthDef(defName, { arg in, switch, frames;
247 z = Select.ar(switch, [
249 K2A.ar(In.kr(in, n_chan))]
251 ScopeOut2.ar(z, scopeBuffer.index, maxBufSize, frames );
253 [\in, bus.index, \switch, if('audio' === bus.rate){0}{1}, \frames, cycle]
257 SynthDef(defName, { arg frames;
258 var z = Array(n_chan);
259 bus.do { |b| z = z ++ b.ar };
260 ScopeOut2.ar(z, scopeBuffer.index, maxBufSize, frames);
270 if( view.notNil ) { {scopeView.stop}.defer };
272 if( synthWatcher.notNil ) { synthWatcher.stop };
285 ServerBoot.remove(this, server);
286 ServerQuit.remove(this, server);
287 CmdPeriod.remove(this);
288 if(scopeBuffer.notNil) {scopeBuffer.free; scopeBuffer=nil};
289 if(window.notNil) { win = window; window = nil; { win.close }.defer; };
292 setProperties { arg numChannels, index, bufsize=4096, zoom, rate;
294 var isRunning = running;
296 if (isRunning) {this.stop};
300 if(index.notNil || numChannels.notNil || rate.notNil) {
301 bus = if(bus.class === Bus) {
305 numChannels ? bus.numChannels,
317 if(bufsize.notNil) { maxBufSize = max(bufsize, 128) };
318 if(zoom.notNil) { yZoom = yZoomSpec.constrain(zoom) };
320 // set other vars related to args
322 busSpec = if(bus.rate === \audio) {aBusSpec} {cBusSpec};
323 cycleSpec = ControlSpec( 64, maxBufSize, \exponential );
324 cycle = cycleSpec.constrain(cycle);
328 yZoomSlider.value = yZoom;
329 cycleSlider.value = cycleSpec.unmap(cycle);
330 rateMenu.value_(if(bus.rate === \audio){0}{1}).enabled_(true);
331 idxNumBox.clipLo_(busSpec.minval).clipHi_(busSpec.maxval).value_(bus.index).enabled_(true);
332 chNumBox.value_(bus.numChannels).enabled_(true);
333 scopeView.yZoom = yZoomSpec.map(yZoom);
335 if (isRunning) {this.run};
338 bufsize { ^maxBufSize }
341 var isSingle = b.class === Bus;
342 var isRunning = running;
344 if (isRunning) {this.stop};
349 busSpec = if(bus.rate === \audio) {aBusSpec} {cBusSpec};
350 rateMenu.value = if(b.rate===\audio){0}{1};
351 idxNumBox.clipLo_(busSpec.minval).clipHi_(busSpec.maxval).value_(bus.index);
352 chNumBox.value = b.numChannels;
355 rateMenu.value = nil;
356 idxNumBox.string = "-";
357 chNumBox.string = "-";
359 rateMenu.enabled = isSingle;
360 idxNumBox.enabled = isSingle;
361 chNumBox.enabled = isSingle;
363 if (isRunning) {this.run};
368 if( bus.class === Bus ) {
371 num = 0; bus.do { |b| num = num + b.numChannels };
376 // will always be clipped between 0 and the amount of channels
377 // at the beginning of the current run
378 numChannels_ { arg n;
379 if( (bus.class === Bus).not ) { ^this };
380 setNumChannels.value(n);
385 if( bus.class === Bus ) { ^bus.index } { nil }
389 if( (bus.class === Bus).not ) { ^this };
390 setIndex.value( busSpec.constrain(i) );
395 if( bus.class === Bus ) { ^bus.rate } { nil }
398 rate_ { arg argRate=\audio;
400 if( (bus.class === Bus).not ) { ^this };
401 val = if(argRate===\audio){0}{1};
403 rateMenu.value = val;
407 if( bus.class === Bus ) {
408 this.rate = if(bus.rate === \control) {\audio} {\control};
412 // [0, 1] -> [64, 8192] frames
414 setCycle.value( cycleSpec.constrain(val) );
415 cycleSlider.value = cycleSpec.unmap(val);
418 // [0, 1] -> [0.125, 16] y scaling factor
421 yZoomSlider.value = val;
424 zoom_ { arg val; this.xZoom_(val ? 1) }
428 styleMenu.value = val;
432 var sz = value.asSize;
433 if( window.notNil ) { window.setInnerExtent(sz.width,sz.height) };
438 sizeToggle = sizeToggle.not;
440 { this.size = largeSize }
441 { this.size = smallSize };
446 var i = server.options.numOutputBusChannels;
447 var c = server.options.numInputBusChannels;
448 this.bus = Bus(\audio, i, c, server);
452 var c = server.options.numOutputBusChannels;
453 this.bus = Bus(\audio, 0, c, server);
458 { char === $i }, { this.toInputBus },
459 { char === $o }, { this.toOutputBus },
460 { char === $ }, { this.run },
461 { char === $s }, { this.style = (scopeView.style + 1).wrap(0,2) },
462 { char === $S }, { this.style = 2 },
463 { char === $j }, { if(this.index.notNil) {this.index = this.index - 1} },
464 { char === $k }, { this.switchRate; },
465 { char === $l }, { if(this.index.notNil) {this.index = this.index + 1} },
466 { char === $- }, { cycleSlider.increment; cycleSlider.doAction },
467 { char === $+ }, { cycleSlider.decrement; cycleSlider.doAction },
468 { char === $* }, { yZoomSlider.increment; yZoomSlider.doAction },
469 { char === $_ }, { yZoomSlider.decrement; yZoomSlider.doAction },
470 { char === $m }, { this.toggleSize },
471 { char === $.}, { this.stop },