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

There is a new version in the "develop" branch. From now on I will push new versions to the develop branch first. Then once a new version has been properly tested I will merge to master.



New features in this version:
- Ability to sync an Alsa Loopback capture device
- Proper command line argument parsing
- Float format capture and playback support
- Improved readme


Who will be first to try it? :)
 
I wonder if your coding productivity has something to do with using rust. I have never tried it but from your strolls it seems to be better than coding in C. Can you please compare from your experience?
I don't do much in C or C++ so not sure I can make a good comparison. But I'll try to summarize my reasons for liking Rust.

The ownership system in Rust is a little annoying before you get used to it. But once you make friends with the borrow checker it makes a lot of sense. It means that it's always clear who owns a certain piece of data. Others can "borrow" it (the Rust equivalent of handing out a pointer), but the borrow checker makes sure that nobody has borrowed it when it's freed. This means you basically don't things like get memory leaks or problems with dangling pointers. Segmentation faults never happen. In C the compiler doesn't care, it will let you hand out a pointer to something and let you keep the pointer after you clear the something. Rust also does bounds-checking on vectors to make sure you don't read or write in the wrong place. That's mostly meant to make it safer by preventing buffer overflows that can be exploited etc.

Threading is built into the language from the start. It's easy to start new threads and pass data safely between threads. This has really helped me save time!

The compiler gives the best error messages and warnings I have seen. If the code doesn't compile, it happens very often that the error message contains a corrected version you can copy.

Cargo and crates.io make handling dependencies super easy. There are plenty of things, they are easy to find, and generally of very high quality.


... and who is Camilla? :)

//
It's my daughers middle name. I had a hard time coming up with a name for the project, but then i came to think about how MySQL and MariaDB got their names :)
 
Henrik, thanks for your description. There was a discussion about using Rust for kernel development, that caught my interest. I will have to look at that language, the ease of coding vs. computing performance looks attractive, esp. for low-power devices. In C I would get stuck fast, too easy to make a mistake.
 
This detail is very interesting! I never tried doing that, thanks for reporting :) It might open up an easy way to catch sample rate changes (at least for the Alsa backend), meaning that automatic switching might be a lot easier than I thought... Need to experiment a bit...
I have experimented a little with this. Unfortunately I was thinking backwards and it seems very difficult to catch sample rate changes. I tested with camilladsp capturing from an alsa loopback at 44100, and used aplay to feed the input side of the loopback with sound at 48000. What happened was that aplay tried to set the device to 48000 but intead got 44100. It printed a warning about this and then went on to play the file slower.



Does anyone know of a way to get some kind of trigger from Alsa when the sample rate should change? I don't really have any idea at the moment. For Pulse it's probably possible to query the device about its input stream, but for alsa? Can you ask the rate plugin what it's doing?
 
Ok I may have an idea that could work for auto switching config. It seems like you can use the alsa "file" plugin to pipe to CamillaDSP, and it can give sample rate and sample format as parameters. Does someone want to experiment a bit?

Add this to ~/.asoundrc :
Code:
pcm.camilla { 
    type file 
    file "| /home/somebody/startcamilla.sh %r %f" 
    format "raw" 
    slave { 
        pcm null 
    } 
}
Save this to /home/somebody/startcamilla.sh and make it executable:

Code:
#!/bin/sh 
camilladsp /home/somebody/config_$1_$2.yml
Next create config files, one for each sample rate and sample format.The one for 44.1kHz and 16-bit input should be named "config_44100_S16_LE.yml". The input device should be type File, and the file "/dev/stdin".

Play sound to the device "camilla" , like:
Code:
aplay -D camilla music.wav
I don't know yet if this approach works, but it's certainly worth a try :)
 
Every alsa device has hw_params which give ranges/specific values for samplerate, samplesize, channel counts, buffer sizes etc. The way to learn if any is supported is to try - see Advanced Linux Sound Architecture / Re: [Alsa-user] query alsa for supported sample rates and formats?

Alsa players can either ask alsa-lib to set exact rate with snd_pcm_hw_params_set_rate ALSA project - the C library reference: Hardware Parameters - it fails if the exact required value is not available. More typically they use use snd_pcm_hw_params_set_rate_near and check if the result corresponds to the required value, e.g. your case of aplay alsa-utils/aplay.c at master * bear24rw/alsa-utils * GitHub

Alsa has also method snd_pcm_hw_params_get_rate which checks if the current configuration space hw_params has the given rate available.

I see alsa crate has get_rate method, but I do not understand how the value v is passed to the method - I see zero there alsa 0.1.2 - Docs.rs . My Rust knowledge is not rusty, but none.

