CamillaDSP - Cross-platform IIR and FIR engine for crossovers, room correction etc.

Hello and thank you for making this project.

I'm hoping to get some help using it on a raspberry pi 4. I downloaded the binary from github and set up an alsa loopback configuration. It works, but I get frequent dropouts in audio playback. They last about a second, but I think that is the time my hdmi audio extractor / receiver DAC chain takes to recover from a break in the stream so I'm pretty sure the actual breaks are short.

They always happen when camilladsp is first started, or returns to playing after going to sleep, but they also happen randomly during normal play.

I'm running 32 bit Raspberry Pi OS on a 2 GB pi 4 and using the PI's HDMI audio output. I've tried to set the priority higher with nice -10 and it helped some with the random drops, but not so much with the early drops.

The first drop typically comes with "warn...: preparing playback" (or words to that effect) but the other drops do not print anything to screen with -v. (And it's hard to tell with the scrolling of -vv.)

Here's a simple test configuration that I believe should be doing nothing but a pass through that causes the problem. I've also tried with actual FIR filters from 8192 to 65536 taps and it seems to make no difference to the drop frequency.


Any ideas? Something obviously stupid in my config perhaps?


Code:
devices:
  samplerate: 44100
  chunksize: 2048
  silence_threshold: -60
  silence_timeout: 10.0
  capture:
    type: Alsa
    channels: 2
    device: "hw:Loopback,1"
    format: S16LE
  playback:
    type: Alsa
    channels: 2
    device: "hw:CARD=b1,DEV=0"
    format: S16LE

filters:
  null_fir:
    type: Conv
    parameters:
      type: File 
      filename: one.txt

pipeline:
  - type: Filter
    channel: 0
    names:
      - null_fir
  - type: Filter
    channel: 1
    names:
      - null_fir
 
Hello and thank you for making this project.

I'm hoping to get some help using it on a raspberry pi 4. I downloaded the binary from github and set up an alsa loopback configuration. It works, but I get frequent dropouts in audio playback. They last about a second, but I think that is the time my hdmi audio extractor / receiver DAC chain takes to recover from a break in the stream so I'm pretty sure the actual breaks are short.

They always happen when camilladsp is first started, or returns to playing after going to sleep, but they also happen randomly during normal play.

I'm running 32 bit Raspberry Pi OS on a 2 GB pi 4 and using the PI's HDMI audio output. I've tried to set the priority higher with nice -10 and it helped some with the random drops, but not so much with the early drops.

The first drop typically comes with "warn...: preparing playback" (or words to that effect) but the other drops do not print anything to screen with -v. (And it's hard to tell with the scrolling of -vv.)

Here's a simple test configuration that I believe should be doing nothing but a pass through that causes the problem. I've also tried with actual FIR filters from 8192 to 65536 taps and it seems to make no difference to the drop frequency.


Any ideas? Something obviously stupid in my config perhaps?


The dropouts that happen early and tha random ones later are two somewhat isolated issues.
The two most likely reasons for the random ones are that you are running a too short chunksize, or that there is a rate mismatch between the hdmi output and the loopback. I would suggest enabling rate adjust, which will adjust the rate of the loopback (without any resampling). Add this to the config:
Code:
devices:
  samplerate: 44100
  chunksize: [B]4096[/B]
   silence_threshold: -60
  silence_timeout: 10.0
[B]  enable_rate_adjust: true[/B]
[B]  target_level: 2048[/B]
[B]  adjust_period: 10[/B]
For the early dropouts, this happens sometimes for some systems, because the playback starts too early. Right now it only waits for a few milliseconds, which lets it buffer a few ms of audio before starting. I willl change this so it calculates a suitable delay based on the settings of samplerate, target_level etc.
For now, try just increasing the chunksize to see if that helps.
 
Thanks for the reply. I meant to mention I've tried several power of 2 chunksizes from 1024 up to 32768 or 65536 (I forget which) and the problems persisted. I'll give the rate adjust a try tomorrow and report back how it went.

Also, in case it wasn't clear and matters "one.txt" just contains a single "1.0".
 
Thanks for the reply. I meant to mention I've tried several power of 2 chunksizes from 1024 up to 32768 or 65536 (I forget which) and the problems persisted. I'll give the rate adjust a try tomorrow and report back how it went.

Also, in case it wasn't clear and matters "one.txt" just contains a single "1.0".
I think I fixed the early buffer underruns now. When you try again, please use this new version: Release v0.3.2-beta4 * HEnquist/camilladsp * GitHub
 
Wow that was a quick turn around! Thank you.

I just did a short test of the beta version and killing and restarting camilladsp several times while audio was playing indeed showed no dropouts when play started. (Will make it much easier to compare the REQ curves I'm experimenting with.) Also no drops when resuming from sleep and I didn't notice any random drops during a brief time letting it run with rate adjust on.

There were consistent rate adjust messages output typically in the range of 99.96% - 99.98% while running so I believe you are right that the rates don't match. Do you know if that is an alsa configuration issue or a hardware issue (separate clocks?) in the pi?

As beta feedback I'll list the warnings that were printed, but they seem more informational than anything.

Each time I started camilladsp:
"Starting playback from Prepared state"

Each time I resumed after letting the sleep idle occur:
"Prepare playback after buffer underrun"

Of course when the audio stops that seems a natural buffer underrun condition to me.
 
Wow that was a quick turn around! Thank you.

I just did a short test of the beta version and killing and restarting camilladsp several times while audio was playing indeed showed no dropouts when play started. (Will make it much easier to compare the REQ curves I'm experimenting with.) Also no drops when resuming from sleep and I didn't notice any random drops during a brief time letting it run with rate adjust on.
Great! Thanks for testing!



There were consistent rate adjust messages output typically in the range of 99.96% - 99.98% while running so I believe you are right that the rates don't match. Do you know if that is an alsa configuration issue or a hardware issue (separate clocks?) in the pi?
This is perfectly normal, the loopback device and the HDMI output use different clock sources so they will never run at exactly the same rate. But the loopback device has a fine adjustment knob, and I use that to keep the rates matched. The adjustment needs to be done continuously since the rates will drift slightly all the time.



As beta feedback I'll list the warnings that were printed, but they seem more informational than anything.

Each time I started camilladsp:
"Starting playback from Prepared state"

Each time I resumed after letting the sleep idle occur:
"Prepare playback after buffer underrun"

Of course when the audio stops that seems a natural buffer underrun condition to me.
Oh I was too quick. The "Starting playback from Prepared state" message doesn't mean there is a problem and should not be a warning. I'll change it to info!
"Prepare playback after buffer underrun" happens both after playback has been paused, and when there was an accidental underrun. The playback thread doesn't know why it happened, only that the audio data arrived late. This is why the message is the same in both cases.
 
This is perfectly normal, the loopback device and the HDMI output use different clock sources so they will never run at exactly the same rate.
Thanks. I figured it was likely to be different clocks once I saw the drift. Too bad the virtual loopback interface doesn't get slaved to the clock of the hardware it's outputting to.


Oh I was too quick. The "Starting playback from Prepared state" message doesn't mean there is a problem and should not be a warning. I'll change it to info!
No worries, I really appreciate the quick fix for the startup issue. As I said I thought the warnings were more informational.

"Prepare playback after buffer underrun" happens both after playback has been paused, and when there was an accidental underrun. The playback thread doesn't know why it happened, only that the audio data arrived late. This is why the message is the same in both cases.
Does the playback thread know it's gone to sleep? If so maybe you could suppress the warning from the first underflow after wake up. Purely cosmetic so if it's not trivial of course leave it as is.

Your websocket configuration option is great. I just tossed together a super quick webpage with buttons for my various REQ test filters. Once I changed the pass through to be a binary file the same length as all the other filters (1.0 followed by zeros) it's really fast and smooth to switch between the filters. Just click a button and there's only the smallest of interruptions in playback.

So much better than what I was doing which was killing camilladsp from the command line and then up arrowing back to whatever mode I wanted to compare. Buttons are easier to use and the short reload time for the filters is much better for A/B comparison.

It will be great in the end too as this will be a headless system controlled via a browser and now I know it will be easy to have a page with a set of filters to choose based on the music/mood/volume level (loudness curves). No need for any server side code at all. I can just sneak an extra webpage into whatever web based music GUI I use. Awesome.
 
Sorry for posting a second time so quickly, but I just noticed something odd with the configuration reloading. Whether I do it via the websocket or by copying over the current config file and sending sighup the volume gets lowered from when I first start camilladsp from the command line with a given filter configuration. Since you NOP requests to change when the configuration hasn't changed (makes sense) you have to switch to another filter and back to hear this.

My filters are all the same length, and all normalized such that the mean squared value of their taps is 1.0.

Example:
Code:
cp cfg1.yaml cfg0.yaml
camilladsp cfg0.yaml
# Sound starts at certain volume

# In another terminal
cp cfg2.yaml cfg0.yaml
kill -1 {camilladsp pid}
# New filters loaded, but volume is lower.  Is it the new filter?
cp cfg1.yaml cfg0.yaml
kill -1 {camilladsp pid}
# No because the original filter is now loaded but volume is still lower.
# Stop camilladsp (ctrl-c, kill, whatever)

camilladsp cfg0.yaml
# Volume is back at original higher level
 
Does the playback thread know it's gone to sleep? If so maybe you could suppress the warning from the first underflow after wake up. Purely cosmetic so if it's not trivial of course leave it as is.
No it doesn't know, it only waits for the next audio chunk. I would like to improve this at some point, and send it a message so it knows the capture is paused. I didn't get around to it since the buffer underruns don't seem to cause any troubles, and the only real "problem" is the unnecessary warning. But I will fix it at some point!
 
Sorry for posting a second time so quickly, but I just noticed something odd with the configuration reloading. Whether I do it via the websocket or by copying over the current config file and sending sighup the volume gets lowered from when I first start camilladsp from the command line with a given filter configuration. Since you NOP requests to change when the configuration hasn't changed (makes sense) you have to switch to another filter and back to hear this.

My filters are all the same length, and all normalized such that the mean squared value of their taps is 1.0.
Thanks for the detailed description! I didn't need to try it myself, I could find the problem right away.
When starting from scratch, and when reloading, the coefficients are read using different functions. The one for reloading has a forgotten factor 2 which was already fixed in the other one.
Compare: camilladsp/fftconv.rs at master * HEnquist/camilladsp * GitHub
with: camilladsp/fftconv.rs at master * HEnquist/camilladsp * GitHub


There will be a new beta tonight.
(btw this is a nice example of why code duplication is bad!)


And please keep reporting bugs if you find them :)
 
I figured it was two different read paths, although I didn't know where in your code to start looking. Agreed that eventually config parsing should probably be rolled into one routine that can be called on start and reload.

I'll toss in a few other feature requests if you don't mind.

It would be nice if warnings were available on the websocket. That way a headless system could say display a clip indicator.

Maybe this is already in place and i just missed it, but some discussion in the documentation on proper scaling of FIR coefficients as different convolvers want different things. Some want them between +/-1, some between +/-32768, etc. I assume the scaling by n-coeffs you're doing has something to do with the gain through an fft/invfft transform pair.

I'm sure this one is annoying as it probably goes into certificate hassles, but browsers these days will not allow ws connections to be made from an https page. Only wss connections. I found that out when I tried to put the webpage on a server on a machine other than the RPI.
 
I figured it was two different read paths, although I didn't know where in your code to start looking. Agreed that eventually config parsing should probably be rolled into one routine that can be called on start and reload.
Fixed and a newly built beta5 is available.



I'll toss in a few other feature requests if you don't mind.

It would be nice if warnings were available on the websocket. That way a headless system could say display a clip indicator.
Sure, I'm always interested in new ideas! Providing the logger output on the websocket would be possible but it would add a fair bit of complexity. You would also need to do a lot of parsing of the messages to get what you want.

What info are you missing on the websocket as it is? It's probably easier to just add the missing bits there.



Maybe this is already in place and i just missed it, but some discussion in the documentation on proper scaling of FIR coefficients as different convolvers want different things. Some want them between +/-1, some between +/-32768, etc. I assume the scaling by n-coeffs you're doing has something to do with the gain through an fft/invfft transform pair.
I have picked what seems to be the most common way. If the coefficients are one of the integer formats, the value range is the full range of the integer type, -2^31 to (2^31-1) for S32LE for example. For Float, the range is -1.0 to +1.0. I will add something in the readme about this.




I'm sure this one is annoying as it probably goes into certificate hassles, but browsers these days will not allow ws connections to be made from an https page. Only wss connections. I found that out when I tried to put the webpage on a server on a machine other than the RPI.
My intention with the websocket server was that it should be used locally with for example a python client. That it could be used directly from a browser is just a bonus, but I see how it can be useful. I'll take a look to see how difficult it would be to add wss.
 
Fixed and a newly built beta5 is available.
Downloaded and confirmed deleting a divide by 2 eliminates a divide by 2. :p Thanks again for pushing a quick fix.

Sure, I'm always interested in new ideas! Providing the logger output on the websocket would be possible but it would add a fair bit of complexity. You would also need to do a lot of parsing of the messages to get what you want.
I'm specifically interested in clipping warnings. I'd be fine with all warnings/errors even just as a copy of what you throw to std out to make it easier on you. Should probably only be sent after a client sends a command to turn them on though. That way for people who just want a quick toggle control or the like they don't need to worry about handling incoming packets.

My intention with the websocket server was that it should be used locally with for example a python client. That it could be used directly from a browser is just a bonus, but I see how it can be useful. I'll take a look to see how difficult it would be to add wss.
And here I was thinking you intentionally chose websocket so it could be controlled from a browser. I'll say again really handy though. A few lines of javascript vs a whole cgi/wsgi/php front end/back end setup. Plus it doesn't get much easier for create a few buttons than a webpage these days.

Presuming you didn't write your own websocket gateway library hopefully it's just something that can be turned on in the one you use.

Fortunately my pi's server doesn't serve the main pages using https yet, so I can run my script on the pi itself with ws instead of wss and use it from any browser. So it's certainly not critical for me.

Browsers are getting more and more pushy about https though. I have some older hardware with embedded servers that will only ever be http and the stupid browsers are constantly wanting to push me to https for them even without any "https everywhere" type extensions or settings turned on. So annoying. Yes browser, I REALLY did mean to type http:// not https://. Stop autocorrecting me. /rant
 
Browsers are getting more and more pushy about https though. I have some older hardware with embedded servers that will only ever be http and the stupid browsers are constantly wanting to push me to https for them even without any "https everywhere" type extensions or settings turned on. So annoying. Yes browser, I REALLY did mean to type http:// not https://.

So very true. Plus they will limit accepted SSL certificate lifetime to 1 year. I wonder how embedded devices will handle that (will have to figure out myself too).
 
So I have read up a little about wss. It's easy to implement, just need to load a certificate from file and set the websocket server up to use it.
But, I discovered that the websocket library I'm using has been without a maintainer for the last 6 months and it doesn't look like anyone is taking over. Should have researched that a bit better when I added it.

Since I have no interest in maintaining the library myself, the best I can do is probably to switch to another one. So instead of building more stuff around the present one I will extend this to a little project:
- Change websocket library
- Implement wss
- Add clipping, buffer levels and other missing stuff to the websocket server
- Replace my home-made message protocol with json (should make it easier to use in the browser!)
- Update pyCamillaDSP to the new protocol.

This will probably keep me busy for a while :)



I also looked at redirecting the console log messages to the websocket. It seems doable, but will require a large effort. It also means the websocket server would need to dig its roots much deeper into the rest of the code, so keeping it as an optional feature will be a mess. I'll put it on the maybe-some-day-list..


The reason I went with websocket and not a plain socket is that I wanted to be able to send messages consisting of many lines. With a socket it's easy to send and receive single lines, but as soon as a message has more than one line you can't just look at line break to know the message is complete. The websocket protocol handles this nicely, so I don't need to :)