UGen v.s. code in timing; very tricky
Dear list. Suppose we have a SinOsc named "s", connected to the dac, that has been playing for some arbitrary amount of time at a arbitrary frequency. Suppose we'd like to disconnect this from the dac while minimising the "click". One thing to try would be this; (1 - s.phase() )::s.period() => now; //strong timing to the rescue! s =< dac; Quite ChucKian, I thought, however this still clicks a bit. Some clickery is to be expected as not all frequencies will have a period that can be expressed in a integer number of samp's but this seemed like a bit much. On a whim I tried; (1 - s.phase() )::s.period() + samp => now; s =< dac; This clicks less and when I print the s.last(), the value is on -average- lower in magnitude. I then tried lining up the shred's timing with the UGen graph's; samp - (now % samp) => now; //yes; that's quite anal (1 - s.phase() )::s.period() => now; s =< dac; This doesn't affect matters quite as much as simply adding a samp to the time advanced; waiting a extra sample generally seems (to me) to decrease the magnitude of the s.last() right before we disconnect it. This would lead us to ask; "where does this samp come from?". I'm a bit at loss here, does anybody have any insights? Yours, Kas.
Ah, the digital world - never enough bits! My first reaction was to think
that this extra sample was a floating point truncation error in the
s.phase() and s.period() math, but then I came to think hmm... would those
minuscule fractions of samples make any difference to a zero crossing
timing? Furthermore, why would a truncation error be weighted? Your
declaration that "the value is on -average- lower in magnitude" started me
thinking 'okay, how average?' So I wrote up a little script:
TriOsc s => dac;
float one;
float two;
float three;
int count;
int count2;
int count3;
int signcount;
int signcount2;
for (int j;j<10000; j++)
{
Std.rand2f(10, 10000)=>s.freq;
10::ms=>now; // arbitrary offset of time
(1-s.phase())::s.period() => now; // a la Kassen
s.last()=>one;
samp=>now;
s.last()=>two;
samp=>now;
s.last()=>three;
if (Std.fabs(one) < Std.fabs(two) && Std.fabs(one) <
Std.fabs(three)) count++;
if (Std.fabs(one) > Std.fabs(two) && Std.fabs(three) >
Std.fabs(two)) count2++;
if (Std.fabs(two) > Std.fabs(three) && Std.fabs(one) >
Std.fabs(three)) count3++;
if ((one <0 && two > 0) || (one >0 && two <0)) signcount++;
if ((two <0 && three > 0) || (two >0 && three <0)) signcount2++;
}
<<
Dear list.
Suppose we have a SinOsc named "s", connected to the dac, that has been playing for some arbitrary amount of time at a arbitrary frequency. Suppose we'd like to disconnect this from the dac while minimising the "click".
One thing to try would be this;
(1 - s.phase() )::s.period() => now; //strong timing to the rescue! s =< dac;
Quite ChucKian, I thought, however this still clicks a bit. Some clickery is to be expected as not all frequencies will have a period that can be expressed in a integer number of samp's but this seemed like a bit much.
On a whim I tried;
(1 - s.phase() )::s.period() + samp => now; s =< dac;
This clicks less and when I print the s.last(), the value is on -average- lower in magnitude. I then tried lining up the shred's timing with the UGen graph's;
samp - (now % samp) => now; //yes; that's quite anal (1 - s.phase() )::s.period() => now; s =< dac;
This doesn't affect matters quite as much as simply adding a samp to the time advanced; waiting a extra sample generally seems (to me) to decrease the magnitude of the s.last() right before we disconnect it.
This would lead us to ask; "where does this samp come from?". I'm a bit at loss here, does anybody have any insights?
Yours, Kas.
_______________________________________________ chuck-users mailing list chuck-users@lists.cs.princeton.edu https://lists.cs.princeton.edu/mailman/listinfo/chuck-users
-- _______________________________________ http://greyrockstudio.blogspot.com
Eric Hedekar
Ah, the digital world - never enough bits! My first reaction was to think that this extra sample was a floating point truncation error in the s.phase() and s.period() math, but then I came to think hmm... would those minuscule fractions of samples make any difference to a zero crossing timing?
Yes, those are good questions. Of course a samp is a very short period but thanks to double floats we have a almost obscene amount of resolution at that scale. I also think that floating point errors only become significant when multiplying very large numbers with very small ones, which I don't think is the case here (but I'm not sure).
Furthermore, why would a truncation error be weighted?
That bit may not be very hard; it may well be the case that if the floats when truncated are truncated towards zero. As we are always dealing with positive numbers that could explain something?
Your declaration that "the value is on -average- lower in magnitude" started me thinking 'okay, how average?' So I wrote up a little script:
Ah-Ha! A scientific approach to investigating a unknown phenomena. :¬) I'll admit, at first I didn't have the zero crossing checks in, so I was
understanding very little, but once I realized that the zero crossings were equally distributed between the two possible locations, things clicked. Obviously the extra sample would "on average" (about 75% of the time) result in a lower s.last() value, because it's the sample around which the zero crossing would pivot. If the zero crossing didn't pivot, then you'd see an even distribution of probability for the lowest value.
I think you are right.
Your initial calculation is advancing time to the end of the period (or so it tries). I think there is a truncation error that is hidden in the math (that would explain why the zero crossing is jumping back and forth between the two locations, with an even distribution), and therefore only half the time you've advanced fully to the end of the period (the other half of the time you're off by one sample).
Hmmmm, I really would expect it to truncate towards zero. Another thing is that I'm not sure at what point the .phase() is actually calculated. When you call to .last() you get the sample at the last UGen tick so that value is at that point on average .5::samp out of date. I'm not sure whether .phase() is also calculated at that moment or whether it can be calculated in between ticks. This could be another factor.
The calculation is not finding the sample closest to the zero crossing - which might be a more precise, yet more laborious approach with little real world improvement.
I think it could be done by dividing the " (1-s.phase())::s.period()" by a samp, then rounding to the nearest integer and advancing time by that many samples, however that's assuming "(1-s.phase())::s.period()" doesn't cause rounding errors itself, a asumption that now seems far from safe. Also; this would demand that we know how the shred's timing and the UGen ticks line up as we want to disconnect right after the lowest sample and not right before.
I'd unchuck items with the inclusion of a while loop: while (s.gain()>0.000000001) { s.gain()/1.005 => s.gain(); samp=>now; } s =< dac;
Though who knows how long that can take.
That will indeed work but that's far from cheap. For audio range signals this should finish sooner (nearly always) while (Std.fabs(s.last()) >0.000000001) { s.gain()/1.005 => s.gain(); samp=>now; } s =< dac; ...but to be honest what I did was simply bring in a Envelope and ramp down over 50::ms which is always click-free though it could have a spectral effect when the context is some granular technique. Very interesting matter, thanks a lot for your help! Yours, Kas.
On Thu, Sep 18, 2008 at 7:23 AM, Kassen
Hmmmm, I really would expect it to truncate towards zero.
I would actually think it might truncate either up or down depending on the extra place value (but I don't know enough about how floating point truncation works to tell you). Does this behavior change from language to language - or architecture to architecture?
Another thing is that I'm not sure at what point the .phase() is actually calculated. When you call to .last() you get the sample at the last UGen tick so that value is at that point on average .5::samp out of date. I'm not sure whether .phase() is also calculated at that moment or whether it can be calculated in between ticks. This could be another factor.
well this one is easy to figure out:
SinOsc s => dac;
30::ms=>now;
<<
The calculation is not finding the sample closest to the zero crossing - which might be a more precise, yet more laborious approach with little real world improvement.
I think it could be done by dividing the " (1-s.phase())::s.period()" by a samp, then rounding to the nearest integer and advancing time by that many samples, however that's assuming "(1-s.phase())::s.period()" doesn't cause rounding errors itself, a asumption that now seems far from safe. Also; this would demand that we know how the shred's timing and the UGen ticks line up as we want to disconnect right after the lowest sample and not right before.
I'm tempted to guess that it's not possible unless we could calculate an audio sample before it is reached i.e. s.next() but I don't think Chuck has that capability (yet). But please prove me wrong! -Eric Hedekar -- _______________________________________ http://greyrockstudio.blogspot.com
Eric Hedekar;
I would actually think it might truncate either up or down depending on the extra place value (but I don't know enough about how floating point truncation works to tell you). Does this behavior change from language to language - or architecture to architecture?
I'd imagine that it does. In audio, BTW, if we're going to be rounding (and we are) I could imagine that rounding towards 0 could avoid exploding feedback loops. Rounding means noise and we wouldn't want noise to build up. I'm really just speculating here but it sounds like a nice idea to me right now.... Another thing is that I'm not sure at what point the .phase() is actually
calculated. When you call to .last() you get the sample at the last UGen tick so that value is at that point on average .5::samp out of date. I'm not sure whether .phase() is also calculated at that moment or whether it can be calculated in between ticks. This could be another factor.
well this one is easy to figure out:
Ok... so that would mean this factor would make the zero crossing appear sooner then we'd expect it as we're calculating it based on "out of date" data. This is looking more and more tricky with various factors pulling the outcome in both directions, apparently. I'm tempted to guess that it's not possible unless we could calculate an
audio sample before it is reached i.e. s.next() but I don't think Chuck has that capability (yet). But please prove me wrong!
I'm going to look into this (I do think it should be possible, that next sample is based on factors we can know) but I also have a sound-design gig that requires quite a bit of material and that needs to be done by Monday... Yours, Kas.
What is the maximum value of an int in ChucK? Is there a symbolic constant for it?
On Thu, Sep 18, 2008 at 9:28 PM, Kassen
2008/9/19 Paul Reiners
What is the maximum value of an int in ChucK? Is there a symbolic constant for it?
New in the latest version!
<<
>>;
For reference though, it uses the "long" keyword in C++ to represent integers. So on 32-bit computers the max should be 0x7FFFFFFF, or 2147483647. On 64-bit computers it will be 0x7FFFFFFFFFFFFFFFFF, or 9223372036854775807. Steve
participants (4)
-
Eric Hedekar
-
Kassen
-
Paul Reiners
-
Stephen Sinclair