ScopeOut2: Requested scope buffer unavailable!

Hi guys,
I’m experiencing some (minor) problem here in using ScopeOut2 and buffers and .scope method.
I’ve found this strange behavior (at least strange to me) in working on a new project.
I was able to reproduce the error below in order for you to better understand what I’m talking about.

Here’s an image showing what I’m trying to do.
I’m using groups, quadraphonic busses, routing from a group to another one.
I’m also using a quadraphonic buffer in order to plot on GUI the scope of the signal.

The most important synth here is the outputstage one which is responsible to control the overall level of the audio passing through it. This is also the synth which is controllable via the GUI (level, solo and mute).
It makes use of the quadraphonic buffer (via ScopeOut2) and it is sending analysis data (amplitude and peak) with SendReply.

If you have the patience and want to follow me in my reasoning, here the first of two code snippets I’ve prepared.
This is the main code, responsible of creating the general stuff, like groups, routing, busses, buffer, defining synths, instantiate them and so on.

(
s = Server.local;
s.options.memSize = 8192 * 128;
s.options.blockSize = 64;
s.options.sampleRate = 48000;
s.options.numOutputBusChannels_(4);
s.latency = 0.20;

s.waitForBoot({
	"defining network".postln;
	~sc = NetAddr.new("127.0.0.1", 15300);

	"defining hardware outputs".postln;
	~out = 0;

	"creating busses".postln;
	~bus = Bus.audio(s, 4);

	"defining groups".postln;
	~grp_main = Group.new();
	~grp_outputstages = Group.tail(~grp_main);
	~grp_music = Group.new(~grp_main, \addToHead);

	"defining scope buffers".postln;
	// define a buffer in order for audio signal to be plotted
	// on the GUI via ScopeOut2
	~buffer_scopeBuf   = Buffer.alloc(s, 1024, 4);

	// define
	SynthDef(\outputstage, {
		|
		in=0, out=0, amp=1.0,
		level = 1.0, lagtime=5.0,
		mute = 1.0,
		replyID=0
		|
		var trigger = Impulse.kr(10);
		var sig = In.ar( in, 4 );
		sig = sig * amp * VarLag.kr( level, lagtime ) * VarLag.kr( mute, 0.05 );

		// scope Ugens and measurements
		ScopeOut2.ar(sig, ~buffer_scopeBuf);
		// measure rms and Peak
		4.do({
			|i|
			SendReply.kr(
				trigger,
				'/levels', [Amplitude.kr(sig[i]), Peak.ar(sig[i], trigger).lag(0, 3)],
				replyID:replyID+i
			);
		});

		// outputs
		Out.ar(out, sig);
	}).add;
	s.sync;

	"output stage creation".postln;
	~synth_outputstage = Synth(\outputstage,
		[
			\in, ~bus,
			\level, 1.0,
			\out, ~out,
			\replyID, ~out,
		],
		target:~grp_outputstages
	);

	"\nWARNING: setup complete!".postln;

	// define some synth to make some test
	SynthDef(\test_sine, {
		|
		atk=0.01,rls=5,amp=1,
		freq=40, out=0, pan=0.0
		|
		var env = EnvGen.ar(Env.perc(atk, rls), doneAction:2);
		var sig = SinOsc.ar(freq) * amp * env;
		sig = PanAz.ar(4, sig, pan, orientation: 0.0);
		Out.ar(out, sig);
	}).add;
	s.sync;


	Pbindef(\test,
		\instrument, \test_sine,
		\out, ~bus,
		\pan, Pwhite(-1.0, 1.0, inf),
		\dur, 0.25,
		\group, ~grp_music
	).quant_(4).play;
});
)

This is the second part of the code, the one you use as a GUI controller for the outputstage synth.

Please don’t mind the strange names for the variables, you will also find variable I’m not using right here. That’s because I’ve taken the code directly from the development code for the project I’m working on, cleaning it up a bit just for the purpose of this post.

Inside this code you will find also the OSC functions to take care of messages coming from the GUI and from the outputstage synth to update the GUI meters.

