electro-music.com   Dedicated to experimental electro-acoustic
and electronic music
 
    Front Page  |  Radio
 |  Media  |  Forum  |  Wiki  |  Links
Forum with support of Syndicator RSS
 FAQFAQ   CalendarCalendar   SearchSearch   MemberlistMemberlist   UsergroupsUsergroups   LinksLinks
 RegisterRegister   ProfileProfile   Log in to check your private messagesLog in to check your private messages   Log inLog in  Chat RoomChat Room 
 Forum index » DIY Hardware and Software » ChucK programming language
New ChucKer needing guidance
Post new topic   Reply to topic Moderators: Kassen
Page 1 of 1 [20 Posts]
View unread posts
View new posts in the last week
Mark the topic unread :: View previous topic :: View next topic
Author Message
pyramind



Joined: Jul 11, 2008
Posts: 4
Location: Turkey/Istanbul

PostPosted: 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...
Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message
kijjaz



Joined: Sep 20, 2004
Posts: 765
Location: bangkok, thailand
Audio files: 4

PostPosted: Fri Jul 11, 2008 1:31 pm    Post subject: Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message Send e-mail Visit poster's website Yahoo Messenger MSN Messenger
Inventor
Stream Operator


Joined: Oct 13, 2007
Posts: 6221
Location: near Austin, Tx, USA
Audio files: 267

PostPosted: Fri Jul 11, 2008 1:56 pm    Post subject: Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message Send e-mail
Frostburn



Joined: Dec 12, 2007
Posts: 255
Location: Finland
Audio files: 9

PostPosted: Fri Jul 11, 2008 3:36 pm    Post subject: Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message
pyramind



Joined: Jul 11, 2008
Posts: 4
Location: Turkey/Istanbul

PostPosted: Fri Jul 11, 2008 5:33 pm    Post subject: Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message
Inventor
Stream Operator


Joined: Oct 13, 2007
Posts: 6221
Location: near Austin, Tx, USA
Audio files: 267

PostPosted: Fri Jul 11, 2008 8:12 pm    Post subject: Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message Send e-mail
kijjaz



Joined: Sep 20, 2004
Posts: 765
Location: bangkok, thailand
Audio files: 4

PostPosted: Fri Jul 11, 2008 10:05 pm    Post subject: Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message Send e-mail Visit poster's website Yahoo Messenger MSN Messenger
Kassen
Janitor
Janitor


Joined: Jul 06, 2004
Posts: 7678
Location: The Hague, NL
G2 patch files: 3

PostPosted: Sat Jul 12, 2008 3:06 am    Post subject: Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message Send e-mail Visit poster's website
Frostburn



Joined: Dec 12, 2007
Posts: 255
Location: Finland
Audio files: 9

PostPosted: Sat Jul 12, 2008 5:21 am    Post subject: Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message
Kassen
Janitor
Janitor


Joined: Jul 06, 2004
Posts: 7678
Location: The Hague, NL
G2 patch files: 3

PostPosted: Sat Jul 12, 2008 6:47 am    Post subject: Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message Send e-mail Visit poster's website
kijjaz



Joined: Sep 20, 2004
Posts: 765
Location: bangkok, thailand
Audio files: 4

PostPosted: Sat Jul 12, 2008 6:55 am    Post subject: Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message Send e-mail Visit poster's website Yahoo Messenger MSN Messenger
kijjaz



Joined: Sep 20, 2004
Posts: 765
Location: bangkok, thailand
Audio files: 4

PostPosted: Sat Jul 12, 2008 7:27 am    Post subject: Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message Send e-mail Visit poster's website Yahoo Messenger MSN Messenger
dewdrop_world



Joined: Aug 28, 2006
Posts: 858
Location: Guangzhou, China
Audio files: 4

PostPosted: Sun Jul 13, 2008 12:08 pm    Post subject: Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message Visit poster's website AIM Address
Kassen
Janitor
Janitor


Joined: Jul 06, 2004
Posts: 7678
Location: The Hague, NL
G2 patch files: 3

PostPosted: Sun Jul 13, 2008 12:23 pm    Post subject: Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message Send e-mail Visit poster's website
pyramind



Joined: Jul 11, 2008
Posts: 4
Location: Turkey/Istanbul

PostPosted: Tue Aug 12, 2008 1:28 pm    Post subject: Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message
Kassen
Janitor
Janitor


Joined: Jul 06, 2004
Posts: 7678
Location: The Hague, NL
G2 patch files: 3

PostPosted: Wed Aug 13, 2008 12:54 am    Post subject: Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message Send e-mail Visit poster's website
pyramind



Joined: Jul 11, 2008
Posts: 4
Location: Turkey/Istanbul

PostPosted: Wed Aug 13, 2008 11:13 am    Post subject: Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message
Kassen
Janitor
Janitor


Joined: Jul 06, 2004
Posts: 7678
Location: The Hague, NL
G2 patch files: 3

PostPosted: Thu Aug 14, 2008 5:07 am    Post subject: Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message Send e-mail Visit poster's website
Underwaterbob



Joined: Sep 03, 2008
Posts: 26
Location: Chungju, South Korea

PostPosted: Fri Sep 05, 2008 7:09 am    Post subject: Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message
Kassen
Janitor
Janitor


Joined: Jul 06, 2004
Posts: 7678
Location: The Hague, NL
G2 patch files: 3

PostPosted: Fri Sep 05, 2008 7:27 am    Post subject: Reply with quote  Mark this post and the followings unread

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
View user's profile Send private message Send e-mail Visit poster's website
Display posts from previous:   
Post new topic   Reply to topic Moderators: Kassen
Page 1 of 1 [20 Posts]
View unread posts
View new posts in the last week
Mark the topic unread :: View previous topic :: View next topic
 Forum index » DIY Hardware and Software » ChucK programming language
Jump to:  

You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum
You cannot attach files in this forum
You can download files in this forum


Forum with support of Syndicator RSS
Powered by phpBB © 2001, 2005 phpBB Group
Copyright © 2003 through 2009 by electro-music.com - Conditions Of Use