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

Another attempt at changing the sample rate with Squeezelite!


I have been experimenting with the Alsa File plugin, and it seems like it could work for changing sample rate. I have successfully tried this with aplay and the command line player cmus.


Start by defining a new PCM device in ~/.asoundrc
Code:
pcm.camillapipe {
    type file
    slave {
        pcm {
            type null
        }
    }
    file "| /path/to/camillapipe.py %r %f %c /path/to/stdin_template.yml"
    format "raw"
}
This calls the camillapipe.py script with sample rate and format as parameters, and then feeds the audio data to it via stdin.
The script then opens a template config, replaces some values, saves it to a temporary location, and starts camilladsp with this config. After that it connects its own stdin to the stdin of camilladsp. Here is the "camillapipe.py" script (this needs to be marked as executable):
Code:
#!/usr/bin/python 
import sys 
import subprocess 
import yaml 
 
FORMATS = { 
    "S16_LE": "S16LE", 
    "S24_LE": "S24LE", 
    "S24_3LE": "S24LE3", 
    "S32_LE": "S32LE", 
    "FLOAT_LE": "FLOAT32LE", 
    "FLOAT64_LE": "FLOAT64LE", 
} 
 
def main(): 
    rate = sys.argv[1] 
    fmt = sys.argv[2] 
    channels = sys.argv[3] 
    configfile = sys.argv[4] 
 
    with open(configfile) as f: 
        config = yaml.load(f, Loader=yaml.SafeLoader) 
 
    if config["devices"].get("enable_resampling", False):  
        config["devices"]["capture_samplerate"] = int(rate) 
    else: 
        config["devices"]["samplerate"] = int(rate) 
 
    config["devices"]["capture"]["type"] = "Stdin" 
    config["devices"]["capture"]["format"] = FORMATS[fmt] 
    if config["devices"]["capture"]["channels"] != int(channels): 
        print(f'Wrong number of channels, got: {channels} expected: {config["devices"]["capture"]["channels"]}', file=sys.stderr) 
        return 
 
 
    with open(r"/tmp/camilladsp.yml", "w") as f: 
        yaml.dump(config, f) 
 
    print(f"Rate: {rate}, format: {fmt}, channels: {channels}", file=sys.stderr) 
    #aplay --quiet -D hw:1,0 -r 44100 -f S32_LE -c 2 
    subprocess.run(["/path/to/camilladsp", "-p1234", "/tmp/camilladsp.yml"], stdin=sys.stdin.buffer) 
 
 
if __name__ == "__main__": 
    main()
Finally this is the template config I used:
Code:
--- 
devices: 
  samplerate: 44100 
  chunksize: 4096 
  queuelimit: 0 
  capture: 
    type: Stdin 
    channels: 2 
    format: S16LE 
  playback: 
    type: Alsa 
    channels: 2 
    device: "hw:0,0" 
    format: S32LE


Last step is to launch squeezelite with "-o camillapipe".
Someone wants to give it a try?


In case the CODE block messes things up again, here the files are as a zip:
View attachment setrate.zip
 
Last edited:
Seashell, please explain more about how it is supposed to be configured. As for now, opencmd and closecmd will never fire. I'm just trying to touch files in /tmp, but nothing.

The asound.conf that you have on github, should the content be added to any existing asound.conf, or does it work for you as it is?
The asound.conf is complete but you need to specify to your player to use the lbparams device. (No default is defined in the asound.conf)

A quick example of how it's defined on github. (Stored as ~.asoundrc in this case)
Code:
aplay -f S16_LE -r 44100 -c 2 -D lbparams < /dev/zero 
close
Playing raw data 'stdin' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo
open
^CAborted by signal Interrupt...
close

Don't forget if you're doing a local version in your home directory it's called .asoundrc. It's /etc/asound.conf for a system wide configuration.

Also the commands specified need to be executable by the user you run the audio player as. And anything the commands try to write to needs to be write accessible by that same user.

Of course the hook library needs to be made and installed as well so alsa can find it.
 
Last edited:
Another attempt at changing the sample rate with Squeezelite!


I have been experimenting with the Alsa File plugin, and it seems like it could work for changing sample rate. I have successfully tried this with aplay and the command line player cmus.


