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
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
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.
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
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.
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
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".
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
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
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
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
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
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
An MQTT 3.1.1 control packet. Mqtt.encode and Mqtt.read convert to and from bytes.
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
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
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
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.
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
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
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
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
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.
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
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
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
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
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
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 |