(
// composite views
var gui_window, gui_compositeView;
var gui_quadro_cv, gui_quadro_vlayout, gui_quadro_scopeView, gui_quadro_slider2D, gui_quadro_slider_level, gui_quadro_slider_spread, gui_quadro_numberbox_level, gui_quadro_mute_button, gui_quadro_slider_lfe_xover_freq, gui_quadro_slider_lfe_sum_compensation;

var gui_quadro_solo_button, gui_room1_solo_button, gui_room2_solo_button, gui_room3_solo_button, gui_room4_solo_button, gui_room5_solo_button, gui_lfe_solo_button;

var gui_meter_critical = 0.99; // 0.99 == -0.0872dB (originariamente -3.dbamp)
var gui_meter_warning  = 0.9;  // 0.9  == -0.915dB  (originariamente -6.dbamp)

var gui_cpu_metering_statictext;

var master_tester_window,master_tester_compositeView;

// DEBUG STUFF END ******************************************************************/

// make a window and bring it to front
gui_window=Window("Sound debugger", Rect(0, 0,510,320)).front;
// create a child of which is a useful element to manage
// all graphical stuff like margins, padding, colors and
// fluid positioning of the elements
gui_compositeView=CompositeView(gui_window,gui_window.view.bounds).background_(Color.black);
// in particular you can use a decorator handle layout management
gui_compositeView.decorator = FlowLayout(gui_compositeView.bounds, margin: 5@5, gap: 5@5);


gui_cpu_metering_statictext = StaticText.new(gui_compositeView, Rect(0,0,400,100))
.string_("empty")
.align_(\center)
.stringColor_(Color.white);

// a routine to write inside the static text box the value of
// * avarage and peak CPU load
// * number of synths and UGEN
(
Routine {
	var runningTime, currentDate;
	var peakCPU, avgCPU, nSynth, nUgen, running;
	inf.do{
		// Set the value of the StaticText to the value in the control bus.
        // Setting GUI values is asynchronous, so you must use .defer in the system clock.
        // Also you must check if the window is still open, since Routine will continue for at least
        // one step after you close the window.

        {
			if(gui_window.isClosed.not) {
				runningTime = TempoClock.default.seconds.asTimeString;
				currentDate = Date.getDate.asString;
				peakCPU     = s.peakCPU.round(0.01);
				avgCPU      = s.avgCPU.round(0.01);
				nSynth      = s.numSynths;
				nUgen       = s.numUGens;
				running     = s.serverRunning;

				// print on GUI
				gui_cpu_metering_statictext.string = "peak CPU: " ++ peakCPU ++
				"\t-\tAvg CPU: " ++ avgCPU ++
				"\n# synths: " ++ nSynth ++
				"\t-\t# ugens: " ++ nUgen ++
				"\ncurrent Date: " ++ currentDate ++
				"\nrunning time: " ++ runningTime ++
				"\nserver running: " ++ running;

				// print on dump file
				//[currentDate, runningTime].postln;
			};
		}.defer;
		1.wait;
	};
}.play;
);

~level_indicators = [];

// GUI /////////////////////////////////////////

gui_quadro_cv = CompositeView(gui_compositeView,Rect(0,0,500,100)).background_(Color.grey);
gui_quadro_cv.decorator = FlowLayout(gui_quadro_cv.bounds, margin: 15@5, gap: 5@5);

gui_quadro_vlayout = VLayoutView(gui_quadro_cv,Rect(0,0,60,100)).background_(Color.grey);
StaticText.new(gui_quadro_vlayout, Rect(0,0,70,20)).string_("quadro").stringColor_(Color.black);

gui_quadro_numberbox_level = NumberBox(gui_quadro_vlayout, Rect(0, 50, 20, 20))
.value_(1.0)
.clipHi_(1.0)
.clipLo_(0.0);

gui_quadro_mute_button = Button(gui_quadro_vlayout, Rect(0,0,20,20))
.states_([
	["M", Color.white, Color.grey],
	["M", Color.black, Color.red]
]);

gui_quadro_slider_level = Slider(gui_quadro_cv, Rect(0, 0, 20, 85))
.value_(1.0);

// now that we have declared all the necessay gui interfaces, we can add actions
gui_quadro_numberbox_level.action_({|nb|
	//("numberBox changed" ++ nb.value).postln;
	~sc.sendMsg("/test/level", nb.value, 0.500);
	gui_quadro_slider_level.value_(nb.value); //TODO: here we need a forward declaration kind of behaviour
});

gui_quadro_mute_button.action_({|bt|
	("button"++bt.value).postln;
	switch( bt.value,
		0, {
			//gui_quadro_numberbox_level.valueAction_(1.0)
			~sc.sendMsg("/test/Mute", 1);
		},
		1, {
			//gui_quadro_numberbox_level.valueAction_(0.0)
			~sc.sendMsg("/test/Mute", 0);
		}
	);
});

gui_quadro_slider_level.action_({
	|sl|
	//("slider changed" ++ sl.value).postln;
	gui_quadro_numberbox_level.valueAction_(sl.value);
});


gui_quadro_scopeView = ScopeView(gui_quadro_cv, Rect(0,0,90,90)); // this is SCScope
gui_quadro_scopeView.bufnum = ~buffer_scopeBuf;
gui_quadro_scopeView.server_(s);
gui_quadro_scopeView.start;

4.do({
	|i|
	~level_indicators = ~level_indicators ++ LevelIndicator(gui_quadro_cv, Rect(0,0,10,85)).front;
	~level_indicators.last.warning  = gui_meter_warning;
	~level_indicators.last.critical = gui_meter_critical;
	~level_indicators.last.drawsPeak = true; // optionally show peak level
	~level_indicators.last.numTicks = 9;
	~level_indicators.last.numMajorTicks = 3;
	//d.style = \led; d.stepWidth = 9;
	~level_indicators.last.style = \continuous;
});

// SOLO ////////////////////////////////////////////////
gui_quadro_solo_button = Button(gui_quadro_vlayout, Rect(0,0,20,20))
.states_([
	["S", Color.white, Color.grey],
	["S", Color.black, Color.yellow]
])
.action_({
	|bt|
	("button"++bt.value).postln;
});


// OSC /////////////////////////////////////////////////
~osc_gui_levels = OSCFunc({arg msg;
	{
		~level_indicators[ msg[2] ].value     = msg[3].ampdb.linlin(-80, 0, 0.0, 1);
		~level_indicators[ msg[2] ].peakLevel = msg[4].ampdb.linlin(-80, 0, 0.0, 1);
	}.defer;
}, '/levels', s.addr);

~osc_quadro_level = OSCFunc({
	| msg, time, addr, recvPort |
	postln("quadro level" + msg[1]);

	~grp_outputstages.do({
		|item|
		item.set(\level, msg[1], \lagtime, msg[2]);
	});

}, "/test/level", recvPort:15300);

~osc_quadro_mute = OSCFunc({
	| msg, time, addr, recvPort |
	postln("quadro mute" + msg[1]);

	~grp_outputstages.do({
		|item|
		item.set(\mute, msg[1]);
	});

}, "/test/Mute", recvPort:15300);
);

