Skip to content

Commit f16663e

Browse files
committed
release packaging architecture and benchmarking tests done
1 parent 6236b58 commit f16663e

File tree

4 files changed

+271
-8
lines changed

4 files changed

+271
-8
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
build
22
**/.DS_Store
3+
release-packaging/OfflineBufferProcesses/classes
4+
release-packaging/OfflineBufferProcesses/plugins
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
### OfflineBufferProcesses, a few utilities to manipulate buffers on the server's non-real-time thread
2+
v.1 by Pierre Alexandre Tremblay (2018)
3+
4+
###### Description
5+
This is a SuperCollider (https://supercollider.github.io/) porting of the bespoke Max (https://cycling74.com/products/max) object ipoke~ v4.1 (http://www.no-tv.org/MaxMSP/), which allows to write to the server buffers without leaving unfilled indices when writing faster than realtime.
6+
7+
This port was made possible thanks to the FluCoMa project (http://www.flucoma.org/) funded by the European Research Council (https://erc.europa.eu/) under the European Union’s Horizon 2020 research and innovation programme (grant agreement No 725899)
8+
9+
###### Why was this utility needed
10+
It was impossible to emulate the musical behaviour of bespoke resampling digital delay pedals in Max and SuperCollider. Now it is.
11+
12+
###### How to Install from binaries (SC 3.9 on Mac required)
13+
1. If you read this, you must have downloaded the binary package. If not, download it from the GitHub repository.
14+
2. Drag the full `IBufWr` folder, with its 3 subfolders (classes, HelpSource, plugins) in your `Extensions` folder. If you don't know what this is, please read the SuperCollider instructions here: (http://doc.sccode.org/Guides/UsingExtensions.html)
15+
3. Enjoy!
16+
17+
###### How to get started
18+
The helpfile gives 3 typical usage of the UGen, and there is a further document that gives more details on the justification, implementation, and example code.
19+
20+
Comments, suggestions and bug reports are welcome.
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
title:: Buffer Writing Guide
2+
summary:: This section illustrates some case uses of writing to buffers
3+
categories:: UGens>Buffer
4+
related:: Classes/BufRd, Classes/BufWr, Classes/IBufWr
5+
6+
description::
7+
8+
This section is a complement to the helpfiles of link::Classes/BufWr:: and link::Classes/IBufWr::, covering specific behaviours of both UGens.
9+
10+
subsection:: Multichannel Inputs to a Single Index
11+
12+
Both link::Classes/BufWr:: and link::Classes/IBufWr:: behave the same way, which is to NOT multichannel expand its input, instead passing the input array as is to the UGen. What that means is that the first input can be an array of up to the same size as the number of channels in the buffer it points to. The index (phase) of the buffer these values will be written to is the same for all.
13+
14+
strong::Example 1: Basic Stereo Buffer Writing::
15+
code::
16+
// allocate a stereo buffer
17+
c = Buffer.alloc(s, s.sampleRate,2);
18+
19+
(
20+
{
21+
BufWr.ar( // built-in buffer writer, since we won't skip indices
22+
Phasor.ar(0, [1, 0.5] , 0, c.numFrames), // basic source, multichannel expanded to 2 different ramps
23+
c.bufnum,
24+
Phasor.ar(0, 1 , 0, c.numFrames),
25+
);
26+
BufRd.ar(
27+
2,
28+
c.bufnum,
29+
Phasor.ar(0, 1 , 0, c.numFrames),// reading at one sample per sample
30+
0,
31+
1
32+
);
33+
}.plot(0.1)
34+
)
35+
::
36+
37+
strong::Example 2: Common Error, too large an input array::
38+
code::
39+
// allocate a stereo buffer
40+
c = Buffer.alloc(s, s.sampleRate,2);
41+
42+
(
43+
// this code will try to write a larger array than the number of channels in the buffer
44+
{
45+
BufWr.ar( // built-in buffer writer, since we won't skip indices
46+
Phasor.ar(0, [1, 0.5, 2] , 0, c.numFrames), // basic source, multichannel expanded to 3 different ramps
47+
c.bufnum,
48+
Phasor.ar(0, 1 , 0, c.numFrames),
49+
);
50+
}.plot(0.1)
51+
)
52+
// check the Post Window, you get told off!
53+
::
54+
55+
strong::Example 3: Common Error, multichannel expansion of the index (phase) input::
56+
code::
57+
// this code will try to multichannel expand the index(phase) input.
58+
// allocate a stereo buffer
59+
c = Buffer.alloc(s, s.sampleRate,2);
60+
61+
(
62+
{
63+
BufWr.ar( // built-in buffer writer, since we won't skip indices
64+
Phasor.ar(0, [1, 2] , 0, c.numFrames), // basic source, multichannel expanded to 2 different ramps
65+
c.bufnum,
66+
Phasor.ar(0, [1, 2] , 0, c.numFrames), // trying two different rates of writing, same speed per input
67+
);
68+
BufRd.ar(
69+
2,
70+
c.bufnum,
71+
Phasor.ar(0, 1 , 0, c.numFrames),// reading at one sample per sample
72+
0,
73+
1
74+
);
75+
}.plot(0.1)
76+
) // if that was working we would expect 2 parallel lines, the 2nd perforated since we skip at the same speed than the values...
77+
::
78+
79+
subsection:: The Main Differences between BufWr and IBufWr
80+
81+
The helpfile of link::Classes/IBufWr:: explains clearly the need of a buffer writing UGen that would not leave unfilled indices should the index(phase) input was not contiguous, and illustrates it with classic musical examples. The addition of the interpolating filling has a few consequences on slight different behaviours between the objects, namely variable CPU taxing, a one-sample lag, and the need of a pausing mechanism.
82+
83+
strong:: Variable CPU taxing::
84+
85+
link::Classes/IBufWr:: needs to check if there is an index or more that has been skipped, which implies some branching in the code, which takes CPU time. Even at one-sample-per-sample writing speed, it is less efficient than link::Classes/BufWr:: because of that check.
86+
87+
code::
88+
// declare a buffer
89+
b = Buffer.alloc(s, s.sampleRate);
90+
91+
// declares a Synth for each writing UGen with potential writing rates
92+
(
93+
SynthDef(\testBufWr,{
94+
arg rate = 1;
95+
var source = SinOsc.ar(200,0,0.0001);
96+
var write_index = Phasor.ar(0, rate , 0, b.numFrames);
97+
BufWr.ar(source, b.bufnum, write_index);
98+
Out.ar(0,BufRd.ar(1,b.bufnum,write_index,0, 1));
99+
}).send;
100+
101+
SynthDef(\testIBufWr,{
102+
arg rate = 1, interp = 0;
103+
var source = SinOsc.ar(200,0,0.0001);
104+
var write_index = Phasor.ar(0, rate , 0, b.numFrames);
105+
IBufWr.ar(source, b.bufnum, write_index, interp);
106+
Out.ar(0,BufRd.ar(1,b.bufnum,write_index,0, 1));
107+
}).send;
108+
)
109+
110+
//create 100 instances of BufWr, observe the server CPU
111+
(
112+
b.zero;
113+
g = Group.new;
114+
100.do({Synth(\testBufWr , target:g)});
115+
)
116+
117+
// Free them
118+
g.free
119+
120+
//create 100 instances of IBufWr with no interpolation, simply sampling-and-holding between skipped indices, observe the server CPU
121+
(
122+
b.zero;
123+
g = Group.new;
124+
100.do({Synth(\testIBufWr , target:g)})
125+
)
126+
127+
// worse by about 40%. Free them.
128+
g.free
129+
130+
131+
//create 100 instances of BufWr writing 10 times faster than realtime, observe the server CPU
132+
(
133+
b.zero;
134+
g = Group.new;
135+
100.do({Synth(\testBufWr, [\rate, 10], g)})
136+
)
137+
// not much has changed since nothing more is done
138+
g.free
139+
140+
//create 100 instances of IBufWr writing 10 times faster than realtime, still only S&H, observe the server CPU
141+
(
142+
b.zero;
143+
g = Group.new;
144+
100.do({Synth(\testIBufWr, [\rate, 10], g)})
145+
)
146+
// now the 10 sample writing per sample start to hit the performance
147+
g.free
148+
149+
//create 100 instances of IBufWr writing 10 times faster than realtime, this time with linear interpolation, observe the server CPU
150+
(
151+
b.zero;
152+
g = Group.new;
153+
100.do({Synth(\testIBufWr, [\rate, 10, \interp, 1], g)})
154+
)
155+
// now the interpolation hits even more.
156+
g.free
157+
::
158+
159+
strong:: 1 sample late::
160+
161+
The way link::Classes/IBufWr:: interpolates is by making the difference between indices. What this implies is that at the end of a vector, the known value of the last sample will not be known until the first value of the next vector. In a code where the writing happens right before the playing, it creates a whole in the output. The discriminating ear will have heard that in the previous examples above. This is in fact written as soon as possible, as one can see in the example below:
162+
163+
code::
164+
b = Buffer.alloc(s, 200);
165+
(
166+
b.zero;
167+
{
168+
IBufWr.ar(
169+
Phasor.ar(0, 1 , 0, b.numFrames),// writing values, incremented one sample per sample
170+
b.bufnum,
171+
Phasor.ar(0, 1 , 0, b.numFrames),// writes one sample per sample
172+
1,
173+
0.5
174+
);
175+
BufRd.ar(
176+
1,
177+
b.bufnum,
178+
Phasor.ar(0, 1 , 0, b.numFrames),
179+
0,
180+
1
181+
);
182+
}.plot(130/44100)
183+
)
184+
// the plot shows playback holes at the end of buffer
185+
// but if the buffer is drawn, there are no wholes!
186+
b.plot
187+
::
188+
189+
strong::negative index::
190+
191+
A feature of link::Classes/IBufWr:: is that the writing can be paused at any time, by providing negative indices. Once the writing starts again, the first index is not interpolated from the previous positive entry.

tests.scd

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,30 +103,80 @@ c.chunkSwap(3,11,-3)
103103
c.getn(0,45, {|m| m.postln});
104104

105105
////////////////////////////////////////////////////////////
106+
// test in benchmarking
106107
1000.do({b.chunkSwap(b.numFrames.rand,b.numFrames.rand,(b.numFrames.rand-(b.numFrames/2)));})
107-
108+
b.free
108109

109110
b=Buffer.read(s,"/Volumes/trucs/archive - pièces PA/asinglewordisnotenough3(invariant).wav")
110111
b.play(true, 0.1)
111112
b.reverse
112113

114+
115+
Task.new({
116+
d = Main.elapsedTime;
117+
0.01.wait;
118+
50.do({b.reverse});
119+
}).play
120+
(c-d).postln
121+
122+
123+
124+
(
125+
f = { |msg, time, replyAddr, recvPort|
126+
if(msg[0] == '/done') {
127+
// "time done %s\n".postf( time, msg, replyAddr, recvPort )
128+
c = time;
129+
}
130+
};
131+
thisProcess.addOSCRecvFunc(f);
132+
d = Main.elapsedTime;
133+
45.do({b.reverse});
134+
)
135+
136+
(c-d).postln
137+
138+
// stop posting.
139+
thisProcess.removeOSCRecvFunc(f);
140+
141+
113142
////////////////////////////////////////////////////////////
114143
//temp files version
115-
144+
Buffer.freeAll
116145
// sets known values
146+
117147
b = Buffer.alloc(s,16);
118148
b.setn(0, Array.series(16,1,1));
149+
b.getn(0,16, {|msg| msg.postln});
119150

120151
b.loadToFloatArray(action: {
121152
arg array;
122-
array.postln;
123-
x = array.reverse;
124-
x.postln;
125-
b.loadCollection(x);
153+
b.loadCollection(array.reverse);
126154
});
127155

128156
b.getn(0,16, {|msg| msg.postln});
129157

158+
/////
159+
b=Buffer.read(s,"/Volumes/machins/professionnel/sons/boucle A(lent)/loopC.aif")
160+
b=Buffer.read(s,"/Volumes/trucs/archive - pièces PA/asinglewordisnotenough3(invariant).wav")
161+
b.play(true, 0.1)
162+
163+
(
164+
d = Main.elapsedTime;
165+
b.loadToFloatArray(action: {
166+
arg array;
167+
b.loadCollection(array.reverse, action:{c = Main.elapsedTime;});
168+
});
169+
)
170+
171+
////////////////////////////
172+
// via osc (too slow for big file)
173+
(
174+
d = Main.elapsedTime;
175+
b.getToFloatArray(wait:1, timeout:30, action:{
176+
arg array;
177+
array.postln;
178+
// b.sendCollection(array.reverse, action:{c = Main.elapsedTime;});
179+
});
180+
)
130181

131-
b = Buffer.read(s,"/Volumes/machins/--à archiver/démarche/BEAST-HOA+cheat/bounces/montage-17.wav")
132-
b.read("/Volumes/machins/--à archiver/démarche/BEAST-HOA+cheat/bounces/montage-17.wav",0,926100000
182+
FloatArray

0 commit comments

Comments
 (0)