Mandarin

Your own music files, played to Sonos rooms, UPnP renderers and your phone

A server on a machine you own scans your music folders. Sonos rooms, the UPnP/DLNA renderers you switch on (a WiiM, a Chord Poly, a streamer, an AV receiver, a TV) and the Mandarin Android app play from it, controlled from a browser, an iPhone home screen or the Android app. No subscription or streaming account is needed.

v1.1.3 Docker MIT Sonos UPnP / DLNA Android app USB DAC
New in v1.1.3 (since v1.1.2). Artists, in the app and in the browser: a wall of every act in your library (Artists in the row at the foot of a phone's or tablet's screen), and a page for each: their picture, where from and when, their links and biography, a band's current members or a person's bands (with that band's albums from their years in it), similar artists, their genres, and Play and Shuffle for all their albums. Album credits under an album's tracks: who played, sang, wrote and made it. Genres as chips on album and artist pages, each opening that genre's albums. And the row at the foot of the screen has its five buttons evenly spaced, with offline shown by a pill at the top of the screen.

How it works

Mandarin is a server and an Android app. The server scans your library, controls Sonos rooms, and controls UPnP/DLNA renderers (anything with AVTransport) through their own transport, gapless where the renderer supports it, with each one's sample rates read from the device. It also plays to the Android app, on the phone or through a USB DAC on its port.

Your phone or browserMandarin's own interface
→
Mandarinlibrary, queue, conversion
→
PlayersSonos rooms, WiiM, Poly, receivers, the app

Tracks go into the speaker's own Sonos queue; a UPnP renderer is handed each next track ahead of time. Each device fetches the audio from the server: the file itself when it can play it, converted to what it takes when it can't. Features lists what Mandarin does.

Features

Tap i on any feature for how to switch it on and use it. New marks what's new in v1.1.3.

🏠Home

Rows of albums under a greeting: Not played in 6 months, Listen later, Playlists, Favourites, Recently played, Smart Picks, Label of the week, Random albums, Library and Browse by genre.

πŸ‘†Phones and tablets

On a touchscreen, either way round: a row at the foot (Menu, Home, Library, Artists, Search) under the mini player, and albums scrolling to the screen's top edge.

🎲Random Album

The first tile under the greeting chooses an album you haven't played in 12 months (until Mandarin has 12 months of history, any album) and offers it to play.

πŸ“…Album of the day

One album, the same on every device, chosen by the server at 00:01. Once played from any device, it's gone until the next 00:01.

⏳Not played in 6 months

Albums you haven't played in six months. The row stays hidden until Mandarin has six months of your listening.

πŸ“šLibrary

Every album, sorted by name, artist, release date, date added, plays, last played or random, and narrowed by genre, decade, label, format, sample rate, bit depth or first letter.

πŸ—„οΈShelf

A record shop's shelf of your collection: covers or spines, A β†’ Z by artist or album, narrowed by genre, artist letter and decade, or spun to land anywhere.

πŸ”Search

Albums, artists and labels as you type, and Qobuz's and Tidal's catalogues beside them: an album from either opens and plays whether or not it is in your library.

πŸ’ΏAlbum pages

Tracks, year and label, write-ups from Wikipedia and Qobuz, and Pitchfork's score where it reviewed the album. Step to the previous or next album of the row you came from.

🧭More by, and similar

Under an album's review: the artist's other albums, the albums they appear on, and, with a Last.fm key, similar artists and albums. Ones in your library open here.

🎀ArtistsNew

Every act in your library as a round picture with their name and album count, in the album wall's columns, sorted A to Z, by most albums or at random.

πŸ§‘β€πŸŽ€Artist pagesNew

Who they are: picture, where from and when, links, biography, genres, a band's current members or a person's bands, similar artists, and Play or Shuffle for all their albums.

πŸ‘₯Album creditsNew

Under an album's tracks: who played, sang, wrote and made it, each with their parts and the tracks they are on, from the files' tags and MusicBrainz.

🎸Genres on pagesNew

An album's genres as small chips under its cover, and an artist's, the most used across their albums. A tap opens that genre's albums.

✏️Edit album

Correct an album's title, artist or year and choose its cover. Edits are kept in the server's database; your music files are not changed.

πŸ—‚οΈMusic folders

Any number of folders the server can see, added and removed in the app. Changes show by themselves where the file system reports them, otherwise at the next scan.

πŸ’½One album per folder

Each folder is one album. Disc folders inside it, such as Disc 1 or CD2, make one album shown disc by disc, with a two-disc symbol on its cover.

πŸ“Untagged files named from folders

A file with no tags takes its artist, album, year, title and track number from its folder and file names. Nothing is written to your files.

🏷️Every tag kept

All of each file's tags are read into the server's database, including MusicBrainz IDs, barcode, catalogue number, ISRC and ReplayGain, and used to identify albums.

πŸ”ŽLibrary Scanner

Names albums from their files' identifiers first, then from MusicBrainz by tracks and lengths, with Apple's iTunes catalogue as a second source. Measures ReplayGain for files without it.

πŸ“¦MusicBrainz pack

An optional download of every MusicBrainz release with a barcode, about 2 GB on disk. Albums with a barcode are matched from it, also while musicbrainz.org is down.

▢️Now playing

Cover, track, transport, volume, the queue and history, and a badge saying what the device is being sent. An optional waveform seek bar is drawn from the audio.

β†ͺ️Move to another device

Pick another room, renderer or phone while music plays and it moves there: the queue goes with it, from the same track and second.

πŸ“»Random Album Radio

When a device's queue is running out, a random album goes on the end: never one the radio played in the last six months. Switched on per device.

πŸ”ˆSonos

Play, queue, play next, shuffle, repeat and volume for each Sonos room, and grouping and ungrouping rooms. Sonos plays up to 24-bit / 48 kHz; Mandarin converts anything above.

πŸ“‘UPnP/DLNA renderers

WiiM, Chord Poly, streamers, AV receivers and TVs on your network, played like a Sonos room. Tracks join gaplessly on a renderer that supports it.

πŸ–₯️Sound devices on the server

A USB DAC, speakers or HDMI on the computer Mandarin runs on, played like a Sonos room. Tracks at the same rate join with nothing between them.

🎚️Audio Devices

Every room, renderer and phone with the rates, depths and formats it takes, your own name for it, and a switch. Renderers play Original or upsampled Γ—2, Γ—4 or Max.

πŸ’ŽDSD

A renderer that says it takes DSD files is sent the DSF or DFF itself; others get PCM. A USB DAC on the phone gets DSD natively or as DoP.

πŸ”ŠVolume Levelling

ReplayGain per device: Off, Track, Album or Auto, a target from βˆ’14 to βˆ’25 LUFS, and a level for tracks with no loudness information.

πŸŽ›οΈDSP

A parametric EQ of up to ten bands per renderer or phone, its curve drawn as you edit, plus headphone profiles from AutoEq or a pasted ParametricEQ.txt.

βš™οΈPlayback first

Playback keeps a processor core of its own, two while DSP is in use. Scanning, ReplayGain and downloads run on the other cores, at the lowest priority.

⭐Smart Picks

Five albums a day from your own library, by artists Deezer lists as related to the ones you've played, topped up from your least-played albums.

πŸ“°Pitchfork

Pitchfork's latest album reviews and Best New Music, read in the app, with Play on any album you own.

🏒Record labels

Labels from your files' tags or a folder level: a Labels screen, logos from Discogs and FanArt.tv, lookups for albums without a label, and merging of duplicate names.

πŸ“†Label of the week

With Record labels on, one label from your library on Home all week, with its albums. A new one each Monday.

❀️Favourites

A heart on every album. Hearted albums gather on a Home row.

πŸ•’Listen later

Albums put aside to play another time, on a Home row and a screen of their own. Each comes off by itself once every track has been played.

🎡Playlists

Playlists you make, and Dynamic Playlists that follow a saved Library view. A playlist can be shared as text and imported, matched against the other library's tracks.

πŸ–ΌοΈShare card

The album as a picture, the duck in its corner, links to hear and read about it, and three acts near what you play, two of them new to you.

πŸ“ΊWall display

A full-screen now playing page for a TV or tablet.

πŸ“±The Android app

Mandarin's interface drawn natively by the app, plus lock-screen and notification controls, volume keys, a widget, a Quick Settings tile, a share sheet, Android Auto and its own updates.

🎧This phone

The phone is a player of its own: speaker, headphones or Bluetooth, with queue, history and radio. At home, your other devices can play to it while the app runs.

πŸ”ŒUSB DAC on the phone

The app's own USB Audio driver plays to a DAC on the phone's port at the file's rate and depth, without Android's mixer; DSD natively or as DoP.

⬇️Downloads

Albums kept on the phone as the original files or Opus 256, played without the server. Smart Picks, the Album of the day and new albums can download by themselves.

πŸ“‚Music on this phone

A folder of music already on the phone, watched for changes, on Home with Focus and Sort and on artists' pages, played like the library. The server never sees it.

✈️Offline mode

One switch: only the music on the phone shows, and the server isn't used. With no connection the app does the same by itself, and reconnects when it can.

🌍Away from home

On mobile data the app reaches the server through its own Tailscale, with no ports opened. Away, only the phone plays: Opus 256 or the original files.

🎨UI Settings

Text sizes, grid layout (Auto, 3 or 2 columns, or List) and tile size, saved on each device. On a desktop, text goes up to +100%.

πŸ‘€One account

One username and password, kept on the server. Devices sign in with SRP, so the password itself is never sent, and every signed-in device is listed.

⏻Restart and shut down

Restart Mandarin, or shut it down so it stops finding and controlling your speakers until you start it again. In Docker, only Restart.

πŸ’ΎBackup and restore

Back up settings, players, collection, API keys (with the Tailscale identity) and the whole database, at home or away: to the server, or from the app to a file.

🎼Qobuz

Sign in with your Qobuz subscription: your favourites and purchases become albums here, and the Qobuz browser plays anything in the catalogue, streamed through the server.

🎡Tidal

Sign in with your Tidal subscription on tidal.com: your favourite albums become albums here, and the Tidal browser plays anything in the catalogue, streamed through the server.

πŸ“Logging

Off by default. Switched on, the Android app and the server log what happened, without personal data, for you to send to the developer when something goes wrong.

πŸ”„Updates

The server updates itself from GitHub from Settings and checks every 12 hours. In the Android app one Update does the server first, then the app.

What each device is sent

Each track goes the way the device can take it, and the Now playing badge says what was sent. Conversion is ffmpeg with the SoX resampler and triangular dither; a converted track plays while it is still being written, the next tracks are prepared ahead, and replays come from a cache.

Sonos

Up to 24-bit / 48 kHz

Within that, the file as stored. Above it, or in a format Sonos can't read, FLAC at 24/48.

UPnP / DLNA renderer

Its own ceiling, read from the device

Within it, the file as stored. Above it, FLAC at the highest rate the device takes in the file's family. Upsample ×2, ×4 or Max if you choose, from its page in Audio Devices.

The Android app

The phone's own output

The file as stored within 24/48, FLAC 24/48 above it. Away from home, Opus 256 by default.

USB DAC on the phone

USB direct

The file at its own rate and depth where the DAC takes it, DSD natively or as DoP, FLAC at the DAC's highest rate otherwise.

Everything else

Formats nothing plays as they are

APE, WavPack, WMA Lossless, 24-bit WAV, more than two channels: FLAC, in stereo.

Install with Docker

On an always-on Linux machine on the same network as your speakers — a NAS, a Raspberry Pi 4/5 on a 64-bit OS, a home server. Fill in your music folders and time zone and copy the command.

Music folders on this machine

      
--network host is required. Speakers are found by multicast, which does not cross Docker's default bridge network, and they fetch audio from this machine's own address. Docker Desktop for macOS and Windows has no real host networking, so this needs a Linux host.
A USB DAC on this machine needs both --device /dev/snd (the devices handed to the container) and --privileged (the container may open them — on most hosts it otherwise can't). Tick the box above to add them. A container already running is re-created with them by the same command. Macs don't use Docker: nothing to add there.
Keep the musicd-server-data volume. It holds the library database, play history, playlists, settings and the artwork and conversion caches. The command above is a swap, not a fresh start — re-run it to update.
More than one music folder? Add each one above. A single folder is mounted as /music; with several, each becomes its own folder inside it — /mnt/nas/Albums as /music/Albums, /mnt/usb/Vinyl as /music/Vinyl — and the scan reads them all as one library. All are mounted read-only. Adding or removing a folder later is the same command re-run; albums in a folder that is gone drop out at the next scan.
The MusicBrainz pack on another drive? The pack (Settings → Library Scanner) is 2 GB, kept in the data volume unless you choose a folder. To keep it on another drive, give its host folder above: it is mounted as /packs, writable (no :ro), and you choose /packs under Folder in the app. Leave the line in every time you re-run the command, or the folder isn't there for the pack.
  1. Open http://<server-ip>:3500 on a device at home and create your account β€” one username and password, kept on the server. The first scan starts straight away; albums appear as it goes.
  2. Pick a room under Settings → Audio Devices, or from the speaker button on Now playing.
  3. Tap an album and Play Now.

If Docker isn't installed: dietpi-software install 162 on DietPi, or curl -fsSL https://get.docker.com | sh on Debian, Ubuntu and Raspberry Pi OS.

On your phone

The same interface everywhere, because it comes from the server.

iPhone & iPad

Add to Home Screen

Open http://<server-ip>:3500 in Safari, tap Share → Add to Home Screen. It opens full screen like an app, with the same interface as the Android app.

Android

The Mandarin app

Enter the port and it finds the server on your Wi-Fi by itself, or type the address. On top of the interface it adds lock-screen and notification controls, volume keys, a now-playing widget, a Quick Settings tile, a share sheet, Android Auto, This phone as a room, downloads, a USB DAC driver, and Tailscale for away from home.

Download mandarin-android.apk — the newest build, published on every version. Sideload on Android 8.0 or newer; the app updates itself from then on. The build itself is named Mandarin-Android-<version>.apk (since v0.9.7); this link keeps one name so it always points at the newest.

Installed before v0.6.10? Those builds used a different signing key, so Android refuses the update. Uninstall once, install the download above and sign in again; downloaded albums need downloading again. Updates install in place after that.

Environment variables

All optional; pass any with -e NAME=value. Everything else is in the app's Settings and kept in the data volume.

VariableDefaultPurpose
PORT3500The interface, the API, and where speakers fetch audio.
TZUTCAlbum of the day changes over at 00:01 and Smart Picks at local midnight.
SONOS_HOSTS—A speaker's IP, for when multicast discovery is unreliable. One is enough.
UPNP_HOSTS—UPnP/DLNA renderers to ask by address (an IP or a description URL), for when multicast discovery misses them.
IDENTIFYon0 leaves the album identification scan out entirely.
ITUNES_COUNTRYUSThe Apple store the identification scan's iTunes lookups use (GB, DE…).
SERVER_IPautoThe address speakers fetch audio from, on hosts with several interfaces.
INCLUDE_ZONES—Offer only these rooms, e.g. Kitchen,Study.
EXCLUDE_ZONES—Offer every room except these.
SCAN_INTERVAL_HOURS6How often the music folders are re-checked in full. Changes are noticed as they happen where the file system reports them.
TRANSCODE_CACHE_GB4Disk kept for converted hi-res tracks.
TRANSCODE_CONCURRENCY2Tracks converted at once.
LASTFM_KEY—A Last.fm key for similar artists and albums on album and artist pages, until one is saved in Settings.
DEBUG—Log every API call.

Logs

Logging is in the Android app and the server, and is off by default: while it is off, nothing is collected. Switch it on in the app, Settings β†’ Setup β†’ Logging, when something isn't right. Use the app as you normally would for 24 hours, then tap Save log: the app's log and the server's are saved together to Downloads/Mandarin on the phone, for you to send to the developer. Clear log empties it; switching logging off deletes it, the server's part too.

The log is made without personal data, and the app says so on the same screen. Please look through it before you send it: what you send is your choice.

Captured

What it captures

  • Screens: each screen opened and how long after the tap, what is on screen, where each tap fell, and anything that doesn't flow: a screen shown for a moment between two others, one opened and closed by itself, a frame that stalls.
  • Requests: each request the app makes to the server: its address on the server (such as /api/album), how long it took and whether it worked. The values in it are left out.
  • The way to the server: home, away (Tailscale) or offline, and networks coming and going.
  • The phone's player: playing or paused, the track's number in the queue, its position and the volume.
  • The app: its memory, crashes and freezes, and Android's own lines for Mandarin.
  • The server: each line it logs, with its own words, numbers, the names of errors and its own addresses; every other value is replaced with […].
Left out

What it leaves out

  • Addresses: no IP addresses (home, Tailscale or internet), web addresses, device hardware addresses or network names.
  • People, rooms and devices: no names of people, rooms or devices, no account names or emails, and no passwords, tokens or keys.
  • What you play and search for: no album, artist, track or playlist names, and no searches.
  • Files and folders: no paths or file names.
  • Your phone: only the app's version and Android's; not the phone's make or model.

The app keeps its log in its own storage (two files of 3 MB at most); the server keeps its part in its data folder (two files of 1 MB at most) while logging is on.

Troubleshooting

http://<server-ip>:3500/api/health answers without signing in; /api/status (signed in) says what the server can see. Forgot the password? docker exec musicd-server /app/server/bin/mandarin-server reset-password, then create the account again.

No rooms

"No Sonos rooms found yet"

Check the container is on host networking and on the same subnet as the speakers, then look at sonos.error in /api/status. Setting SONOS_HOSTS to one speaker's IP skips discovery.

Silence

A room skips every track

The speakers can't reach the server. On a machine with several network interfaces set SERVER_IP to its LAN address, and check nothing blocks the port.

No albums

The library is empty

/api/status shows music_dir and index_count. Check the -v …:/music:ro mount and that the folder is readable.

Hi-res

96 kHz files won't play

/api/health must say "ffmpeg": true. The Docker image includes it; a hand-run server needs ffmpeg on the PATH.