| Author |
Message |
pyramind
Joined: Jul 11, 2008 Posts: 4 Location: Turkey/Istanbul
|
Posted: Fri Jul 11, 2008 12:26 pm Post subject:
New ChucKer needing guidance Subject description: I want to know if ChucK will do it for me... |
 |
|
Hello dear ChucKers, first post yay!
For some time, I was postponing my first encounter with ChucK, today I decided to take a stab at it.
I want to use ChucK for a specific functionality in mind: rapid prototyping for DSP operations on signals. I'm fairly good with coding, and my main programming environment for sound is SuperCollider. As some of you probably may know, SC has a pretty steep learning curve, and I'm glad I've taken my time with it, I'm pretty happy with the SC approach to electronic music and composition.
But with SC, afaik, there is no elegant way to do signal operations on sub-block level without writing custom UGens with C++, and it is not that much of intuitive. I'm only comfortable with C++, when I precisely know what I want my code to do and I'm looking for.
So in this sense, I find the timing scheme in ChucK pretty impressive, I think ChucK still works with blocks but it seems that you can work with time fractions in sample or even sub-sample accuracy. I've gone through the documentation and now have a basic grasp on how to ChucK but still couldn't find what I've been looking for.
So sorry if this is a noob question, but could someone give me a few pointers on how I would go for making operations on outputs of UGens with sample level accuracy? The documentation explains how one would go for connecting UGens to other UGens but I could not find relevant info on how I would manipulate the output of UGens directly by coding custom structures in ChucK.
So as an example I want to build a very simple scenario: Making an averaging filter and making it work with the output of a SinOsc UGen.
What it needs to do:
Take the output sample from a SinOsc UGen, average it with the previous sample value and route this value to dac.
([z] + [z-1]) / 2 to dac.
And I'm a bit lost on how to grab the output from an UGen to use it with arrays, or logic blocks, common language operators etc.
Could someone give me a few pointers on how to make this happen?
Thanks in advance. |
|
|
Back to top
|
|
 |
kijjaz

Joined: Sep 20, 2004 Posts: 765 Location: bangkok, thailand
Audio files: 4
|
Posted: Fri Jul 11, 2008 1:31 pm Post subject:
|
 |
|
For me, for this kind of situation,
I might want use a small delay line.
this calculate ([z] + [z-1]) / 2
| Code: | Noise s => Gain mix => dac;
s => Delay d => mix;
samp => d.max => d.delay;
.5 => mix.gain;
2::second => now; |
this calculate ([z] - [z-1]) / 2
| Code: | Noise s => Gain mix => dac;
s => Delay d => mix;
samp => d.max => d.delay;
-1 => d.gain;
.5 => mix.gain;
2::second => now; |
but for a more complex filter type, you might consider some other ChucK's available filter UGens.
combination with Delays are possible also (see Delay, DelayL, DelayA for different delay and interpolation types)
Let's share more about this.
I'll wait to see some of your new patches soon ^_^
- - -
Another way might be using an array to store previous values of the input.
the last value of each UGen can be read by its .last() function.
this will definitely be the same as the above one.
| Code: | Noise s => blackhole;
Step output => dac;
float y;
while(true)
{
(s.last() + y) / 2 => output.next;
s.last() => y;
samp => now;
} |
but if you also want to do other things while the update is running,
here's one way to do it:
make a function that keeps updating.
when you spork it, you can do other things while the function instant is running.
| Code: | Noise s => blackhole;
Step output => dac;
spork ~ update();
// actually you can do other things here..
// while the function keeps updating.
2::second => now;
fun void update()
{
float y;
while(true)
{
(s.last() + y) / 2 => output.next;
s.last() => y;
samp => now;
}
} |
|
|
|
Back to top
|
|
 |
Inventor
Stream Operator

Joined: Oct 13, 2007 Posts: 6221 Location: near Austin, Tx, USA
Audio files: 267
|
Posted: Fri Jul 11, 2008 1:56 pm Post subject:
|
 |
|
Welcome, new ChucKist pyramind!
I would have said some of what kijjaz said, but he said what I was going to say and then some, plus he said it before I did!
May your journey with ChucK be fun and adventurous! _________________ "Let's make noise for peace." - Kijjaz |
|
|
Back to top
|
|
 |
Frostburn

Joined: Dec 12, 2007 Posts: 255 Location: Finland
Audio files: 9
|
Posted: Fri Jul 11, 2008 3:36 pm Post subject:
|
 |