Start by defining a new PCM device in ~/.asoundrc
Code:
pcm.camillapipe {
    type file
    slave {
        pcm {
            type null
        }
    }
    file "| /path/to/camillapipe.py %r %f %c /path/to/stdin_template.yml"
    format "raw"
}
This calls the camillapipe.py script with sample rate and format as parameters, and then feeds the audio data to it via stdin.
The script then opens a template config, replaces some values, saves it to a temporary location, and starts camilladsp with this config. After that it connects its own stdin to the stdin of camilladsp. Here is the "camillapipe.py" script (this needs to be marked as executable):
Code:
#!/usr/bin/python 
import sys 
import subprocess 
import yaml 
 
FORMATS = { 
    "S16_LE": "S16LE", 
    "S24_LE": "S24LE", 
    "S24_3LE": "S24LE3", 
    "S32_LE": "S32LE", 
    "FLOAT_LE": "FLOAT32LE", 
    "FLOAT64_LE": "FLOAT64LE", 
} 
 
def main(): 
    rate = sys.argv[1] 
    fmt = sys.argv[2] 
    channels = sys.argv[3] 
    configfile = sys.argv[4] 
 
    with open(configfile) as f: 
        config = yaml.load(f, Loader=yaml.SafeLoader) 
 
    if config["devices"].get("enable_resampling", False):  
        config["devices"]["capture_samplerate"] = int(rate) 
    else: 
        config["devices"]["samplerate"] = int(rate) 
 
    config["devices"]["capture"]["type"] = "Stdin" 
    config["devices"]["capture"]["format"] = FORMATS[fmt] 
    if config["devices"]["capture"]["channels"] != int(channels): 
        print(f'Wrong number of channels, got: {channels} expected: {config["devices"]["capture"]["channels"]}', file=sys.stderr) 
        return 
 
 
    with open(r"/tmp/camilladsp.yml", "w") as f: 
        yaml.dump(config, f) 
 
    print(f"Rate: {rate}, format: {fmt}, channels: {channels}", file=sys.stderr) 
    #aplay --quiet -D hw:1,0 -r 44100 -f S32_LE -c 2 
    subprocess.run(["/path/to/camilladsp", "-p1234", "/tmp/camilladsp.yml"], stdin=sys.stdin.buffer) 
 
 
if __name__ == "__main__": 
    main()
Finally this is the template config I used:
Code:
--- 
devices: 
  samplerate: 44100 
  chunksize: 4096 
  queuelimit: 0 
  capture: 
    type: Stdin 
    channels: 2 
    format: S16LE 
  playback: 
    type: Alsa 
    channels: 2 
    device: "hw:0,0" 
    format: S32LE


Last step is to launch squeezelite with "-o camillapipe".
Someone wants to give it a try?


In case the CODE block messes things up again, here the files are as a zip:
View attachment 883517

I had a try. I don't think camillapipe.py is begin called when I start Squeezelite playback. The loopbacks in asound should remain and the camillapipe.pcm should appended to asound.conf? Also modprobe in startup? I tried running camillapipe.py from the command line as a test with guessed arguments and got errors about number of channels :

root@DietPi:/home/tc# /home/tc/camillapipe.py 44100 S16LE 2 /home/tc/stdin_template.yml
File "/home/tc/camillapipe.py", line 32
print(f'Wrong number of channels, got: {channels} expected: {config["devices"]["capture"]["channels"]}', file=sys.stderr)
^
SyntaxError: invalid syntax


I think I can see how the rate is being changed but how can it load different .yml files which point to filters (which are in .dbl format in my case)?
 
Another attempt at changing the sample rate with Squeezelite!


I have been experimenting with the Alsa File plugin, and it seems like it could work for changing sample rate. I have successfully tried this with aplay and the command line player cmus.


Start by defining a new PCM device in ~/.asoundrc
Code:
pcm.camillapipe {
    type file
    slave {
        pcm {
            type null
        }
    }
    file "| /path/to/camillapipe.py %r %f %c /path/to/stdin_template.yml"
    format "raw"
}
This calls the camillapipe.py script with sample rate and format as parameters, and then feeds the audio data to it via stdin.
The script then opens a template config, replaces some values, saves it to a temporary location, and starts camilladsp with this config. After that it connects its own stdin to the stdin of camilladsp. Here is the "camillapipe.py" script (this needs to be marked as executable):
Code:
#!/usr/bin/python 
import sys 
import subprocess 
import yaml 
 
FORMATS = { 
    "S16_LE": "S16LE", 
    "S24_LE": "S24LE", 
    "S24_3LE": "S24LE3", 
    "S32_LE": "S32LE", 
    "FLOAT_LE": "FLOAT32LE", 
    "FLOAT64_LE": "FLOAT64LE", 
} 
 
