• WANTED: Happy members who like to discuss audio and other topics related to our interest. Desire to learn and share knowledge of science required. There are many reviews of audio hardware and expert members to help answer your questions. Click here to have your audio equipment measured for free!

Using a Raspberry Pi as equaliser in between an USB source (iPad) and USB DAC

Introduction

I would like to connect my dac and amp to my iPad to be able to listen to Apple Music on my HD600. However, I want to eq the sound and this is not possible on iOS. That's where the pi comes in: I want to put it in between my dac and the iPad, take the sound from the iPad, eq it, and output it to my dac. This is possible on a Raspberry Pi 4, as it support OTG mode for the usb power input port.

Required hardware

Obviously you need to have a Raspberry Pi 4 (it might also work on a Pi Zero). Earlier Pis don't support OTG, so won't work.

I also bought this thingie:
972b5428-7a6c-48ab-9c43-e65941f1402a_1000x.jpg

This allows to plug in the Pi's power source and a data cable that will be connected to the iPad on one side, and a single cable with data and power combined to the Pi. This way the Pi stays powered on when no sound source is connected and doesn't draw any power from the iPad when it is connected. (You can buy one here or on other Pi shops.)

Finally, you need a bunch of cables :).

Setup the Pi

In the rest of this description I'm using DietPi as Linux distribution, but it should work quite similar on other distributions.

So, first install DietPi as per the instructions on their site. Make sure audio is enabled in the configuration program that is automatically run the first time you boot into DietPi.