|
If you know your way around filters and their transfer functions you might want to try OnePole and friends or even BiQuad.
They have .a(i) and .b(i) coefficients that determine how the input is filtered.
| Code: | | y[n] = x[n]*b0 + x[n-1]*b1 + x[n-2]*b2 - y[n-1]*a1 - y[n-2]*a2; |
where x[] is the input signal and y[] is the output. _________________ To boldly go where no man has bothered to go before. |
|
|
Back to top
|
|
 |
pyramind
Joined: Jul 11, 2008 Posts: 4 Location: Turkey/Istanbul
|
Posted: Fri Jul 11, 2008 5:33 pm Post subject:
|
 |
|
Wow, thanks a lot for the responses. Sucking the samples with blackhole, and polling the values with the last() method, then making the operations and sending the new value to a "next" value of a Step reference does the trick beautifully.
A filter was just an example of a simple operation here. The flexibility of working with samples in this manner is just great.
So if I were to make the calculations in 0.5::samp intervals and sending the values to dac at 1::samp intervals, I would be doing 2x oversampling effectively right? That's great.
One more little thing: Using references to adc does not work, so to get input from a sound interface I simply write "adc.next() * someoperation => out.next", is this the right way to do it? If I need the input signal somewhere else in my code can I safely use adc again and again all around? Or is there a better way to do it?
Thanks again for the fast responses, greatly appreciated. I'm a happy noob ChucKer today. _________________ http://www.earslap.com |
|
|
Back to top
|
|
 |
Inventor
Stream Operator

Joined: Oct 13, 2007 Posts: 6221 Location: near Austin, Tx, USA
Audio files: 267
|
Posted: Fri Jul 11, 2008 8:12 pm Post subject:
|
 |
|
pyramind, I always just use adc => whatever to access the adc. In the miniAudicle there is a preferences menu where you can select the adc source. Maybe your system has the wrong source selected? I can only guess.
You also can use adc.chan(n), where n selects a channel. This is documented on the unit generators web page. So for example, someday when I get a USB sound card, I will first select that as the source and then refer to the mic or guitar input with the adc.chan(n) syntax.
I do not know anything about adc.next() except that all ugens have the .last() method, I think, for referencing the ugen's output from the code section of your ChucK program. Hope that helps and I have not confused you further. Good luck and happy coding! _________________ "Let's make noise for peace." - Kijjaz |
|
|
Back to top
|
|
 |
kijjaz

Joined: Sep 20, 2004 Posts: 765 Location: bangkok, thailand
Audio files: 4
|
Posted: Fri Jul 11, 2008 10:05 pm Post subject:
|
 |
|
To poll for input, for adc, you might want to select channel also:
adc.chan(0).last() => ...
adc.chan(1).last() => ...
usually each chan(n) can be chucked into other UGens so that each inputs are seperated. |
|
|
Back to top
|
|
 |
Kassen
Janitor


Joined: Jul 06, 2004 Posts: 7678 Location: The Hague, NL
G2 patch files: 3
|
Posted: Sat Jul 12, 2008 3:06 am Post subject:
|
 |
