Skip to content

Networking

OSC messages, discovery and query interfaces for connecting Field to other applications and devices.

16 source file(s), 366 public symbol(s), 213 of them carrying KDoc. Everything below is the documentation that lives beside the code — the same text the completion documentation panel shows in the editor. A blank summary means the declaration has no KDoc yet, not that this page dropped it.

RtpMidiBox.kt

The box-facing side of RTP-MIDI: rtpMidi(...) opens or joins an AppleMIDI network session that a DAW, an iPad app or macOS MIDI Network Setup can connect to, and rtpMidiDiscover() lists the sessions already on the network.

Sessions are shared per port across boxes and outlive a stopped box; each box's listeners are replaced when it re-runs. The wire protocol is in RtpMidiSession.kt and RtpMidiCodec.kt.

src/jvmMain/kotlin/afield/io/midi/rtp/RtpMidiBox.kt — read the source

Used in: Network MIDI: RTP-MIDI sessions

Top-level functions

Declaration Summary
fun Box.rtpMidi(name: String = "Field", port: Int = RtpMidiSession.DEFAULT_PORT, advertise: Boolean = true): BoxRtpMidi An RTP-MIDI (AppleMIDI) network session named name on UDP port and port + 1, advertised over Bonjour so macOS MIDI Network Setup, iPad apps and rtpMIDI list it. The first call for a port opens it; later calls, from any box, share it.
fun Box.rtpMidiDiscover(timeoutMs: Long = 1500): List<Mdns.Service> AppleMIDI sessions on the local network, found over Bonjour for timeoutMs.

RtpMidiSessions

object RtpMidiSessions

Process-wide RTP-MIDI state behind Box.rtpMidi: one session per control port, its Bonjour advertisement, the per-box callbacks and the latest controller and note values.

Declaration Summary
val sessions: Map<Int, RtpMidiSession> Sessions currently open, by control port.
fun close(port: Int)
fun closeAll()
fun discover(timeoutMs: Long = 1500): List<Mdns.Service> AppleMIDI sessions on the local network (_apple-midi._udp), collected for timeoutMs.

BoxRtpMidi

class BoxRtpMidi internal constructor(private val box: Box, private val entry: RtpMidiSessions.Entry)

A box's view of an RTP-MIDI session: send to every connected peer, and react to what they send. Callbacks run on Field's main loop with this box as context.