now try to evaluate this line in order to plot the bus

~bus.scope;

If you are experiencing the same error I’m getting, the console will show you something like this

ScopeOut2: Requested scope buffer unavailable! (index: 0, channels: 4, size: 4096)

what does it means?
what kind of error is this and how can I prevent my code from generating it?
Maybe I’m doing something wrong in buffer allocation? Or maybe I’m using the ScopeOut2 class inappropriately?

The same error also appears if I try to use the following line

s.scope;

It seems the stethoscope windows is not showing the correct signal at all.

For example it you use the GUI to set the output stage level to -inf (drag the slider all the way down), you sould not hear anything on your headphones (which is perfectly correct) however you shoud always see something happening on the ~bus, which is feeding the outputstage synth but the scope is showing silence on all 4 channels.

Why the scope seems to show only the hardware output?

I hope I’ve explained the point here and I will provide more details If you want to dig deeper into it.
Thank you so much for your help and support

I looked into this a bit, but I’m afraid I don’t have time to trace through all of the Stethoscope code. It’s rather complex (perhaps unnecessarily complex).

AFAICS, ~bus.scope should allocate its own bus, but it doesn’t.

There seems to be some fancy logic in Stethoscope to disallow multiple scopes on the same server.

s.boot;

s.scope;  // OK

// focuses the existing scope window
s.scope;

// this does *not* open a new scope on the other bus!
// it *updates* the existing scope window to point to bus 4
Bus(\audio, 4, 2, s).scope;

// or...
thisProcess.recompile;

s.boot;

Bus(\audio, 4, 2, s).scope;

// nope, not even this way
Bus(\audio, 6, 2, s).scope;

IMO this is faulty design. It requires additional complexity, and the “benefit” from the user’s point of view is that there is a useful scenario (multiple scope windows pointing to different buses) that is currently impossible. Even if you explicitly request scopes on different buses, you can’t get it this way. (Or, the only way is to build the scope views yourself – but, why would we require users to re-architect something that already exists?)

So… log a bug I guess.

hjh

1 Like

Hmm… “faulty design” or not, it seems there is a limitation in the shared memory communication for scopes.

I took the help file example from ScopeView, copied it and changed variable names (below), and when I run both scopes together, they show the same contents. I hear two distinct synths but the scope windows look identical.

It’s all quite murky to me. sclang’s Server object has a scopeBufferAllocator which I suppose is intended to support multiple scope buffers, but it seems to be unused.

I’m afraid I’m out of time for this morning. Stethoscope code seems a bit thorny to trace through, and documentation is scant… as far as I can see right now, it might simply not be supported to have multiple scopes with different contents.

hjh

// boot the server (if not already booted)
s.boot;