|
| pyramind wrote: |
So if I were to make the calculations in 0.5::samp intervals and sending the values to dac at 1::samp intervals, I would be doing 2x oversampling effectively right? That's great. |
Yes, true, you can if you feel you need it; ChucK's timing abstraction is far more detailed then sample level.
| Quote: | | One more little thing: Using references to adc does not work, |
Could you be a little more precise here? Give a example of what doesn't work?
| Quote: | so to get input from a sound interface I simply write "adc.next() * someoperation => out.next", is this the right way to do it?
|
Likely you would connect adc to some Ugen using the ChucK operator directly like;
adc => LPF my_filter => dac;
But you can also use adc.last() to get the last value that came in as a float to be use in your code. Every Ugen and the adc&dac's have ".last()" and it always gives the last sample that was calculated by that Ugen (or the last that came in at the adc or was output by the dac).
".next()" is a function that the Step and Impulse Ugens have and sets the value those will output next tick of the sample clock.
Ugens only work at ticks of the sample clock so the only things you can talk about is the last value they calculated or the value to use next tick.
| Quote: | | If I need the input signal somewhere else in my code can I safely use adc again and again all around? Or is there a better way to do it? |
Yes, absolutely. adc only refers to the abstraction of the soundcard input, it doesn't create objects; you don't need to define and name it (and in fact I don't think you even *can*. This is different from Ugens that need to be defined and named. Though adc and dac behave like Ugens in some ways (you can call .last(), connect to and from them...) they aren't Ugens and just abstractions.
| Quote: | | Thanks again for the fast responses, greatly appreciated. I'm a happy noob ChucKer today. |
Great! _________________ Kassen |
|
|
Back to top
|
|
 |
Frostburn

Joined: Dec 12, 2007 Posts: 255 Location: Finland
Audio files: 9
|
Posted: Sat Jul 12, 2008 5:21 am Post subject:
|
 |
|
| pyramid wrote: | | So if I were to make the calculations in 0.5::samp intervals and sending the values to dac at 1::samp intervals, I would be doing 2x oversampling effectively right? |
I don't think it works that way. The timing is sub sample accurate but each Ugen is only ticked once per sample no matter what you do. It would require a whole lot of additional code for the UGens and I don't see that in the source.
I'd need to test it to make sure but the way I've done it in PyChucK is that the last value set to the dac within a sample is the one that stays in effect. Every time the current sample changes all UGens change their .last attribute and it stays constant within a sample. _________________ To boldly go where no man has bothered to go before. |
|
|
Back to top
|
|
 |
Kassen
Janitor


Joined: Jul 06, 2004 Posts: 7678 Location: The Hague, NL
G2 patch files: 3
|
Posted: Sat Jul 12, 2008 6:47 am Post subject:
|
 |
|
| Frostburn wrote: |
I don't think it works that way. The timing is sub sample accurate but each Ugen is only ticked once per sample no matter what you do. It would require a whole lot of additional code for the UGens and I don't see that in the source.
I'd need to test it to make sure but the way I've done it in PyChucK is that the last value set to the dac within a sample is the one that stays in effect. Every time the current sample changes all UGens change their .last attribute and it stays constant within a sample. |
Yes, that's right but you might still like some sort of hombebrew filter to work at a oversampled rate, that might be useful under rare conditions. Of course in between "ticks" you won't get new values from Ugens after the first nor is it useful to deliver more then one value to a Step Ugen in between ticks but you can still oversample in-between if you feel the need. I think this could be useful in custom filters/eq's. At that point the timing info will only refer to the order in which things happen; because of the double floats used for the time/dur datatypes I wouldn't be surprised if we could reason about moments in-between CPU cycles but in practice that's quite meaningless aside from defining execution order.
Of course; whether your CPU will like such antics is a wholly different matter... :¬) _________________ Kassen |
|
|
Back to top
|
|
 |
kijjaz

Joined: Sep 20, 2004 Posts: 765 Location: bangkok, thailand
Audio files: 4
|
Posted: Sat Jul 12, 2008 6:55 am Post subject:
|
 |
