Mandarin DAC Bridge

Audirvana and Mandarin, sharing your USB DACs. The bridge holds each DAC exclusively, switches it to every track's own sample rate, plays the samples untouched, and offers it on your network as a UPnP renderer, so either app can play to it, but never both at once. Optionally also as a Squeezebox player, a Spotify Connect speaker (Spotify Soloist), a Qobuz Connect speaker (QobuzProxy) and a Plex player (Caldera Headless); on a Mac, the Music and Spotify apps play through it too. Beside Roon Bridge, with a now-playing screen. On macOS, Windows and Linux.

v1.2.0 macOS 14+ · macOS 27 Windows 10 · 11 Linux · Docker Bit-perfect DSD as DoP UPnP / DLNA Squeezebox Spotify Connect Qobuz Connect Plex .NET 10 MIT
Install on a Mac Install on Windows Install with Docker

How it works

On a Mac, Audirvana takes a USB DAC in exclusive (hog) mode to switch its sample rate. While it holds the DAC, nothing else can see it, so Mandarin finds no DAC. The bridge takes each DAC itself, and offers it to both apps over the network.

Whichever app starts playing owns that DAC. If the other app tries to change anything while it plays, the bridge refuses it (UPnP error 705, Transport is locked). The owner keeps the DAC for 10 seconds after it stops, so it doesn't lose it between tracks.

What it does

Nothing to set up. Plug a DAC in and it appears, on the page and on the network, within a few seconds.

🔒Exclusive, always

Each USB DAC is held whether it's playing or not, so no app can grab it behind your back. On a Mac that's Core Audio's hog mode; on Linux it's the ALSA hw: device, held open.

🎚️Bit-perfect, rate-switched

The DAC switches to every track's own rate: 44.1, 96, 192, up to 768 kHz. The samples reach it unchanged, and the track is resampled only when the DAC can't take its rate.

🦆One app at a time

Audirvana or Mandarin, whichever is playing, is in control. The other is kept out until it stops, and the page shows who has each DAC and who was kept out.

💿DSD as DoP

DSF and DFF files go to the DAC as DoP, untouched (DSD64 to DSD512, as far as the DAC's rates reach). DoP is on automatically on Linux when a DAC reports native DSD; on a Mac and Windows, switch it on per DAC. Where DoP is off, DSD is converted to PCM.

⏭️Gapless

When an app sends the next track ahead of time, a track at the same rate follows with no gap. A different rate plays out first, then the DAC switches.

🔊The DAC's own volume

Headphone DACs like the DragonFly have no knob: their volume is set from the computer and applied in the DAC, so the stream stays bit-perfect. The apps' volume controls set it (UPnP, Spotify, Qobuz, LMS), and the DAC's page has a slider. A DAC feeding an amplifier can be set to Fixed; one with no volume of its own plays at 100%.

📱A page per DAC

Open http://<machine>:55500 on any phone or computer: a tile per DAC, with its name and model. Tap one for its rates, depths, DSD and formats; the ✕ takes you back.

Install on a Mac

The easy way: one line in Terminal. macOS 14 or newer, macOS 27 included, Apple silicon or Intel. No Docker needed.

  1. Open Terminal: press Command-Space, type Terminal and press Return.
  1. Paste this line and press Return.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/meltface-80/Mandarin-DAC-Bridge/main/tools/mac/install.sh)"
  1. Answer what it asks. Allow if macOS asks to let mandarin-dac-bridge find devices on your network. Only if there is no ready-made build for your Mac does it ask to install Apple's command line tools, to build it there.
  1. Done. The page opens in your browser at http://localhost:55500 with a tile for each USB DAC. The bridge starts by itself whenever you log in. On a phone, open the address Terminal shows at the end.
What it installs: the bridge, one program, and its own small ffmpeg built with SoX, the resampler the bridge needs (a few MB), both in ~/Mandarin-DAC-Bridge. No Homebrew. A ffmpeg-full that an earlier installer added is offered for removal, with the packages only it needed (press Return to tidy up); Homebrew, Mandarin's ffmpeg and anything you installed yourself stay. To update, paste the same line again; what you set on the page is kept (environment variables added to the launch file by hand are not: add them again).
Prefer to install by hand?

