DSPreamp, a CamillaDSP based preamp

CamillaDSP need no introduction and what is it not to love? An Open Source DSP written in Rust by a fellow Swede! I have read about different uses cases like active XO and room correction but why try to use it as a preamp?

My first experience with a DSP was the ADAU1701 based one by 3e Audio. It started out as an active XO but ended up as a preamp. The background and build is documented on my blog. The Linkwitz Transform worked really great with my current speakers but relying on an old laptop with proprietary SigmaStudio and a shady USB-interface for maintenance never feel right. That is why I got quite excited when I discovered CamillaDSP, here was an opportunity to replace proprietary tech with Open Source hardware and software! I just need a proof of concept and that is why I started my latest project - DSPreamp.

The code took me just hours to write but integrating hardware and software into something resembling a preamp took weeks. I have now bench tested everything I set out to do and the result exceeded my expectations. It will probably take some time until I get the time to build the final product but I want to take the opportunity to share my findings if anyone is planning to go down the same route.

Putting together a complex system is always a matter of tradeoffs. I have made a set of choices, your mileage may vary but know that I made them deliberately based on my specific needs. First of all, I decided to go with all analog controls. It would almost have been easier to go with rotary encoders and a display (easily enable remote control etc.) but that is not how I envision the final product. Another deliberate design decision was to run CamillaDSP and the ADC @ 88.2 kHz but more about that and sound quality in a future post.

I decided to run CamillaDSP on a Raspberry Pi and build around good but cheap and available hardware. I stayed away from USB based capture and output devices and opted for a HiFiBerry DAC+ ADC instead, it is one of the few ADC available as a HAT for the RPi. I initially planned to route input through a switch but I basically only got one analog source these days, my turntable. I use AirPlay a lot for casual listening and I therefor wanted to integrate it as a separate transport.

It might be tempting to add a web based music player managing my collection of ripped CD albums (all in FLAC format) but I don’t want a kitchen sink kind of mess without separation of concerns. I want the preamp to be a preamp and keep the music player on another device (RPi, NAS or whatever). What I would need is a lossless CD-quality transport over IP, preferable Open Source (and not proprietary like AirPlay). Looking for a solution led me to ROC Toolkit.

Another problem with the RPi is the lack of ADC for analog controls like potentiometers and power management. Overlays like gpio-shutdown and gpio-poweroff is great but HiFiBerry is hogging some crucial GPIO pins. Re-configuring the overlays and soldering a couple of pin headers on the HiFiBerry DAC+ ADC card solves the problem and makes it possible to manage power with an external Micro Controller.

I discovered the Raspberry Pi Pico while shopping for an 8 channel ADC for the RPi. The cost was more or less the same so I grabbed a Pico. It has 3 ADC and a bunch of GPIO pins. I decided to make it into a standalone front-end for my analog controls and to use it as the power controller for my RPi. It ended up as another project, meet rpi-sidekick. I tried to keep it generic enough for future reuse in other projects.

I now got pretty much all the bits and pieces needed for my bench tests. CamillaDSP, Shairport Sync and ROC was all configured to run as systemd services and I started to put together a minimal Linux image for the RPi. To get everything going with buildroot felt like too much work so I started out with pi-gen but even stage1 ended up too bloated with NetworkManager, Avahi, D-Bus etc. I therefor turned to Arch Linux ARM and ended up pretty bare metal systemd. I wanted to start-out with just ALSA and no sound server like Pulse of Pipewire to keep boot times fast and things not too complex. I ended up compiling Shairport Sync and ROC without support for Avahi or Pulse. Still a lot of work to be done to optimize it but it was enough proof of concept for me to call it a success.

The functionallity of the preamp was modeled after some of my favorite vintage amps (especially Yamaha CA-800 and Kenwood Model 600) and are:
  • A log volume control ranging from -80 to 0 dB attenuation with 12 o'clock @ -20 dB.
  • Baxandall tone controls, bass and treble ranging from -10 to 10 dB with individual defeat and two different turnover fq.
  • High and low cut filters with individual defeat and two different turnover fq.
  • Selectable digital RIAA filters and amplification on analog input (None, Moving Magnet (MM) or Moving Coil (MC))
  • Mono filter
  • Automatic Adaptive Loudness filter (on or off)
  • Mute filter (-20 dB)
  • 3 x input transports (ADC, hw:Loopback,1,0 and hw:Loopback,1,1)
  • Linkwitz Transform filter (parameters provided as command line arguments to dspreamp)

