Is it possible to get the output of a ugen as an array of floats (audio buffer)?

I’m new to super collider, and just bumped into something that I thought would be easy to find in the documentation or online examples, but I’m stumped.

What I’d like to do is send the live audio from super collider as a float array via OSC. This float array would be used to visualize the audio buffer as a waveform in an external app.

I’ve verified I can send and receive a float array via OSC, for example:

b = NetAddr.new("127.0.0.1", 9912);  
~msg =  [\test, 2.231, 0.2, 0.233];
b.sendMsg(*~msg);

but I can’t figure out how to get access to the playing back audio as a float array. Most of the references I can find to the Buffer object refer to loading samples from disk.

Any tips appreciated. Thx!

Have a look at how s.scope is implemented, you should be able to find everything in there.
I think it uses a buffer?

Hrmm - being new to supercollider I’m not sure exactly how to find the s.scope implementation. I poked around in the github repo but couldn’t track it down. Would this approach require writing my own ugen or extension for supercollider (Not sure what the right nomenclature to use here is)? Thx for the suggestion!

I found some other classes that look like they might be useful, but they’re undocumented. For example:
https://doc.sccode.org/Classes/ScopeBuffer.html
https://doc.sccode.org/Classes/ScopeOut.html

I also found this RecordBuf ugen.

in the IDE you can get to the implementation of everything using control-i. so you’d place your cursor over the word scope, do control-i and you get a list of classes which implement a scope method. there you can scroll to the one you like the see and get a preview or open the file with enter.

Ahhh - that’s super helpful. So if i’m understanding correctly, you can modify those .sc files or add your own classes as you like? Just make sure to recompile class library ?

Thx!

Ok, so I’ve spent a few hours digging through the implementation of scope, which led me to stethoscope and scopeView. This certainly looks like it should do what I’m after (get an array of waveform values that I can send to another program). However, I can’t seem to find the obvious spots where array values are retrieved from the server, and plotted on the stethoscope gui.

I worry I’m missing something super obvious just due to being unfamiliar with the language and how supercollider works in general. OR maybe this is just a harder problem than I think it is.

Oh, and this Scope2 ugen seems almost perfect, except there’s no documentation and I’m not quite sure how to use it :\

The ScopeView documentation makes it seem like the right use case.

It is optimized to efficiently perform frequent plotting of the contents of a Buffer into which a ScopeOut2 UGen is writing. It will periodically poll the buffer for data and update the plot, as long as the ScopeOut2 UGen is writing into it; the buffer will not be plotted otherwise.

Do not modify those sc files. They are supercolliders standard library and you will effectively be rolling your own version of supercollider: don’t.

Sorry for suggesting you look at scope, I thought you wanted a scope in SC, I guess I didn’t read your question.

Do this instead.

(
var relay_buffer = Buffer.alloc(s, 1024);

{
	var sig = SinOsc.ar(220);
	var phase = Phasor.ar(0, BufRateScale.kr(relay_buffer), 0, BufFrames.kr(relay_buffer));
	var phase_reset_trig = Trig.ar(Slope.ar(phase) < 0);
	BufWr.ar(sig, relay_buffer, phase);
	SendReply.ar(phase_reset_trig, '/buffer_refresh', 1);
	sig;
}.play;

OSCdef(\k, {
	|msg|
	relay_buffer.loadToFloatArray(action: {|b| 
        b.postln;
        // ~some_net_addr.send(b);
    });
}, '/buffer_refresh');

)