def main(): 
    rate = sys.argv[1] 
    fmt = sys.argv[2] 
    channels = sys.argv[3] 
    configfile = sys.argv[4] 
 
    with open(configfile) as f: 
        config = yaml.load(f, Loader=yaml.SafeLoader) 
 
    if config["devices"].get("enable_resampling", False):  
        config["devices"]["capture_samplerate"] = int(rate) 
    else: 
        config["devices"]["samplerate"] = int(rate) 
 
    config["devices"]["capture"]["type"] = "Stdin" 
    config["devices"]["capture"]["format"] = FORMATS[fmt] 
    if config["devices"]["capture"]["channels"] != int(channels): 
        print(f'Wrong number of channels, got: {channels} expected: {config["devices"]["capture"]["channels"]}', file=sys.stderr) 
        return 
 
 
    with open(r"/tmp/camilladsp.yml", "w") as f: 
        yaml.dump(config, f) 
 
    print(f"Rate: {rate}, format: {fmt}, channels: {channels}", file=sys.stderr) 
    #aplay --quiet -D hw:1,0 -r 44100 -f S32_LE -c 2 
    subprocess.run(["/path/to/camilladsp", "-p1234", "/tmp/camilladsp.yml"], stdin=sys.stdin.buffer) 
 
 
if __name__ == "__main__": 
    main()
Finally this is the template config I used:
Code:
--- 
devices: 
  samplerate: 44100 
  chunksize: 4096 
  queuelimit: 0 
  capture: 
    type: Stdin 
    channels: 2 
    format: S16LE 
  playback: 
    type: Alsa 
    channels: 2 
    device: "hw:0,0" 
    format: S32LE


Last step is to launch squeezelite with "-o camillapipe".
Someone wants to give it a try?


In case the CODE block messes things up again, here the files are as a zip:
View attachment 883517

I had a try. I don't think camillapipe.py is begin called when I start Squeezelite playback. The loopbacks in asound should remain and the camillapipe.pcm should appended to asound.conf? Also modprobe in startup? I tried running camillapipe.py from the command line as a test with guessed arguments and got errors about number of channels :

root@DietPi:/home/tc# /home/tc/camillapipe.py 44100 S16LE 2 /home/tc/stdin_template.yml
File "/home/tc/camillapipe.py", line 32
print(f'Wrong number of channels, got: {channels} expected: {config["devices"]["capture"]["channels"]}', file=sys.stderr)
^
SyntaxError: invalid syntax


I started squeezelite with this command then initiated playback from Logitech Media Centre as per normal :

squeezelite -n DietPi -o squeeze -a 160:4::1 -b 10000:20000 -r 44100-192000:2500 -U -U -z -o camillapipe

I think I can see how the sample rate is being changed but how can it load different .yml files which point to filters (which are in .dbl format in my case)?
 
root@DietPi:/home/tc# /home/tc/camillapipe.py 44100 S16LE 2 /home/tc/stdin_template.yml
File "/home/tc/camillapipe.py", line 32
print(f'Wrong number of channels, got: {channels} expected: {config["devices"]["capture"]["channels"]}', file=sys.stderr)
^
SyntaxError: invalid syntax


...

I think I can see how the sample rate is being changed but how can it load different .yml files which point to filters (which are in .dbl format in my case)?
Your /usr/bin/python is probably 2.7. Try changing the first line of camillapipe.py to "#!/usr/bin/python3"


You don't need the loopbacks for this.


It doesn't do coefficient file switching yet. This first version is just to check that the idea works.


So camilla automatically exits when the pipe is closed when reading from stdin?
Yes, if the wait (-w) flag isn't given.
 
I tested on my rpi with aplay and mpd.

Problem 1) First user I ran aplay with created the tmp file and had ownership and sole write permission. So when I then tried to play from mpd it failed with "Unknown error 256". Changed tmp file to 777.

Problem 2) I suspect camilla is too slow to close as when I click a 48000 rate track while a 44100 track is playing in mpd mpd fails. The log shows camilla can't open the websocket (old instance still running I bet) and lots of ALSA lib pcm_file broken pipe errors.
 
Here's the mpd log.