Compile kernel
Until DietPi or you distro contains version 5.18 of the kernel you'll need to compile the kernel
  1. Install software to compile kernel: apt install git bc bison flex libssl-dev make
  2. Download kernel source: git clone --branch rpi-5.18.y https://github.com/raspberrypi/linux
  3. cd linux
  4. Configure the kernel:
    Code:
    KERNEL=kernel8
    make bcm2711_defconfig
  5. Compile the kernel: make -j4 Image.gz modules dtbs
  6. Install the kernel:
    Code:
    sudo make modules_installsudo cp arch/arm64/boot/dts/broadcom/*.dtb /boot/
    sudo cp arch/arm64/boot/dts/overlays/*.dtb* /boot/overlays/
    sudo cp arch/arm64/boot/dts/overlays/README /boot/overlays/
    sudo cp arch/arm64/boot/Image.gz /boot/$KERNEL.img
Make the Pi function as an USB audio gadget
  1. Add the line dtoverlay=dwc2 to /boot/config.txt.
  2. Add two lines to /etc/modules:
    Code:
    dwc2
    g_audio
  3. Create file /etc/modprobe.d/usb_g_audio.conf:
    Code:
    #load the USB audio gadget module with the following options
    options g_audio c_srate=44100,48000,88200,96000,176400,192000,352800,384000,705600,768000 c_ssize=4 p_chmask=0
  4. Reboot.

After reboot the Pi functions as an USB audio gadget that accepts audio with a sample rate in the list c_srate (I made it match the rates my Topping E30 accepts), and a sample size of 32 bits (c_ssize=4 means 4 bytes, equals 32 bits). For this, a new alsa device has been created, hw:UAC2Gadget. My dac is hw:E30, but you can find out by running arecord -l (and aplay -l to find your dac). p_chmask=0 is there to indicate this is a playback device to the host, not a capture device.

CamillaDSP
Install dependencies:
Code:
apt install python3 python3-websocket python3-aiohttp python3-jsonschema

Until a new version 1.1 is released, you'll need to compile CamillaDSP yourself:
  1. sudo apt-get install pkg-config libasound2-dev openssl libssl-dev
  2. git clone --branch next11 https://github.com/HEnquist/camilladsp.git
  3. RUSTFLAGS='-C target-feature=+neon -C target-cpu=native' cargo build --release
Move the executable to /usr/local/bin/.
Create a set of config files for each samplerate you want to use. Here's one for 44100Hz, /usr/local/etc/camilladsp-44100.yml:
Code:
devices:
  samplerate: 44100
  chunksize: 1024
  silence_threshold: -60
  silence_timeout: 3.0
  enable_rate_adjust: true
  stop_on_rate_change: true
  capture:
    type: Alsa
    channels: 2
    device: "hw:UAC2Gadget"
    format: S32LE
  playback:
    type: Alsa
    channels: 2
    device: "hw:E30"
    format: S32LE

filters:
...
What needs to be changed is between the files is the samplerate and maybe the chuncksize (see the CamillaDSP documentation).

Now we need to compile a utility that starts CamillaDSP when the sample rate changes:
  1. git clone https://github.com/pavhofman/gaudio_ctl.git
  2. cd gaudio_ctl
  3. cargo build --release
  4. Move the executable to /usr/local/bin/.
To make things work at startup, create /etc/systemd/system/camilladsp.service:
Code:
[Unit]
Description=CamillaDSP Daemon
After=syslog.target
StartLimitIntervalSec=10
StartLimitBurst=10

[Service]
Type=simple
ExecStart=gaudio_ctl -y "/usr/local/bin/camilladsp --loglevel error --port 1234 /usr/local/etc/camilladsp-{R}.yml"
Restart=always
RestartSec=1
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=camilladsp
User=root
Group=root
CPUSchedulingPolicy=fifo
CPUSchedulingPriority=10

[Install]
WantedBy=graphical.target

Start with systemctl start camilladsp. To have this service start at reboot, type systemctl enable camilladsp.

Optional: Udev rule
To make your DAC go into standby mode, you can create a file /etc/udev/rules.d/usb-power.rules. You'll have to lookup the required idVendor and idProduct for your DAC. For the Topping E30, I have:

Code:
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="152a", ATTR{idProduct}=="8750", TEST=="power/control", ATTR{power/control}="auto"
 
Last edited:
what do your patches in CDSP branch next11 accomplish?
Support for playback rate adjust - for capture chain (ADC -> CDSP -> gadget device) https://github.com/HEnquist/camilladsp/commit/71eec93d757e248cdae5b3d0b188889bb43206d5 , different method for pitch calculation with less fluctuation https://github.com/HEnquist/camilladsp/commit/c1329c076b73a26fa44a7b355b6cf5e6445d8c87 , support for playback side stalling (the gadget alsa device stalls when USB hosts pauses capture, capture stalling support is already in CDSP 1.0) https://github.com/HEnquist/camilladsp/commit/503226456cea14df436dc6dc84f6ad5add3b0cdd , refactoring of the timing and thresholds code to extract the key parameters into separate structs https://github.com/HEnquist/camilladsp/commit/4199290e6832a2d0e3280a434b3537fbf212b42b plus some minor changes.
(I actually hoped those were supporting the dynamic rate switching)
That would require adding support in the configuration code of CDSP - reading a different config, restarting the three key threads (capture/processing/playback). IMO restarting whole CDSP with a different config is not a seriously suboptimal solution, I perfectly understand why Henrik has other priorities.
 
Introduction

I would like to connect my dac and amp to my iPad to be able to listen to Apple Music on my HD600. However, I want to eq the sound and this is not possible on iOS. That's where the pi comes in: I want to put it in between my dac and the iPad, take the sound from the iPad, eq it, and output it to my dac. This is possible on a Raspberry Pi 4, as it support OTG mode for the usb power input port.

Required hardware

Obviously you need to have a Raspberry Pi 4 (it might also work on a Pi Zero). Earlier Pis don't support OTG, so won't work.

I also bought this thingie:
972b5428-7a6c-48ab-9c43-e65941f1402a_1000x.jpg

This allows to plug in the Pi's power source and a data cable that will be connected to the iPad on one side, and a single cable with data and power combined to the Pi. This way the Pi stays powered on when no sound source is connected and doesn't draw any power from the iPad when it is connected. (You can buy one here or on other Pi shops.)

Finally, you need a bunch of cables :).

Setup the Pi

In the rest of this description I'm using DietPi as Linux distribution, but it should work quite similar on other distributions.

So, first install DietPi as per the instructions on their site. Make sure audio is enabled in the configuration program that is automatically run the first time you boot into DietPi.

Compile kernel
Until DietPi or you distro contains version 5.18 of the kernel you'll need to compile the kernel
  1. Install software to compile kernel: apt install git bc bison flex libssl-dev make
  2. Download kernel source: git clone --branch rpi-5.18.y https://github.com/raspberrypi/linux
  3. cd linux
  4. Configure the kernel:
    Code:
    KERNEL=kernel8
    make bcm2711_defconfig
  5. Compile the kernel: make -j4 Image.gz modules dtbs
  6. Install the kernel:
    Code:
    sudo make modules_installsudo cp arch/arm64/boot/dts/broadcom/*.dtb /boot/
    sudo cp arch/arm64/boot/dts/overlays/*.dtb* /boot/overlays/
    sudo cp arch/arm64/boot/dts/overlays/README /boot/overlays/
    sudo cp arch/arm64/boot/Image.gz /boot/$KERNEL.img
Make the Pi function as an USB audio gadget
  1. Add the line dtoverlay=dwc2 to /boot/config.txt.
  2. Add two lines to /etc/modules:
    Code:
    dwc2
    g_audio
  3. Create file /etc/modprobe.d/usb_g_audio.conf:
    Code:
    #load the USB audio gadget module with the following options
    options g_audio c_srate=44100,48000,88200,96000,176400,192000,352800,384000,705600,768000 c_ssize=4 p_chmask=0
  4. Reboot.

After reboot the Pi functions as an USB audio gadget that accepts audio with a sample rate in the list c_srate (I made it match the rates my Topping E30 accepts), and a sample size of 32 bits (c_ssize=4 means 4 bytes, equals 32 bits). For this, a new alsa device has been created, hw:UAC2Gadget. My dac is hw:E30, but you can find out by running arecord -l (and aplay -l to find your dac). p_chmask=0 is there to indicate this is a playback device to the host, not a capture device.

CamillaDSP
Install dependencies:
Code:
apt install python3 python3-websocket python3-aiohttp python3-jsonschema

Until a new version 1.1 is released, you'll need to compile CamillaDSP yourself:
  1. sudo apt-get install pkg-config libasound2-dev openssl libssl-dev
  2. git clone --branch next11 https://github.com/HEnquist/camilladsp.git
  3. RUSTFLAGS='-C target-feature=+neon -C target-cpu=native' cargo build --release
Move the executable to /usr/local/bin/.
Create a set of config files for each samplerate you want to use. Here's one for 44100Hz, /usr/local/etc/camilladsp-44100.yml:
Code:
devices:
  samplerate: 44100
  chunksize: 1024
  silence_threshold: -60
  silence_timeout: 3.0
  enable_rate_adjust: true
  stop_on_rate_change: true
  capture:
    type: Alsa
    channels: 2
    device: "hw:UAC2Gadget"
    format: S32LE
  playback:
    type: Alsa
    channels: 2
    device: "hw:E30"
    format: S32LE

filters:
...
What needs to be changed is between the files is the samplerate and maybe the chuncksize (see the CamillaDSP documentation).

Now we need to compile a utility that starts CamillaDSP when the sample rate changes:
  1. git clone https://github.com/pavhofman/gaudio_ctl.git
  2. cd gadget_ctl
  3. cargo build --release
  4. Move the executable to /usr/local/bin/.
To make things work at startup, create /etc/systemd/system/camilladsp.service:
Code:
[Unit]
Description=CamillaDSP Daemon
After=syslog.target
StartLimitIntervalSec=10
StartLimitBurst=10

[Service]
Type=simple
ExecStart=gaudio_ctl -y "/usr/local/bin/camilladsp --loglevel error --port 1234 /usr/local/etc/camilladsp-{R}.yml"
Restart=always
RestartSec=1
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=camilladsp
User=root
Group=root
CPUSchedulingPolicy=fifo
CPUSchedulingPriority=10

[Install]
WantedBy=graphical.target

Start with systemctl start camilladsp. To have this service start at reboot, type systemctl enable camilladsp.

Optional: Udev rule
To make your DAC go into standby mode, you can create a file /etc/udev/rules.d/usb-power.rules. You'll have to lookup the required idVendor and idProduct for your DAC. For the Topping E30, I have:

Code:
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="152a", ATTR{idProduct}=="8750", TEST=="power/control", ATTR{power/control}="auto"
Thank you so much DeLub for such a detailed doc! Answers all my questions. I think other people will benefit from it too. You can ask the forum admin to edit/replace the top post.



Thank you phofman for your great work. Seems the clock syncing is already handled by CDSP and user don't need to do anything extra.

That would require adding support in the configuration code of CDSP - reading a different config, restarting the three key threads (capture/processing/playback). IMO restarting whole CDSP with a different config is not a seriously suboptimal solution, I perfectly understand why Henrik has other priorities.
AFAIK CDSP allows loading a different config in runtime through websocket instead of killing and restarting.
There're benefits in doing that --- volume levels, for instance, could be preserved.
My understanding is thaat currently https://github.com/pavhofman/gaudio_ctl.git does not remember volume after a rate change.

CDSP on Mac for instance, will stop and throw CAPTUREFORMATCHANGE when there's a rate change from capture device.
The datastruct also returns the new rate it changes into.
I can then use script to monitor and update the config in runtime.
If I run your https://github.com/pavhofman/gaudio_ctl.git without letting it controlling camilladsp (by omitting -y) and run camilladsp independently, will CDSP throw the same CAPTUREFORMATCHANGE on a rate change?
 
Last edited:
AFAIK CDSP allows loading a different config in runtime through websocket instead of killing and restarting.
There're benefits in doing that --- volume levels, for instance, could be preserved.
My understanding is thaat currently https://github.com/pavhofman/gaudio_ctl.git does not remember volume after a rate change.
I forgot about that. The current setup works actually quite fine, but instead I could write a small python script that gets called by gaudio_ctl (instead of start CDSP), points to the new appropriate config file and reloads.
 
I forgot about that. The current setup works actually quite fine, but instead I could write a small python script that gets called by gaudio_ctl (instead of start CDSP), points to the new appropriate config file and reloads.
I see. that makes a lot of sense.
I have a complete setup with lots of dynamic DSPs controlled by a python script under Mac already, using info provided by CAPTUREFORMATCHANGE to switch to new sample rate.
I'll try if the same script works in gadget mode.
If not, I'll use the method you recommended --- letting gaudio_ctl trigger a python script to set it.

Will report back when I have time to setup / compile everything.
 
I have a complete setup with lots of dynamic DSPs controlled by a python script under Mac already, using info provided by CAPTUREFORMATCHANGE to switch to new sample rate.
Can you share how you did that? Maybe show (part of) that script?
 
Can you share how you did that? Maybe show (part of) that script?
Sure. Suppose you have a connection to camilladsp, c:
c = camilladsp.CamillaConnection("127.0.0.1", 1234)
c.connect()
You can put the following in a while loop to repeatedly (with a time interval) query the stop reason. If it is inactive due to capture rate change, just get the new rate and update it to the old config.

Python:
if c.get_state() == camilladsp.ProcessingState.INACTIVE:
  reason = c.get_stop_reason()
  if reason == camilladsp.StopReason.CAPTUREFORMATCHANGE:
    config = c.get_previous_config()
    config['devices']['samplerate'] = int(reason.data)
    c.set_config(config)
Works very reliably under Mac.
 
Last edited:
AFAIK CDSP allows loading a different config in runtime through websocket instead of killing and restarting.
There're benefits in doing that --- volume levels, for instance, could be preserved.
My understanding is thaat currently https://github.com/pavhofman/gaudio_ctl.git does not remember volume after a rate change.

Correct, restarting the CDSP process cannot keep its internal state stored in SharedData.processing_status. gaudio_ctl is a general-purpose utility, running CDSP is just one of possible options.

Reloading a new config seems to be fully implemented in CDSP then, very good.
CDSP on Mac for instance, will stop and throw CAPTUREFORMATCHANGE when there's a rate change from capture device.
The datastruct also returns the new rate it changes into.
I can then use script to monitor and update the config in runtime.
If I run your https://github.com/pavhofman/gaudio_ctl.git without letting it controlling camilladsp (by omitting -y) and run camilladsp independently, will CDSP throw the same CAPTUREFORMATCHANGE on a rate change?
Look at the source code (e.g. the IntelliJ community edition with their Rust plugin is perfect for navigating larger Rust projects). StatusMessage::CaptureFormatChange is generated when the capture thread while capturing detects relative deviation from the incoming samplerate larger than RATE_CHANGE_THRESHOLD_VALUE (0.04). It's used when capturing e.g. from SPDIF or pipe where the capture device does not get closed between the rate change.

When USB host starts playback at a different samplerate, the device gets interrupted and reconfigured with different samplerate hw_param. The current samplerate is available in Capture Rate ctl, to which a client can subscribe to be notified of changes. CDSP would have to subscribe to this ctl element and monitor it. That's what the gaudio_ctl utility does (also in rust). Of course it would be possible to implement this feature in CDSP directly, I may talk to Henrik about doing it myself in the alsadevice module.

For now the python method described by DeLub (python script communicating with running CDSP instance instead of its cold restart) would work just as well.
 
CDSP would have to subscribe to this ctl element and monitor it. That's what the gaudio_ctl utility does (also in rust). Of course it would be possible to implement this feature in CDSP directly, I may talk to Henrik about doing it myself in the alsadevice module.
Yeah. I would expect it has consistent behavior across systems (linux, windows, mac) and sources (spdif, usb, etc).
Right now I can confirm it works for all CoreAudio sources, including USB interfaces.
For now the python method described by DeLub (python script communicating with running CDSP instance instead of its cold restart) would work just as well.
Thank you for confirming that. I'll find a time to cross compile the new kernel and try it out!
 
Last edited:
Yeah. I would expect it has consistent behavior across systems (linux, windows, mac) and sources (spdif, usb, etc).
A matter of HW and drivers, no consistence even between SPDIF sources. ESI Juli can detect incoming SPDIF samplerate, allowing the linux driver to interrupt the stream when it detects incoming samplerate change. Prodigy192 driver cannot detect the incoming SPDIF samplerate (the SPDIF receiver chip has no external clock available), resulting just in different capture bitrate at the original samplerate, which is what the CDSP rate change check detects.
Right now I can confirm it works for all CoreAudio sources, including USB interfaces.
USB capture on host can receive significantly different rate only when the device starts sending fewer/more data - again as a result of some SPDIF reception.
 
I've created a small script that reloads the configuration of CDSP on sample rate change:
Python:
import sys
from camilladsp import CamillaConnection

try:
    new_rate = sys.argv[1]
    cdsp = CamillaConnection("127.0.0.1", 1234)
    cdsp.connect()
except:
    print("Something went wrong. arg = ", new_rate)
    print("Make sure the first argument is a number with the new sample rate,")
    print("and that CamillaDSP is running at the localhost.")
    sys.exit()

if (new_rate == "0"):
    cdsp.stop()
else:
    cdsp.set_config_name("/usr/local/etc/camilladsp-{}.yml".format(new_rate))
    cdsp.reload()
This is called by /usr/local/bin/gaudio_ctl -y "/usr/bin/python3 /usr/local/bin/update_samplerate.py {R}"

Two things:
  • I want gaudio_ctl to also call this script when the sample rate becomes 0 (e.g., host device unplugged).
  • gaudio_ctl assumes the command it executes runs continuously and kills it on change. This is not required in this case.
Therefore I recompiled gaudio_ctl and made this dirty hack in executor.rs:
Code:
fn decide_kill_run(last_rate: usize, rate: usize) -> (bool, bool) {
//    let do_kill = /* any change in rate, unless it was zero */ last_rate > 0 && last_rate != rate;
    let do_kill = false;