Alsa cannot change rate on the fly, the device/stream must be closed and opened again with a different rate. A good example is incoming SPDIF stream to a soundcard - its rate can change at any time. If the SPDIF receiver driver detects change in the incoming rate (just a parameter in preamble/headers of the SPDIF stream), it closes the currently open stream linux/ak4117.c at master * torvalds/linux * GitHub , which will cause in everything down the stream to return the reading/writing methods with an error.

snd-aloop is not really friendly for robust solutions exactly for this reason - you do not know what is going on the other side. It is safe to use but it shields you from the other side which is not good.

A reliable solution should be part of some chain - e.g. jackd client, pulseaudio plugin, alsa plugin, gstreamer plugin.

As of that piped alsa file plugin - I added the piping functionality to the plugin exactly to be able to run simple scripts - sox, ecasound etc. While it is reliable in terms of stream management (opening, closing, rates, all "signalled" to the piped process reliably), it is not suitable for low-latency and time-critical cases due to the large unpredictable pipe. IMO such solution is not suitable for production-level DSP.

My personal opinion for a DSP engine - currently the best option is a plugin to pulseaudio for ease of coding, or alsa plugin for versatility (but much more complicated API).
 
Confused about card names

Hello.

So i'am waiting to recieve some cheap usbdac so i can test it on my shop instead dragging it all down to livingroom everytime i would try something out.

I did a fresh install on the camilladsp.
Right now i am trying to make it work with the internal soundcard buildin the RPI4, but i'am very confused all the aliases, card names, hw names and soo on :confused:

Belive me i tried reading all possible about ALSA, but i think the more i read the more confused i get right now :p

So i am trying to understand the default /etc/asound.conf file, but i can't.
I have pasted in RED the quistions about it i have, hoping some kind would explain? (3 lines are marked)

Also the syntax is hard to understand
The line
card 1: Loopback [Loopback], device 0: Loopback PCM [Loopback PCM]
Is the same as "hw:1,0" and the same as hw:CARD=Loopback,DEV=1, but as i see it the config files do not accept whatever you use?

- What is best practice to use ?

I have the following config files :
/etc/asound.conf
/home/pi/.asoundrc
/home/pi/Cfilters/highpass.yml (camilladsp filter)

#
# Place your global alsa-lib configuration here...
#
#
pcm.!default {
type plug
slave.pcm "camilladsp"
}

pcm.camilladsp {

# Use the ALSA plug-in "plug" for rate-/format-conversion.
type plug

# Forward the audio stream to the ALSA loopback-device
slave {
pcm {

# Direct hardware access
type hw

# Loopback card name
#
# Has to match "id" in the options of the snd-aloop module <--- HOWTO find ? and what format (hw:x,x or CARD=xx etc... ?)
card "Loopback"

# Loopback device ID
device 0 <--- device number appears here

# Number of audio channels
#
# Has to match the number of channels in music player app
# and in the CamillaDSP input configuration
channels 2

# Format of audio stream
#
# Has to match the format defined in the
# of the CamillaDSP input configuration
format "S32_LE"

# Sampling-rate of audio stream
#
# Has to match the sampling-rate defined in the
# CamillaDSP configuration
rate 44100
}
}
}


ctl.!default {
type hw
card "Loopback"
}
# Create an ALSA default control-device for the ALSA loopback-device.
ctl.camilladsp {

# Direct hardware access
type hw

# Loopback card name
#
# Has to match "id" in the options of the snd-aloop module
card "Loopback" <--- No device number here ?
}
 
Henrik,

In the exampleconfigs there are examples to mix to mono...
Channels in 2 / out 2..

So for destination 0, channel left and right are mixed to 1 destination.
The channels can get low or high passes.

What I need for a mono speaker is that channel left and right are (down)mixed, get a lowpass, and sent to dest. 0.

Same for destination 1, downmix, but with a highpass.

Is this possible with the mixer?
 
Henrik,

In the exampleconfigs there are examples to mix to mono...
Channels in 2 / out 2..

So for destination 0, channel left and right are mixed to 1 destination.
The channels can get low or high passes.

What I need for a mono speaker is that channel left and right are (down)mixed, get a lowpass, and sent to dest. 0.

Same for destination 1, downmix, but with a highpass.

Is this possible with the mixer?


Yes certainly, just mix to mono and then filter channel 0 with a lowpass, and 1 with a highpass. Try this:
Code:
filters: 
  lowpass_2k: 
    type: Biquad 
    parameters: 
      type: Lowpass 
      freq: 2000 
      q: 0.7 
  highpass_2k: 
    type: Biquad 
    parameters: 
      type: Highpass 
      freq: 2000 
      q: 0.7 
 
