Skip to content

SSC Protocol

Varitras edited this page Nov 1, 2026 · 7 revisions

SSC Protocol

Neumann KH DSP speakers are controlled over the Sennheiser Sound Control protocol (SSC, version 1). This page describes the protocol as it was observed on real hardware.

Tested hardware: KH 120 II (firmware 1_7_3) and KH 750 DSP (firmware 2_1_2). The KH 750 DSP reports its product as KH 750. Behaviour on other models or firmware versions is not tested on hardware.

Source marks [measured], [in use], [khtool], [Sennheiser]: see the legend on Home. Where Sennheiser's general developer guide and the Neumann devices disagree, the measurement wins and both are named.

Transport

Property Value Source
Transport TCP [measured]
Port 45 [measured]
Address family IPv6 only; the devices have no IPv4 address [measured]
Address type Link-local (fe80::/10); this needs a scope ID (interface), e.g. fe80::1%eth0 on Linux or fe80::1%7 on Windows. Link-local is the range fe80::/10, not just the text prefix fe80 [measured]
Network segment Client and speaker must share the same layer-2 segment, because link-local addresses are not routed [measured]
Global addresses A global IPv6 address changes when the provider prefix changes, while the device stays reachable on link-local. Store the link-local address [measured]
Authentication None: requests are answered without any login, so anyone in the segment can talk to the speaker. See Limits-and-Untested. The 401 authorisation needed a KH 750 returns for writes on LOCAL comes from the rear switch, not from a login [measured]
Standby A speaker in standby does not answer SSC at all. No connection is possible, so standby looks the same as powered off (both models). See Device-Behaviour [measured]
mDNS Service _ssc._tcp.local., port 45, TXT key sscvers = 1.1; details in Identity-and-Discovery [measured]
Other Sennheiser products Sennheiser's guide makes UDP the mandatory transport and TCP optional; its example product speaks UDP over IPv4 only. Do not assume the Neumann transport for other SSC devices [Sennheiser]

Framing

  • One JSON object per line. [measured]
  • The devices end each reply line with CR+LF (\r\n). [measured]
  • Requests ended with CR+LF are accepted. [measured]
  • JSON itself has no NaN or Infinity (RFC 8259), so a client must not send non-finite numbers; how the devices react to them is not tested on hardware.

Requests and answers

An address such as /audio/out/level is written as nested JSON objects. A get puts null at the leaf; a set puts the new value there. [measured]

Operation Request Answer Source
get The path as nested objects, with null at the leaf The same nesting with the current value at the leaf [measured]
set The same nesting with the new value at the leaf The same nesting with the value the device now holds (echo) [measured]
several leaves in one message Several leaves in one tree; gets and sets may be mixed Every leaf that works is answered or applied; the rest come back as errors in the same line [measured]
error any An osc/error tree; the code sits nested at the failed address (see Error replies) [measured]

Sennheiser's guide allows a device to adapt an out-of-range set value instead of failing, and to reply with the adapted value [Sennheiser]. On the Neumann devices the reaction depends on the path. Only two paths were tested, each value restored afterwards [measured]:

Path Set Answer Value afterwards Source
audio/out/delay on the KH 750 (range 0–1000) 2000, -5 Echo 1000 and 0: clamped to the limit, the "adapted value" behaviour 1000, 0 [measured], [Sennheiser]
ui/logo/brightness on the KH 120 II (range 0–125) 200, -10 406 not acceptable unchanged [measured]

Other paths are not tested on hardware; a client should compare the echo with the value it sent, and should not send a value outside the range at all.

Every message gets a reply [Sennheiser]; on the tested devices every leaf query was answered [measured]. Silence means the device or the connection is gone.

Example exchange (placeholder values)

-> {"audio":{"out":{"level":null}}}
<- {"audio":{"out":{"level":85.0}}}

-> {"audio":{"out":{"level":80.0}}}
<- {"audio":{"out":{"level":80.0}}}