//    let do_run = /* should run */ rate > 0 && (/* new start */  last_rate == 0 || /* restart */ do_kill);
    let do_run = last_rate != rate;
    (do_kill, do_run)
}
 
OK I tried it with my huge DSP pipeline with complicated script. Works flawlessly. Thank you so much phofman and DeLub

A few comments:
1.
cd gadget_ctl
This should be gaudio_ctl

2.
KERNEL=kernel8
Add a note that this is not always the case -- the current recommended version is still 32bit.

3. Why the audio device name is Playback Inactive? I changed it via function_name=CamillaDSP but it's still that ugly name.
Screen Shot 2022-05-10 at 19.32.20.png


4. I use the following script to do the rate update:

Python:
#!/usr/bin/python3
import camilladsp
import sys

c = camilladsp.CamillaConnection("127.0.0.1", 1234)

msg = ""

try:
  c.connect()
  rate = int(sys.argv[1])
  config = c.get_config()
  if config == None:
    config = c.get_previous_config()
  config['devices']['samplerate'] = rate
  c.set_config(config)
  msg = "Successfully adjust to the new sample rate!"

except ConnectionRefusedError as e:
  msg = "Can't connect to CamillaDSP, is it running? Error:" + str(e)
  retry = True
except camilladsp.CamillaError as e:
  msg = "CamillaDSP replied with error:" + str(e)
  retry = True
except IOError as e:
  msg = "Websocket is not connected:" + str(e)
  retry = True
finally:
  print(msg)

and the system daemon line is just

Code:
ExecStart=gaudio_ctl -y "/home/pi/camilladsp/set_rate.py {R}"

I'll wait for the change to come into camilladsp so I don't need multiple script and daemon to control it...
 
Last edited:
Two things:
  • I want gaudio_ctl to also call this script when the sample rate becomes 0 (e.g., host device unplugged).
  • gaudio_ctl assumes the command it executes runs continuously and kills it on change. This is not required in this case.
Therefore I recompiled gaudio_ctl and made this dirty hack in executor.rs:
Code:
fn decide_kill_run(last_rate: usize, rate: usize) -> (bool, bool) {
//    let do_kill = /* any change in rate, unless it was zero */ last_rate > 0 && last_rate != rate;
    let do_kill = false;
//    let do_run = /* should run */ rate > 0 && (/* new start */  last_rate == 0 || /* restart */ do_kill);
    let do_run = last_rate != rate;
    (do_kill, do_run)
}
Thanks a lot for your important contributions. I will add a corresponding parameter which will switch gaudio_ctl to this mode of operation.
 
Back
Top Bottom