I have made all my source code available through Codeberg (I left GitHub after the Copilot incident). There are still a lot of information to add in future posts and snippets to be added to the git repository. Especially about ROC streaming from an external music player like MPD. Stay tuned…

DSPreamp @ Codeberg
RPi-Sidekick @ Codeberg

I would also like to take the opportunity to thank Henrik Enquist for sharing CamillaDSP and for patiently answering my questions about its inner workings...
 
Last edited:
  • Like
Reactions: 2 users
Some extra info on input and filter options. My initial idea was to use an input switch like the one in the picture below. The easiest option would be a mechanical switch but I envisioned relay based switching. I therefor implemented support for logical radio buttons in the RPi-Sidekick but each input pin would also need at-least another output pin driving a relay. It was limiting my other options so I eventually scrapped the code (along with a generic support for rotary encoders)...

Another thing to consider is the type of signal the ADC is supposed to be used with (turntable, tape deck etc.). The current implementation of DSPreamp provides 0 dBFS to CamillaDSP from the ADC @ 88.2 kHz. I wanted to get rid of a separate RIAA amplifier in the sound chain and try to implement the option of digital RIAA filters insted. The filter coefficients for the biquad is for 88.2 kHz (and they are a function of the sample rate) and the RIAA EQ error is as low as 0.008 dB. The filter coefficients produces a bit gained filter, around +12.5 dB, this has been compensated for in the DSPreamp code.

The RIAA filter is intended to be used with a three way (on-off-on) toggle switch (like a lot of the other filters in DSPreamp). One "on" goes to the MM GPIO pin and the other "on" goes to the MC GPIO pin. The switch in "off" state will result in a defeated RIAA filter, e.g. line level in. The same goes for example tone controls. E.g. the on-off-on switch goes to treble turnover fq. One "on" to high fq. turnover, the other "on" to low fq. turnover, the "off" state then defeats treble controls. The same for bass control and you got individual defeats for treble and bass. The same for high and low cut filter and so on. The different input options does not have to be wired this way. Shorting the MM RIAA filter GPIO to ground would make the MM RIAA filter permanent on the analog input for example. I hope you get the idea...

InputSwitch.png
 
Last edited:
Some extra info on power management. This might be obvious to most but please make sure you got common ground between the RPi and the Pico, and also make sure not to draw too much current from the GPIO pins (for relay or status LED). This is how power is intended to be wired between the Pico and the RPi.

PowerManagement.png


The Pico is switching the RPi power on and off and is also signalling to the RPi to shutdown via the gpio-poweroff overlay and sensing when the RPi has shutdown via the gpio-shutdown overlay. Serial communication wires, wires for gpio-overlays, power switch and status LED has been omitted in the picture. For documentation of wiring please refer to RPi-Sidekick.
 
As of the GPIO output load - have you considered using mosfets instead of relays? E.g. https://hackaday.com/2015/09/16/learn-and-build-a-high-side-switch/ Typically it's also cheaper and higher reliability. Also for switching analog inputs integrated analog mosfet switches are usually cheaper and easier to operate, unless absolutely top-performance is required (like in audio measurement devices).

But hats off to your project!
 
Some extra info on using ROC as an IP based sound transport (wired or wireless). This is an imaginary example where music files are kept on a Debian based server with playback enabled by the use of MPD and a web based player like myMPD. There is probably many ways to get this to work but I will use ALSA Loopback as the output device for MPD and as the capture device for roc-send. PipeWire got built in support for ROC and is another alternative, especially for desktop integration on the sending side. But this is an example of how to do it with bare bone ALSA.

Install ALSA Tools, MPD, myMPD and ROC on the server (you might have to compile ROC from source).

Enable ALSA Loopback, e.g.:
sudo nano /etc/modules-load.d/snd-aloop.conf
Add the line snd-aloop save and reboot.
Code:
snd-aloop

The command aplay -l should now contain loopback as avalable output devices, e.g.:

** List of PLAYBACK Hardware Devices **
card 0: Loopback [Loopback], device 0: Loopback PCM [Loopback PCM]
Subdevices: 8/8
Subdevice #0: subdevice #0
Subdevice #1: subdevice #1
Subdevice #2: subdevice #2
Subdevice #3: subdevice #3
Subdevice #4: subdevice #4
Subdevice #5: subdevice #5
Subdevice #6: subdevice #6
Subdevice #7: subdevice #7
card 0: Loopback [Loopback], device 1: Loopback PCM [Loopback PCM]
Subdevices: 8/8
Subdevice #0: subdevice #0
Subdevice #1: subdevice #1
Subdevice #2: subdevice #2
Subdevice #3: subdevice #3
Subdevice #4: subdevice #4
Subdevice #5: subdevice #5
Subdevice #6: subdevice #6
Subdevice #7: subdevice #7
card 1: Intel [HDA Intel], device 0: Generic Analog [Generic Analog]
Subdevices: 1/1
Subdevice #0: subdevice #0


Next up is to make roc-send run as a systemd service, create a service unit, e.g.:
sudo nano /etc/systemd/system/roc.service
Add the following text (replace 192.168.x.x with the IP or hostname of your receiver), save and exit.
Code:
[Unit]
Description=ROC Receiver
After=sound.target
Requires=network-online.target
After=network.target network-online.target

[Service]
ExecStart=/usr/bin/roc-send -s rtp+rs8m://192.168.x.x:10001 -r rs8m://192.168.x.x:10002 -c rtcp://192.168.x.x:10003 -i alsa://hw:0,1 --rate=44100

User=root
Group=root

[Install]
WantedBy=multi-user.target

Enable the service with: sudo systemctl enable roc.service
Start the service with: sudo systemctl start roc.service
Get status of the service with: sudo systemctl status roc.service

Next up is to set loopback as the output device in MPD, e.g.:
sudo nano /etc/mpd.conf
Change output settings to loopback, save and exit.
Code:
audio_output {
    type            "alsa"
    name            "Loopback"
    device          "hw:0,0"
}

That is the sending part, it will send sound @ 0 dBFS to the receiver via ROC. It is perfect if you use a volume control on the receiving end but not if you want to use the volume control on the sending end like in myMPD. This can be solved by ALSA, e.g.:
sudo nano /etc/asound.conf
Add the following text, save and exit.
Code:
pcm.aloop {
   type hw
   card 0
}

ctl.aloop {
   type hw
   card 0
}

pcm.roc {
  type plug;
  slave.pcm {
    type softvol;
    slave {
      pcm "dmix:CARD=Loopback,DEV=0";
    }
    control {
      name "soft"
      card "Loopback"
    }
  }
}

defaults.pcm.card 0
defaults.ctl.card 0

Next, change MPD output device to "roc", i.e.:
sudo nano /etc/mpd.conf
Change output settings to roc, save and exit.
Code:
audio_output {
    type            "alsa"
    name            "roc"
}

Next up is the receiving part. Make sure ALSA Tools and ROC is installed on the receiver and enable ALSA Loopback as on the sender.

The command arecord -l should now contain loopback as avalable capture devices, e.g.:

** List of CAPTURE Hardware Devices **
card 0: Loopback [Loopback], device 0: Loopback PCM [Loopback PCM]
Subdevices: 8/8
Subdevice #0: subdevice #0
Subdevice #1: subdevice #1
Subdevice #2: subdevice #2
Subdevice #3: subdevice #3
Subdevice #4: subdevice #4
Subdevice #5: subdevice #5
Subdevice #6: subdevice #6
Subdevice #7: subdevice #7
card 0: Loopback [Loopback], device 1: Loopback PCM [Loopback PCM]
Subdevices: 8/8
Subdevice #0: subdevice #0
Subdevice #1: subdevice #1
Subdevice #2: subdevice #2
Subdevice #3: subdevice #3
Subdevice #4: subdevice #4
Subdevice #5: subdevice #5
Subdevice #6: subdevice #6
Subdevice #7: subdevice #7
card 1: sndrpihifiberry [snd_rpi_hifiberry_dacplusadc], device 0: HiFiBerry DAC+ADC HiFi multicodec-0 [HiFiBerry DAC+ADC HiFi multicodec-0]
Subdevices: 1/1
Subdevice #0: subdevice #0