Download mandarin-dac-bridge-osx-arm64.tar.gz (Apple silicon) or -osx-x64 (Intel) from the latest release, then:

mkdir -p ~/Mandarin-DAC-Bridge
tar -xzf ~/Downloads/mandarin-dac-bridge-osx-*.tar.gz -C ~/Mandarin-DAC-Bridge
xattr -dr com.apple.quarantine ~/Mandarin-DAC-Bridge   # downloaded by a browser: let macOS run it
~/Mandarin-DAC-Bridge/mandarin-dac-bridge --list    # the DACs it can see
~/Mandarin-DAC-Bridge/mandarin-dac-bridge           # run it; Control-C stops it

Install on Windows

Windows 10 or 11, x64 or ARM64. One line in PowerShell. The bridge holds each USB DAC in WASAPI exclusive mode, so Windows' mixer never touches the samples.

  1. Open PowerShell: right-click Start and choose Terminal (or Windows PowerShell).
  1. Paste this line and press Enter.
powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/meltface-80/Mandarin-DAC-Bridge/main/tools/windows/install.ps1 | iex"
  1. Allow if Windows asks to let mandarin-dac-bridge use the network (private networks).
  1. Done. The page opens at http://localhost:55500. The bridge starts by itself at every sign-in.
What it installs: the bridge, one program, and its own ffmpeg with SoX, in %LOCALAPPDATA%\Mandarin-DAC-Bridge, started by a hidden scheduled task. (With no ready-made build for the PC, it builds the bridge there, and gets ffmpeg with SoX from winget's Gyan.FFmpeg.) To update, paste the same line again; your settings are kept. In Sound settings, the DACs the bridge holds show as in use: that's the exclusive hold. Windows' own sound goes to another output.

Install with Docker

On an always-on Linux machine with the DACs plugged into it: DietPi on an Intel i5, a NUC, a Raspberry Pi 4/5 on a 64-bit OS. Choose docker run or docker compose, fill in what you need and copy the command. Docker builds the bridge from this repository the first time.


      

--network host is required. Audirvana and Mandarin find the bridge by multicast, which doesn't cross Docker's bridge network. Docker Desktop for Mac and Windows has no real host networking: on a Mac, use the one-line install.
The sound devices. -v /dev/snd:/dev/snd and --device-cgroup-rule 'c 116:* rmw' work together: the mount keeps the list of sound devices current, so a DAC plugged in later is found too, and the rule lets the container open every sound device, and no other device on the host. Leave out either and the DACs may not appear.
Mandarin on the same machine? If its container has --device /dev/snd, switch off its local output for the DACs (Settings → Audio Devices), so the bridge can hold them.
  1. Open http://<server-ip>:55500: a tile for each USB DAC.
  2. Point Audirvana and Mandarin at the "(Bridge)" devices.
  3. To update, run the same command again. The settings volume is kept.

If Docker isn't installed: dietpi-software install 162 (Docker) and 134 (Docker Compose) on DietPi, or curl -fsSL https://get.docker.com | sh on Debian, Ubuntu and Raspberry Pi OS. Building from the repository needs git (apt install git).

Point the apps at the bridge

Each DAC appears on your network as "<its name> (Bridge)", for example Topping D90 (Bridge).

Audirvana

Output: the Bridge, over UPnP

Settings → Audio → choose "<DAC> (Bridge)" in the UPnP list, not the DAC itself. While Audirvana still holds the DAC directly, its tile on the bridge's page says Waiting · Audirvana has the DAC. The bridge takes the DAC the moment Audirvana lets go of it.

Mandarin

A zone like any renderer

Settings → Audio Devices → choose "<DAC> (Bridge)". If Mandarin's own local output for that DAC is switched on, switch it off: the bridge owns the DAC now. Mandarin's volume sets the DAC's own, where it has one (Volume).

More ways to play

UPnP is always on. These are optional. Squeezebox, Spotify, Qobuz and the Mac's apps go through the same exclusive, rate-switched path and the same one-app-at-a-time rule: whatever is playing keeps the others out until it stops. Caldera (Plex) and Roon Bridge play to the DAC themselves, and the bridge lets go of it for them while nothing plays through it.

Squeezebox · squeezelite

Lyrion Music Server, and Roon

Switch on Squeezebox under Connections on the bridge's page. Each DAC then becomes a player, "<DAC> (Bridge)", for Lyrion Music Server (LMS) — and for Roon, with Settings → Setup → Squeezebox support on. The bridge speaks the Squeezebox protocol itself rather than running squeezelite, because squeezelite would need the DAC to itself. The server is found by itself; type its address if it isn't.

FLAC, PCM, AIFF, MP3 and Ogg are taken as they come, gapless. DSD is converted by the server. LMS's volume sets the DAC's own, where it has one; for a DAC feeding an amplifier, set the player's volume control in LMS to fixed at 100%.

Spotify Connect · Linux

Through Spotify Soloist

Spotify Soloist is Spotify's own Spotify Connect client for Linux. Switch on Spotify Connect on the bridge's page and each DAC appears in the Spotify app's device list. It needs:

· Your own Soloist API key, made at Spotify for Developers (needs Premium), typed on the page. It is never shown again.
· Soloist, which may not be passed around: Download Soloist on the page fetches Spotify's own build for the machine. A build lasts 90 days; the page shows until when.
· Linux with glibc 2.38 or later (Debian 13, Ubuntu 24.04, Raspberry Pi OS trixie), or the Docker image on anything older. There is no Soloist for macOS.
· PulseAudio (sudo apt install pulseaudio; in the Docker image already).

Soloist plays into a private PulseAudio with no sound card, only a pipe the bridge reads, so its sound takes the same exclusive path as everything else, at 44.1 kHz, paced by the DAC: nothing is resampled. If another app has the DAC, Spotify is paused. Spotify's volume sets the DAC's own, where it has one; otherwise it stays at 100%.

Soloist takes the key only on its command line, so other accounts on the same machine could read it from the process list; on a machine you share, run the bridge in Docker. On a Mac, the Spotify app itself plays through the bridge instead (below).

Qobuz Connect · Mac, Windows, Linux

Through QobuzProxy

Switch on Qobuz Connect on the bridge's page, press Install QobuzProxy (it fetches QobuzProxy into a private Python environment; Python 3.10 or later must be on the machine, and is in the Docker image), then Sign in to Qobuz on QobuzProxy's own page. Each DAC then appears in the Qobuz app's device list, as "<DAC> (Bridge)".

QobuzProxy speaks Qobuz Connect and hands each track to the DAC's UPnP device, so Qobuz plays through the bridge like Audirvana does, in hi-res up to the DAC's best rate, under the same one-app-at-a-time rule.

Plex · Linux

Through Caldera Headless

Switch on Plex · Caldera Headless on the bridge's page, press Download Caldera (from Caldera), then Sign in to Plex: the page shows a code to enter at plex.tv/link. Each DAC then appears as a player in Plexamp, Plex for iOS and the Plex web app.

Caldera plays to the DAC itself, bit-perfect. The bridge shares the DAC with it: whoever plays first has it, and the other waits. What Caldera plays shows on the DAC's page and on the now-playing screen.

This Mac · Apple Music & Spotify

The Music and Spotify apps, bit-perfect

Neither app can play to a DAC the bridge holds, or switch its rate per track. So, as Arco does for Roon, a small virtual output, DAC Bridge, is the Mac's output while this is on: the app plays to it, and the bridge plays the same samples to the DAC you choose, at each track's own rate (Spotify at 44.1 kHz), with the virtual output's clock following the DAC's. Install the output once (your password; the Mac's sound stops for a few seconds):

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/meltface-80/Mandarin-DAC-Bridge/main/tools/mac/driver.sh)"
# remove it again:  … driver.sh)" -- --remove

Needs Apple's command line tools (it offers to install them). Then switch on This Mac: Apple Music & Spotify on the bridge's page. The first time, macOS asks to allow Microphone access: that is how the bridge reads the DAC Bridge output (it never listens to a microphone). Switched off, the Mac's output goes back to what it was.

New: on a test Mac the DAC Bridge output gives back what it gets, sample for sample, at 44.1, 96 and 192 kHz. Playing from the Music and Spotify apps themselves hasn't been tried on a real Mac yet.

Roon Bridge

Downloaded from Roon Labs, run beside the bridge

Switch on Roon Bridge under Connections and press Download Roon Bridge. Like RoPieee, the bridge fetches Roon's own build from Roon Labs. On Linux and in Docker it's unpacked, checked with Roon's own check, and run by the bridge (no root, no system service; it updates itself). On a Mac the app goes into your own Applications folder (~/Applications) and is opened; on Windows Roon's installer opens. Then in Roon: Settings → Audio, and enable the DACs under this machine's name.

Roon Bridge plays to the DACs itself, so while it's on the bridge lets go of each DAC whenever nothing plays through it, and takes it back when an app plays to the bridge (once Roon has let go). A Roon Bridge installed separately is used as it is. Or skip Roon Bridge: Roon's Squeezebox support plays to the bridge directly, with Roon's Squeezebox limits.

Roon now playing: switch it on under Connections, then enable Mandarin DAC Bridge in Roon (Settings → Extensions). The pages and the now-playing screen then show the title, artist, album and cover of what Roon plays to each DAC, through Roon Bridge or the Squeezebox player, from Roon's own extension API.

Now-playing screen

A monitor beside the DAC

Open http://<machine>:55500/now full-screen on any screen: cover, title, artist, album, time and format of what's playing, from any app, and a clock when nothing is. It follows whichever DAC is playing, or one you pick (move the mouse). On a Linux machine with a monitor plugged in and no desktop, this sets it up to start at boot:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/meltface-80/Mandarin-DAC-Bridge/main/tools/linux/display.sh)"
# the bridge on another machine:  … display.sh)" -- --url http://192.168.1.20:55500/now
# remove it again:                 … display.sh)" -- --uninstall

Uninstall

Removing the bridge gives the DACs straight back to the machine and its apps. Point Audirvana and Mandarin at the DACs directly again afterwards.

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/meltface-80/Mandarin-DAC-Bridge/main/tools/mac/uninstall.sh)"
It stops the bridge, removes it from login, and deletes ~/Mandarin-DAC-Bridge (the program, its own ffmpeg, its settings and its log). Homebrew and its ffmpegs stay, because Mandarin uses them too. If you kept the ffmpeg-full an earlier installer added, the install line with -- --tidy-only removes it. The DAC Bridge output, if you installed it, stays until driver.sh removes it (above), and a Roon Bridge the bridge downloaded stays in ~/Applications.
By hand instead
launchctl unload ~/Library/LaunchAgents/app.mandarin.dacbridge.plist
rm -f ~/Library/LaunchAgents/app.mandarin.dacbridge.plist
rm -rf ~/Mandarin-DAC-Bridge

