moonosc

    OSC 1.0 core codecs, complete segment patterns and indivisible timed bundle batches

    osc
    open-sound-control
    protocol
    routing
    streaming
    Download zip
    Author
    Version
    0.2.0
    License
    Apache-2.0
    Last updated
    20 days ago
    Downloads
    36

    No dependencies

    #MoonOSC

    A portable, bounded OSC 1.0 (26 March 2002) core library profile in MoonBit: required i/f/s/b wire types, all normative segment-pattern rules, all-matching-method dispatch plans, and indivisible timed bundle batches. Not an OSC 1.1 or full-server conformance claim. It performs no socket IO and invokes no callbacks.

    协议目标、完整必需语义、永久排除边界和三个可执行案例: OSC 1.0 conformance map。 严格入口覆盖本配置的全部必需语义;实际计时、调用和连续执行由宿主履行。

    #Install

    moon add CYang-dep/moonosc@0.2.0

    In your moon.pkg:

    import { "CYang-dep/moonosc" @osc }

    let packet = @osc.Message({ address: "/mixer/gain", arguments: [@osc.float_argument(0.5)] })
    let wire = @osc.encode_osc10(packet)
    let recovered = @osc.decode_osc10(wire)

    These functions raise OscError; handle errors at your application boundary. The complete public interface is pkg.generated.mbti.

    #Delivered workflows

    ConsumerInput and processingObservable resultRun
    Audio toolsTyped gain message, codec roundtrip, brace-pattern fanout, exact signature checksTwo accepted destinations; no callbacks before validationmoon run examples/audio
    Interactive mediaFuture screen show/commit plus nested light cueOne indivisible screen batch and one light batchmoon run examples/media
    Device gatewaysSLIP input arriving one byte at a time, decoding and JSON diagnosticsOne reconstructed packet; exact replay including escaped bytesmoon run examples/device

    All examples are deterministic and assertion-backed. They require no devices, servers, credentials, or audio driver.

    #Capabilities

    • OSC messages and nested bundles; canonical big-endian fields and zero padding; strict full-payload consumption, type tags and bounded lengths/depth/counts.
    • Core tags i f s b; generic opt-in tags h d t S c r m T F N I and recursive [...] arrays. Floats are raw IEEE bits: NaN payloads and signed zero are preserved.
    • Non-NUL ASCII OSC strings; character arguments restricted to ASCII 0..127. This intentionally does not implement non-ASCII protocol extensions.
    • Compiled * ? [ranges] [!ranges] {literal,alternatives} patterns. Wildcards do not cross slashes. Brace choices are literal segment strings, including empty alternatives; nested alternatives, recursive //, and generic regex are not supported.
    • Ordered method registry and signature-aware inert dispatch plans; fanout has a shared work budget. Default contract failure is all-or-nothing; partial plans are explicit. This is not an indexed router or executing RPC framework.
    • NTP-era-zero timetags, explicit Unix conversion (1900 through the 2036 rollover), checked tick arithmetic; generic flatten lets nested immediate inherit parent time, whereas strict APIs compare raw child times without repair.
    • Generic per-message Scheduler: transactional admission, stable minheap, cancellation, polling, explicit exclusive late-event cutoff and clearing without ticket reuse.
    • Transactional size-prefix and SLIP feeds with bounded frame/chunk/output counts, split-header/escape support, EOF checks, reset and rollback on failure.
    • Strict hex utilities, validated structural summaries and moonosc/1 lossless JSON tagged arrays. All numeric values use fixed-width hex, never JSON numbers.

    #Strict profile versus compatibility APIs

    Use encode_osc10 / decode_osc10 and Osc10Scheduler for the documented profile. plan_osc10(packet, absolute_now) returns owned batches; poll(now,max_batches=1) can return many messages in one batch. Run them consecutively, preserving message order and passing identical arguments to every matching method. Never insert an unrelated callback/await between messages in a batch. See the host execution loop in the conformance map; the library does not execute it for you.

    Generic encode / decode additionally support the optional OSC 1.0 tag table h/t/d/S/c/r/m/T/F/N/I/arrays. These are opt-in, not OSC 1.1 claims. Generic flatten / Scheduler retain per-message behavior and nested-immediate inheritance for compatibility: they do not provide atomic bundle execution. Strict APIs reject a child raw timetag earlier than its parent, including sentinel 1 under a future parent. No implicit type coercion or missing-typetag guessing.

    #Validation

    moon info && moon fmt moon fmt --check moon check --deny-warn --target wasm-gc moon test --deny-warn --target wasm-gc moon build --deny-warn --target wasm-gc python scripts/reference_vectors.py --target wasm-gc python scripts/audit.py moon run examples/conformance

    Repeat check/test/build/examples for wasm, js and native. Native execution requires a C toolchain (CI uses Ubuntu). CI installs the official stable toolchain on clean runners and tests all four targets. Local development was initially checked using Moon 0.1.20260920 and then migrated to the canonical Moon 0.1.20261012 syntax after the first real CI run. Use Moon 0.1.20261012 or newer; CI logs record the actual release.

    The test suite includes 256 full-model roundtrips with every shortened message prefix rejected, 240 wildcard pairs against a recursive short-input oracle, 512 queue mutations against a scan model, every split of both stream encodings, and 256 independently encoded Python stdlib message/bundle vectors. These are correctness checks, not performance claims or proof of complete conformance.

    The 0.2.0 tests additionally check every required pattern form, exact core wire, optional-type exclusion, malformed input, strict parent/child timing, owned snapshots, transactional batch admission and 96 batches against an independent sorted model.

    #Boundaries

    This is a reusable library, not a complete OSC server. Users provide transports, clock synchronization, NTP-era choice, authentication, thread synchronization, application contracts, and callback execution. No hard-realtime guarantees, audio synthesis, DSP, discovery, delivery acknowledgement, clock drift model, or automatic competition registration. The core profile permanently excludes OSC 1.1 recursive //, Unicode protocol fields, recursive brace-pattern extensions, legacy typetag guessing and implicit numeric coercion. Malformed packets are rejected, not repaired. See the conformance map for exact limits and host obligations.

    #License

    Original implementation under Apache-2.0, relying on MoonBit core only. Bounded ecosystem research and rejected alternatives are documented in duplicate-check; it is not a claim of uniqueness.

    OscError

    pub suberror OscError {
    Invalid(String, Int)
    Limit(String)
    } derive(Eq,
    Debug
    )

    OscError::equal

    fn OscError::equal(OscError, OscError) -> Bool

    OscError::not_equal

    fn OscError::not_equal(x : OscError, y : OscError) -> Bool

    OscError::to_repr

    Argument

    pub(all) enum Argument {
    I32(Int)
    F32(UInt)
    Text(String)
    Blob(Bytes)
    I64(Int64)
    F64(UInt64)
    Time(TimeTag)
    Symbol(String)
    Character(Int)
    Rgba(UInt)
    Midi(UInt)
    True
    False
    Nil
    Impulse
    Array(Array[Argument])
    } derive(Eq,
    Debug
    )

    Argument::equal

    fn Argument::equal(Argument, Argument) -> Bool

    Argument::not_equal

    fn Argument::not_equal(x : Argument, y : Argument) -> Bool

    Argument::to_repr

    DispatchPlan

    pub(all) struct DispatchPlan {
    accepted : Array[Route]
    rejected : Array[Rejection]
    } derive(Eq,
    Debug
    )

    DispatchPlan::equal

    DispatchPlan::not_equal

    fn DispatchPlan::not_equal(x : DispatchPlan, y : DispatchPlan) -> Bool

    Dispatcher

    pub struct Dispatcher {
    // private fields
    }

    Dispatcher::add

    fn Dispatcher::add(self : Dispatcher, id : Int, address : String, signature? : String?) -> Unit raise OscError

    Dispatcher::new

    fn Dispatcher::new(capacity? : Int) -> Dispatcher raise OscError

    Dispatcher::plan

    fn Dispatcher::plan(self : Dispatcher, message : Message, all_or_nothing? : Bool, work_limit? : Int, limits? : Limits) -> DispatchPlan raise OscError

    Limits

    pub(all) struct Limits {
    packet_bytes : Int
    string_bytes : Int
    blob_bytes : Int
    arguments : Int
    elements : Int
    depth : Int
    } derive(Eq,
    Debug
    )

    Limits::default

    fn Limits::default() -> Limits

    Limits::equal

    fn Limits::equal(Limits, Limits) -> Bool

    Limits::not_equal

    fn Limits::not_equal(x : Limits, y : Limits) -> Bool

    Limits::to_repr

    Message

    pub(all) struct Message {
    address : String
    arguments : Array[Argument]
    } derive(Eq,
    Debug
    )

    Message::equal

    fn Message::equal(Message, Message) -> Bool

    Message::not_equal

    fn Message::not_equal(x : Message, y : Message) -> Bool

    Message::to_repr

    Message::type_tags

    fn Message::type_tags(self : Message, limits? : Limits) -> String raise OscError

    Osc10Batch

    pub(all) struct Osc10Batch {
    due : TimeTag
    messages : Array[Message]
    } derive(Eq,
    Debug
    )

    Osc10Batch::equal

    fn Osc10Batch::equal(Osc10Batch, Osc10Batch) -> Bool

    Osc10Batch::not_equal

    fn Osc10Batch::not_equal(x : Osc10Batch, y : Osc10Batch) -> Bool

    Osc10Scheduler

    pub struct Osc10Scheduler {
    // private fields
    }

    Osc10Scheduler::cancel

    fn Osc10Scheduler::cancel(self : Osc10Scheduler, ticket : UInt64) -> Bool

    Osc10Scheduler::clear

    fn Osc10Scheduler::clear(self : Osc10Scheduler) -> Int

    Osc10Scheduler::enqueue

    fn Osc10Scheduler::enqueue(self : Osc10Scheduler, packet : Packet, now : TimeTag, limits? : Limits) -> Array[UInt64] raise OscError

    Transactional admission; capacity counts indivisible batches, not messages.

    Osc10Scheduler::len

    fn Osc10Scheduler::len(self : Osc10Scheduler) -> Int

    Osc10Scheduler::new

    fn Osc10Scheduler::new(capacity? : Int) -> Osc10Scheduler raise OscError

    Osc10Scheduler::next_due

    fn Osc10Scheduler::next_due(self : Osc10Scheduler) -> TimeTag?

    Osc10Scheduler::poll

    fn Osc10Scheduler::poll(self : Osc10Scheduler, now : TimeTag, max_batches? : Int) -> Array[ScheduledOsc10Batch] raise OscError

    max_batches never splits a batch, even when it contains many messages.

    Packet

    pub(all) enum Packet {
    Message(Message)
    Bundle(TimeTag, Array[Packet])
    } derive(Eq,
    Debug
    )

    Packet::equal

    fn Packet::equal(Packet, Packet) -> Bool

    Packet::not_equal

    fn Packet::not_equal(x : Packet, y : Packet) -> Bool

    Packet::to_repr

    PacketSummary

    pub(all) struct PacketSummary {
    wire_bytes : Int
    messages : Int
    bundles : Int
    arguments : Int
    array_arguments : Int
    blob_bytes : Int
    max_depth : Int
    } derive(Eq,
    Debug
    )

    PacketSummary::equal

    PacketSummary::not_equal

    fn PacketSummary::not_equal(x : PacketSummary, y : PacketSummary) -> Bool

    Pattern

    pub struct Pattern {
    // private fields
    }

    Pattern::matches

    fn Pattern::matches(self : Pattern, path : String, work_limit? : Int) -> Bool raise OscError

    Pattern::source

    fn Pattern::source(self : Pattern) -> String

    Rejection

    pub(all) struct Rejection {
    route : Route
    expected : String
    actual : String
    } derive(Eq,
    Debug
    )

    Rejection::equal

    fn Rejection::equal(Rejection, Rejection) -> Bool

    Rejection::not_equal

    fn Rejection::not_equal(x : Rejection, y : Rejection) -> Bool

    Route

    pub(all) struct Route {
    id : Int
    address : String
    } derive(Eq,
    Debug
    )

    Route::equal

    fn Route::equal(Route, Route) -> Bool

    Route::not_equal

    fn Route::not_equal(x : Route, y : Route) -> Bool

    Route::to_repr

    Router

    pub struct Router {
    // private fields
    }

    Router::add

    fn Router::add(self : Router, id : Int, address : String) -> Unit raise OscError

    Router::len

    fn Router::len(self : Router) -> Int

    Router::new

    fn Router::new(capacity? : Int) -> Router raise OscError

    Router::remove

    fn Router::remove(self : Router, id : Int) -> Bool

    Router::resolve

    fn Router::resolve(self : Router, address : String, work_limit? : Int) -> Array[Route] raise OscError

    The message supplies the pattern; registered destinations are literal paths.

    Router::snapshot

    fn Router::snapshot(self : Router) -> Array[Route]

    Scheduled

    pub(all) struct Scheduled {
    ticket : UInt64
    due : TimeTag
    message : Message
    } derive(Eq,
    Debug
    )

    Scheduled::equal

    fn Scheduled::equal(Scheduled, Scheduled) -> Bool

    Scheduled::not_equal

    fn Scheduled::not_equal(x : Scheduled, y : Scheduled) -> Bool

    ScheduledOsc10Batch

    pub(all) struct ScheduledOsc10Batch {
    ticket : UInt64
    due : TimeTag
    messages : Array[Message]
    } derive(Eq,
    Debug
    )

    ScheduledOsc10Batch::equal

    ScheduledOsc10Batch::not_equal

    Scheduler

    pub struct Scheduler {
    // private fields
    }

    Scheduler::cancel

    fn Scheduler::cancel(self : Scheduler, ticket : UInt64) -> Bool

    Cancel one ticket. Removing a non-root node restores the heap in both directions.

    Scheduler::clear

    fn Scheduler::clear(self : Scheduler) -> Int

    Clear pending messages without recycling ticket identities.

    Scheduler::discard_before

    fn Scheduler::discard_before(self : Scheduler, cutoff : TimeTag) -> Array[Scheduled] raise OscError

    Explicit late-event policy: discard events strictly before cutoff, returning them in stable order. Equality is retained. Validation happens before any mutation.

    Scheduler::enqueue

    fn Scheduler::enqueue(self : Scheduler, packet : Packet, now : TimeTag, limits? : Limits) -> Array[UInt64] raise OscError

    Admission is transactional: flatten and all capacities checked before mutation.

    Scheduler::len

    fn Scheduler::len(self : Scheduler) -> Int

    Scheduler::new

    fn Scheduler::new(capacity? : Int) -> Scheduler raise OscError

    Scheduler::next_due

    fn Scheduler::next_due(self : Scheduler) -> TimeTag?

    Scheduler::poll

    fn Scheduler::poll(self : Scheduler, now : TimeTag, max_count? : Int) -> Array[Scheduled] raise OscError

    SizeFramer

    pub struct SizeFramer {
    // private fields
    }

    Size-prefix framing only: returned frames still require decode().

    SizeFramer::buffered

    fn SizeFramer::buffered(self : SizeFramer) -> Int

    SizeFramer::feed

    fn SizeFramer::feed(self : SizeFramer, chunk : Bytes) -> Array[Bytes] raise OscError

    Entire feed is transactional, including completed frames before a bad header. On failure no input is accepted: retry corrected bytes or explicitly reset().

    SizeFramer::finish

    fn SizeFramer::finish(self : SizeFramer) -> Unit raise OscError

    SizeFramer::new

    fn SizeFramer::new(max_frame? : Int, max_feed? : Int, max_frames? : Int) -> SizeFramer raise OscError

    SizeFramer::reset

    fn SizeFramer::reset(self : SizeFramer) -> Unit

    SlipFramer

    pub struct SlipFramer {
    // private fields
    }

    RFC 1055 SLIP byte-stuffing, constrained to nonempty aligned OSC frames.

    SlipFramer::buffered

    fn SlipFramer::buffered(self : SlipFramer) -> Int

    SlipFramer::feed

    fn SlipFramer::feed(self : SlipFramer, chunk : Bytes) -> Array[Bytes] raise OscError

    Empty ENDs are synchronization markers. Invalid escapes roll back the whole feed.

    SlipFramer::finish

    fn SlipFramer::finish(self : SlipFramer) -> Unit raise OscError

    SlipFramer::new

    fn SlipFramer::new(max_frame? : Int, max_feed? : Int, max_frames? : Int) -> SlipFramer raise OscError

    SlipFramer::reset

    fn SlipFramer::reset(self : SlipFramer) -> Unit

    TimeTag

    pub(all) struct TimeTag {
    seconds : UInt
    fraction : UInt
    } derive(Eq,
    Debug
    )

    TimeTag::add_ticks

    fn TimeTag::add_ticks(self : TimeTag, ticks : UInt64) -> TimeTag raise OscError

    TimeTag::bits

    fn TimeTag::bits(self : TimeTag) -> UInt64

    TimeTag::distance_ticks

    fn TimeTag::distance_ticks(self : TimeTag, later : TimeTag) -> UInt64 raise OscError

    TimeTag::equal

    fn TimeTag::equal(TimeTag, TimeTag) -> Bool

    TimeTag::from_bits

    fn TimeTag::from_bits(bits : UInt64) -> TimeTag

    TimeTag::from_unix

    fn TimeTag::from_unix(seconds : Int64, nanoseconds? : Int) -> TimeTag raise OscError

    Era 0 only, floors nanoseconds to the preceding 2^-32-second tick.

    TimeTag::immediate

    fn TimeTag::immediate() -> TimeTag

    TimeTag::is_immediate

    fn TimeTag::is_immediate(self : TimeTag) -> Bool

    TimeTag::not_equal

    fn TimeTag::not_equal(x : TimeTag, y : TimeTag) -> Bool

    TimeTag::resolve

    fn TimeTag::resolve(self : TimeTag, now : TimeTag) -> TimeTag raise OscError

    TimeTag::to_repr

    TimeTag::to_unix

    fn TimeTag::to_unix(self : TimeTag) -> (Int64, Int) raise OscError

    TimedMessage

    pub(all) struct TimedMessage {
    due : TimeTag
    message : Message
    order : Int
    } derive(Eq,
    Debug
    )

    TimedMessage::equal

    TimedMessage::not_equal

    fn TimedMessage::not_equal(x : TimedMessage, y : TimedMessage) -> Bool

    compile_pattern

    fn compile_pattern(text : String) -> Pattern raise OscError

    decode

    fn decode(bytes : Bytes, limits? : Limits) -> Packet raise OscError

    decode_message

    fn decode_message(bytes : Bytes, limits? : Limits) -> Message raise OscError

    decode_osc10

    fn decode_osc10(bytes : Bytes, limits? : Limits) -> Packet raise OscError

    double_argument

    fn double_argument(value : Double) -> Argument

    encode

    fn encode(packet : Packet, limits? : Limits) -> Bytes raise OscError

    encode_message

    fn encode_message(message : Message, limits? : Limits) -> Bytes raise OscError

    encode_osc10

    fn encode_osc10(packet : Packet, limits? : Limits) -> Bytes raise OscError

    flatten

    fn flatten(packet : Packet, now : TimeTag, limits? : Limits) -> Array[TimedMessage] raise OscError

    Returns owned message trees in depth-first wire order, not sorted due order.

    float_argument

    fn float_argument(value : Float) -> Argument

    from_json

    fn from_json(value : Json, limits? : Limits) -> Packet raise OscError

    hex_decode

    fn hex_decode(text : String, max_bytes? : Int) -> Bytes raise OscError

    hex_encode

    fn hex_encode(bytes : Bytes, max_bytes? : Int) -> String raise OscError

    matches

    fn matches(pattern : String, path : String, work_limit? : Int) -> Bool raise OscError

    parse_json

    fn parse_json(text : String, limits? : Limits) -> Packet raise OscError

    Text parsing has both a byte/code-unit cap and parser nesting cap.

    plan_osc10

    fn plan_osc10(packet : Packet, now : TimeTag, limits? : Limits) -> Array[Osc10Batch] raise OscError

    Owns data and preserves each bundle's direct-message atomic group. Hosts must run an entire returned group without unrelated callback processing.

    size_frame

    fn size_frame(bytes : Bytes, max_frame? : Int) -> Bytes raise OscError

    slip_frame

    fn slip_frame(bytes : Bytes, max_frame? : Int) -> Bytes raise OscError

    Adds leading and trailing END for synchronization; worst case 2*n+2 bytes.

    summarize

    fn summarize(packet : Packet, limits? : Limits) -> PacketSummary raise OscError

    First validates all budgets; never inspect untrusted models recursively first.

    to_json

    fn to_json(packet : Packet, limits? : Limits) -> Json raise OscError

    Versioned tagged-array schema. Integers and IEEE/NTP bits are fixed-width hex, never JSON numbers: NaN payloads, signed zero and all 64-bit values survive.

    validate_address

    fn validate_address(address : String) -> Unit raise OscError

    validate_method

    fn validate_method(address : String) -> Unit raise OscError