|
Yes, the UGens doesn't generate below sub-sample rate,
but you can calculate things before each sample pass.
the UGens will change its .last() only when each sample of now is passed.
but any calculations (for example, variables, functions) can store or change values anywhere in time.
let me demonstrate by this code first:
| Code: | now => time StartTime;
Noise s => blackhole;
for(int i; i < 20; i++)
{
<<< "Time: ", now - StartTime, " s.last() = ", s.last() >>>;
.3::samp => now;
} |
and let me demonsrate sub-sample calculation from this example:
| Code: | now => time StartTime;
Noise s => blackhole;
0 => float MovingAverage;
.4 => float Weight;
for(int i; i < 20; i++)
{
s.last() * Weight + MovingAverage * (1 - Weight) => MovingAverage;
<<< "Time: ", now - StartTime, " s.last() = ", s.last(), "Average = ", MovingAverage >>>;
.3::samp => now;
} |
So actually, audio can be re-generated from sub-sample calculations,
for example, using the above example's Moving Average to update to .. for example... to a Step that output to dac,
We can see that it'll appear to be low-pass filter in some way.
(I don't know the theory -_-")
I hope we can see your implementation with sub-sample resolution very soon ^_^. |
|
|
Back to top
|
|
 |
kijjaz

Joined: Sep 20, 2004 Posts: 765 Location: bangkok, thailand
Audio files: 4
|
Posted: Sat Jul 12, 2008 7:27 am Post subject:
|
 |
|
This is one example on using arrays to route delays to form simple filter.
| Code: | 50 => int N; // number of delay samples
Noise s;
Delay d[N]; // prepare sample delay units
Gain a[N]; // prepare parameter a
Gain mixer => dac;
.05 => mixer.gain;
s => d[0];
for(int i; i < N; i++)
{
// connect each sample delay to the next one to form a virtual delay line
// (except for the last one)
if (i <N> d[i+1];
// connect to gain ( to simulate An[z-n] ) and sum to 'mixer'
// so now, we can set each a[i].gain to set all the parameter values
d[i] => a[i] => mixer;
// set delay time to a sample
samp => d[i].max => d[i].delay;
}
// set parameter values: (just an example)
for(int i; i <N> a[i].gain;
}
second => now; |
Last edited by kijjaz on Sat Jul 12, 2008 7:29 am; edited 1 time in total |
|
|
Back to top
|
|
 |
dewdrop_world

Joined: Aug 28, 2006 Posts: 858 Location: Guangzhou, China
Audio files: 4
|
Posted: Sun Jul 13, 2008 12:08 pm Post subject:
|
 |
|
I should perhaps clarify that the issue in supercollider is sub-block feedback, not sub-block operations in general. The normal delay UGens can delay a signal for less than the duration of a block, so pretty much any feedforward operation should be possible to do in a SynthDef, without writing c++.
For one example, yesterday I wrote a simple brute-force time domain convolution SynthDef:
| Code: | var kernelbuf = Buffer.alloc(s, numCoefficients, 1);
kernelbuf.sendCollection(coefficients);
a = { |buf|
var sig = WhiteNoise.ar,
convo = sig * Index.ar(buf, 0),
delay1 = sig;
(1 .. numCoefficients-1).do({ |i|
delay1 = Delay1.ar(delay1);
convo = convo + (delay1 * Index.ar(buf, i));
});
convo
}, [buf: kernelbuf]); |
Of course this isn't fast and it isn't suitable for a large kernel, but it's mathematically correct and it sounds right.
The feedback limitation rules out cool things like custom IIR filters (although Nick Collins has a LTI UGen for that, which looks very cool), banded waveguides, most types of physical modeling, etc. That's a genuine problem and my read of ChucK is that it's a great place to do exactly that kind of prototyping.
James _________________ ddw online: http://www.dewdrop-world.net
sc3 online: http://supercollider.sourceforge.net |
|
|
Back to top
|
|
 |
Kassen
Janitor


Joined: Jul 06, 2004 Posts: 7678 Location: The Hague, NL
G2 patch files: 3
|
Posted: Sun Jul 13, 2008 12:23 pm Post subject:
|
 |
|
| dewdrop_world wrote: | | I should perhaps clarify that the issue in supercollider is sub-block feedback, not sub-block operations in general. The normal delay UGens can delay a signal for less than the duration of a block, so pretty much any feedforward operation should be possible to do in a SynthDef, without writing c++. |
Yes, that's good to clarify. I think that's BTW a property of normal block processing in general, at least that's what I saw so far in modular VST's like Tassman.
| Quote: | That's a genuine problem and my read of ChucK is that it's a great place to do exactly that kind of prototyping.
|
I think so, yes. Of course once you go beyond Ugens and want to do that kind of thing with inline coding running at sample rate you will have a issue in that that very quickly becomes expensive. I wonder to what degree context-sensitive block processing will help us there.
This kind of thing is simply is very hard. _________________ Kassen |
|
|
Back to top
|
|
 |
pyramind
Joined: Jul 11, 2008 Posts: 4 Location: Turkey/Istanbul
|
Posted: Tue Aug 12, 2008 1:28 pm Post subject:
|
 |
|
Hello again!
Sorry for the idle time. I've now found the time to tinker with this stuff again. For building filters, I'd like to use the arrays approach(instead of delays) for building filters as I for now just want to prototype. It will be easier to translate into native code later, and after a failing attempt with this approach, I came back to this topic and noticed a thing:
This:
| Quote: | | Code: | Noise s => Gain mix => dac;
s => Delay d => mix;
samp => d.max => d.delay;
.5 => mix.gain;
2::second => now; |
|
should be functionally equivalent to this:
| Quote: | | Code: | Noise s => blackhole;
Step output => dac;
float y;
while(true)
{
(s.last() + y) / 2 => output.next;
s.last() => y;
samp => now;
} |
|
But they produce different results(just listen), and I'm wondering why? _________________ http://www.earslap.com |
|
|
Back to top
|
|
 |
Kassen
Janitor


Joined: Jul 06, 2004 Posts: 7678 Location: The Hague, NL
G2 patch files: 3
|
Posted: Wed Aug 13, 2008 12:54 am Post subject:
|
 |
|
Hmmmm, quite interesting. I think the issue is that the second example may have a 2::samp delay because the value will take one sample to go from .last() to .next() and one sample to travel through your loop?
This is a unexpected (to me now) side-effect of mixing Ugens and code and how code can't be "ticked" by other Ugens like Ugens can.
I have to think about this, interesting topic. _________________ Kassen |
|
|
Back to top
|
|
 |
pyramind
Joined: Jul 11, 2008 Posts: 4 Location: Turkey/Istanbul
|
Posted: Wed Aug 13, 2008 11:13 am Post subject:
|
 |
|
I'm nowhere close to a knowledgable person on filters but judging from my experience with a spectrum analyser, actually the second example works as intended(ie. lowpass at nyquist/2).
The delay based one somehow works as a band reject filter at nyquist/2. If I knew the filter equation for that, maybe I could see what was going on there. _________________ http://www.earslap.com |
|
|
Back to top
|
|
 |
Kassen
Janitor


Joined: Jul 06, 2004 Posts: 7678 Location: The Hague, NL
G2 patch files: 3
|
Posted: Thu Aug 14, 2008 5:07 am Post subject:
|
 |
|
Yes, you are right, the timing is right and I made a reasoning error.
For a second I thought "Delay" might be interpolating but it turns out not to be and just do straight passing of samples (according to the docs). Hmmmm. I'm a bit out of ideas at this point. Quite mysterious. _________________ Kassen |
|
|
Back to top
|
|
 |
Underwaterbob
Joined: Sep 03, 2008 Posts: 26 Location: Chungju, South Korea
|
Posted: Fri Sep 05, 2008 7:09 am Post subject:
|
 |
|
Your question made me curious about what it would sound like. I've been using ChucK for one day and have no idea how to implement it.
I'm a Csound whore so I made an instrument that takes two identical sine waves and puts them exactly one sample out of phase with each other and then averages them.
| Quote: |
instr 1
iamp = ampdb(p4)
inote = cpspch(p5)
;the function 1-(inote/44100) calculates the phase value (from 0 to 1) for oscil
;needed to put the two sounds exactly one sample out of phase
iphs = 1-(inote/44100)
;envelope
k1 expseg iamp, p3-.02, iamp, .02, .01
;oscillator 1
a3 oscil k1, inote, 1
;oscillator 2 exactly one sample out of phase (behind) oscillator 1
a2 oscil k1, inote, 1, iphs
;take an average of the two values
a1 = (a2 + a3)/2
;output
out a1
endin
|
The end result is more or less indistinguishable from a single sine wave at the same frequency since we're talking about 100 samples per wavelength at A440 and any two consecutive values are nearly the same.
With a more chaotic signal it would probably make a slightly more noticeable difference. Also my model isn't recursive. It's always averaging the two signals and not the new sample and the previous average. I imagine a recursive example wouldn't make much more difference:
assuming a sawtooth wave (linear amplitude increase) imagine:
sample 1: 0
sample 2: 1
sample 3: 2
sample 4: 3
and so on
the output would be:
sample 1: (0+1)/2 = .5
sample 2: (2+.5)/2 = 1.25
sample 3: (3+1.25)/2 = 2.125
sample 4: (4+2.125)/2 = 3.0625
sample 5: (5+3.0625)/2 = 4.03125
Initially we see a bit of difference but the fraction (after the decimal) of the amplitude value grows into insignificance very quickly since we are dividing it by two for every sample that goes by. Meaning your final signal is almost identical to the original.
There's a chance I misunderstood your goal entirely? |
|
|
Back to top
|
|
 |
Kassen
Janitor


Joined: Jul 06, 2004 Posts: 7678 Location: The Hague, NL
G2 patch files: 3
|
Posted: Fri Sep 05, 2008 7:27 am Post subject:
|
 |
|
| Underwaterbob wrote: |
With a more chaotic signal it would probably make a slightly more noticeable difference. Also my model isn't recursive. It's always averaging the two signals and not the new sample and the previous average. I imagine a recursive example wouldn't make much more difference:
|
Actually, with recursion (feedback) averaging becomes a simple LP filter. This does matter quite a bit... except not in this particular case. Since a sine has no harmonics there is nothing to "filter" and the one thing you can hope for will be a shift in phase and a decrease in volume.
I'm still a bit baffled by what Pyramid ran into. _________________ Kassen |
|
|
Back to top
|
|
 |
|