Declaration Summary
val session: RtpMidiSession
val port: Int
val peers: List<RtpMidiSession.Peer>
val connected: Boolean
fun invite(host: String, port: Int = RtpMidiSession.DEFAULT_PORT) Invite another session; port is its control port (5004 for macOS's default session).
fun noteOn(pitch: Int, velocity: Int = 100, channel: Int = 0)
fun noteOff(pitch: Int, channel: Int = 0)
fun controlChange(controller: Int, value: Int, channel: Int = 0)
fun programChange(program: Int, channel: Int = 0)
fun pitchBend(value: Int, channel: Int = 0)
fun sysex(bytes: ByteArray)
fun send(message: MidiMessage)
fun allNotesOff()
fun onMessage(key: String = "message", block: (MidiMessage, RtpMidiSession.Peer) -> Unit) Every message received, with the peer that sent it. Re-running the box replaces it.
fun onNote(key: String = "note", block: (pitch: Int, velocity: Int, channel: Int) -> Unit) Note on (velocity > 0) and note off (velocity 0, including note-on with velocity 0).
fun onControl(key: String = "control", block: (controller: Int, value: Int, channel: Int) -> Unit) Control changes as (controller, value 0..127, channel).
fun knob(controller: Int, channel: Int = 0, default: Double = 0.0): Double The last value of controller on channel as 0..1, or default until it has moved.
fun raw(controller: Int, channel: Int = 0): Int? Raw 0..127 value of controller, or null if untouched.
val held: Map<Pair<Int, Int>, Int> Notes held now, as (channel, pitch) to velocity.
val messageCount: Long Messages received since the session opened.
fun stopListening(key: String? = null)
fun close()
override fun toString()

RtpMidiCodec.kt

Packet encoding and decoding for RTP-MIDI (RFC 6295) and the AppleMIDI session commands around it.

RtpMidiLoopbackTest covers running status, long headers, segmented SysEx, real-time bytes inside running status, skipped recovery journals and session-packet round trips.

src/jvmMain/kotlin/afield/io/midi/rtp/RtpMidiCodec.kt — read the source

RtpMidiCodec

object RtpMidiCodec

Wire formats for RTP-MIDI (RFC 6295) and the AppleMIDI session protocol that macOS, iOS, rtpMIDI on Windows and most network-MIDI hardware use around it.

Declaration Summary
const val PAYLOAD_TYPE
const val PROTOCOL_VERSION
fun dataLength(status: Int): Int Data bytes that follow status, or -1 for SysEx (runs to F7) and undefined bytes.
data class Command(val delta: Int, val bytes: ByteArray) One command and its delta time, in RTP timestamp units, from the previous one (or the header timestamp).
fun encodeCommands(commands: List<Command>, runningStatus: Boolean = true): ByteArray Encodes commands as a MIDI command section. The first delta is written only when non-zero (the Z flag). With runningStatus, a channel message that repeats the previous status omits it. No journal is written.
data class Decoded(val commands: List<Command>, val journalPresent: Boolean, val sysexPending: ByteArray?) The result of decodeCommands: the commands, whether a journal followed, and SysEx carried across packets.
fun decodeCommands(data: ByteArray, offset: Int, length: Int = data.size - offset, sysexPending: ByteArray? = null): Decoded Decodes a MIDI command section starting at offset of data. A journal (J flag) is skipped, since the header's length says where the command list ends. sysexPending is an unfinished segmented SysEx from the previous packet of the same stream.
data class RtpPacket(val sequence: Int, val timestamp: Long, val ssrc: Int, val commands: List<Command>, val journal: Boolean, val sysexPending: ByteArray? = null)
fun encodeRtp(sequence: Int, timestamp: Long, ssrc: Int, commands: List<Command>, runningStatus: Boolean = true): ByteArray
fun isRtpMidi(data: ByteArray, length: Int)
fun decodeRtp(data: ByteArray, length: Int, sysexPending: ByteArray? = null): RtpPacket
sealed class Session
fun isSession(data: ByteArray, length: Int)
fun encode(s: Session): ByteArray
fun decodeSession(data: ByteArray, length: Int): Session? Decodes a session packet, or null for one this implementation does not know.

RtpMidiSession.kt

A UDP AppleMIDI session that accepts or sends invitations, keeps peer clocks in sync and sends and receives MIDI as javax.sound.midi messages.

Most box code reaches it through rtpMidi(...) in RtpMidiBox.kt; RtpMidiLoopbackTest connects two sessions over loopback UDP.

src/jvmMain/kotlin/afield/io/midi/rtp/RtpMidiSession.kt — read the source

Top-level functions

Declaration Summary
fun toMidiMessage(bytes: ByteArray): MidiMessage? Raw status-first bytes as a javax.sound.midi message, or null for bytes that are not one.

RtpMidiSession

class RtpMidiSession(val name: String = "Field", port: Int = DEFAULT_PORT, bindAddress: String? = null, @Volatile var acceptInvitations: Boolean = true, val syncIntervalMs: Long = 10_000) : Closeable

An AppleMIDI network session: the same thing as a session in macOS's Audio MIDI Setup → MIDI Network Setup, rtpMIDI on Windows, or an iPad app's "network MIDI".

Declaration Summary
val name: String
var acceptInvitations: Boolean
val syncIntervalMs: Long
enum class State
class Peer internal constructor(val address: InetAddress, val controlPort: Int, val initiator: Boolean, val token: Int) A remote participant. Times are in 100 µs units, the AppleMIDI clock.
val port: Int
val ssrc: Int
var closed
var lastError: String?
var malformed Packets that did not parse; they are dropped and counted.
fun now(): Long Our clock, in 100 µs units.
val peers: List<Peer>
val connectedPeers: List<Peer>
fun onMessage(block: (MidiMessage, Long, Peer) -> Unit): Closeable Called with each MIDI message received, its sender timestamp in microseconds, and the peer, on the receive thread.
fun onPeerChange(block: (Peer) -> Unit): Closeable Called when a peer connects, finishes a clock sync, or leaves.
fun invite(host: String, port: Int = DEFAULT_PORT): Peer Invites the session at host:port (its control port). The invitation is repeated every second, up to 12 times, until it is accepted or refused.
fun disconnect(peer: Peer) Ends the connection to peer with BY.
fun sendBytes(vararg messages: ByteArray) Sends raw MIDI messages, each status-first, in one packet to every connected peer.
fun send(message: MidiMessage)
fun noteOn(pitch: Int, velocity: Int = 100, channel: Int = 0)
fun noteOff(pitch: Int, velocity: Int = 0, channel: Int = 0)
fun controlChange(controller: Int, value: Int, channel: Int = 0)
fun programChange(program: Int, channel: Int = 0)
fun pitchBend(value: Int, channel: Int = 0) Pitch bend, -8192..8191 with 0 as centre.
fun sysex(bytes: ByteArray) A SysEx message; bytes may include the F0/F7 framing or not.
fun allNotesOff() Note off on every note of every channel (CC 123), for a stuck note.
override fun close()
override fun toString()

MqttBox.kt

The box-facing MQTT surface: mqtt and mqtt(url) return a BoxMqtt for publishing and subscribing, and mqttBroker(port) starts an embedded broker.

Callbacks arrive on the main Runloop, so they can touch boxes, sliders and graphics directly. Connections and brokers outlive a stopped box and are shared by url and port; subscriptions belong to the box that made them. MqttConnections holds that shared state.

src/jvmMain/kotlin/afield/io/mqtt/MqttBox.kt — read the source

Used in: MQTT: sensors, devices and brokers

Top-level functions

Declaration Summary
fun Box.mqtt(url: String, clientId: String? = null, will: Pair<String, String>? = null, username: String? = null, password: String? = null): BoxMqtt MQTT against the broker at url (tcp://host:1883). The first call for a url makes the connection, with clientId, will (topic to payload, published retained by the broker if Field vanishes) and credentials; later calls reuse it.
fun Box.mqttBroker(port: Int = Mqtt.PORT, bindAddress: String = "127.0.0.1"): MqttBroker Start (or reuse) an embedded broker on port. Bind to "0.0.0.0" so devices on the network can connect to this machine. It keeps running when the box stops.

Top-level values

Declaration Summary
val Box.mqtt: BoxMqtt MQTT against the default broker (an embedded one unless AFIELD_MQTT_URL is set).

MqttConnections

object MqttConnections

Process-wide MQTT state behind Box.mqtt: one client per broker url, the embedded brokers, and the queue that brings callbacks onto the main Runloop.

Declaration Summary
var historyLength How many numeric values per topic BoxMqtt.series keeps.
fun dispatch(r: Runnable) Runs r on the main Runloop when there is one (inside Field), directly otherwise (tests, jvm-main, where no Runloop turns and a queued callback would never run).
fun broker(port: Int = Mqtt.PORT, bindAddress: String = "127.0.0.1"): MqttBroker The embedded broker on port, started on first use and kept until closeAll.
val defaultUrl: String by lazy The url Box.mqtt connects to: AFIELD_MQTT_URL if set, else an embedded broker on 1883, or on a free port if something else already has 1883.
fun client(url: String, clientId: String? = null, will: MqttPacket.Will? = null, username: String? = null, password: String? = null): MqttClient The client for url, made (and connected in the background) on first use.
fun closeClient(url: String)
fun closeAll() Close every client and embedded broker.

BoxMqtt

class BoxMqtt internal constructor(private val box: Box, val client: MqttClient)

MQTT from a box, against one broker. Get one with mqtt (the default broker) or mqtt("tcp://192.168.1.10:1883").

Declaration Summary
val url: String
val connected: Boolean
fun subscribe(filter: String, qos: Int = 0, key: String = filter, handler: (topic: String, message: MqttMessage) -> Unit) Call handler with (topic, message) for every message matching filter. + matches one level (sensors/+/temp), # everything below (sensors/#). Runs on the main Runloop.
fun track(filter: String, qos: Int = 0) Subscribe to filter with no callback, so latest, number and series fill in for its topics. A connection only receives topics something has subscribed to.
fun toOsc(filter: String, osc: afield.io.osc.OscClient, address: (String) -> String = { "/$it" }) Forward every message matching filter as OSC through osc (oscClient(host, port)), to the address address makes from the topic (by default the topic itself: sensors/a/temp goes to /sensors/a/temp). A numeric payload is sent as one float, numeric JSON fields as one message each at <address>/<field>, anything else as one string.
fun toDmx(topic: String, out: afield.io.dmx.DmxOutput, universe: Int, channel: Int, path: String? = null, range: ClosedFloatingPointRange<Double> = 0.0..1.0) Drive one DMX channel from a numeric topic: values in range map to 0..255 on 1-based channel of universe in out (artnetOut(...) / sacnOut(...)). path reads a JSON field.
fun unsubscribe(key: String? = null) End this box's subscription under key, or all of them on this broker.
val subscriptions: List<String> This box's subscription keys on this broker.
fun publish(topic: String, value: Any?, qos: Int = 0, retain: Boolean = false) Publish value to topic. A String or number goes as text, a ByteArray as is, and a Map, List or JsonElement as JSON. retain keeps it on the broker for later subscribers.
fun clearRetained(topic: String) Clear a retained message.
val latest: Map<String, MqttMessage> The latest message on every topic this connection has received.
operator fun get(topic: String): MqttMessage? The latest message on topic, or null.
fun number(topic: String, path: String? = null, default: Double = 0.0): Double The latest numeric value on topic (or default); with path, a number inside JSON.
fun text(topic: String, default: String = ""): String The latest text on topic, or default.
fun series(topic: String, path: String? = null): List<Double> The recent numeric values on topic, oldest first (up to MqttConnections.historyLength), for showSignals("temp" to mqtt.series("sensors/a/temp")). With path, a number inside a JSON payload, as in MqttMessage.number: series("sensors/garden/env", "temp").
fun close() Disconnect from this broker and forget its subscriptions (every box's).
override fun toString()

MqttBroker.kt

An embedded MQTT 3.1.1 broker, MqttBroker, for talking MQTT inside Field with nothing else installed.

It binds to loopback by default; bind "0.0.0.0" to accept devices on the local network. The class documentation lists which parts of the protocol it implements.

src/jvmMain/kotlin/afield/io/mqtt/MqttBroker.kt — read the source

MqttBroker

class MqttBroker(port: Int = 0, bindAddress: String = "127.0.0.1", val maxPacketSize: Int = 1 shl 20) : AutoCloseable

A small MQTT 3.1.1 broker that runs inside Field, so boxes, tests and demos can talk MQTT with nothing installed and no network.

Declaration Summary
val maxPacketSize: Int Largest packet accepted from a client.
val port: Int The bound port.
val url: String tcp://127.0.0.1:<port>, the url a MqttClient connects to.
var isClosed
val onMessage Called (on a broker thread) for every message the broker routes; for logging and tests.
val clients: Set<String> Ids of the connected clients.
val retainedTopics: Set<String> Topics holding a retained message.
fun publish(topic: String, payload: ByteArray, qos: QoS = QoS.AT_MOST_ONCE, retain: Boolean = false)+1 overload · also String Publish from inside the process, as if a client had sent it.
override fun close()
override fun toString()

MqttClient.kt

An MQTT 3.1.1 client over plain TCP, MqttClient, with the message, subscription and connection event types it hands back.

The client reconnects with backoff, re-subscribes after a reconnect and re-sends unacknowledged QoS 1 publishes. Box code normally reaches it through mqtt, which routes callbacks onto the main Runloop.

src/jvmMain/kotlin/afield/io/mqtt/MqttClient.kt — read the source

MqttMessage

class MqttMessage(val topic: String, val payload: ByteArray, val qos: QoS = QoS.AT_MOST_ONCE, val retained: Boolean = false, val receivedNanos: Long = System.nanoTime())

One received message. text, number and json read the payload.

Declaration Summary
val topic: String
val payload: ByteArray
val qos: QoS
val retained: Boolean True when this is the broker replaying a retained message to a new subscription.
val receivedNanos: Long System.nanoTime() when it arrived.
val text: String The payload as UTF-8.
val number: Double? The payload as a number: plain text ("21.5"), or a JSON number. Null when it is neither — for a JSON object, use json or number with a key.
val json: JsonElement The payload parsed as JSON; throws on text that is not JSON.
val jsonOrNull: JsonElement? json, or null when the payload is not JSON.
fun number(path: String): Double? A number inside a JSON payload, by a dotted path: number("temp") for {"temp": 21.5}, number("env.temp"), number("values.2") for an array index.
fun string(path: String): String? A string inside a JSON payload, by dotted path; see number.
fun at(path: String): JsonElement? The JSON element at a dotted path, or null.
override fun toString()

MqttSubscription

class MqttSubscription internal constructor(val filter: String, val qos: QoS, internal val handler: (MqttMessage) -> Unit, private val owner: MqttClient) : AutoCloseable

A subscription made by MqttClient.subscribe; close it to stop the callback.

Declaration Summary
val filter: String
val qos: QoS
var isClosed
val count Messages delivered to this subscription so far.
override fun close()

MqttConnectionEvent

data class MqttConnectionEvent(val connected: Boolean, val reason: String)

A connection change, passed to MqttClient.onConnection.

MqttClient

class MqttClient(val url: String, val clientId: String = "field4-" + UUID.randomUUID().toString().take(8), val keepAliveSeconds: Int = 30, val cleanSession: Boolean = true, val will: MqttPacket.Will? = null, val username: String? = null, val password: String? = null, val autoReconnect: Boolean = true, val connectTimeoutMs: Int = 3000, private val dispatch: (Runnable) -> Unit = { it.run() }) : AutoCloseable

An MQTT 3.1.1 client over plain TCP.

Declaration Summary
val url: String
val clientId: String
val keepAliveSeconds: Int
val cleanSession: Boolean
val will: MqttPacket.Will?
val username: String?
val password: String?
val autoReconnect: Boolean
val connectTimeoutMs: Int
val host: String
val port: Int
var isClosed
var isConnected
var lastError: String? Why the last connection attempt failed or dropped, for a status line.
val connections Successful connections so far (1 + reconnects).
val received Messages received, publishes sent.
val sent
val latest The most recent message on every topic received, by topic.
val onConnection Called (through dispatch) when the connection comes up or goes down.
val onReceive Called on the reader thread for every message, before any subscription callback. Keep it short.
fun awaitConnected(timeoutMs: Long = 5000): Boolean Blocks until connected; false after timeoutMs.
fun publish(topic: String, payload: ByteArray, qos: QoS = QoS.AT_MOST_ONCE, retain: Boolean = false): CompletableFuture<Unit> Publish payload to topic. With retain, the broker keeps it and hands it to every later subscriber; an empty retained payload clears the topic. The future completes when the message has left (QoS 0) or been acknowledged (QoS 1/2). A QoS 0 message published while disconnected is dropped and its future fails; QoS 1 is queued and sent on reconnect.
fun publish(topic: String, payload: String, qos: QoS = QoS.AT_MOST_ONCE, retain: Boolean = false)
fun subscribe(filter: String, qos: QoS = QoS.AT_MOST_ONCE, handler: (MqttMessage) -> Unit): MqttSubscription Subscribe to filter (+ one level, # the rest). Retained matches arrive at once.
val filters: List<String> The live subscriptions' filters.
val brokerSubscriptions: Map<String, QoS> The filters subscribed at the broker: the cover of filters.
override fun close() Disconnect cleanly (the broker does not publish the will) and stop reconnecting.
override fun toString()

MqttCodec.kt

The MQTT 3.1.1 wire format: the MqttPacket types, Mqtt.encode and Mqtt.read to convert them to and from bytes, and MqttTopic for topic names and + / # filter matching.

The client and the embedded broker both use this codec. MqttCodecTest and MqttTopicSubsumesTest cover it.

src/jvmMain/kotlin/afield/io/mqtt/MqttCodec.kt — read the source

MqttProtocolException

class MqttProtocolException(message: String) : IOException(message)

A malformed MQTT packet, or one this implementation refuses.

QoS

enum class QoS(val value: Int)

Delivery guarantee of a publish or a subscription. QoS 2 is not implemented; see Mqtt.

Constants: AT_MOST_ONCE, AT_LEAST_ONCE, EXACTLY_ONCE.

MqttPacket

sealed class MqttPacket

An MQTT 3.1.1 control packet. Mqtt.encode and Mqtt.read convert to and from bytes.

Declaration Summary
data class Will(val topic: String, val payload: ByteArray, val qos: QoS = QoS.AT_MOST_ONCE, val retain: Boolean = false)
data class Connect(val clientId: String, val cleanSession: Boolean = true, val keepAliveSeconds: Int = 60, val will: Will? = null, val username: String? = null, val password: ByteArray? = null, val protocolLevel: Int = 4) : MqttPacket()
data class ConnAck(val sessionPresent: Boolean, val returnCode: Int) : MqttPacket() returnCode 0 is accepted; 1–5 are the 3.1.1 refusals (see Mqtt.connackReason).
class Publish(val topic: String, val payload: ByteArray, val qos: QoS = QoS.AT_MOST_ONCE, val retain: Boolean = false, val dup: Boolean = false, val packetId: Int = 0) : MqttPacket()
data class PubAck(val packetId: Int) : MqttPacket()
data class PubRec(val packetId: Int) : MqttPacket()
data class PubRel(val packetId: Int) : MqttPacket()
data class PubComp(val packetId: Int) : MqttPacket()
data class Subscribe(val packetId: Int, val filters: List<Pair<String, QoS>>) : MqttPacket()
data class SubAck(val packetId: Int, val returnCodes: List<Int>) : MqttPacket() One return code per filter: the granted QoS, or 0x80 for failure.
data class Unsubscribe(val packetId: Int, val filters: List<String>) : MqttPacket()
data class UnsubAck(val packetId: Int) : MqttPacket()
data object PingReq : MqttPacket()
data object PingResp : MqttPacket()
data object Disconnect : MqttPacket()

Mqtt

object Mqtt

The MQTT 3.1.1 wire format (OASIS standard, 2014): fixed header, remaining-length varint, length-prefixed UTF-8 strings, and every control packet.

Declaration Summary
const val PORT
const val MAX_REMAINING_LENGTH Largest remaining length the 4-byte varint can express (§2.2.3).
fun connackReason(code: Int): String
fun encodeRemainingLength(length: Int, out: ByteArrayOutputStream) §2.2.3: seven bits per byte, least significant group first, top bit = more follows.
fun remainingLengthBytes(length: Int): ByteArray
fun readRemainingLength(input: InputStream): Int Reads a remaining-length varint; refuses a fifth byte, as §2.2.3 requires.
fun encode(p: MqttPacket): ByteArray
fun read(input: InputStream, maxPacketSize: Int = MAX_REMAINING_LENGTH): MqttPacket? Reads one packet; null on a clean end of stream between packets.
fun decode(bytes: ByteArray): MqttPacket Decodes a whole packet held in bytes (header, length and body).
fun decode(header: Int, body: ByteArray): MqttPacket

MqttTopic

object MqttTopic

Topic names, topic filters and the + / # wildcards (MQTT 3.1.1 §4.7).

Declaration Summary
fun isValidName(topic: String): Boolean A name you can publish to: non-empty, no wildcards.
fun isValidFilter(filter: String): Boolean A filter you can subscribe to: + fills a whole level, # only as the whole last level.
fun subsumes(f: String, g: String): Boolean Whether every topic g matches is also matched by f (both valid filters). a/# subsumes a, a/+ and a/b/c; a/+ subsumes a/b; # and +/# subsume each other; every filter subsumes itself. A filter whose first level is a wildcard never subsumes one whose first level is a literal $-level, since leading wildcards do not match $ topics (§4.7.2).
fun cover(filters: Map<String, QoS>): Map<String, QoS> The smallest set of filters that matches every topic filters match: each filter subsumed by another is dropped (of two equivalent ones, the lexicographically smaller is kept), and each kept filter takes the highest QoS among the filters it covers.
fun matches(filter: String, topic: String): Boolean Whether topic matches filter. sport/# matches sport itself; + matches an empty level; a filter starting with a wildcard does not match a topic starting with $ (§4.7.2).

Mdns.kt

A dependency-free multicast DNS and DNS-SD implementation, sized to advertise and find OSCQuery services on the local network.

Mdns answers queries for the services an OscQueryServer registers and runs the one-shot browse behind Box.oscQueryDiscover.

src/jvmMain/kotlin/afield/io/osc/query/Mdns.kt — read the source

Mdns

object Mdns

Just enough multicast DNS (RFC 6762) and DNS-SD (RFC 6763) to advertise and find OSCQuery services, with no dependency: a responder that answers PTR/SRV/TXT/A questions for the services registered with advertise, and a one-shot browse.

Declaration Summary
const val TYPE_A
const val TYPE_PTR
const val TYPE_TXT
const val TYPE_SRV
const val TYPE_ANY
data class Service(val instance: String, val type: String, val host: String, val port: Int, val addresses: List<String>) A service found by browse.
fun advertise(instance: String, type: String, port: Int): Closeable Advertise instance as a type service (_oscjson._tcp, _osc._udp) on port until the returned handle is closed, which sends a goodbye.
fun browse(type: String = "_oscjson._tcp", timeoutMs: Long = 1500): List<Service> Services of type currently answering, collected for timeoutMs.

OscQueryBox.kt

The box-facing OSCQuery entry points: Box.oscQuery to publish parameters, Box.oscQueryRemote and Box.oscQueryDiscover to reach other services, and Box.oscQueryMirror to show a remote's namespace as Field controls.

OscQueryServices keeps one server per HTTP port across box runs and brings callbacks onto the main Runloop; BoxOscQuery is the per-box view that owns the parameters a box declares.

src/jvmMain/kotlin/afield/io/osc/query/OscQueryBox.kt — read the source

Used in: OSCQuery: controls other tools can find

Top-level functions

Declaration Summary
fun Box.oscQuery(name: String = "Field", httpPort: Int = OscQueryServices.DEFAULT_HTTP_PORT, oscPort: Int = if (httpPort == 0) 0 else httpPort + 1, advertise: Boolean = true): BoxOscQuery This box's OSCQuery service on httpPort (OSC UDP on oscPort), advertised over DNS-SD as name. The first call for a port starts it; later calls, from any box, share it.
fun Box.oscQueryRemote(host: String, httpPort: Int): OscQueryRemote A remote OSCQuery service at host:httpPort, cached so re-running a box reuses its connection. A cached remote re-reads the namespace when it is asked for again, so params lists what the service has now, not what it had when the first box connected. Callbacks (listen) run on the main Runloop.
fun Box.oscQueryDiscover(timeoutMs: Long = 1500): List<Mdns.Service> OSCQuery services on the local network, found over DNS-SD (_oscjson._tcp) for timeoutMs.
fun Box.oscQueryMirror(remote: OscQueryRemote, channel: String = "controls", filter: (OscQueryRemote.Node) -> Boolean = { true }) Field controls mirroring every numeric and boolean parameter of remote: a slider for each float or int with a range, a checkbox for each boolean. Moving one sets the remote; when the remote supports LISTEN, its own changes move the Field control.

OscQueryServices

object OscQueryServices

Process-wide OSCQuery state behind Box.oscQuery: one OscQueryServer per HTTP port, the remotes a box has opened, and the queue that brings callbacks onto the main Runloop.

Declaration Summary
const val DEFAULT_HTTP_PORT HTTP port Box.oscQuery uses when none is given; OSC UDP is the next port up.
fun dispatch(r: Runnable) Runs r on the main Runloop inside Field, directly where no Runloop turns (tests, jvm-main).
fun server(name: String, httpPort: Int, oscPort: Int, advertise: Boolean): OscQueryServer
fun remote(host: String, port: Int): OscQueryRemote
fun closeAll() Close every server and remote. Their parameters are gone; boxes re-create them when re-run.

BoxOscQuery

class BoxOscQuery internal constructor(private val box: Box, val server: OscQueryServer)

A box's view of an OSCQuery service: declare parameters, and their callbacks run in this box's context on the main Runloop when TouchDesigner, Max, Chataigne or VDMX set them.

Declaration Summary
val url: String http://<host>:<port>/, the URL a remote browses.
val httpPort: Int
val oscPort: Int
fun param(path: String, type: String, initial: List<Any?>, range: List<Pair<Double?, Double?>> = emptyList(), choices: List<Any?> = emptyList(), access: OscAccess = OscAccess.READ_WRITE, description: String? = null, onSet: ((List<Any?>) -> Unit)? = null): OscQueryParam A parameter of any OSC type ("f", "ii", "s", "T", ...). onSet gets every value a remote sends, coerced to type and clipped to range.
fun float(path: String, range: ClosedFloatingPointRange<Double> = 0.0..1.0, initial: Double = range.start, description: String? = null, access: OscAccess = OscAccess.READ_WRITE, onSet: ((Double) -> Unit)? = null): OscQueryParam A float over range.
fun int(path: String, range: IntRange? = null, initial: Int = range?.first ?: 0, description: String? = null, onSet: ((Int) -> Unit)? = null): OscQueryParam An int over range.
fun bool(path: String, initial: Boolean = false, description: String? = null, onSet: ((Boolean) -> Unit)? = null): OscQueryParam A boolean (OSC T/F): a toggle in TouchDesigner and Chataigne.
fun string(path: String, initial: String = "", description: String? = null, onSet: ((String) -> Unit)? = null): OscQueryParam A string.
fun menu(path: String, choices: List<String>, initial: String = choices.first(), description: String? = null, onSet: ((String) -> Unit)? = null): OscQueryParam A string limited to choices (RANGE: [{"VALS": ...}]), which remotes show as a menu.
fun vector(path: String, initial: List<Double>, range: ClosedFloatingPointRange<Double> = 0.0..1.0, description: String? = null, onSet: ((List<Double>) -> Unit)? = null): OscQueryParam An n-float vector, each over range: "ff" for an XY pad, "fff" for a colour.
fun describe(path: String, description: String) Describe a container (/synth), shown by remote browsers.
operator fun get(path: String): OscQueryParam?
fun controls(prefix: String = "/" + box.name.ifBlank { box.id }, channel: String = "controls", block: OscControls.() -> Unit) Field controls that are also OSCQuery parameters: each slider or checkbox appears in this box's output and in the namespace under prefix. Dragging it in Field runs the callback and tells every listening remote; a remote setting it moves the Field control and runs the callback.
class OscControls internal constructor(private val q: BoxOscQuery, private val group: ControlGroupBuilder, private val base: String) The builder controls hands its block: ControlGroupBuilder's controls, published.

OscQueryCodec.kt

Encodes and decodes the OSC 1.0 messages that OSCQuery sends in WebSocket binary frames.

OscQueryCodec works on byte arrays with no socket, which is what the WebSocket side of OscQueryServer and OscQueryRemote needs; UDP traffic goes through JavaOSC instead.

src/jvmMain/kotlin/afield/io/osc/query/OscQueryCodec.kt — read the source

OscQueryCodec

object OscQueryCodec

The OSC 1.0 message encoding OSCQuery puts in WebSocket binary frames: an address, a type-tag string and big-endian arguments, each padded to four bytes. Handles i f s d h T F N, which is every type an OSCQuery parameter here can have; a bundle or an unknown tag decodes to null.

Declaration Summary
data class Packet(val address: String, val arguments: List<Any?>)
fun tagOf(value: Any?): Char The OSC type tag for value: i Int, h Long, f Float, d Double, s String, T/F, N.
fun encode(address: String, arguments: List<Any?>): ByteArray
fun decode(bytes: ByteArray): Packet?
fun decodeAll(bytes: ByteArray): List<Packet> Every message in bytes: one, or the contents of a #bundle (nested bundles included).

OscQueryRemote.kt

The client side of OSCQuery: OscQueryRemote reads another application's namespace over HTTP, sets its parameters over OSC UDP, and follows changes over its WebSocket.

It talks to TouchDesigner, Max, Chataigne, VDMX or another Field, and finds them on the local network with OscQueryRemote.discover. OscQueryRoundTripTest drives it against an OscQueryServer in the same process.

src/jvmMain/kotlin/afield/io/osc/query/OscQueryRemote.kt — read the source

OscQueryRemote

class OscQueryRemote(val host: String, val httpPort: Int, var dispatch: (Runnable) -> Unit = { it.run() }) : Closeable

A remote OSCQuery service — TouchDesigner, Max, Chataigne, VDMX, another Field — read over HTTP, set over OSC UDP, and followed over its WebSocket.

Declaration Summary
val host: String
val httpPort: Int
var dispatch: (Runnable) -> Unit
inner class Node internal constructor(val path: String) One parameter of the remote namespace, as last read.
var root: JsonObject
var hostInfo: JsonObject
val name: String
val oscHost: String
val oscPort: Int
val supportsListen: Boolean
val params: List<Node> Every parameter (a node with a TYPE), by full path.
operator fun get(path: String): Node
fun refresh(): OscQueryRemote Re-read HOST_INFO and the namespace. Throws if the host does not answer.
fun onNamespaceChanged(handler: () -> Unit) Run handler after the remote adds, removes or changes a path (needs the WebSocket open).
fun send(path: String, args: List<Any?>)
override fun close()

OscQueryServer.kt

The OSCQuery service Field publishes: a namespace of typed parameters served as JSON over HTTP, set over OSC UDP, and pushed to WebSocket listeners.

OscQueryParam is one address with its type tags, value, range and set callbacks; OscQueryServer holds the namespace and runs the HTTP/WebSocket and UDP sides. Box code usually reaches it through Box.oscQuery in OscQueryBox.kt rather than constructing a server directly.

src/jvmMain/kotlin/afield/io/osc/query/OscQueryServer.kt — read the source

Used in: OSCQuery: controls other tools can find

OscAccess

enum class OscAccess(val code: Int)

OSCQuery ACCESS: what a remote may do with a node.

Constants: NONE, READ, WRITE, READ_WRITE.

OscQueryParam

class OscQueryParam internal constructor(val server: OscQueryServer, val path: String, val type: String, initial: List<Any?>, val range: List<Pair<Double?, Double?>>, val values: List<Any?>, val access: OscAccess, val description: String?)

One parameter in an OscQueryServer's namespace: an OSC address with a type, a current value, an optional range, and the callbacks that run when a remote sets it.

Declaration Summary
val server: OscQueryServer
val path: String
val type: String
val range: List<Pair<Double?, Double?>> One (min, max) per value, or empty. Either end may be null.
val values: List<Any?> Allowed values (RANGE: [{"VALS": [...]}]), for menus; empty when any value is allowed.
val access: OscAccess
val description: String?
var value: List<Any?>
val number: Double First value as a Double (0.0 when it is not a number); the common case of a one-float parameter.
fun set(vararg v: Any?) Set the value from Field. Remote listeners (LISTEN over the WebSocket) are told; this parameter's own onSet callbacks are not, since Field already knows.
fun onSet(handler: (List<Any?>) -> Unit) Run handler whenever a remote sets this parameter.

OscQueryServer

class OscQueryServer(val name: String = "Field", httpPort: Int = 0, oscPort: Int = 0, val bindAddress: String = "0.0.0.0", advertise: Boolean = true, var dispatch: (Runnable) -> Unit = { it.run() }) : Closeable

An OSCQuery service: the namespace of params, served as JSON over HTTP at http://<host>:<httpPort>/ (with a WebSocket on the same port for LISTEN), set over OSC UDP at oscPort, and advertised over DNS-SD as _oscjson._tcp and _osc._udp when advertise is on.

Declaration Summary
val name: String
val bindAddress: String
var dispatch: (Runnable) -> Unit
val oscPort: Int
val httpPort: Int
val all: Map<String, OscQueryParam> Every parameter, by path.
operator fun get(path: String): OscQueryParam?
fun add(path: String, type: String, initial: List<Any?>, range: List<Pair<Double?, Double?>> = emptyList(), values: List<Any?> = emptyList(), access: OscAccess = OscAccess.READ_WRITE, description: String? = null): OscQueryParam Add (or replace) a parameter. Replacing keeps the current value when the type is unchanged, so re-running a box does not reset what a remote has set.
fun describe(path: String, description: String) Describe a container node (/synth), shown by browsers next to its contents.
fun remove(path: String)
fun setFromRemote(path: String, args: List<Any?>) Set path as though a remote had: callbacks run, listeners hear it.
fun hostInfo(): JsonObject
fun node(path: String): JsonObject? The JSON for path: a leaf parameter, or a container whose CONTENTS are built from paths below it.
var isClosed
override fun close()

Ltc.kt

Linear timecode as audio: LtcEncoder renders SMPTE LTC frames to samples, LtcDecoder reads them back from a live or recorded signal, and TimecodeWav writes and reads the mono .wav files LTC usually travels in.

LtcFrame is the 80-bit frame itself; LtcDecodedFrame is what the decoder reports for each frame it finds. The decoder tolerates gain, DC offset, noise, speed changes and reverse play, which TimecodeTest checks against encoder output.

src/jvmMain/kotlin/afield/io/timecode/Ltc.kt — read the source

Used in: SMPTE Timecode: LTC and MIDI Time Code

LtcFrame

data class LtcFrame(val timecode: Timecode, val userBits: Int = 0, val colorFrame: Boolean = false)

One LTC (linear timecode) frame: the 80 bits SMPTE 12M records on an audio track once per video frame. Bits 0..63 carry the timecode in BCD interleaved with 32 user bits; bits 64..79 are the fixed sync word 0011 1111 1111 1101, which tells a reader where a frame ends and which way the tape is moving.

Declaration Summary
val timecode: Timecode
val userBits: Int
val colorFrame: Boolean
fun toBits(): BooleanArray The 80 bits in transmission order (bit 0 first).

LtcEncoder

class LtcEncoder(val rate: TimecodeRate, val sampleRate: Int = 48000, val amplitude: Double = 0.5)

Writes LTC as biphase-mark audio: the level flips at the start of every bit, and a 1 flips again half-way through. The output is a square wave between -amplitude and +amplitude; bit boundaries are placed on the nearest sample of the exact (fractional) position, so a long render does not drift even at 29.97.

Declaration Summary
val rate: TimecodeRate
val sampleRate: Int
val amplitude: Double
val samplesPerBit: Double Samples per bit at this rate, fractional.
fun encode(start: Timecode, count: Int, userBits: Int = 0): FloatArray Encode count consecutive frames starting at start.
fun encodeFrame(frame: LtcFrame, out: MutableList<Float>) Encode one frame, appending its samples to out.

LtcDecodedFrame

data class LtcDecodedFrame(val frame: LtcFrame, val sampleStart: Double, val sampleEnd: Double, val reverse: Boolean, val speed: Double)

A frame the LtcDecoder found, with where it sat in the input.

Declaration Summary
val frame: LtcFrame
val sampleStart: Double Earliest sample of the frame in the input stream (the start of bit 0, or of bit 79 when reverse).
val sampleEnd: Double Sample at which the frame's last bit ended — the earliest moment the frame can be known.
val reverse: Boolean True if the frame was read backwards (tape or playhead running in reverse).
val speed: Double Measured playback speed relative to the nominal rate (1.0 = real time).
val timecode: Timecode

LtcDecoder

class LtcDecoder(val sampleRate: Int = 48000, val rate: TimecodeRate? = null)

A streaming LTC reader. Push audio in with feed in any block size; frames come back as they complete, with absolute sample positions counted from the first sample ever fed.

Declaration Summary
val sampleRate: Int
val rate: TimecodeRate?
var framesDecoded: Int Frames decoded so far, across all calls.
fun feed(samples: FloatArray, offset: Int = 0, length: Int = samples.size - offset): List<LtcDecodedFrame> Feed length samples from samples at offset. Returns the frames completed during this block.
fun decodeAll(samples: FloatArray): List<LtcDecodedFrame> Decode a whole buffer at once.

TimecodeWav

object TimecodeWav

Mono 16-bit PCM .wav writing and reading, enough for LTC files.

Declaration Summary
fun write(file: File, samples: FloatArray, sampleRate: Int = 48000)
fun read(file: File): Pair<FloatArray, Int> Read a mono 16-bit PCM file written by write (or any canonical 44-byte-header mono wav). Returns samples and rate.

Mtc.kt

MIDI Time Code for Field's MIDI ports: Mtc builds quarter-frame and full-frame messages and parses full frames, and MtcDecoder reassembles incoming messages into Timecode.

The extension functions sendMtcFullFrame and sendMtcQuarterFrames send from a MidiOutput, and mtcDecoder attaches a decoder to a MidiInput. To follow MTC as a smooth clock, use TimecodeClock.followMtc.

src/jvmMain/kotlin/afield/io/timecode/Mtc.kt — read the source

Used in: SMPTE Timecode: LTC and MIDI Time Code

Top-level functions

Declaration Summary
fun MidiOutput.sendMtcFullFrame(tc: Timecode) Send tc as a full-frame SysEx (a locate).
fun MidiOutput.sendMtcQuarterFrames(tc: Timecode) Send the eight quarter frames for tc. Call once every two frames while running.
fun MidiInput.mtcDecoder(): MtcDecoder Decode MTC arriving on this input.

Mtc

object Mtc

MIDI Time Code: SMPTE timecode carried over MIDI.

Declaration Summary
fun quarterFrames(tc: Timecode): IntArray The eight quarter-frame data bytes (piece << 4 | nibble) describing tc.
fun fullFrame(tc: Timecode, device: Int = 0x7F): ByteArray The full-frame SysEx bytes for tc, device 0x7F meaning "all devices".
fun parseFullFrame(bytes: ByteArray): Timecode? Parse a full-frame SysEx (with or without the leading F0). Returns null if it is not one.
fun quarterFrameMessages(tc: Timecode): List<ShortMessage> quarterFrames as Java Sound messages.
fun fullFrameMessage(tc: Timecode): SysexMessage fullFrame as a Java Sound message.

MtcDecoder

class MtcDecoder

Reassembles MTC into timecode. Hand it every incoming MIDI message with accept (or acceptBytes); it ignores anything that is not MTC. current is the most recent time and onTimecode fires for every full frame and for every completed quarter-frame cycle (forward or backward).

Declaration Summary
var current: Timecode? The latest time decoded, or null before the first.
var reverse: Boolean True if the latest quarter frames ran backwards.
fun onTimecode(block: (Timecode) -> Unit) Called with each decoded time, on whichever thread delivered the MIDI.
fun accept(message: MidiMessage)
fun acceptBytes(bytes: ByteArray) Accept raw MIDI bytes: F1 dd or a full-frame SysEx.
fun quarter(data: Int) Accept one quarter-frame data byte (piece << 4 | nibble).

Timecode.kt

The SMPTE timecode value types: TimecodeRate (24, 25, 29.97 drop-frame, 30) and Timecode, a frame count at a rate with HH:MM:SS:FF labelling, parsing and arithmetic.

The LTC, MTC and clock code in this package all exchange Timecode values.

src/jvmMain/kotlin/afield/io/timecode/Timecode.kt — read the source

Used in: SMPTE Timecode: LTC and MIDI Time Code

TimecodeRate

enum class TimecodeRate(val nominalFps: Int, val dropFrame: Boolean, val fps: Double, val mtcCode: Int)

SMPTE timecode: the HH:MM:SS:FF clock that lighting desks, video servers and DAWs share.

Constants: FPS_24, FPS_25, FPS_2997_DF, FPS_30.

Declaration Summary
val nominalFps: Int Frames counted per labelled second (24, 25 or 30).
val dropFrame: Boolean True for 29.97 drop-frame: labels ;00 and ;01 are skipped at every minute except each tenth.
val fps: Double Real frames per second of wall time.
val mtcCode: Int The two-bit rate code MTC carries (0 = 24, 1 = 25, 2 = 29.97 DF, 3 = 30).
val framesPerDay: Int Number of distinct frames in 24 hours, after which timecode wraps.
override fun toString(): String

Timecode

data class Timecode(val frames: Int, val rate: TimecodeRate) : Comparable<Timecode>

A position on a SMPTE timecode clock. frames is the count of real frames since 00:00:00:00, always in 0 until rate.framesPerDay.

Declaration Summary
val hours: Int
val minutes: Int
val seconds: Int
val frame: Int
val totalSeconds: Double Wall-clock seconds since 00:00:00:00 (for 29.97 this is the real, not the labelled, time).
operator fun plus(n: Int): Timecode Add n frames, wrapping at 24 hours.
operator fun minus(n: Int): Timecode Subtract n frames, wrapping at 24 hours.
operator fun minus(other: Timecode): Int Frames from other to this one (no wrapping).
override fun compareTo(other: Timecode): Int
override fun toString(): String "HH:MM:SS:FF", or "HH:MM:SS;FF" for drop-frame.

TimecodeClock.kt

TimecodeClock gives a box a timecode it can read at any moment, either generated or following incoming LTC audio or MIDI Time Code.

src/jvmMain/kotlin/afield/io/timecode/TimecodeClock.kt — read the source

TimecodeClock

class TimecodeClock(val rate: TimecodeRate, private val nanoTime: () -> Long = System::nanoTime)

A timecode clock a box can read: now answers "what timecode is it?" at any moment, from any thread.

Declaration Summary
var freewheelSeconds: Double How long, in seconds, a followed clock keeps freewheeling after its last frame before locked goes false and it stops.
var source: String Where the clock's time came from most recently: "generate", "ltc", "mtc" or "none".
val speed: Double Playback speed of the source as last measured (1.0 = real time, negative = reverse).
fun generate(start: Timecode = Timecode(0, rate)): TimecodeClock Start generating time from start at real speed.
fun locate(tc: Timecode, speed: Double = this.speed.takeIf { it != 0.0 } ?: 1.0) Jump to tc keeping the current mode (a locate).
val locked: Boolean True while generating, or while a followed source has delivered a frame within freewheelSeconds.
val seconds: Double Current time in seconds since 00:00:00:00 — the number an animation or keyframe timeline wants.
fun now(): Timecode The current timecode (the frame containing seconds).
var lastLtcFrame: LtcDecodedFrame? The last LTC frame decoded, for display.
fun followLtc(sampleRate: Int = 48000): TimecodeClock Follow LTC audio delivered to feedLtc at sampleRate.
fun feedLtc(samples: FloatArray, offset: Int = 0, length: Int = samples.size - offset): List<LtcDecodedFrame> Feed a block of LTC audio, as it arrives. A decoded frame anchors the clock at the moment its last bit ended — which, playing forward, is the start of the next frame.
fun followMtc(decoder: MtcDecoder): TimecodeClock Follow MTC from decoder (which you feed, or which MidiInput.mtcDecoder feeds).
fun followMtc(input: MidiInput): TimecodeClock Follow MTC arriving on input.
override fun toString(): String