Trouble debugging audio glitch

Hi all,

I’ve spent the last three days trying to debug an audio glitch in a Csound instrument I’ve made, but I’ve gotten nowhere. The instrument is a looper with a crossfade that uses the mincer opcode. The code is included below. Note that the k-variables defined at the top in the braces are default values defined for csoundengine.

Here’s an overview of how the looper works:

  • The first while loop moves kphase_main across the function table where the wav sample is stored.
  • When kphase_main crosses the kboundary_right it resets to the starting point of the sample and then activates the crossfade that starts where kphase_main left off.
  • The second while loop then moves kphase_fade through the crossfade tail while a decreasing amplitude envelope is triggered for kcrossfade_duration.
  • When kcrossfade_duration has passed the crossfade tail deactivates.

Here’s the code:

instr looper

{kplayback_position = 0, kamp = 0.5, initial_phase = 0, kboundary_left = 0, kboundary_right = 0, kfunction_table = 1, kphase_reset = 0, kloop_mode = 1, kcrossfade_duration = 0.3}
             
ispeed = 1
kamp_global_env expsegr 0.005, 0.01, 1, 0.01, 0.001

kphase_main init initial_phase
aphase_main init 0
kfade_active init 0
aphase_fade init 0
kcount_fade init 0
kamp_fade_env init 1
kcount_test init 1

ktrig changed2 kphase_reset
if (ktrig == 1) then
    kphase_main = kboundary_left
endif

if (kphase_main > kboundary_right) then
    if kloop_mode == 0 then
        turnoff
    elseif kloop_mode == 1 then
        kfade_active = 1
        kphase_fade = kphase_main
        kphase_main = kboundary_left
    endif
endif

kindex = 0
while (kindex < ksmps) do
    aphase_main[kindex] = kphase_main / sr
    kphase_main = kphase_main + 1
    kindex += 1
od

kamp_main_total = a(kamp_global_env) * kamp
asig_main mincer aphase_main, kamp_main_total, ispeed, kfunction_table, 1

if kfade_active == 1 then
    kfade_frames = round((kcrossfade_duration * sr) / ksmps)

    if (kcount_fade < kfade_frames) then
        kindex = 0
        while (kindex < ksmps) do
            aphase_fade[kindex] = kphase_fade / sr
            kphase_fade = kphase_fade + 1
            kindex += 1
        od

        kamp_fade_env = (kfade_frames - kcount_fade) / kfade_frames
        kcount_fade += 1

        kamp_fade_total = kamp_global_env * kamp_fade_env * kamp
        asig_fade mincer aphase_fade, kamp_fade_total, ispeed, kfunction_table, 1

    else
        kfade_active = 0
        kcount_fade = 0
        asig_fade = 0
        aphase_fade = 0
    endif

endif

chnmix asig_main + asig_fade, "mix"
endin

The crossfade works beautifully for the first play through the loop. Starting with the second play through the loop the crossfade starts with a split-second audio glitch that sounds as though the kphase_fade is briefly reading from a different part of the function table. I’ve isolated this audio glitch to just the crossfade mincer, not the main mincer. I’ve done a bunch of print debugging to ensure that the k-variables (e.g. phase, amplitude envelope) have the values they are supposed to, and everything looks correct. I don’t see evidence that the kphase_fade is briefly reading from a different part of the table.

I can’t figure out why the first activation of the crossfade sounds perfect and then future activations begin with a split second audio glitch. Are any of you able to see where the bug in the code might be?

Thanks!
Jason

Perhaps someone will see what I don’t, but from what I notice the kamp_global_env is only functioning to fade in the first loop, not subsequent loops. The kamp_fade_env always starts at 1 but subsequently fades to zero. It would seem the initial kamp_global_env, which is is used around line 81, fades in the very first loop but none of the following. Therefore they are starting at 1, causing the clicks.

In this pic you can see the first asig_fade on the top, the kamp_global_env below:

The global env is affecting only the first instance of asig_fade.

In the next pic you can see the global env is still open so not eliminating the click when the 2nd & 3rd loop start as kamp_fade_env jumps instantly from 0 to 1 as printk2 seems to confirm:

If you output only asig_fade, not asig_main + asig_fade, it’s much easier to hear.