mixers: 
  to_mono: 
    channels: 
      in: 2 
      out: 2 
    mapping: 
      - dest: 0 
        sources: 
          - channel: 0 
            gain: -6 
            inverted: false 
          - channel: 1 
            gain: -6 
            inverted: false 
      - dest: 1 
        sources: 
          - channel: 0 
            gain: -6 
            inverted: false 
          - channel: 1 
            gain: -6 
            inverted: false 
 
pipeline: 
  - type: Mixer 
    name: to_mono 
  - type: Filter 
    channel: 0 
    names: 
      - lowpass_2k 
  - type: Filter 
    channel: 1 
    names: 
      - highpass_2k
 
Yes certainly, just mix to mono and then filter channel 0 with a lowpass, and 1 with a highpass. Try this:
Code:
filters: 
  lowpass_2k: 
    type: Biquad 
    parameters: 
      type: Lowpass 
      freq: 2000 
      q: 0.7 
  highpass_2k: 
    type: Biquad 
    parameters: 
      type: Highpass 
      freq: 2000 
      q: 0.7 
 
mixers: 
  to_mono: 
    channels: 
      in: 2 
      out: 2 
    mapping: 
      - dest: 0 
        sources: 
          - channel: 0 
            gain: -6 
            inverted: false 
          - channel: 1 
            gain: -6 
            inverted: false 
      - dest: 1 
        sources: 
          - channel: 0 
            gain: -6 
            inverted: false 
          - channel: 1 
            gain: -6 
            inverted: false 
 
pipeline: 
  - type: Mixer 
    name: to_mono 
  - type: Filter 
    channel: 0 
    names: 
      - lowpass_2k 
  - type: Filter 
    channel: 1 
    names: 
      - highpass_2k

This is exactly what I did. Unfortunately no succes.

I try to achieve:

Left output from soundcard goes to the woofer.
Right to the tweeter.

So for example the woofer, it should get channel left and right mixed together and this downmixed signal should get a lowpass on the downmixed signal.

Hope you don't mind I bother you with this.
 
Hello.

So i'am waiting to recieve some cheap usbdac so i can test it on my shop instead dragging it all down to livingroom everytime i would try something out.

I did a fresh install on the camilladsp.
Right now i am trying to make it work with the internal soundcard buildin the RPI4, but i'am very confused all the aliases, card names, hw names and soo on :confused:

Belive me i tried reading all possible about ALSA, but i think the more i read the more confused i get right now :p

So i am trying to understand the default /etc/asound.conf file, but i can't.
I have pasted in RED the quistions about it i have, hoping some kind would explain? (3 lines are marked)

Also the syntax is hard to understand
The line
Is the same as "hw:1,0" and the same as hw:CARD=Loopback,DEV=1, but as i see it the config files do not accept whatever you use?

- What is best practice to use ?

I have the following config files :
/etc/asound.conf
/home/pi/.asoundrc
/home/pi/Cfilters/highpass.yml (camilladsp filter)


It's perfectly normal to be confused by Alsa!


Let's start with the "device" confusion. A "device" is not a sound card! Instead a sound card (pci card, usb dac, virtual Loopback device etc) is called a "card". A card can have several devices. The Loopback device for example has two devices, 0 and 1. Then each device can have more than one subdevice. So "hw:Loopback:1,0" means card "Loopback", device 1, subdevice 0.


Then the questions:

Code:
# Loopback card name
        #
        # Has to match "id" in the options of the snd-aloop module  [COLOR=Red]<--- HOWTO find ? and what format (hw:x,x or CARD=xx etc... ?)[/COLOR]
        card    "Loopback"

        # Loopback device ID
        device    0  [COLOR=Red]<--- device number appears here[/COLOR]
The "id" of the loopback "card" is it's name, you can use the name instead of the card number. By default the name is "Loopback", but you can change it by passing a parameter to the snd-aloop kernel module. Don't bother with that, just leave it as "Loopback"!
You can choose either device 0 or 1. If you send sound to device 0, subdevice 0, you can record the same sound from device 1, subdevice 0. The opposite also works.





Code:
ctl.!default {
    type hw
    card "Loopback"
}
# Create an ALSA default control-device for the ALSA loopback-device.
ctl.camilladsp {

    # Direct hardware access
        type    hw

    # Loopback card name
    #
    # Has to match "id" in the options of the snd-aloop module
        card    "Loopback"  [COLOR=red]<--- No device number here ?[/COLOR]
 }
This is the the ctl-interface, just for controlling. It should point to the Loopback "card", not a specific device on that card. Just leave it as it is.


Hope this clears things up a bit! :)
 
This is exactly what I did. Unfortunately no succes.

I try to achieve:

Left output from soundcard goes to the woofer.
Right to the tweeter.

So for example the woofer, it should get channel left and right mixed together and this downmixed signal should get a lowpass on the downmixed signal.

Hope you don't mind I bother you with this.
Hmm that should work. How did it fail? Can you post your full config file?