Closed jasonhallen closed 2 years ago
This does not appear to be an issue with csound-api. If I run this C program (which, of course, doesn’t depend on csound-api)—
#include <csound/csound.h>
#include <assert.h>
uintptr_t perform(void *data)
{
return csoundPerform((CSOUND *)data);
}
int main(int argc, char *argv[])
{
CSOUND *Csound = csoundCreate(NULL);
assert(csoundSetOption(Csound, "--output=dac") == CSOUND_SUCCESS);
char *orchestra =
"0dbfs = 1\n"
"instr 1\n"
" kenv line 0, p3, 1\n"
" asig oscil kenv, 220\n"
" ktrig metro 20\n"
" kmeter max_k asig, ktrig, 1\n"
" printk2 kmeter\n"
" chnset kmeter, \"meter\"\n"
"endin\n";
assert(csoundCompileOrc(Csound, orchestra) == CSOUND_SUCCESS);
char *score =
"i 1 0 1\n"
"i 1 1 1\n"
"i 1 2 1\n"
"e\n";
assert(csoundReadScore(Csound, score) == CSOUND_SUCCESS);
assert(csoundStart(Csound) == CSOUND_SUCCESS);
void *threadID = csoundCreateThread(perform, (void *)Csound);
size_t timeInterval = 50;
size_t maxTime = 3000;
for (unsigned int i = 0; i < maxTime / timeInterval; i++) {
MYFLT meterValue = csoundGetControlChannel(Csound, "meter", NULL);
printf("%.17f\n", meterValue);
csoundSleep(timeInterval);
}
csoundStop(Csound);
csoundJoinThread(threadID);
csoundDestroy(Csound);
return 0;
}
—I get, for all practical purposes, the same output that you get when running csound-api. From printk2
:
i1 0.00000
i1 0.04875
i1 0.09887
i1 0.14875
i1 0.19887
i1 0.24875
i1 0.29886
i1 0.34875
i1 0.39886
i1 0.44875
i1 0.49886
i1 0.54875
i1 0.59886
i1 0.64875
i1 0.69886
i1 0.74875
i1 0.79886
i1 0.84875
i1 0.89886
i1 0.94875
From logging the result of chnset
:
0.00000000000000000
0.00000000000000000
0.09886562427796818
0.09886562427796818
0.19886502865086275
0.19886502865086275
0.29886443302376170
0.29886443302376170
0.34875075720072124
0.39886383739666692
0.44875016157362646
0.54874956594653168
0.54874956594653168
0.64874897031943690
0.64874897031943690
0.74874837469234212
0.84874777906524734
0.84874777906524734
0.94874718343815256
0.94874718343815256
In addition, you can see how csound-api calls the underlying csoundGetControlChannel
function:
There is no throttling of the output of csoundGetControlChannel
. The complexity of this function is from setting a JavaScript return value and an input object property.
My guess at the underlying issue is that csoundGetControlChannel
is (as the name suggests) for getting values of control signals, not volume metering, but as this issue appears to be independent of csound-api, there isn’t much that can be done about this in Node.js.
Thank you SO MUCH, Nate! I really appreciate you taking the time to walk me through this even though it was totally independent of csound-api
. This is fascinating that this is the actual behavior of the Csound API. It still strikes me as surprising that outbound control signals can't be sent faster than 10 times per second. I'll take this issue to the Csound community for their comments.
Thanks again! Jason
I'll take this issue to the Csound community for their comments.
Good luck!
One more thing; your code example contains:
if (csound.Start(Csound) === csound.SUCCESS) {
csound.PerformAsync(Csound, () => csound.Destroy(Csound))
}
function readCsoundValues(){
let amplitude = csound.GetControlChannel(Csound, "meter")
console.log(amplitude)
setTimeout(readCsoundValues, 50)
}
readCsoundValues()
Doing this is risky. After the performance ends and csound.Destroy(Csound)
is called, you’ll be repeatedly passing a destroyed Csound
object to csound.GetControlChannel
, which will eventually lead to a segfault. You should probably do something like this (not tested):
if (csound.Start(Csound) === csound.SUCCESS) {
const intervalObj = setInterval(() => {
const amplitude = csound.GetControlChannel(Csound, 'meter')
console.log(amplitude)
}, 50)
csound.PerformAsync(Csound, () => {
clearInterval(intervalObj)
csound.Destroy(Csound)
})
}
Hi Nate,
I posted about this on the Csound Forum, and Rory suggested this could be addressed with performKsmps
. Since I needed this to be non-blocking in Node.js I decided to try .PerformKsmpsAsync()
from csound-api
. Here's how I rewrote the program:
const csound = require('csound-api')
const Csound = csound.Create()
csound.SetOption(Csound, '--output=dac')
csound.CompileOrc(Csound, `
0dbfs = 1
ksmps = 50
instr 1
kenv line 0, p3, 1
asig oscil kenv, 220
ktrig metro 20
kmeter max_k asig, ktrig, 1
printk2 kmeter
chnset kmeter, "meter"
endin
`)
csound.ReadScore(Csound, `
i 1 0 1
i 1 1 1
i 1 2 1
e
`)
if (csound.Start(Csound) === csound.SUCCESS) {
csound.PerformKsmpsAsync(Csound, () => {
testKsmpsAsync()
}, () => {csound.Destroy(Csound)})
}
function testKsmpsAsync() {
let amplitude = csound.GetControlChannel(Csound, "meter")
console.log(amplitude)
}
With the metro
set to 20 times per second I got these results when calling .GetControlChannel()
and printing console.log(amplitude)
. There are still repeated values but the resolution is better.
0.051020104271887
0.051020104271887
0.15079275262579955
0.15079275262579955
0.2006790768027554
0.2006790768027554
0.25056571420852136
0.25056571420852136
0.30045210074777545
0.35033848728702954
0.4002253152814872
0.45011175684631904
0.5011319811739879
0.5510184227388174
0.5510184227388174
0.6009054387375272
0.6009054387375272
0.6507919279912071
0.7006784172448869
0.7006784172448869
0.8004520432202912
0.8004520432202912
0.9002256982093464
0.9002256982093464
0.9501122608305181
0.9501122608305181
Then when I set metro
to 100 I get these results which have the best resolution yet.
0.04027285494939373
0.0600906852729396
0.08049865311389513
0.15079275262579955
0.1802717774391319
0.25056571420852136
0.27097218735666745
0.3390020432191236
0.37074500053666504
0.4297047098505638
0.47051735542115114
0.5306118937342816
0.5510184227388174
0.6190439318322294
0.6394546067652224
0.6893411363710183
0.7097503581294351
0.7301585153919423
0.8004520432202912
0.850338572826087
0.9002256982093464
0.9297051470542858
0.9909208882845262
0.9909208882845262
Maybe you can help me understand one thing though. My understanding of .PerformKsmpsAsync()
is that every control cycle it yields control to the controlPeriodFunction
, in this case testKsmpsAsync()
. With sr = 41000
and ksmps = 50
that would mean there are 820 control cycles per second. However, console.log(amplitude)
only logs about 26 values per second on average.
Can you help me understand why the number of console logs is so much smaller than the number of control cycles? Is this just the inherent latency of the Node.js event loop? Or perhaps this is another case where this is actually the behavior of the Csound API and nothing to do with Node.js or csound-api
.
For the purposes of animating a volume meter 26 values per second is probably fine, but clearly I don't have a good mental model of how this is all supposed to work. I can also bring these questions to the Csound Forum going forward because I don't want to take too much of your time.
Thanks, Jason
Can you help me understand why the number of console logs is so much smaller than the number of control cycles?
I’ll give it my best shot!
This does have something to do with Node.js, but it seems to be a general limitation, not something specific to csound-api.
First, while your math is correct, Csound’s default sample rate is 44,100 Hz, not 41,000 Hz. This means there are in fact 882 control cycles per second (that is, kr
is 882) when ksmps
is 50. Thus, it seems reasonable to expect the progress function passed as the second argument to csound.PerformKsmpsAsync
to be called 882 times per second, after every 50 samples.
If you change your score to—
i 1 0 1
e
—so that Csound performs for 1 second, and the Csound performance section of your code to—
if (csound.Start(Csound) === csound.SUCCESS) {
let count = 0;
while (!csound.PerformKsmps(Csound)) {
count++;
}
csound.Destroy(Csound);
console.log(count);
}
—882 is logged, as expected. This at least tells us that csound-api is doing what it’s supposed to when csound.PerformKsmps
is used, but that function blocks the main thread.
If you change the Csound performance section to—
if (csound.Start(Csound) === csound.SUCCESS) {
let count = 0;
csound.PerformKsmpsAsync(
Csound,
() => { count++; },
() => {
csound.Destroy(Csound);
console.log(count);
}
);
}
—you should see that not only is a number (much) less than 882 logged, the number isn’t the same each time you run that code.
Here is where csound.PerformKsmpsAsync
runs csoundPerformKsmps
(the C function) in a loop:
We can modify this code to log a count of control cycles without too much difficulty:
void Execute(const Nan::AsyncProgressWorker::ExecutionProgress& executionProgress) {
unsigned int count = 0;
while (!csoundPerformKsmps(wrapper->Csound)) {
count++;
executionProgress.Signal();
if (wrapper->eventHandler->CsoundDidPerformKsmps(wrapper->Csound))
break;
if (raisedSignal)
break;
}
printf("\n==> %u\n\n", count);
}
If you then rebuild csound-api (node-gyp rebuild
) and re-run the JavaScript code in Node.js, you should see towards the end of the output:
==> 882
This means that 882 control cycles really are being processed by csound-api. So what’s happening?
The “gotcha” seems to be this line:
This effectively tells Node.js to run the progress function passed as the second argument to csound.PerformKsmpsAsync
at some point; NAN’s documentation calls this “best-effort delivery”. So, if executionProgress.Signal()
runs after every 50 samples, that does not mean the progress function will actually run that often. It just means that Node.js has been told to run the progress function that often. How often the progress function actually runs is up to Node.js’ internals (specifically the libuv threading library, if memory serves).
Hi Nate,
My apologies for my delayed response. Thank you again for taking the time to carefully explain all of this to me. I've learned a lot from you.
As an experiment, I used the OSCsend
opcode in my Csound instrument to send out amplitude values every k-cycle. I then ran an OSC server in the main process in Electron. I got beautiful high resolution values from that, easily 100+ values per second though I didn't test out the ceiling on that. Awesome!
All I had to do was pass the OSC values from the main process in Electron to the renderer process via inter-process communication. And there I got stuck with the same old bottleneck of 10 values/second. [face-palm]
So the quest continues, but this is clearly in the realm of Electron and nothing to do with csound-api
. I appreciate all your help and all the work you've put into the API! Thanks very much,
Jason
No worries!
I appreciate all your help and all the work you've put into the API!
Thank you!
Hi Nate!
I've got an issue for you, though it might be a Node issue rather than
csound-api
. I'm on an Mac with an M1 processor.Issue
When I call
.GetControlChannel()
when running.PerformAsync()
in Node I'm getting repeated values from Csound. I would expect the values to be changing each time the method is called. It's like there's some kind of throttling that is limiting the output of.GetControlChannel()
. I'm hoping to find out what the underlying issue is.Code Example
Here's a quick overview of what's happening.
Csound
An oscillator outputs an oscillator signal (
asig
) with an amplitude that ramps up from 0 to 1. A metronome triggersmax_k
to read the amplitude ofasig
20 times a second. Csound then sends the amplitude value out to the "meter
" channel.printk2
shows that the amplitude increases incrementally for each k-cycle as expected. Here's the output.Node
A function calls
.GetControlChannel()
20 times per second and prints the output. I would expect the output to similarly increase each time the function is called, but it repeats the values twice. Here's the output.Investigation
I tried to increase the resolution on both sides to fix the problem, but it didn't change anything. For example, I increased the Csound metronome to send the amplitude 100 times per second, but it didn't change the output in Node. When I increased the
.GetControlChannel()
calls in Node to 100 times per second I just got more repeated values like this.In fact, Node only ever outputs 10 to 11 different values per second no matter how many times the function is called per second, as though it is throttled to reading
.GetControlChannel()
around 10 times per second. When I time how long it takes Node to get a result from.GetControlChannel()
I get an average of about 0.015ms, which seems fast enough to be called 100 times per second.I understand that the timing between
.GetControlChannel()
calls in Node won't be consistent, but timing doesn't matter as much as resolution in this case.Do you know if there is some kind of throttling happening within
csound-api
that is limiting the.GetControlChannel()
to sending 10 times per second? Otherwise, the problem might be within Node. I'm admittedly new to Node, so I could also be misunderstanding how it works.Thanks as always for your guidance!
Jason