Make roc-recv run as a systemd service, create a service unit, e.g.:
sudo nano /etc/systemd/system/roc.service
Add the following text (replace 192.168.x.x with the IP or hostname of your receiver), save and exit.
Code:
[Unit]
Description=ROC Receiver
After=sound.target
Requires=network-online.target
After=network.target network-online.target

[Service]
ExecStart=/usr/bin/roc-recv -s rtp+rs8m://0.0.0.0:10001 -r rs8m://0.0.0.0:10002 -c rtcp://0.0.0.0:10003 -o alsa://hw:Loopback,0,0 --rate=44100

User=root
Group=root

[Install]
WantedBy=multi-user.target

Enable the service with: sudo systemctl enable roc.service
Start the service with: sudo systemctl start roc.service
Get status of the service with: sudo systemctl status roc.service

You can now let CamillaDSP use "hw:Loopback,1,0" as the capture device and use ROC as the transport. Output device of CamillaDSP would in this example be my HiFiBerry DAC+ ADC device, i.e.: "hw:sndrpihifiberry,0,0".
 
Last edited:
As of the GPIO output load - have you considered using mosfets instead of relays? E.g. https://hackaday.com/2015/09/16/learn-and-build-a-high-side-switch/ Typically it's also cheaper and higher reliability. Also for switching analog inputs integrated analog mosfet switches are usually cheaper and easier to operate, unless absolutely top-performance is required (like in audio measurement devices).

But hats off to your project!
Yes I have, many times, but I always end up with a bunch of micro relays because of the performance benefits (but I doubt I would be able to hear the difference in a blind test). And thanks for the compliment :)
 
Thanks for sharing this cool project! I really like the sidekick, it looks like something that could find a home in quite a few other projects too.
Thanks, I might add a couple of variants, at least one including support for rotary encoders.
I'm nog familiar with roc but it looks interesting. Are there senders for other platforms than Linux?
ROC officially supports the following platforms:
  • GNU/Linux
  • macOS
  • Android
I have only compiled it for Linux ARM (both under Raspberry Pi OS Bookworm and Arch Linux ARM, on a RPi 4B and on a M1 Mac using UMT, the last option does not require cross-compiling for the RPi and is blazingly fast).
 
What kind of time synchronisation deviation could one expect between several (4-20) clients communicating with one server for ROC - on a good, local "line"?
I don’t know because I have not tested it myself but one of the main goals of the project is to “implement real-time streaming with guaranteed latency”. Maybe check with the developers or the PipeWire community, they seem to test out ROC quite a bit…
 
Ps. I will primary use ROC as a transport between my media server (Linux) and my DAC (Linux). What’s great is that I no longer need to run the player software (e.g. MPD) on the same device as the DAC. I can now use ROC to decouple them. E.g. I no longer need to mount media files over NFS to bring them to the audio device. And I can use MPD or any other player capable of outputting to alsa loopback on a server with my media files, with a nice web interface, streaming lossless audio over the network to my audio device. It is quite neat…
 
Thanks. I went through those and could not find any mention of the rate control feedback. In fact it reads specifically:

Roc deals with it by adjusting the stream rate on the receiver side using a resampler. See details in our documentation.

IMO it does continually resample on the receiver side. I find that a bit pitty that it does not use the feedback RTCP receiver messages for this. LMS or DLNA network protocols provide feedback, IMO that's more suitable for audio-only (where the latency and multicast/multiple renderers are not critical). But good resampling is not audible so no big deal.
 
I was initially planning for a USB based digital input since HiFiBerry doesn’t have a board with both ADC and digital input (and you can’t stack them). My major concern with USB input was clocking (and I mainly listening to vinyls making the ADC my top priority). I put it on a back burner when I found ROC but you just got my attention. Power and serial communication is already over GPIO so the USB C is available. I might have to explore it as an input option... Any suggestions for a suitable USB device?
 
In gadget mode the Pi becomes the device. You just connect a USB cable between a computer and the usb c port of the pi. The pi then appears as an audio device to the computer. This virtual supports async usb, and exposes an alsa control for fine-tuning the rate. Camilladsp (running on the pi) can use this to keep the virtual usb device in sync with the actual playback device.