-> {"audio":{"out":{"dimm":null}}}
<- {"osc":{"error":[{"audio":{"out":{"dimm":[404,{"desc":"address not found"}]}}}]}}

The dimm example reflects a real observation: audio/out/dimm does not exist, and both tested models answer it with exactly this line [measured].

Containers and bundled messages

Query Result Source
One concrete leaf, e.g. {"device":{"name":null}} Answered [measured]
Every typed leaf the device's own osc/schema lists Answered: 52 of 52 on the KH 120 II, 131 of 131 on the KH 750 (the 12 untyped osc/* protocol nodes are covered in SSC-Paths) [measured]
Container, e.g. {"device":null} or the EQ container audio/out/eq2 404 at the container; a container is not dumped (both models) [measured]
Bundled get with one unknown leaf, e.g. {"audio":{"out":{"level":null,"dimm":null}}} One line: the known value plus a 404 for the rest [measured]
Bundled set of two valid leaves Both set, one echo [measured]
Bundled set, one valid and one unknown or read-only leaf The valid leaf is set; the other comes back as 404 or 405 in the same line [measured]
Get and set mixed in one message Both answered [measured]
Address pattern (*, ?) See Address patterns [measured]

Example of a partial answer (both models) [measured]:

-> {"audio":{"out":{"level":null,"dimm":null}}}
<- {"audio":{"out":{"level":100.0}},"osc":{"error":[{"audio":{"out":[404,{"desc":"address not found"}]}}]}}

Because the error of a bundle can sit at a parent node (see below), one leaf per message keeps every reply unambiguous; it costs one request per value. A bundle also has to stay within the request limit of 29 JSON tokens (see Error codes): two 10-band EQ leaves in one message are already refused. Per-path reference: SSC-Paths.

Error replies

osc/error is a list. The [code, {"desc": text}] pair is not flat under osc/error; it sits inside a copy of the request's address tree [measured]:

{"osc":{"error":[{"audio":{"out":{"dimm":[404,{"desc":"address not found"}]}}}]}}

Where the pair sits [measured]:

Case Code and desc Position of the pair Source
Unknown path 404 address not found The first missing node: audio/out5/level gets it at out5 [measured]
Container get 404 address not found The container [measured]
Unknown leaf inside a bundle 404 address not found The leaf's parent (audio/out, not dimm) [measured]
Set on an unknown path 404 address not found The leaf, as for a get [measured]
Set while the KH 750's CONTROL MODE switch is on LOCAL (even with the current value) 401 authorisation needed The leaf [measured]
Set on a read-only field 405 method not allowed The leaf [measured]
Set with a wrong value type (e.g. a string for mute) 406 not acceptable The leaf [measured]
Set with an out-of-range value on a path that refuses it (ui/logo/brightness, KH 120 II) 406 not acceptable The leaf [measured]
Set of an EQ array shorter than the band count 406 not acceptable The leaf [measured]
Malformed osc/schema or osc/limits request 454 parameter address not found; {} instead of null gives 406 osc/schema or osc/limits [measured]
Subscription partly or wholly failed 210 partial success, with failed_addresses osc/state/subscribe [measured]

Because the position varies (first missing node, parent, leaf), it does not reliably name the failed path. A client should take the first [int, {...}] pair anywhere in the osc/error tree. [measured]

Error codes

Code Observed meaning Source
210 Subscription request partly failed; failed_addresses lists the failed addresses with their own code (406 for a container, 454 for an unknown address) [measured]
401 set while the KH 750's CONTROL MODE switch is on LOCAL; reads still work. desc authorisation needed, which Sennheiser's error list contains as well. On the KH 120 II a set on LOCAL was refused with 401 as well [measured], [Sennheiser]
404 The path does not exist on this model, or a container was queried [measured]
405 set on a field that cannot be written. Sennheiser's error list has no 405 [measured], [Sennheiser]
406 Wrong value type in a set, an out-of-range value on some paths (ui/logo/brightness; audio/out/delay on the KH 750 clamps instead), a too short EQ array, or a wrong argument form in osc/schema / osc/limits [measured]
413 request too long: the request has more than 29 JSON tokens — every object, array, key and scalar, null entries and the osc/xid included. The byte length does not matter (855 bytes in 29 tokens were taken, 151 bytes in 31 refused). The reply {"osc":{"error":[413,{"desc":"request too long"}]}} carries no xid. Measured on both models (KH 750: a 31-token get bundle refused, 29 tokens answered, a 10-band EQ set with xid at 23 tokens taken) [measured]
454 Malformed osc/schema / osc/limits request; unknown address in a subscription [measured]