Even during the first loop, the asig_main & asig_fade are not in phase, zero-crossing points don’t match. This is initially masked by the global env.

I’m not sure about a fix though, without syncing at a-rate. But perhaps this explains why there is no audible click at the start.
Hopefully someone has a better answer than mine.

Best,
Scott

Hi Scott,

Thanks very much for taking the time to examine the code, run it yourself, and analyze the results! Following your lead I’ll look into the kamp_global_env more. I did just mess with it a bit and got strange results, so maybe that’s the culprit somehow. For example, instead of setting kamp_global_env with a expsegr I just set it to 1. In theory this shouldn’t make a significant change, but it introduced a squealing tone in certain scenarios of playback. I’ll have to investigate this.

kamp_global_env is only supposed to fade in when the instrument first starts, maintain a level of 1 for most of the duration of the instrument, and then fade to 0 when the instrument is stopped. It’s just supposed to prevent clicking when the instrument starts and stops.

kamp_fade_env, on the other hand, is supposed to manage the fading amplitude envelope of the crossfade tail, and it’s supposed to be triggered multiple times while the instrument is playing and the sample loops back to the start.

I’ll report back after I’ve dug into this more. Thank you for your suggestions!

Jason

I still haven’t gotten rid of the bug. To rule out the amplitude envelopes I removed kamp_global_env and kamp_fade_env. So the fade mincer looks like this:

kamp_fade_total = kamp
asig_fade mincer aphase_fade, kamp_fade_total, ispeed, kfunction_table, 1

I still get exactly the same behavior where the first pass sounds good and any further passes have that audio glitch at the start of the fade tail. Close listening suggests to me that the brief audio glitch at the start of the tail is actually the final snippet (maybe final k-frame?) of the previous fade tail.

It’s almost like the aphase_fade is retaining the last value it had when kfade_active was last equal 1. This is surprising because I am setting aphase_fade to 0 whenever kfade_active switches to from 1 to 0. I’ve done print debugging to see the values of aphase_fade at the start of the fade tails, and they look like the correct values.

I’m stumped! Does anyone have any other ideas?

Thanks!
Jason

Hi Jason.

I noticed if ksmps = 32, with each new loop asig_fade goes out of phase/sinc with asig_main by 32 samples. So after 1 loop the offset is 32 samples, after 2 loops 64 samples etc. Is this deliberate?

In this pic, asig_main is on the top (left channel), asig_fade on the bottom (right channel):

You can clearly see the gap at .3 seconds (+ 32 samples). When I zoom in closer that gap is exactly 32 samples. So the next gap occurs at .6 seconds + 64 samples and the gap is again 32 samples. Then .9 + 96 samples and so on.

It does appear (perhaps this is what you meant?) that, after the ksmps gap, asig_fade is starting at the same point or 1 sample past where it was previously playing in the table (just prior to the gap).

A few quick questions:

  • what ksmps are you using?

  • do you want both mincer outputs to retain perfect phase with each other? From my limited understanding of the code it seemed that the intention is for both to be in phase/sinc with each other but it’s quite possible I’m just not reading it correctly, starting to think that might be the case.

  • if they are supposed to stay in phase, then aside from enveloping the second, what’s the point of using a second mincer opcode? You could probably just copy the audio output from the first mincer & envelope that. There’s probably several ways to do it such as using a metro triggering at kcrossfade_duration which triggers an envelope like triglinseg or trigexpseg, or an UDO

  • or is asig_phase supposed to restart at the first index of the table when each new loop begins (after the ksmps gap/delay) & so deliberately going more out of phase with asig_main after each loop?

Forgive me if I’m a little dense here :rofl:

This is an interesting sounding instr so I’m a little intrigued & don’t have much experience with mincer.

Scott

Hi Scott,

First of all, thanks for sticking with this! Let me provide a little more context about the instrument. It’s being controlled by Python with Eduardo Moguillansky’s csoundengine. The defaults defined at the top of the instrument (shown below in the curly brackets) are only used when the corresponding variables aren’t defined when scheduling the instrument event within Python.