Settings

None are needed. On a DAC's page you can switch the bridge off for that DAC (it lets go and the machine can use it), choose how DSD files go (Auto, DoP or PCM), share it with other players while idle, and hand the DAC over to another app at once (Release). Squeezebox, Spotify, Qobuz, Plex and the Mac's apps are switched on under Connections. The rest are environment variables. A switch, server address or key set by a variable only sets how the bridge starts: once it is set on the page, the page's choice wins.

VariableDefaultWhat it does
PORT55500The page and the UPnP devices.
BRIDGE_IP—The address to advertise, if the machine is on several networks.
BRIDGE_HOSTNAMES—Other names you open the page by (your own DNS names), comma-separated. A browser that reached the bridge by any other name is refused.
LOCK_GRACE_S10How long an app keeps a DAC after it stops or pauses.
MANDARIN_PORT3500Mandarin's port, used to recognise its streams.
BRIDGE_ALL_OUTPUTS—1 also offers non-USB outputs.
BRIDGE_NAME_SUFFIX" (Bridge)"Added to each DAC's name on the network.
DATA_DIRdata/Where settings.json lives (/data in Docker).
FFMPEGits own, beside it; else one with SoXThe decoder.
SQUEEZELITE—1 starts with Squeezebox on (the page's switch, once used, wins).
LMS_SERVER—The Squeezebox server's address, if it isn't found by itself.
SPOTIFY—1 starts with Spotify Connect on (Spotify Soloist, Linux).
SOLOIST_API_KEY—Your Soloist API key, instead of typing it on the page.
SOLOISTdownloadedWhere the soloist program is, if not downloaded by the page.
QOBUZ—1 starts with Qobuz Connect on (QobuzProxy).
QOBUZPROXY_SOURCEGitHub releaseAnother place to install QobuzProxy from (a local copy, a fork).
CALDERA—1 starts with Plex through Caldera Headless on (Linux).
MAC_APPS—1 starts with the Music and Spotify apps on (macOS).
ROON_BRIDGE—1 starts with Roon Bridge on (downloaded from Roon Labs).
ROON_API—1 starts with Roon now playing on (Roon's extension API).
ROON_COREfound by itselfThe Roon Core, host:port, if it can't be found on the network.
SOLOIST_VERBOSE—1 runs Spotify Soloist with its detailed log.

Mac, Windows and Linux, side by side

macOSWindowsLinux / Docker
Exclusive holdCore Audio hog modeWASAPI exclusive modeALSA hw: device, held open
Rate switchingThe DAC's physical format, per trackThe exclusive stream's format, per trackhw params, per track
Bit-perfectUp to 24-bitUp to 32-bitUp to 32-bit
DSDDoP: switch it on per DACDoP: switch it on per DACDoP: on automatically for DACs reporting DSD
SpotifyThe Spotify app, through DAC Bridge—Spotify Connect (Soloist)
Apple MusicThe Music app, through DAC Bridge——
Qobuz ConnectQobuzProxyQobuzProxyQobuzProxy
Plex——Caldera Headless
Settings live~/Library/LaunchAgents/app.mandarin.dacbridge.plist (written afresh by each update)user environment variables (setx LOCK_GRACE_S 20), then sign in again; leave PORT at 55500environment: in the compose file, or -e

Troubleshooting

The log: ~/Mandarin-DAC-Bridge/data/bridge.log on a Mac, %LOCALAPPDATA%\Mandarin-DAC-Bridge\data\bridge.log on Windows, docker logs mandarin-dac-bridge in Docker.

A tile says "Waiting"

Another app has the DAC

The page names the app. Point it at the bridge's UPnP device instead, or quit it; the bridge takes the DAC within two seconds.

No "(Bridge)" devices in the apps

Discovery can't get through

The bridge and the apps must be on the same network. On a Mac, allow mandarin-dac-bridge under System Settings → Privacy & Security → Local Network. In Docker, use --network host. On a machine with several networks, set BRIDGE_IP.

"Transport is locked"

The other app is playing

That's the bridge keeping the apps apart. Stop the one playing (it keeps the DAC 10 seconds), or press Release on the DAC's page.

No DACs in Docker

The container can't see /dev/snd

Check cat /proc/asound/cards on the host lists the DAC, and that the command still has both -v /dev/snd:/dev/snd and --device-cgroup-rule 'c 116:* rmw'. docker exec mandarin-dac-bridge /app/mandarin-dac-bridge --diagnose shows what the container sees.

The Music app plays, the DAC is silent

macOS isn't letting the bridge read DAC Bridge

Allow mandarin-dac-bridge under System Settings → Privacy & Security → Microphone. Check that the Mac's sound output is DAC Bridge. mandarin-dac-bridge --loopback-test checks the output gives back exactly what it gets.