Code:
[2020-10-12T06:22:43Z INFO  camillalib::alsadevice] Starting playback from Prepared state
[2020-10-12T06:22:58Z ERROR camillalib::socketserver] Failed to start websocket server: Address already in use (os error 98)
[2020-10-12T06:22:58Z ERROR camilladsp] Playback error: ALSA function 'snd_pcm_open' failed with error 'EBUSY: Device or resource busy'
Rate: 48000, format: S16_LE, channels: 2
ALSA lib pcm_file.c:358:(snd_pcm_file_write_bytes) write failed: Broken pipe
ALSA lib pcm_file.c:358:(snd_pcm_file_write_bytes) write failed: Broken pipe
ALSA lib pcm_file.c:358:(snd_pcm_file_write_bytes) write failed: Broken pipe
ALSA lib pcm_file.c:358:(snd_pcm_file_write_bytes) write failed: Broken pipe
mpd: pcm_file.c:397: snd_pcm_file_add_frames: Assertion `file->wbuf_used_bytes < file->wbuf_size_bytes' failed.
[2020-10-12T06:23:31Z INFO  camillalib::alsadevice] Starting playback from Prepared state
[2020-10-12T06:23:33Z INFO  camilladsp] Capture finished
[2020-10-12T06:23:33Z INFO  camilladsp] Playback finished
Rate: 44100, format: S16_LE, channels: 2
[2020-10-12T06:23:46Z INFO  camillalib::alsadevice] Starting playback from Prepared state
[2020-10-12T06:23:52Z INFO  camilladsp] Capture finished
[2020-10-12T06:23:52Z INFO  camilladsp] Playback finished
Rate: 44100, format: S16_LE, channels: 2
[2020-10-12T06:23:53Z INFO  camillalib::alsadevice] Starting playback from Prepared state
[2020-10-12T06:23:57Z INFO  camilladsp] Capture finished
[2020-10-12T06:23:57Z INFO  camilladsp] Playback finished
Rate: 48000, format: S16_LE, channels: 2

It works if I click stop first and then click another song. That's when you see the finished status. The error is when I just click a track while another one is playing.

I think your system relies on the alsa device being closed, but I don't think that has to happen. I believe a player can keep the device open and do a release on the hw_params and load new ones. I suspect that is what mpd does when I change formats while it is still playing a song. That or it is just a race condition, but I don't see your closing print statements which makes me wonder.

I suppose I should log from a good state to bad.
 
Last edited:
I do not think hw_params can be changed on the fly, without closing the PCM stream first.

It's just the whole cycle is too fast for camilladsp to properly exit and release its resources.

I added the pipe support into the alsa file plugin mostly for testing purposes, I do not think it is usable for a "production" setup. A piped chain has too many issues, compared to a proper alsa-lib linked chain.

IMO the proper way is to fix the snd-aloop notification feature. Please can you ping that thread in alsa-devel mailing list and ask if there is a way to fix it? IMO it should not be difficult for any of the alsa maintainers. Thanks.
 
I do suspect this is just a race condition in this case. I'm not sure about the hw_pararms being changeable or not. I don't mean without closing the stream, but without closing the device. My suspicion only comes from the existence of a hw_free callback. I know very little alsa.

Since you were able to add pipe support to the file plugin can't you submit a fix for the pcm_notify? I'm not going to bother with them as they didn't even respond to my simple query about getting the hw_params. And their response to you was yeah we broke it, we should do something more complicated to fix it. I feel trying to get them to fix it is like tilting a windmills.

I don't know that pipes are really the issue. Henrik what happens if you just add a sleep for one second at the start of camilla as a simple test. The websocket will be closed by then. This will tell you if alsa will wait for whatever setup time you need. If it does the fix if you want to run this way is to make camilla not fail when the websocket is taken but wait until it is ready (or some reasonable timeout).

As long as alsa will wait there are several ways one could fix the race condition.
 
Last edited:
IMO for a short moment you end up with two camilla processes running, the older one has not exited and released the websocket port fast enough yet and the new one hits the "address already in use" error.

If the new camilla waits a bit with consuming samples from the incoming pipe, you will hit the alsa broken-pipe error, IMO. It could buffer the samples though, but that would further increase latency and ruin usability of the chain.

IME pipes are just a hack which always produces undesirable side effects. Also pipes have no fixed latency which can be a problem with lip-sync etc.

IMO camilla should run just one process and close/re-open its in/out alsa device with proper params as required.

I know sometimes it takes a bit of patience and persistence to have a bug fixed but that's how it works. I do not take it as us and them, we all contribute to a common goal. I am sorry I do not have enough knowledge of the snd-aloop infrastructure to fix the notify parameter.
 
Yeah all these things are just workarounds for the broken notify. I could send a reminder request for it to be fixed. What would be the proper way to do that?



I believe my pipe hack could work quite well with squeezelite, since (afaik) it closes the device on sample rate changes, and you can even ask for a delay before it opens it again. The latency is of course difficult to control, but this isn't for movies so I don't think it matters.


It would be pretty easy to modify the python script to make CamillaDSP wait for the port to become available. Just connect to the websocket of the old running process, and wait for it to close.
 
You could probably do that but it would get pretty clunky. CamillaDSP supports only one resampling, on the capture side. And then you have to resample all channels. To upsample again you would need to run two instances, the first downsampling and doing the processing, then feeding the audio via a pipe to second instance that upsamples to the original rate.
I thought decimation was mainly used when doing FIR with direct computation (on embedded systems where FFT may not be available and stuff like that), which gets very heavy for long filters. CamillaDSP does FFT-based convolution which is much much faster for long filters. I'm not sure you would gain anything by downsampling

Frequency resolution of a FIR filter is FIRlength/'Sampling frequency' so for a given FIRlength the frequency resoution is propotional with 1/SF
So with a SF of 96kHz and decimation to 960Hz the resolution of the filter gets 100 times better. (Same delay, thogh)
So with the linux hardware of today, its only needed for filters longer then about 1/2 second if the hardware can go full trottle.

At the same time for a 3 way speaker, the filter can be really light on resourses when decimation is used for bass section.
(In the example 2 x 2 second FIR is run at a 22% resourses on 168 MHZ STM32)

And the higher the mid/tweeter sampling rate, the more cycles to save.

So I wote for decimator and interpolator 'blocks' in CamillaDSP :)
(But really no hurry at all, just when all ideas of new functionality has dried up)
 
Last edited:
I could send a reminder request for it to be fixed. What would be the proper way to do that?

IMO shortly describing your use case may do, either as a continuation of that thread or a new one with link to the previous "outcome".

First step is registering to alsa-devel mailinglist Alsa-devel Info Page

If continuing the thread 'Re: Functionality of pcm_notify in snd-aloop?' - MARC , I usually download the raw message https://marc.info/?l=alsa-devel&m=158558111526539&q=mbox , import the message into my mail client (TB), and reply.



I believe my pipe hack could work quite well with squeezelite, since (afaik) it closes the device on sample rate changes.

I really believe every client must close the device first before changing the samplerate. This message talks only about stopping (I do not know if it's equivalent to closing) [alsa-devel] snd_pcm_open fails with -16 error
 
I really believe every client must close the device first before changing the samplerate. This message talks only about stopping (I do not know if it's equivalent to closing) [alsa-devel] snd_pcm_open fails with -16 error
The message you link to reads to me as stopping is not the same as closing. The first part of the reply after the original author states he is closing and reopening the same device is "You should just set new hw_params on the same device."

So you have to stop the stream but you can leave the device open. I also believe this is why the pcm hook has the call backs it does: hw_params, hw_free, and hw_close. Look at the official ctl_elems hook. The hw_close hook is only used to clean up allocated memory.

Henrik to capture this behavior you can still use the pcm_hook even if avoiding loopback. Wrap the alsa file in the alsa hook and use either sighup or the websocket to tell camilla to update the parameters.

I tested the hook for race conditions and alsa was fine with arbitrary sleeps it just blocks until the hook is done.

I don't know if a pipe brings more care on camilla's end to make sure that all existing pipe data has been read before the parameter change though. I assume the loopback feature handles flushing existing data at the hw_free stage.
 
You are right, the device takes hw_params and configures samplerate etc. at snd_pcm_prepare. Stopping the stream (SND_PCM_STATE_RUNNING -> SND_PCM_STATE_SETUP) intentionally is by snd_pcm_drain or snd_pcm_drop, then the hw_params can be changed and re-installed by subsequent snd_pcm_prepare (pcmdev.prepare() in rust).
 
I tested on my rpi with aplay and mpd.

.../snip/

Are you running this on Raspian? And it somehow runs?


I tested it on piCorePlayer and got this error:
Code:
tc@SqueezePi2:~$ sudo /home/tc/camilladsp/camilladsp -p1234 -v -w 2> /home/tc/DSP_Engine/camilladsp.log &
tc@SqueezePi2:~$ sudo squeezelite -n SqueezePi2 -o camillapipe -a 160:4::1 -b 10000:20000 -r 44100-192000:2500 -s 192.168.1.5
Traceback (most recent call last):
  File "/home/tc/camilladsp/camillapipe.py", line 5, in <module>
    import yaml 
ModuleNotFoundError: No module named 'yaml'
The yaml python module is missing in piCorePlayer. The only available piCore "extension" related to python is: "python3.6-dev.tcz". And I guess that won't help...
Are there other ways to install python modules in piCore?