{kplayback_position = 0, kamp = 0.5, initial_phase = 0, kboundary_left = 0, kboundary_right = 0, kfunction_table = 1, kphase_reset = 0, kloop_mode = 1, kcrossfade_duration = 0.3}

When I’m scheduling this instrument I’m often providing different initial_phase, kboundary_left, and kboundary_right values than the defaults. A typical example might be:

initial_phase = 40000, kboundary_left = 40000, kboundary_right = 100000

It seems like you and I are seeing different behavior when we run the instrument. Here’s what I see when I run the instrument with only asig_fade being output. The first pass looks normal, but the next two passes have short bursts of sound at the beginning.

There may also be the phasing issue that you mentioned at play, so I’m examining my code to see where that is being introduced. Perhaps the phasing issue and the bursts of sound at the beginning of the fades are related.

To answer your questions:

  • Currently the ksmps is 64.
  • Keep in mind that the two mincers shouldn’t be playing in sync at the same time, which seems to be what your screenshots are showing. Whenever they are playing at the same time they should be reading from completely different parts of the function table (i.e. the main mincer is reading from the start of the loop and the fade mincer is reading from the end of the loop).
  • I’m not sure I understand the question of why I’m using two separate mincers. It seems like we have different ideas about what’s going on. In order to have a crossfade I need a way to simultaneously play from two different parts of the function table. A single mincer opcode can’t do that, so I’m using two. In the past I’ve used recursion (i.e. calling a new instance of the instrument) to handle the crossfades, but for my current use case it’s much more convenient to keep the loop going within the same active instance.
  • Yeah, I think this is closer to what I’m going for.

Thanks!
Jason

Hi Jason. Thank you for the clarifications, that certainly helps. I did understand that you were triggering via csoundengine but due to my limitations (Csound for Android) couldn’t emulate that so used default values, not picking up on the fact I should have toyed with boundaries. That partially explains differing results. Doh!

I see/hear now how they are reading from different positions in the loop. However, again different results so probably can’t be of any help although the code generally works fine for me.

I’m not seeing the bursts. As I mentioned before, the fact you’re not experiencing a burst on the first iteration if using the global env is to be expected. But if you’re not seeing a glitch on the first loop when the global env is disabled but you do on iterations then that might point to a problem outside of Csound itself.

There is however a click when each fade segment begins, apart from the first due to the fact that it’s the only one enveloped (when using the global env as I am). Unfortunately, this is another issue you will face when you sort out the bursts. When the fade loop starts abruptly & isn’t enveloped, unless the boundary is at a zero-crossing point clicks will occur, sometimes not so bad, others quite noticeable. The envelope you created will eliminate clicks at the end of each loop but not the beginning.

So I guess I misconstrued the bursts I see now that you’re experiencing with the inherent click which I thought was the glitch.

As for the bursts you’re experiencing (left channel is asig_main, right is asig_fade without the env):


I don’t seem to be experiencing them. Could it be an issue related to communication with python? Engine settings like buffer size? For example, does altering the buffer size have any effect on the size of the burst?

Have you tried the code without calling it from python? If nothing else that might help to isolate the issue.

FWIW, there is another forum on Discord where several users seem to use python (often ctcsound) & there tends to be a little more traffic there. Also a listserv forum. Each forum generally has a slightly different makeup.

Unfortunately I have no experience using Csound with python so can’t be of much help there although I am curious to know if you experience the same issue running it as I did inside of Csound alone using:

initial_phase = 32000
kboundary_left = 32000
kboundary_right = 110000

using the stock “anditsall.wav”.

Hopefully you can find a solution, and if you do maybe post it for future reference.

Best,
Scott u

Hi Scott,

I’m glad we’re on the same page now. I suspected that the csoundengine aspect was the confusion point. Wow, that’s very interesting that the bursts are not happening when you run it! That gives me hope that somehow I can get to the bottom of this.

I just ran the instrument without Python. It’s just vanilla Csound triggered at the command line. The bursts are still there, so that rules out Python/csoundengine. I also ran it with various ksmps values, and I was getting the same behavior.

Thanks for pointing me toward other places to get help. I’ll reach out to the email list after I’ve done more poking and prodding of the instrument.

I appreciate the attention you’ve given this, and I’ll post here once I’ve figured out the issue.

Best,
Jason