No 400 was seen in any probe of the full command tree, the error cases or the bundled messages; its real format on Neumann is not recorded [measured]. Sennheiser's guide lists 400 (message not understood) among other codes such as 310 (subscription terminates) and 414 (request too complex) [Sennheiser].

Writable or not

The device's own osc/limits reports a writeable flag [measured], and the khtool schema dump carries the same flag [khtool] (schema dump). It is unreliable: 17 fields that osc/limits reports as writable refuse a write, 7 on the KH 120 II and 10 on the KH 750, among them ui/control_mode on both models; where the reply was recorded it was 405. It also fails the other way: audio/out/delay, audio/out/mute, device/standby/auto_standby_time and device/standby/level accept writes on both models but carry no writeable flag at all, as do mute_persist and mixer/levels on every output, and neither does audio/out/level, which clients write in normal operation [in use]. Only a write test on hardware shows whether a field can be written. [measured] The fields concerned are listed in SSC-Paths and Limits-and-Untested.

Introspection: osc/schema and osc/limits

Both tested firmware versions answer osc/schema and osc/limits [measured].

Request Answer Source
{"osc":{"schema":null}} One level of the tree as a list: [{"device":{},"osc":{},"audio":{},"m":{},"ui":{},"warnings":null}]; containers are {}, leaves null [measured]
{"osc":{"schema":[{"audio":null}]}} The next level below audio; walk the tree level by level [measured]
{"osc":{"limits":[{"audio":{"out":{"level":null}}}]}} The leaf as a list with one object: always type (Number, String, Boolean); depending on the leaf also desc, units, min, max, inc (step), option (allowed values), count, length, const, subscr, writeable [measured]
[null], [{}], or limits without a list 454 nested at osc/schema / osc/limits [measured]
{} instead of null at the address 406 nested at osc/schema / osc/limits [measured]

The writeable flag in the answer is unreliable; see Writable or not. [measured]

A full walk with one request per level and one osc/limits request per leaf took 3.4 s on a KH 120 II (64 entries: 52 typed leaves plus 12 untyped osc nodes) and 7.4 s on a KH 750 (143 entries), when the client returned as soon as each reply had arrived. Waiting a fixed 0.4 s after every reply instead made the same walk take over 30 s. [measured]

Address patterns, xid, ping and version

Feature Request Answer Source
Supported pattern characters {"osc":{"feature":{"pattern":null}}} "*?" [measured]
Pattern * {"audio":{"out":{"*":null}}} Every leaf directly under audio/out in one line; sub-containers such as eq2 are not included [measured]
Pattern in a container name {"audio":{"*":{"level":null}}} level of every output (one on the KH 120 II, three on the KH 750) [measured]
Pattern ? {"audio":{"out?":{"level":null}}} out1 and out2 on the KH 750 [measured]
No match e.g. out? on the KH 120 II {}, no error [measured]
Transaction id {"osc":{"xid":4711},"audio":{"out":{"level":null}}} The answer plus "osc":{"xid":4711} in the same line; string ids work too. Error replies echo it too, next to error in the same osc object: {"osc":{"error":[...],"xid":4711}} (401, 404 for an unknown path or osc method, 405, 406, a partly answered bundle). The xid adds 4 tokens to the 29-token request limit: a set of a 20-entry list four levels deep (an eq3 leaf of the KH 120 II) is exactly 29 tokens without it and refused with it (413), so such a request goes without an xid and its reply is matched by its path (see Error codes) [measured]
Ping {"osc":{"ping":null}} {"osc":{"ping":null}} [measured]
Protocol version {"osc":{"version":null}} "1.1" [measured]
Subscription support {"osc":{"feature":{"subscription":null}}} true [measured]

