Development contract

Hamnsignal Client Guide

A practical guide for building another listening or visual client for the same live work without copying the browser renderer.

The Short Version

A Hamnsignal client can be a receiver, interpreter, player, visualizer, recorder, or some smaller tool that only uses part of the public stream.

The public contract is the audio URL plus the normalized relay event stream. A client can use either one independently. For example, one client might only play the music, while another might read thirty seconds of graphics events and render a still image.

Production Endpoints

Audio https://hamnsignal.se/live.mp3
Graphics stream wss://hamnsignal.se/ws

The root page uses the same public paths. A new client can use these endpoints directly without needing access to the Hamnsignal server or repository.

Minimum Client

A combined audio-visual browser client can start with three things: an audio element, a WebSocket, and a small event dispatcher.

const audio = new Audio("https://hamnsignal.se/live.mp3");
audio.preload = "none";

const socket = new WebSocket("wss://hamnsignal.se/ws");

socket.addEventListener("message", (event) => {
  const packet = JSON.parse(event.data);
  if (packet.type === "voice_start") drawVoice(packet.fields);
  if (packet.type === "voice_end") releaseVoice(packet.fields);
  if (packet.type === "state_update") updateState(packet.fields);
  if (packet.type === "live_arrival") markArrival(packet.fields);
  if (packet.type === "weather_state") updateWeather(packet.fields);
  if (packet.type === "river_state") updateRiver(packet.fields);
  if (packet.type === "traffic_state") updateTraffic(packet.fields);
});

Browsers usually require a user gesture before audio can start. Browser-based audio clients work best when audio start and stream reconnects are treated as deliberate playback state rather than as side effects of visual reconnection.

Event Envelope

Relay messages are JSON objects. Unknown fields are optional, and unknown event types can be ignored.

{
  "time_unix_ms": 1788500000000,
  "source": "supercollider",
  "path": "/voice/start",
  "type": "voice_start",
  "args": ["..."],
  "fields": {}
}

The stable parts are type, time_unix_ms, and fields. The args array exists for lower-level compatibility and inspection; fields is the friendlier surface for new work.

Core Event Types

voice_start

A musical voice has entered. Useful fields include nodeId, layer, freq, amp, pan, attack, release, brightness, stability, synth, source, phraseState, phrasePos, and motif.

layer is deliberately broad: current values include body, cantus, and cantus_support. Use source as well when a client needs finer identity. In particular, a rare low counterpoint reply is announced with source: "counterpoint" and may still use layer: "body".

{
  "type": "voice_start",
  "fields": {
    "nodeId": 4152,
    "layer": "body",
    "freq": 164.81,
    "amp": 0.031,
    "pan": -0.05,
    "attack": 6.8,
    "release": 14.2,
    "brightness": 0.26,
    "stability": 0.94,
    "synth": "ambient_fm_voice",
    "source": "chord"
  }
}

voice_end

A previously announced voice is leaving. Match by nodeId. This is useful for fading or retiring visual marks; the voice does not have to disappear instantly.

state_update

A semantic or phrase state changed. For scope: "semantic", the value may describe the atmosphere that future voices will read. For scope: "phrase", it can mark a dramaturgical state such as opening, repose, or return.

{
  "type": "state_update",
  "fields": {
    "scope": "semantic",
    "key": "brightness",
    "value": 0.28
  }
}

live_arrival

A selected ferry or tram arrival reached the relay. This event can be visually direct even when SuperCollider chooses to answer it sparsely.

{
  "type": "live_arrival",
  "fields": {
    "site": "masthuggstorget",
    "mode": "tram",
    "line": "3",
    "platform": "A",
    "source": "vasttrafik"
  }
}

weather_state

A normalized environmental value. Useful keys include temperature, wind_speed, wind_direction, precipitation, global_radiation, and occasional event-shaped weather keys such as precipitation_event. After the relay has fetched a value, it replays the latest state to a newly connected WebSocket client.

{
  "type": "weather_state",
  "fields": {
    "key": "precipitation",
    "source": "weatherPrecipitation",
    "raw_value": 0.7,
    "normalized_value": 0.233
  }
}

river_state

The current Gota alv level at Eriksberg. It uses the same raw-value, normalized-value, and observation-time fields as weather, but is its own slow state path. The most recently fetched value is also replayed to a newly connected client.

{
  "type": "river_state",
  "fields": {
    "key": "river_level",
    "source": "goteborg_river_service",
    "label": "riverLevel",
    "raw_value": 0.11,
    "normalized_value": 0.555
  }
}

traffic_state

The relay polls the Järntorget, Rosenlund, and Gullbergsvass segments every two minutes during the musical gate window and every ten minutes from 21:00 to 03:00 Europe/Stockholm. raw_value is the latest eligible segment speed in km/h. normalized_value is the observed traffic pressure and remains available to clients around the clock; it is not the sound gate. queue_active reports whether the relay has confirmed the condition, while musical_value is the value actually allowed into the sound engine. musical_gate_open identifies the 03:00-21:00 musical window.

For the sound engine, a positive musical_value opens the fixed drum field after its own slow fade; its effective threshold is > 0.003. Once open, traffic does not alter tempo, pattern, fills, or accents. The musical value only changes the voice amplitude modestly, from 72 percent of base amplitude at 0.45 to 100 percent at 1.0. Other clients should normally use normalized_value for graphics and use musical_value only when they want to mirror the artwork's drum gate.

{
  "type": "traffic_state",
  "fields": {
    "key": "e45_queue",
    "source": "trafikverket_traffic_flow",
    "label": "e45Queue",
    "raw_value": 31.0,
    "normalized_value": 0.595,
    "musical_value": 0.0,
    "musical_gate_open": false,
    "queue_active": true
  }
}

Suggested Visual Mapping

A client does not need to look like the main browser work. The same stream can support a wall piece, a phone view, a still-image renderer, an ASCII surface, a desktop music player, or something else entirely.

  • freq: vertical position, register, glyph weight, or brightness.
  • amp: size, opacity, local energy, or density of mark.
  • pan: horizontal position, stereo-side bias, or directional text flow.
  • attack: fade-in time, emergence speed, or delay before full visibility.
  • release: persistence, memory trail length, or fade-out time.
  • brightness: colour temperature, contrast, edge clarity, or symbol selection.
  • stability: steadiness, jitter amount, line confidence, or blur.
  • live_arrival: a passing trace, pulse, column, horizon mark, or textural interruption.
  • weather_state: background pressure rather than foreground notification.
  • river_state: a slow baseline for width, weight, height, or persistence.
  • traffic_state: a temporary condition that can introduce regularity, pressure, or a repeated visual interval.

Useful Boundaries

Stream contract

The audio stream and relay WebSocket are enough to follow the live work. Public data sources can also be read independently when a client wants its own context.

Audio source

The central SuperCollider system is the source of the live music stream. A client may simply play it, process it locally, visualize it, or ignore it.

Loose fields

Missing or unfamiliar fields are normal extension points. The safest response is graceful absence rather than hard failure.