Thank you! I don’t think I ever would have figured that out :P. Works perfectly, except I had to add a snippet for converting the FloatArray to a normal array. That seems to be necessary in order for the OSC message to be sent successfully. It’s not very pretty, but this works (note my floatArrayToList function:

(
var relay_buffer = Buffer.alloc(s, 1024);
var floatArrayToList = {
	arg arr;
	var list = List.new(arr.size);
	for (0, arr.size-1, { arg i; list.add(arr[i]) });
	list;
	//list.postln;
};

{
	var sig = SinOsc.ar(220);
	var phase = Phasor.ar(0, BufRateScale.kr(relay_buffer), 0, BufFrames.kr(relay_buffer));
	var phase_reset_trig = Trig.ar(Slope.ar(phase) < 0);
	BufWr.ar(sig, relay_buffer, phase);
	SendReply.ar(phase_reset_trig, '/buffer_refresh', 1);
	sig;
}.play;

OSCdef(\k, {
	|msg|
	relay_buffer.loadToFloatArray(action: {|b|
        b.postln;
        // ~some_net_addr.send(b);
		~o.sendMsg(\waveform, *floatArrayToList.value(b));
    });
}, '/buffer_refresh');
)

This help is very very much appreciated :smiley:

Has this been tested with a variety of hardware buffer sizes? I’m quite sure it will yield broken data unless the hardware buffer is exactly 1024, which is not guaranteed.

hjh

I did notice that depending on how often I trigger SendReply() and what the buffer size is, I can end up with unexpected sizes in my buffer. Because I’m only using this for buffer visualization and not to actually reproduce the audio, I just ignore the cases where the buffer size is incorrect and it seems fine - at least from a visual standpoint. So far I’ve tried 1024, 512, and 128.

If there are suggestions for improvement i’ll certainly try them. I do wonder if there’s a better way to optimize that floatArrayToList() function which seems quite wasteful.

You didn’t notice actual discontinuities in the signal?

If I run the original code with a 256-sample hardware buffer (in JACK, I have 256 x 2 periods):

scbuf-broken-01-256

  1. From the beginning… scsynth processes 256 samples, then 256 more, then 256 more, and 256 more.
  2. Now Phasor will hit the end, and wrap back around to 0.
  3. The trigger fires SendReply. This message takes a little time to reach the language client.
  4. The server continues processing the next 256 samples.
  5. Meanwhile, the language has received the message and sent the request to the server for the buffer-ful of data. At this point, the buffer contains 256 samples of new data, and 768 samples of old data.

etc. etc… the plot shows 2048 samples = 2x1024. In the first half, the first quarter of the (256 samples) don’t line up with the rest; same in the second half. Even for visualization, I think this is not what you want.

The programming principle at play is about reading and writing from/to the same collection – in a database, if you are writing to and querying the same table concurrently, table locking is essential – otherwise, part of the query might resolve before a write, and another part after, so the result would be inconsistent.

It is not safe to read and write at the same time from the same area. But the original suggestion always reads the entire buffer – with that approach, it’s impossible to ensure good data.

A better approach is to allocate a larger buffer, and always read from the section that has just finished writing (but which is not being written now).

(
var chunkSize = 1024;
var numChunks = 8;
var relay_buffer = Buffer.alloc(s, chunkSize * numChunks);

var synth = {
	var sig = SinOsc.ar(220);
	var phase = Phasor.ar(0, 1, 0, chunkSize);
	// btw this is already guaranteed to be only a single-sample trigger
	// Trig.ar is not needed
	var trig = HPZ1.ar(phase) < 0;
	var partition = PulseCount.ar(trig) % numChunks;
	BufWr.ar(sig, relay_buffer, phase + (chunkSize * partition));
	SendReply.ar(trig, '/buffer_refresh', partition);
	sig;
}.play;

a = Signal.new;

OSCdef(\k, { |msg|
	// the partition to retrieve is the one BEFORE the latest transition point
	var partition = (msg[3] - 1) % numChunks;

	// also I'm using getn
	// loadToFloatArray goes through a disk file
	// that is faster for long collections
	// for short collections, this is very inefficient
	// you do not need to create/destroy 43 temp files per second here!
	relay_buffer.getn(partition * chunkSize, chunkSize, { |data|
		// here I'll be lazy and assume messages arrive in sequence
		// though in theory this may not be the case?
		a = a ++ data;  // this is for my test plot, you can delete

		// floatArrayToList not needed
		~o.sendMsg(\waveform, *(data.as(Array)));
	});
}, '/buffer_refresh');

synth.onFree {
	OSCdef(\k).free;
	// original code example leaks buffer references
	// that is also not a good habit
	relay_buffer.free;
};

c = synth;
)

c.free;

a[0..2047].plot;

And it’s clean.

scbuf-fixed-01-256

As stated elsewhere… I am trying to reduce my involvement on the forum, because of work and other pressures. But, this forum is also a resource for users facing a similar question in the future; it’s not quite ideal to leave a suggestion in place that makes a problem look easier than it really is, while producing an invalid result… I had hoped someone else might step up to fix it, but, didn’t happen. Anyway, hope that helps.

hjh

3 Likes

I have a little side question: How come (HPZ1.ar(phase) < 0) - and in the code from Jordan (Trig.ar(Slope.ar(phase) < 0) works when the phaser starts at 0? One would think it should be <= 0 but this is obviously not the case.

In this case, you want a trigger at the end of a recorded chunk, not the beginning – you do not need a synth-onset trigger here.

hjh

1 Like

Well - thanks very much for the detailed explanation. As you suggested, hopefully this will help others down the road too. It’s already been enormously helpful to me personally.

You didn’t notice actual discontinuities in the signal?

The reason I wasn’t noticing the discontinuities that you pointed out it is my visualization is (currently) only for the most recent buffer values sent over OSC. Once I implement this correct solution, I will be able to visualize a longer time sampling of the data without discontinuities, which is great.

Where is the “buy you a coffee” button!? :))

1 Like

Following up because I integrated jamshark70’s suggestions into my project. It works flawlessly on my mac. I’m appreciative for the detailed explanation in how to do this correctly, and also for the clearly huge optimization in eliminating my unnecessarily slow “floatArrayToList()” function.

One strange thing I’m seeing though, is that on my raspberry PI (where the project will eventually run), the getn function never seems to get called.

relay_buffer.getn(partition * chunkSize, chunkSize, { |data|
		~o.sendMsg(\waveform, *(data.as(Array)));
	});

if I do some “printf” debugging and send strings to post with .postln, I can see that the lines above relay_buffer.getn are being executed. However as far as I can tell nothing gets executed that’s inside the getn() function.

Anyone have insights into why the exact same file might run fine on a mac, but fail on a pi?

Ok - I got this code running on the PI as well. Not sure why this matters on one platform vs. another, but on the PI I had to take the partition variable, and cast it to an int. This works:

relay_buffer.getn(partition.asInteger * chunkSize, chunkSize, { |data|
		~o.sendMsg(\waveform, *(data.as(Array)));
	});
1 Like

This method also worked for me on Ubuntu 18.04.5 LST and SuperCollider 3.11.0.

Thank you for this beautiful discussion!