This guide describes what you need to implement in order to integrate sendspin-cpp into your application. The library provides the Sendspin protocol, audio decoding, and time synchronization. You provide the audio output, network readiness, and optional persistence.
Integration follows this pattern:
- Create a
SendspinClientwith a configuration struct - Add roles (player, controller, metadata, artwork, visualizer, color) depending on what your application needs
- Implement listener interfaces for the roles you added
- Implement a network provider (required) and optionally a persistence provider
- Wire listeners and providers to the client and roles
- Start the server and run the main loop
The only role with a required callback is the player role (on_audio_write). All other listener methods have default no-op implementations.
Include sendspin/client.h for the client class, config types, and shared types. Role headers must be included explicitly for any roles you use:
#include "sendspin/client.h" // SendspinClient, providers, listeners
#include "sendspin/player_role.h" // PlayerRole, PlayerRoleListener
#include "sendspin/controller_role.h" // ControllerRole, ControllerRoleListener
#include "sendspin/metadata_role.h" // MetadataRole, MetadataRoleListener
#include "sendspin/artwork_role.h" // ArtworkRole, ArtworkRoleListener
#include "sendspin/visualizer_role.h" // VisualizerRole, VisualizerRoleListener
#include "sendspin/color_role.h" // ColorRole, ColorRoleListenerOnly include the role headers you need. client.h includes sendspin/config.h (all configuration structs, including SendspinClientConfig) and sendspin/types.h transitively.
using namespace sendspin;
// Optional: set log level before creating the client (host builds only, no-op on ESP-IDF)
SendspinClient::set_log_level(LogLevel::INFO);
SendspinClientConfig config;
config.client_id = "my-device-mac-addr"; // Unique identifier (e.g., MAC address)
config.name = "Living Room Speaker"; // Friendly display name
config.product_name = "My Speaker"; // Device product name (optional)
config.manufacturer = "My Company"; // Manufacturer name (optional)
config.software_version = "1.0.0"; // Software version string (optional)
SendspinClient client(std::move(config));Add only the roles your application needs. All roles must be added before calling start_server().
The player role handles audio decoding and synchronized playback. It requires a configuration struct that declares which audio formats your hardware supports.
PlayerRoleConfig player_config;
player_config.audio_formats = {
{SendspinCodecFormat::FLAC, 2, 44100, 16},
{SendspinCodecFormat::FLAC, 2, 48000, 16},
{SendspinCodecFormat::OPUS, 2, 48000, 16},
{SendspinCodecFormat::PCM, 2, 44100, 16},
{SendspinCodecFormat::PCM, 2, 48000, 16},
};
player_config.audio_buffer_capacity = 1000000; // Ring buffer size in bytes (default: 1000000)
player_config.fixed_delay_us = 0; // Fixed delay offset in microseconds
player_config.initial_static_delay_ms = 0; // Initial user-adjustable delay
player_config.extra_startup_silence_ms = 50; // Extra startup silence for decode headroom (default: 50)
auto& player = client.add_player(std::move(player_config));Each AudioSupportedFormatObject declares a codec/channels/sample_rate/bit_depth combination. The server selects from these when establishing an audio stream.
The stream parameters negotiated by the server are available via get_current_stream_params(), which returns a ServerPlayerStreamObject with these fields:
| Field | Type | Description |
|---|---|---|
codec |
std::optional<SendspinCodecFormat> |
Audio codec |
sample_rate |
std::optional<uint32_t> |
Sample rate in Hz |
channels |
std::optional<uint8_t> |
Number of channels |
bit_depth |
std::optional<uint8_t> |
Bits per sample |
codec_header |
std::optional<std::string> |
Codec-specific header data |
Call is_complete() on the object to check if all fields have values.
Lets your application send transport commands (play, pause, next, etc.) and receive the server's controller state (volume, mute, repeat, shuffle, supported commands).
auto& controller = client.add_controller();Receives track metadata (title, artist, album, progress, etc.) from the server.
auto& metadata = client.add_metadata();Receives album artwork images from the server. Requires a configuration struct declaring preferred image formats per slot.
ArtworkRoleConfig artwork_config;
artwork_config.preferred_formats = {
{0, SendspinImageSource::ALBUM, SendspinImageFormat::JPEG, 300, 300},
};
auto& artwork = client.add_artwork(std::move(artwork_config));Receives real-time beat, loudness, peak frequency, and spectrum data synchronized to playback.
VisualizerSupportObject vis_support;
vis_support.types = {
VisualizerDataType::BEAT,
VisualizerDataType::LOUDNESS,
VisualizerDataType::F_PEAK,
VisualizerDataType::SPECTRUM,
};
vis_support.buffer_capacity = 8192;
vis_support.batch_max = 4;
vis_support.spectrum = VisualizerSpectrumConfig{
.n_disp_bins = 32,
.scale = VisualizerSpectrumScale::MEL,
.f_min = 40,
.f_max = 16000,
.rate_max = 30,
};
auto& visualizer = client.add_visualizer({.support = vis_support});Receives an RGB color palette derived by the server from the currently playing audio (e.g., extracted from album artwork). Useful for LED matrices, status lights, or themed displays. Server-to-client only; no configuration.
auto& color = client.add_color();The on_audio_write method is the only pure virtual (required) method in the entire library.
struct MyPlayerListener : PlayerRoleListener {
// REQUIRED: Write decoded PCM audio to your audio output.
// Called from a background thread. May block up to timeout_ms.
// Must return the number of bytes actually written.
size_t on_audio_write(uint8_t* data, size_t length, uint32_t timeout_ms) override {
return my_audio_output.write(data, length, timeout_ms);
}
// Optional: Called when a new audio stream starts.
// Use this to configure your audio output with the new stream parameters.
void on_stream_start() override {
auto& params = player_ref.get_current_stream_params();
my_audio_output.configure(*params.sample_rate, *params.channels, *params.bit_depth);
}
// Optional: Called when the audio stream ends.
void on_stream_end() override {
my_audio_output.clear();
}
// Optional: Called when the server changes the volume.
void on_volume_changed(uint8_t volume) override {
my_audio_output.set_volume(volume);
}
// Optional: Called when the server changes the mute state.
void on_mute_changed(bool muted) override {
my_audio_output.set_muted(muted);
}
// Optional: Called when the server changes the static delay.
void on_static_delay_changed(uint16_t delay_ms) override { }
};Your audio output must report back when audio frames have been played. This feedback drives the library's synchronization. Call notify_audio_played() from your audio output callback:
// In your audio output's playback callback (e.g., PortAudio callback):
player.notify_audio_played(frames_played, current_timestamp_us);frames_played: Number of audio frames (not bytes) just playedtimestamp: Client timestamp in microseconds when the audio will finish playing (e.g., fromstd::chrono::steady_clock)
This method is thread-safe and is expected to be called from an audio callback thread.
struct MyMetadataListener : MetadataRoleListener {
void on_metadata(const ServerMetadataStateObject& md) override {
// Overwrite display state on every call so server clears (nullopt) propagate.
display_title(md.title.value_or(""));
display_artist(md.artist.value_or(""));
display_album(md.album.value_or(""));
if (md.progress) {
update_progress_bar(md.progress->track_progress, md.progress->track_duration);
} else {
clear_progress_bar();
}
}
};The ServerMetadataStateObject contains these fields (all optional except timestamp):
| Field | Type | Description |
|---|---|---|
timestamp |
int64_t |
Server clock µs at which this metadata becomes valid; delivery is held until the synced client clock reaches it |
title |
std::optional<std::string> |
Track title |
artist |
std::optional<std::string> |
Track artist |
album_artist |
std::optional<std::string> |
Album artist |
album |
std::optional<std::string> |
Album name |
artwork_url |
std::optional<std::string> |
Artwork URL |
year |
std::optional<uint16_t> |
Release year |
track |
std::optional<uint16_t> |
Track number |
progress |
std::optional<MetadataProgressObject> |
Playback progress (see below) |
MetadataProgressObject contains track_progress (ms), track_duration (ms), and playback_speed.
A field is nullopt when the server has not provided it or has explicitly cleared it. Listeners that mirror metadata into display state should overwrite the displayed value on every on_metadata() call (using e.g. value_or("")) so that server clears propagate.
You can also poll track progress at any time:
uint32_t progress_ms = metadata.get_track_progress_ms(); // Interpolated
uint32_t duration_ms = metadata.get_track_duration_ms(); // 0 = unknown/livestruct MyControllerListener : ControllerRoleListener {
void on_controller_state(const ServerStateControllerObject& state) override {
// Update UI with server-side volume, mute, repeat, and shuffle state
update_volume_slider(state.volume);
update_mute_button(state.muted);
update_repeat_icon(state.repeat);
update_shuffle_icon(state.shuffle);
// Enable/disable buttons based on supported commands
enable_buttons(state.supported_commands);
}
};The artwork role uses a dedicated decode thread for the CPU-bound decode step and the main loop for scheduled display. on_image_decode() fires on the decode thread immediately when encoded image data arrives; once decode returns, the server display timestamp is handed off to the main loop, which fires on_image_display() once the timestamp is reached. If a newer frame for the same slot finishes decoding before its predecessor's display fires, only the newer one is delivered. Lifecycle callbacks also fire on the main loop thread.
struct MyArtworkListener : ArtworkRoleListener {
// THREAD SAFETY: Called from the dedicated decode thread.
// Decode the encoded image synchronously (e.g., JPEG to bitmap).
// The data pointer is valid for the duration of this call.
void on_image_decode(uint8_t slot, const uint8_t* data, size_t length,
SendspinImageFormat format) override {
decoded_images[slot] = decode_image(data, length, format);
}
// Called from the main loop thread once the server display timestamp is reached.
// Swap the decoded image onto the display.
void on_image_display(uint8_t slot) override {
display.show_image(slot, decoded_images[slot]);
}
// Called from the main loop thread when artwork should be cleared.
void on_image_clear(uint8_t slot) override {
display.clear_slot(slot);
}
};struct MyVisualizerListener : VisualizerRoleListener {
// THREAD SAFETY: Called from a dedicated drain thread. Copy data quickly
// and defer heavy processing.
void on_visualizer_frame(const VisualizerFrame& frame) override {
if (frame.loudness) update_vu_meter(*frame.loudness);
if (frame.peak_freq) update_peak_display(*frame.peak_freq);
if (!frame.spectrum.empty()) update_spectrum_bars(frame.spectrum);
}
// Called from the drain thread on beat events.
void on_beat(int64_t client_timestamp) override {
trigger_beat_animation();
}
// Called from the main loop thread.
void on_visualizer_stream_start(const ServerVisualizerStreamObject& stream) override { }
void on_visualizer_stream_end() override { }
void on_visualizer_stream_clear() override { }
};struct MyColorListener : ColorRoleListener {
void on_color(const ServerColorStateObject& c) override {
if (c.background_dark) set_dark_bg(*c.background_dark);
if (c.background_light) set_light_bg(*c.background_light);
if (c.primary) set_primary((*c.primary)[0], (*c.primary)[1], (*c.primary)[2]);
// accent, on_dark, on_light...
}
// Called when the connection is lost and cached colors are dropped.
// Reset any displayed colors to a neutral or default state.
void on_color_clear() override {
reset_to_defaults();
}
};The ServerColorStateObject contains a timestamp and six optional RgbColor fields (std::array<uint8_t, 3>, ordered [R, G, B]):
| Field | Description |
|---|---|
timestamp |
Server clock µs at which this color update becomes valid; delivery is held until the synced client clock reaches it, or fires immediately if there is no active connection |
background_dark |
Background suitable for dark mode; safe contrast with white text and on_dark |
background_light |
Background suitable for light mode; safe contrast with black text and on_light |
primary |
Dominant color, not adjusted for contrast |
accent |
Secondary or complementary color, not adjusted for contrast |
on_dark |
Light foreground for use on dark backgrounds |
on_light |
Dark foreground for use on light backgrounds |
A field is nullopt when the server has not provided it or has explicitly cleared it; listeners do not need to distinguish those cases.
The library needs to know when the network is available. This is the only required provider.
struct MyNetworkProvider : SendspinNetworkProvider {
bool is_network_ready() override {
return wifi_is_connected(); // Your platform's network check
}
};On host platforms where the network is always available, return true:
struct HostNetworkProvider : SendspinNetworkProvider {
bool is_network_ready() override { return true; }
};Allows the library to persist and restore state across reboots. Useful on embedded devices.
struct MyPersistenceProvider : SendspinPersistenceProvider {
// Save/load the hash of the last server that was playing audio.
// Used to prioritize reconnection to the same server.
bool save_last_server_hash(uint32_t hash) override {
return nvs_write("last_server", hash);
}
std::optional<uint32_t> load_last_server_hash() override {
uint32_t hash;
if (nvs_read("last_server", &hash)) return hash;
return std::nullopt;
}
// Save/load the player's user-adjustable static delay.
bool save_static_delay(uint16_t delay_ms) override {
return nvs_write("static_delay", delay_ms);
}
std::optional<uint16_t> load_static_delay() override {
uint16_t delay;
if (nvs_read("static_delay", &delay)) return delay;
return std::nullopt;
}
};Receives client-level events.
struct MyClientListener : SendspinClientListener {
// Called when group state changes (playback state, group name, etc.)
void on_group_update(const GroupUpdateObject& group) override {
if (group.playback_state) update_playback_indicator(*group.playback_state);
if (group.group_name) update_group_display(*group.group_name);
}
// Called after a time sync burst completes.
void on_time_sync_updated(float error) override {
log_sync_quality(error);
}
// Called when the library needs low-latency networking (e.g., during active streaming).
// Use this to disable WiFi power saving on ESP32.
void on_request_high_performance() override {
esp_wifi_set_ps(WIFI_PS_NONE);
}
// Called when the library no longer needs low-latency networking.
void on_release_high_performance() override {
esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
}
};Listeners and providers are set as raw pointers. They must outlive the client.
MyPlayerListener player_listener;
MyMetadataListener metadata_listener;
MyControllerListener controller_listener;
MyClientListener client_listener;
MyNetworkProvider network_provider;
MyPersistenceProvider persistence_provider;
player.set_listener(&player_listener);
metadata.set_listener(&metadata_listener);
controller.set_listener(&controller_listener);
client.set_listener(&client_listener);
client.set_network_provider(&network_provider); // Required
client.set_persistence_provider(&persistence_provider); // Optional// Start the WebSocket server and sync task.
// Task priorities and PSRAM settings are taken from SendspinClientConfig.
if (!client.start_server()) {
// Handle failure
return 1;
}
// Optionally initiate a client-side connection to a known server URL.
// Without this, the client waits for incoming server connections.
client.connect_to("ws://192.168.1.10:8928/sendspin");
// Main loop: call loop() periodically to process events.
while (running) {
client.loop();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
// Clean shutdown
client.disconnect(SendspinGoodbyeReason::SHUTDOWN);If you added the controller role, use it to send playback commands:
controller.send_command(SendspinControllerCommand::PLAY);
controller.send_command(SendspinControllerCommand::PAUSE);
controller.send_command(SendspinControllerCommand::NEXT);
controller.send_command(SendspinControllerCommand::PREVIOUS);
controller.send_command(SendspinControllerCommand::STOP);
controller.send_command(SendspinControllerCommand::SHUFFLE);
controller.send_command(SendspinControllerCommand::UNSHUFFLE);
controller.send_command(SendspinControllerCommand::REPEAT_OFF);
controller.send_command(SendspinControllerCommand::REPEAT_ONE);
controller.send_command(SendspinControllerCommand::REPEAT_ALL);
// Volume and mute take additional arguments
controller.send_command(SendspinControllerCommand::VOLUME, 75); // Volume 0-100
controller.send_command(SendspinControllerCommand::MUTE, {}, true); // Mute on
controller.send_command(SendspinControllerCommand::MUTE, {}, false); // Mute offIn addition to the references returned by add_*(), you can access roles at any time through the client's accessor methods. These return nullptr if the role was not added.
if (auto* p = client.player()) {
p->update_volume(75);
}
if (auto* c = client.controller()) {
c->send_command(SendspinControllerCommand::NEXT);
}
if (auto* m = client.metadata()) {
uint32_t progress = m->get_track_progress_ms();
}
if (auto* a = client.artwork()) { /* ... */ }
if (auto* v = client.visualizer()) { /* ... */ }
if (auto* col = client.color()) { /* ... */ }Use these accessors when the role reference from add_*() is out of scope.
Note: Role registration methods (
add_player(), etc.), accessor methods (player(), etc.), and their backing members are conditionally compiled based onSENDSPIN_ENABLE_*flags. When a role is disabled at build time, callingadd_player()orclient.player()is a compile error, not a runtime nullptr. See Compile-Time Role Selection below.
Report local state changes back to the server:
player.update_volume(75);
player.update_muted(false);
player.update_static_delay(50); // User-adjustable delay in ms
// Enable/disable static delay adjustment by the server. When disabled, the stored delay
// is not applied to sync timing and is reported as 0 in client state.
player.set_static_delay_adjustable(true);Report the client's overall state to the server. Use this when your device switches to an external audio source or encounters an error:
client.update_state(SendspinClientState::EXTERNAL_SOURCE); // Playing from another source
client.update_state(SendspinClientState::ERROR); // Error condition
client.update_state(SendspinClientState::SYNCHRONIZED); // Back to normalThe client and roles expose query methods for polling state in your main loop or UI update cycle:
// Client state
bool connected = client.is_connected(); // Active connection with completed handshake
bool synced = client.is_time_synced(); // Time filter has received at least one measurement
const GroupUpdateObject& group = client.get_group_state(); // Group id, name, playback state (all optional)
// Player state
uint8_t vol = player.get_volume();
bool muted = player.get_muted();
uint16_t delay = player.get_static_delay_ms();
int32_t fixed = player.get_fixed_delay_us();
auto& stream = player.get_current_stream_params();
// Controller state
auto& ctrl = controller.get_controller_state(); // volume, muted, repeat, shuffle, supported_commands
// Metadata
uint32_t progress = metadata.get_track_progress_ms(); // Interpolated
uint32_t duration = metadata.get_track_duration_ms();
// Timestamp conversion
int64_t client_ts = client.get_client_time(server_timestamp);Most listener callbacks fire on the main loop thread (the thread calling client.loop()). The exceptions are:
| Callback | Thread |
|---|---|
PlayerRoleListener::on_audio_write() |
Sync task background thread |
ArtworkRoleListener::on_image_decode() |
Dedicated artwork decode thread |
ArtworkRoleListener::on_image_display() |
Main loop thread |
VisualizerRoleListener::on_visualizer_frame() |
Dedicated visualizer drain thread |
VisualizerRoleListener::on_beat() |
Dedicated visualizer drain thread |
| All other listener methods | Main loop thread |
PlayerRole::notify_audio_played() is thread-safe and is designed to be called from an audio output callback thread.
A minimal integration that receives and discards audio:
#include "sendspin/client.h"
#include "sendspin/player_role.h"
using namespace sendspin;
struct MinimalPlayer : PlayerRoleListener {
size_t on_audio_write(uint8_t* data, size_t length, uint32_t timeout_ms) override {
return length; // Discard audio
}
};
struct AlwaysReady : SendspinNetworkProvider {
bool is_network_ready() override { return true; }
};
int main() {
SendspinClientConfig config;
config.client_id = "minimal-example";
config.name = "Minimal Client";
SendspinClient client(std::move(config));
PlayerRoleConfig player_config;
player_config.audio_formats = {{SendspinCodecFormat::PCM, 2, 44100, 16}};
auto& player = client.add_player(std::move(player_config));
MinimalPlayer player_listener;
AlwaysReady network;
player.set_listener(&player_listener);
client.set_network_provider(&network);
client.start_server();
while (true) {
client.loop();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}By default all roles are enabled. You can disable roles at build time to exclude their code (and dependencies like audio decoders) from the binary. This is useful on constrained targets where flash space matters.
Pass -D options to cmake:
# Disable the player role (excludes decoder, sync task, audio ring buffer)
cmake -B build -DSENDSPIN_ENABLE_PLAYER=OFF
# Disable all optional roles, keep only the player
cmake -B build -DSENDSPIN_ENABLE_CONTROLLER=OFF \
-DSENDSPIN_ENABLE_METADATA=OFF \
-DSENDSPIN_ENABLE_ARTWORK=OFF \
-DSENDSPIN_ENABLE_VISUALIZER=OFF \
-DSENDSPIN_ENABLE_COLOR=OFFAvailable options (all ON by default):
| Option | Controls |
|---|---|
SENDSPIN_ENABLE_PLAYER |
Player role, audio decoders (micro-flac, micro-opus), sync task |
SENDSPIN_ENABLE_CONTROLLER |
Controller role |
SENDSPIN_ENABLE_METADATA |
Metadata role |
SENDSPIN_ENABLE_ARTWORK |
Artwork role |
SENDSPIN_ENABLE_VISUALIZER |
Visualizer role |
SENDSPIN_ENABLE_COLOR |
Color role |
When SENDSPIN_ENABLE_PLAYER is OFF, the micro-flac and micro-opus dependencies are not fetched.
Role flags are exposed via Kconfig under Component config → sendspin-cpp:
CONFIG_SENDSPIN_ENABLE_PLAYER=y
CONFIG_SENDSPIN_ENABLE_CONTROLLER=y
CONFIG_SENDSPIN_ENABLE_METADATA=y
CONFIG_SENDSPIN_ENABLE_ARTWORK=y
CONFIG_SENDSPIN_ENABLE_VISUALIZER=y
CONFIG_SENDSPIN_ENABLE_COLOR=y
When a role is disabled, its add_*() method, accessor method, and backing member are removed from client.h via #ifdef guards. Attempting to call client.add_player() when SENDSPIN_ENABLE_PLAYER is OFF produces a compile error. The corresponding role header can still be included (it defines protocol types and the listener interface), but the role class cannot be instantiated.
Main client configuration passed to the SendspinClient constructor.
| Field | Type | Default | Description |
|---|---|---|---|
client_id |
std::string |
— | Unique client identifier (e.g., MAC address) |
name |
std::string |
— | Friendly display name shown in the Sendspin UI |
product_name |
std::optional<std::string> |
unset | Device product name; sent in client/hello only when set |
manufacturer |
std::optional<std::string> |
unset | Manufacturer name (e.g., "ESPHome"); sent in client/hello only when set |
software_version |
std::optional<std::string> |
unset | Software version string; sent in client/hello only when set |
mac_address |
std::optional<std::string> |
auto-detected | MAC address of the network interface, lowercase colon-separated (e.g., "aa:bb:cc:dd:ee:ff"), sent in client/hello. Left unset, the library auto-detects it. ESP-IDF uses the default network interface (Wi-Fi or Ethernet). Host uses a best-effort from the active routable interface. Set explicitly to override (recommended on multi-homed hosts). |
httpd_psram_stack |
bool |
false |
Allocate HTTP server task stack in PSRAM (ESP-IDF only) |
httpd_priority |
unsigned |
5 |
FreeRTOS priority for the HTTP server task (ESP-IDF only) |
websocket_priority |
unsigned |
5 |
FreeRTOS priority for the WebSocket client task (ESP-IDF only) |
server_port |
uint16_t |
8928 |
WebSocket server port |
server_max_connections |
uint8_t |
2 |
Maximum simultaneous WebSocket connections (default supports the handoff protocol) |
httpd_ctrl_port |
uint16_t |
0 |
ESP-IDF httpd control port; 0 uses ESP_HTTPD_DEF_CTRL_PORT + 1 to avoid conflict with the web_server component |
time_burst_size |
uint8_t |
8 |
Number of messages per time sync burst |
time_burst_interval_ms |
int64_t |
10000 |
Milliseconds between time sync bursts |
time_burst_response_timeout_ms |
int64_t |
10000 |
Milliseconds before a burst message times out |
websocket_payload_location |
MemoryLocation |
PREFER_EXTERNAL |
Memory placement for the per-connection WebSocket payload reassembly buffer (sized to the largest incoming frame, holds raw audio chunks delivered by httpd). PREFER_EXTERNAL tries SPIRAM first and falls back to internal RAM; PREFER_INTERNAL does the reverse. Use PREFER_INTERNAL on devices with slow PSRAM (e.g., plain ESP32) to avoid stuttering. ESP-IDF only; ignored on host. |
json_arena_size |
size_t |
2048 |
Size in bytes of a fixed internal-RAM scratch buffer used to parse incoming JSON protocol messages, instead of the default PSRAM. Costs this many bytes of internal RAM permanently but removes PSRAM traffic from the network task on every message. Messages too large for the budget fall back to PSRAM; the default covers steady-state traffic (including the FLAC stream-start header), while large track-metadata messages may spill over (but those arrive only once per song). Set to 0 to disable and keep PSRAM-only behaviour. On host there is no PSRAM distinction, so the arena is just a fixed scratch buffer for the parse (still used, harmless). |
Configuration passed to client.add_player().
| Field | Type | Default | Description |
|---|---|---|---|
audio_formats |
std::vector<AudioSupportedFormatObject> |
{} |
Audio formats the player supports; advertised to the server during the hello handshake. The server selects one when establishing a stream. |
audio_buffer_capacity |
size_t |
1000000 |
Internal ring buffer size in bytes. Larger buffers absorb more jitter at the cost of memory. |
fixed_delay_us |
int32_t |
0 |
Fixed platform-level delay offset in microseconds (e.g., a known I2S pipeline delay). Applied on top of the user-adjustable static delay. |
initial_static_delay_ms |
uint16_t |
0 |
Initial value for the user-adjustable static delay in milliseconds. Overridden by the persisted value if a SendspinPersistenceProvider is set. |
extra_startup_silence_ms |
uint16_t |
50 |
Extra silence inserted at stream start, after the first playback notification and before the first decoded chunk reaches the sink. Added on top of the initial-sync priming silence to give the decode pipeline more slack to stay ahead of the sink, preventing the initial-playback stutter caused by the decoder briefly falling behind. Larger values trade a longer startup delay for more underflow protection; set to 0 to disable. |
psram_stack |
bool |
false |
Allocate sync/decode task stack in PSRAM (ESP-IDF only) |
priority |
unsigned |
6 |
FreeRTOS priority for the sync/decode task (ESP-IDF only). The default value, 6, is one above the default httpd_priority (5). If you customize priorities, keep this above httpd_priority so the HTTP server task cannot starve the decoder during the initial burst of encoded audio that fills the buffer at stream start. |
decode_buffer_location |
MemoryLocation |
PREFER_EXTERNAL |
Memory placement preference for the decode transfer buffer. PREFER_EXTERNAL tries SPIRAM first and falls back to internal RAM; PREFER_INTERNAL does the reverse. ESP-IDF only; ignored on host. |
Each entry in audio_formats is an AudioSupportedFormatObject:
| Field | Type | Description |
|---|---|---|
codec |
SendspinCodecFormat |
Audio codec (FLAC, OPUS, or PCM) |
channels |
uint8_t |
Number of audio channels |
sample_rate |
uint32_t |
Sample rate in Hz |
bit_depth |
uint8_t |
Bits per sample |
Configuration passed to client.add_artwork().
| Field | Type | Default | Description |
|---|---|---|---|
preferred_formats |
std::vector<ImageSlotPreference> |
{} |
Image slot preferences advertised to the server during the hello handshake. Each entry declares a slot index, image source, format, and resolution. |
psram_stack |
bool |
false |
Allocate decode thread stack in PSRAM (ESP-IDF only) |
priority |
unsigned |
2 |
FreeRTOS priority for the decode thread (ESP-IDF only) |
Each entry in preferred_formats is an ImageSlotPreference:
| Field | Type | Description |
|---|---|---|
slot |
uint8_t |
Artwork slot index (0–3) |
source |
SendspinImageSource |
Image source (ALBUM or ARTIST) |
format |
SendspinImageFormat |
Image format (JPEG, PNG, or BMP) |
width |
uint16_t |
Desired image width in pixels |
height |
uint16_t |
Desired image height in pixels |
Configuration passed to client.add_visualizer().
| Field | Type | Default | Description |
|---|---|---|---|
support |
VisualizerSupportObject |
- | Visualizer capabilities advertised to the server during the hello handshake |
psram_stack |
bool |
false |
Allocate drain thread stack in PSRAM (ESP-IDF only) |
priority |
unsigned |
2 |
FreeRTOS priority for the drain thread (ESP-IDF only) |
VisualizerSupportObject fields:
| Field | Type | Description |
|---|---|---|
types |
std::vector<VisualizerDataType> |
Data stream types to receive (BEAT, LOUDNESS, F_PEAK, SPECTRUM) |
buffer_capacity |
size_t |
Internal buffer size for incoming visualizer frames |
batch_max |
uint8_t |
Maximum number of frames to process per drain cycle |
spectrum |
std::optional<VisualizerSpectrumConfig> |
Spectrum analysis parameters; required when SPECTRUM is in types |
VisualizerSpectrumConfig fields:
| Field | Type | Description |
|---|---|---|
n_disp_bins |
uint8_t |
Number of frequency bins to receive |
scale |
VisualizerSpectrumScale |
Frequency scale (MEL, LOG, or LIN) |
f_min |
uint16_t |
Minimum frequency in Hz |
f_max |
uint16_t |
Maximum frequency in Hz |
rate_max |
uint16_t |
Maximum spectrum update rate in Hz |
| Value | Description |
|---|---|
FLAC |
FLAC lossless audio |
OPUS |
Opus lossy audio |
PCM |
Raw PCM audio |
UNSUPPORTED |
Unsupported codec |
| Value | Description |
|---|---|
PLAY |
Start playback |
PAUSE |
Pause playback |
STOP |
Stop playback |
NEXT |
Skip to next track |
PREVIOUS |
Skip to previous track |
VOLUME |
Set volume (pass value via volume parameter) |
MUTE |
Set mute state (pass value via mute parameter) |
REPEAT_OFF |
Disable repeat |
REPEAT_ONE |
Repeat current track |
REPEAT_ALL |
Repeat all tracks |
SHUFFLE |
Enable shuffle |
UNSHUFFLE |
Disable shuffle |
SWITCH |
Switch source |
| Value | Description |
|---|---|
VOLUME |
Volume adjustment from the server |
MUTE |
Mute state change from the server |
SET_STATIC_DELAY |
Static delay adjustment from the server |
These represent commands the server can send to the player. The player advertises which commands it supports. Enable SET_STATIC_DELAY with player.set_static_delay_adjustable(true).
| Value | Description |
|---|---|
SYNCHRONIZED |
Normal synchronized state |
ERROR |
Error state |
EXTERNAL_SOURCE |
Playing from an external source |
| Value | Description |
|---|---|
ANOTHER_SERVER |
Disconnecting to connect to another server |
SHUTDOWN |
Device is shutting down |
RESTART |
Device is restarting |
USER_REQUEST |
User requested disconnect |
| Value | Description |
|---|---|
PLAYING |
Audio is playing |
STOPPED |
Audio is stopped |
| Value | Description |
|---|---|
OFF |
Repeat disabled |
ONE |
Repeat current track |
ALL |
Repeat all tracks |
| Value | Description |
|---|---|
JPEG |
JPEG image |
PNG |
PNG image |
BMP |
BMP image |
| Value | Description |
|---|---|
ALBUM |
Album artwork |
ARTIST |
Artist image |
NONE |
No image source |
| Value | Description |
|---|---|
BEAT |
Beat detection events |
LOUDNESS |
Loudness level |
F_PEAK |
Peak frequency |
SPECTRUM |
Frequency spectrum bins |
| Value | Description |
|---|---|
MEL |
Mel scale (perceptual) |
LOG |
Logarithmic scale |
LIN |
Linear scale |
| Value | Description |
|---|---|
NONE |
No logging |
ERROR |
Errors only |
WARN |
Warnings and above |
INFO |
Informational and above (default) |
DEBUG |
Debug and above |
VERBOSE |
All messages |
Set with SendspinClient::set_log_level(). Only affects host builds; ESP-IDF builds use the ESP log level system.
| Value | Description |
|---|---|
PREFER_EXTERNAL |
Prefer SPIRAM, fall back to internal RAM (ESP-IDF only) |
PREFER_INTERNAL |
Prefer internal RAM, fall back to SPIRAM (ESP-IDF only) |
Used by SendspinClientConfig::websocket_payload_location to control where the per-connection WebSocket payload reassembly buffer is allocated, and by PlayerRoleConfig::decode_buffer_location to control where the player's decode transfer buffer is allocated. Ignored on host platforms (no internal/external distinction).