This is raw, unfiltered and experimental.
Resonate is a multi-room music experience protocol. The goal of the protocol is to orchestrate all devices that make up the music listening experience. This includes outputting audio on multiple speakers simultaneously, screens and lights visualizing the audio or album art, and wall tablets providing media controls.
- Resonate Server - orchestrates all devices, generates audio streams, manages players and clients, provides metadata
- Resonate Client - a client that can play audio, visualize audio, display metadata, or provide music controls. Has different possible roles (player, metadata, controller, visualizer). Every client has a unique identifier
- Player - receives audio and plays it in sync. Has its own volume and mute state and preferred format settings
- Controller - controls the Resonate group this client is part of
- Metadata - displays metadata. Has preferred format for cover art
- Visualizer - visualizes music. Has preferred format for audio features
- Resonate Group - a group of clients. Each client belongs to exactly one group, and every group has at least one client. Every group has a unique identifier. Each group has the following states: list of member clients, volume, mute, and active session (may be null)
- Resonate Session - details the currently playing media and its playback state. Has associated metadata and a unique identifier. Each session is associated with exactly one group
- Resonate Stream - client-specific details on how the server is formatting and sending binary data. Each client receives its own independently encoded stream based on its capabilities and preferences. The server sends audio chunks as far ahead as the client's buffer capacity allows
Resonate has two standard ways to establish connections: Server and Client initiated.
Resonate Servers must support both methods described below.
Clients announce their presence via mDNS using:
- Service type:
_resonate._tcp.local. - Port: The port the Resonate client is listening on (recommended:
8927) - TXT record:
pathkey specifying the WebSocket endpoint (recommended:/resonate)
The server discovers available clients through mDNS and connects to each client via WebSocket using the advertised address and path.
Note: Do not manually connect to servers if you are advertising _resonate._tcp.
If clients prefer to initiate the connection instead of waiting for the server to connect, the server must be discoverable via mDNS using:
- Service type:
_resonate-server._tcp.local. - Port: The port the Resonate server is listening on (recommended:
8927) - TXT record:
pathkey specifying the WebSocket endpoint (recommended:/resonate)
Clients discover the server through mDNS and initiate a WebSocket connection using the advertised address and path.
Note: Do not advertise _resonate._tcp if the client plans to initiate the connection.
Note: After this point, Resonate works independently of how the connection was established. The Resonate client is always the consumer of data like audio or metadata, regardless of who initiated the connection.
While custom connection methods are possible for specialized use cases (like remotely accessible web-browsers, mobile apps), most clients should use one of the two standardized methods above if possible.
Once the connection is established, Client and Server are going to talk.
The first message must always be a client/hello message from the client to the server.
Once the server receives this message, it responds with a server/hello message. Before this handshake is complete, no other messages should be sent.
WebSocket text messages are used to send JSON payloads.
Note: In field definitions, ? indicates an optional field (e.g., field?: type means the field may be omitted).
Message format:
{
"type": "stream/start",
"payload": { ... }
}WebSocket binary messages are used to send audio chunks, media art, and visualization data. The first byte is a uint8 representing the message type.
Clients continuously send client/time messages to maintain an accurate offset from the server's clock. The frequency of these messages is determined by the client based on network conditions and clock stability.
Binary audio messages contain timestamps in the server's time domain indicating when the audio should be played. Clients use their computed offset to translate server timestamps to their local clock for synchronized playback.
- Each client is responsible for maintaining synchronization with the server's timestamps
- Clients maintain accurate sync by adding or removing samples using interpolation to compensate for clock drift
- When a client cannot maintain sync (e.g., buffer underrun), it should mute its audio output and continue buffering until it can resume synchronized playback
- The server is unaware of individual client sync synchronized accuracy - it simply broadcasts timestamped audio
- Late-joining clients receive audio with future timestamps only, allowing them to start playback in sync with existing clients
sequenceDiagram
participant Client
participant Server
Note over Client,Server: WebSocket connection established
Note over Client,Server: Text messages = JSON payloads, Binary messages = Audio/Art/Visualization
Client->>Server: client/hello (roles and capabilities)
Server->>Client: server/hello (server info)
alt Player role
Client->>Server: player/update (initial state, volume, muted)
end
loop Continuous clock sync
Client->>Server: client/time (client clock)
Server->>Client: server/time (timing + offset info)
end
Client->>Server: group/get-list
Server->>Client: group/list (available groups + state)
Client->>Server: group/join (join a group)
alt Client has active stream
Server->>Client: stream/end
end
alt New group has active stream
Server->>Client: stream/start (codec, format details)
end
Server->>Client: session/update (group_id, playback_state, metadata)
Server->>Client: group/update (commands, group_volume, members)
loop During playback
Server->>Client: binary Type 1 (audio chunks with timestamps)
Server-->>Client: binary Type 2 (media art)
Server-->>Client: binary Type 3 (visualization data)
end
alt Player requests format change
Client->>Server: stream/request-format (codec, sample_rate, etc)
Server->>Client: stream/update (new format)
end
alt Controller role
Client->>Server: group/command (play/pause/volume/etc)
end
alt Player role state changes
Client->>Server: player/update (state changes)
end
Client->>Server: group/unjoin (leave group)
alt Client has active stream
Server->>Client: stream/end
end
Server->>Client: stream/end (stop playback)
alt Player role
Client->>Server: player/update (idle state)
end
This section describes the fundamental messages that establish communication between clients and the server. These messages handle initial handshakes, ongoing clock synchronization, and stream lifecycle management. Every Resonate client and server must implement all messages in this section regardless of their specific roles. Role-specific payload details are documented in their respective role sections.
First message sent by the client after establishing the WebSocket connection. Contains information about the client's capabilities and roles.
This message will be followed by a server/hello message from the server.
Players that can output audio should have the role player.
client_id: string - uniquely identifies the client for groups and de-duplicationname: string - friendly name of the clientdevice_info?: object - optional information about the deviceproduct_name?: string - device model/product namemanufacturer?: string - device manufacturer namesoftware_version?: string - software version of the client (not the Resonate version)
version: integer - version that the Resonate client implementssupported_roles: string[] - at least one of:player- outputs audiocontroller- controls the current Resonate groupmetadata- displays metadatavisualizer- visualizes audio
player_support?: object - only ifplayerrole is set (see player support object details)metadata_support?: object - only ifmetadatarole is set (see metadata support object details)visualizer_support?: object - only ifvisualizerrole is set (see visualizer support object details)
Sends current internal clock timestamp (in microseconds) to server.
Once received by the server, the server responds with a server/time message containing timing information to establish clock offsets.
client_transmitted: integer - client's internal clock timestamp in microseconds
Response to the client/hello message with information about the server.
Only after receiving this message should the client send any other messages (including client/time and the initial player/update message if the client has the player role).
server_id: string - identifier of the servername: string - friendly name of the serverversion: integer - latest supported version of Resonate
Response to the client/time message with timestamps to establish clock offsets.
For synchronization, all timing is relative to the server's monotonic clock. These timestamps have microsecond precision and are not necessarily based on epoch time.
client_transmitted: integer - client's internal clock timestamp received in theclient/timemessageserver_received: integer - timestamp that the server received theclient/timemessage in microsecondsserver_transmitted: integer - timestamp that the server transmitted this message in microseconds
When a new stream starts.
player?: object - only sent to clients with theplayerrole (see player object details)metadata?: object - only sent to clients with themetadatarole that specified supported picture formats (see metadata object details)visualizer?: object - only sent to clients with thevisualizerrole (see visualizer object details)
When the format of the messages changes for the ongoing stream. Deltas updating only the relevant fields.
player?: object - only sent to clients with theplayerrole (see player object details)metadata?: object - only sent to clients with themetadatarole that specified supported picture formats (see metadata object details)visualizer?: object - only sent to clients with thevisualizerrole (see visualizer object details)
Player should stop streaming and clear buffers - report idle state. Visualizer should stop visualizing and clear buffers.
No payload.
Delta updates that must be merged into existing state. Fields set to null should be nullified. The server should null the metadata whenever a session is ended.
group_id: string - group identifierplayback_state?: 'playing' | 'paused' | 'stopped' - only sent to clients withcontrollerormetadatarolesmetadata?: object - only sent to clients withmetadatarole (see metadata object details)
This section describes messages specific to clients with the player role, which handle audio output and synchronized playback. Player clients receive timestamped audio data, manage their own volume and mute state, and can request different audio formats based on their capabilities and current conditions.
The player_support object in client/hello has this structure:
player_support: objectsupport_formats: object[] - list of supported audio formats in priority order (first is preferred)codec: 'opus' | 'flac' | 'pcm' - codec identifierchannels: integer - supported number of channels (e.g., 1 = mono, 2 = stereo)sample_rate: integer - sample rate in Hz (e.g., 44100)bit_depth: integer - bit depth for this format (e.g., 16, 24)
buffer_capacity: integer - max size in bytes of compressed audio messages in the buffer, that are yet to be playedsupported_commands: string[] - subset of:volume,mute
Informs the server of player state changes. Only for clients with the player role.
This message must always be sent after establishing the connection and state updates, including when the volume was changed through a player/command received from the server or when a volume button was pressed locally.
Must be sent immediately after receiving server/hello and whenever any state changes.
state: 'synchronized' | 'error' - state of the player, should always besynchronizedunless there is an error preventing current or future playback (unable to keep up, issues keeping the clock in sync, etc)volume: integer - range 0-100muted: boolean - mute state
Request different stream format (upgrade or downgrade). Only for clients with the player role.
codec?: 'opus' | 'flac' | 'pcm' - requested codec identifierchannels?: integer - requested number of channels (e.g., 1 = mono, 2 = stereo)sample_rate?: integer - requested sample rate in Hz (e.g., 44100, 48000)bit_depth?: integer - requested bit depth (e.g., 16, 24)
Response: stream/update with the new format.
Note: Clients should use this message to adapt to changing network conditions or CPU constraints. The server maintains separate encoding for each client, allowing heterogeneous device capabilities within the same group.
Request the player to perform an action, e.g., change volume or mute state.
command: 'volume' | 'mute' - must be one of the values listed insupported_commandsin theplayer_supportobject in theclient/hellomessagevolume?: integer - volume range 0-100, only set ifcommandisvolumemute?: boolean - true to mute, false to unmute, only set ifcommandismute
The player object in stream/start has this structure:
player: objectcodec: string - codec to be usedsample_rate: integer - sample rate to be usedchannels: integer - channels to be usedbit_depth: integer - bit depth to be usedcodec_header?: string - Base64 encoded codec header (if necessary; e.g., FLAC)
The player object in stream/update has this structure with delta updates:
player: objectcodec: string - codec to be usedsample_rate: integer - sample rate to be usedchannels: integer - channels to be usedbit_depth: integer - bit depth to be usedcodec_header?: string - Base64 encoded codec header (if necessary; e.g., FLAC)
Binary messages should be rejected if there is no active stream.
- Byte 0: message type
1(uint8) - Bytes 1-8: timestamp (big-endian int64) - server clock time in microseconds when this data should be presented/played
- Rest of bytes: encoded audio frame
The timestamp indicates when exactly the first sample in the chunk should leave the device.
This section describes messages specific to clients with the controller role, which enables the client to control the Resonate group this client is part of, and basic switching between groups.
Every client which lists the controller role in the supported_roles of the client/hello message needs to implement all messages in this section.
Control the group that's playing. Only valid from clients with the controller role.
command: 'play' | 'pause' | 'stop' | 'next' | 'previous' | 'volume' | 'mute' |repeat_off|repeat_one|repeat_all|shuffle|unshuffle- must be one of the values listed ingroup/updatefieldsupported_commandsvolume?: integer - volume range 0-100, only set ifcommandisvolumemute?: boolean - true to mute, false to unmute, only set ifcommandismute
Request the server to switch this client to a different group.
For a client with the player role this will:
- Cycle the group this client is part of through all combinations of: all other playing groups, all other playing players, and just itself
For a client without the
playerrole this will: - Cycle the group this client is part of through all combinations of: all other playing groups, and all other playing players
No payload.
Group state update.
supported_commands: string[] - subset of: 'play' | 'pause' | 'stop' | 'next' | 'previous' | 'volume' | 'mute' |repeat_off|repeat_one|repeat_all|shuffle|unshufflevolume: integer - range 0-100muted: boolean - mute state
This section describes messages specific to clients with the metadata role, which handle display of track information, artwork, and playback state. Metadata clients receive session updates with track details and can optionally receive artwork in their preferred format and resolution.
The metadata_support object in client/hello has this structure:
metadata_support: objectsupport_picture_formats: string[] - supported media art image formats (empty array if no art desired)media_width?: integer - max width in pixels (if only width set, scales preserving aspect ratio)media_height?: integer - max height in pixels (if only height set, scales preserving aspect ratio)
The metadata object in stream/start (sent to clients that specified supported picture formats) has this structure:
metadata: objectart_format: 'bmp' | 'jpeg' | 'png' - format of the encoded image
The metadata object in stream/update has this structure with delta updates:
metadata: objectart_format: 'bmp' | 'jpeg' | 'png' - format of the encoded image
The metadata object in session/update has this structure:
Clients can calculate the current track position at any time using the last received values: current_track_progress_ms = max(min(metadata.track_progress + (current_time - metadata.timestamp) * metadata.playback_speed / 1000000, metadata.track_duration), 0)
metadata: objecttimestamp: integer - server clock time in microseconds for when this metadata is validtitle?: string | null - track titleartist?: string | null - primary artist(s)album_artist?: string | null - album artist(s)album?: string | null - album nameartwork_url?: string | null - URL to artwork image. Useful for clients that want to forward metadata to external systems or for powerful clients that can fetch and process images themselvesyear?: integer | null - release yeartrack?: integer | null - track numbertrack_progress?: integer | null - current playback position in milliseconds (since start of track, at the giventimestamp)track_duration?: integer | null - total track length in millisecondsplayback_speed?: integer | null - playback speed multiplier * 1000 (e.g., 1000 = normal speed, 1500 = 1.5x speed, 500 = 0.5x speed)repeat?: 'off' | 'one' | 'all' | null - repeat modeshuffle?: boolean | null - shuffle mode enabled/disabled
Binary messages should be rejected if there is no active stream.
- Byte 0: message type
2(uint8) - Bytes 1-8: timestamp (big-endian int64) - server clock time in microseconds when this data should be presented/played
- Rest of bytes: encoded image
The timestamp indicates when this artwork becomes valid for display.
This section describes messages specific to clients with the visualizer role, which create visual representations of the audio being played. Visualizer clients receive audio analysis data like FFT information that corresponds to the current audio timeline.
The visualizer_support object in client/hello has this structure:
visualizer_support: object- Desired FFT details (to be determined)
buffer_capacity: integer - max size in bytes of visualization data messages in the buffer, that are yet to be displayed
The visualizer object in stream/start has this structure:
visualizer: object- FFT details (to be determined)
The visualizer object in stream/update has this structure with delta updates:
visualizer: object- FFT details (to be determined)
Binary messages should be rejected if there is no active stream.
- Byte 0: message type
3(uint8) - Bytes 1-8: timestamp (big-endian int64) - server clock time in microseconds when this data should be presented/played
- Rest of bytes: visualization data
The timestamp indicates when this visualization data corresponds to the audio.