Subscriptions

A client can ask the device to push value changes instead of polling. Request: {"osc":{"state":{"subscribe":[ <address tree> ]}}}; parameters go in a "#" key of the tree, e.g. {"#":{"lifetime":3},"m":{"in":{"level":null}}}. [measured]

The details below were measured on both models, one unit each, with the same results [measured].

Aspect Observed Source
Acknowledgement An echo of the request (with "#" if given), then one initial notification per address [measured]
Notification Identical to a get reply, e.g. {"m":{"in":{"level":[-121.3,-121.4]}}} [measured]
Rate A changing value about every 0.2 s (5 per second); a value that does not change sends only the initial notification [measured]
Default lifetime About 10 s, then notifications stop silently. Sennheiser's guide expects a 310 message at the end; none arrived [measured], [Sennheiser]
lifetime (s) Honoured: 3 stops after about 3 s [measured]
Renewal Sending the same subscription again resets the lifetime (renewed at 0, 2.5 and 5 s with lifetime 3: notifications until about 8 s) [measured]
count Limits the stream, but not exactly: 5 ended after 7 notifications including the initial one [measured]
max (ms) Honoured: 1000 gave one notification per second [measured]
min (ms) No visible effect [measured]
Cancel {"#":{"cancel":true}, <address>} is echoed; no further notifications [measured]
Several addresses One tree with several leaves works [measured]
Pattern audio/out/* The leaves are subscribed; the sub-containers are refused with 210 and failed_addresses (406 each): mixer, eq2, eq3, plus eq1 on the KH 750 [measured]
Unknown address 210 with failed_addresses value 454 (not 404) [measured]
Listing {"osc":{"state":{"subscribe":null}}} returned [{}] while a subscription was active, so it is no use as a check [measured]
Scope A subscription belongs to its connection [Sennheiser]

Notifications arrive on the same connection as normal replies, so a reader has to route lines by their content (or by xid), not by "the next line answers my last request". [measured]

Pitfall: two "osc" keys in one reply

A reply line can carry two top-level "osc" members in one object. Measured on both models with a subscription whose pattern partly failed [measured]:

{"osc":{"error":[{"osc":{"state":{"subscribe":[210,{"desc":"partial success","failed_addresses":{...}}]}}}]},"osc":{"state":{"subscribe":[...]}}}

Standard JSON parsers keep only the last member, so the error disappears without notice. A client that combines errors with subscriptions or xid has to parse with a hook that keeps duplicate keys (in Python, object_pairs_hook). A single get or set without xid or subscription carries at most one osc member. [measured]

Response timing

Item Value Source
Answer to one leaf query Exactly one line, after 1–47 ms (mean 19 ms), in 20 of 20 queries on both models [measured]
54 paths of a KH 750, one request per path 1.55 s [measured]
Split replies None seen: every answer came as one complete line [measured]

Several connections at once

Connections open at the same time to one speaker were all answered: two on a KH 120 II, three on a KH 750. The first connection kept working after the others were opened and after they were closed. A connection left open does not block a new one while a free slot remains. [measured]

On the KH 750, requests sent on two connections at the same moment were each answered on their own connection, 20 of 20 pairs, in 17–80 ms [measured].

Item Observed Source
Extra connections With one client connection open, a probe opened 3 more connections to each model; each answered a get [measured]
The next one Timed out at connect: no refusal, no reset (both models) [measured]
Total Probably 4 connections per speaker [measured]

Practical consequence: when the Neumann software (e.g. MA 1) and other clients use up the slots, a new connection times out instead of being refused. [measured]

See also: SSC-Paths · Identity-and-Discovery · Device-Behaviour · Limits-and-Untested