// execute the following two blocks in succession:
(
f = Buffer.alloc(s,1024,2);
b = Bus.audio(s,2);

w = Window.new.front;
w.onClose = { // free everything when finished
    c.stop; a.free; d.free; f.free; b.free;
    "SynthDefs, busses and buffers have all been freed.".postln;
};
c = ScopeView(w.view,w.view.bounds.insetAll(10,10,10,10));
c.bufnum = f.bufnum;
c.server = s; // Important: one must assign the ScopeView to a server
)

(
// listening to the bus, using ScopeOut2 to write it to the buffer
a = SynthDef("monoscope", { arg bus, bufnum;
    var z;
    z = In.ar(bus, 2);

    ScopeOut2.ar(z, bufnum);
    Out.ar(0, z);
}).play(
    target: RootNode(s),
    args: [\bus, b.index, \bufnum, f.bufnum],
    addAction: \addToTail // make sure it goes after what you are scoping
);

// making noise onto the buffer
d = SynthDef("noise", { arg bus;
    var z;
    z = LFSaw.ar(SinOsc.kr(0.1).range(300,1000),[0,1]*pi) * 0.1;
    Out.ar(bus, z);
}).play(
    s,
    [\bus,b.index]
);
c.start; // Tell the ScopeView to start
CmdPeriod.doOnce({w.close});
)


// execute the following two blocks in succession:
(
g = Buffer.alloc(s,1024,2);
z = Bus.audio(s,2);

v = Window.new.front;
v.onClose = { // free everything when finished
    x.stop; m.free; q.free; g.free; z.free;
    "SynthDefs, busses and buffers have all been freed.".postln;
};
x = ScopeView(v.view,v.view.bounds.insetAll(10,10,10,10));
x.bufnum = f.bufnum;
x.server = s; // Important: one must assign the ScopeView to a server
)

(
// listening to the bus, using ScopeOut2 to write it to the buffer
m = SynthDef("monoscope", { arg bus, bufnum;
    var z;
    z = In.ar(bus, 2);

    ScopeOut2.ar(z, bufnum);
    Out.ar(0, z);
}).play(
    target: RootNode(s),
    args: [\bus, z.index, \bufnum, g.bufnum],
    addAction: \addToTail // make sure it goes after what you are scoping
);

// making noise onto the buffer
q = SynthDef("noise", { arg bus;
    var z;
    z = LFSaw.ar(SinOsc.kr(0.1).range(300,1000),[0,1]*pi) * 0.1;
	Out.ar(bus, z * Lag.kr(ToggleFF.kr(Dust.kr(8)), 0.005)
	);
}).play(
    s,
    [\bus,z.index]
);
x.start; // Tell the ScopeView to start
CmdPeriod.doOnce({v.close});
)

^^ I would expect, after running those four code blocks, that the scopes would show different contents, but:

Thank you @jamshark70,
I’ve tried your code. I think I’ve understood your point.
Trying to evaluate also the following line

s.scope;

I’ve noticed the same error I had in my code

Somehow this seems to solve the problem. I’m not sure what is different. Maybe I made a mistake with the variables before.

(
s.waitForBoot {
	b = Bus.audio(s, 4);
	a = {
		[
			SinOsc.ar(440),
			Saw.ar(440),
			Pulse.ar(440),
			VarSaw.ar(440, 0, 0.2)
		]
	}.play(outbus: b);

	c = Buffer.alloc(s, 1024, 2);
	d = Buffer.alloc(s, 1024, 2);

	SynthDef("monoscope", { arg bus, bufnum;
		var z;
		z = In.ar(bus, 2);
		ScopeOut2.ar(z, bufnum);
	}).add;
	s.sync;

	e = Synth(\monoscope, [bus: b.subBus(0, 2), bufnum: c],
		s.defaultGroup, \addAfter);
	f = Synth(\monoscope, [bus: b.subBus(2, 2), bufnum: d],
		s.defaultGroup, \addAfter);

	g = Window.new.front;
	g.onClose = { // free everything when finished
		[c, e, f].do(_.free);
	};
	h = ScopeView(g.view, g.view.bounds.insetAll(10, 10, 10, 10));
	h.bufnum = c.bufnum;
	h.server = s; // Important: one must assign the ScopeView to a server
	h.start;

	i = Window.new.front;
	i.onClose = { // free everything when finished
		[d, f].do(_.free);
	};
	j = ScopeView(i.view, i.view.bounds.insetAll(10, 10, 10, 10));
	j.bufnum = d.bufnum;
	j.server = s; // Important: one must assign the ScopeView to a server
	j.start;
};
)

EDIT: What I mean by “solve the problem” is that the two scope windows show different contents.

This doesn’t solve the problem with s.scope printing the error message.

I think this is because Stethoscope uses a scopeBufferAllocator but there’s no attempt to coordinate this with the regular buffer allocator.

It looks to me like something was partially implemented, but not completed or used consistently.

hjh

1 Like