Skip to main content

arpeg

🧬 An expressive arpeggiator 🧬

Play in a browser

The browser instrument offers a built-in synth, an on-screen and computer keyboard, note orders, latch, tempo, gate, transpose, and pitch bend. It uses the existing Rust/WebAssembly player with no backend or JavaScript dependencies. Build with node web/build.mjs, then serve dist/web over HTTP. Click Start audio to enable sound. The initial audio host uses a 40 ms presentation delay to preserve note spacing through ordinary polling jitter.

Engine

The project design is in plan/arpeggiator.md. The Python package and the Rust event core render held and latched notes in ascending, descending, and played order against an exact beat grid. Both cores process live held and latched notes incrementally. Python also captures expressive MIDI phrases with controller entry state and retained source events. Marked sample regions can now be lowered to source-frame notes and realized through enge's existing sampler. Live sample playback is not wired yet.

The Python package uses uFor's portable profile and capture contracts. The Rust crate in crates/arpeg-core contains event decisions only; it has no dependency on Python, MIDI device libraries, or audio processing.

The readable Python live engine is arpeg/live.py. LiveArpeggiator accepts a uFor profile and provides note_on, note_off, advance, clear, and stop methods with exact beat times. Its behavior is covered in test/test_live.py and the shared live traces. The standalone executable uses the Rust engine, so Python is not required when playing from MIDI ports.

Held and latched banks also accept Euclidean rhythm in Python and Rust, for live playing or file rendering. Three pulses in eight steps gives 10010010; positive rotation moves hits later. Rests leave note selection unchanged, while gates still release at their scheduled times. The mask repeats on the local step clock, including through empty banks and bank edits. Zero pulses is silence and full pulses is the ordinary grid. Exact masks, rotations, and event traces are shared in the fixture. Recorded-history playback currently requires grid rhythm.

Custom step patterns repeat a list of hit, rest, and tie steps, each with an exact beat duration. A hit can specify repeats to divide its duration into evenly spaced attacks of one selected note. Selection advances once per hit; rests and ties do not select a new note. Gates use each attack's subdivision duration. Consecutive ties sustain the final attack through the chain, with the gate fraction applied to the final tie; an already longer gate is preserved. Ties can cross the cycle boundary, and leading ties without an occurrence are silent. Stop, clear, or an empty bank cancels pending attacks and releases owned notes. Chord edits preserve an already selected repeat group. The shared traces check the Python and Rust schedulers with irregular polling. Custom patterns currently support live held and latched banks; file rendering and recorded history reject them.

Weighted walks move through the bank in pitch order, breaking equal-pitch ties by source identity. moves contains signed rank offsets, weights contains their positive integer weights, and movement wraps around the bank. start = "lowest" plays the lowest note first; start = "move" applies a weighted move from that position before playing. When a chord edit removes the selected note, on_remove = "lowest" restarts at the lowest note; on_remove = "rank" keeps the previous rank modulo the new bank size, then applies a move. Both defaults are "lowest". A retained note continues from its current rank. Bank-edit retrigger uses the start policy.

probability = "2/3" admits each eligible hit with that exact probability. The default is 1; 0 is silence. Rejected hits leave selection unchanged and do not move the rhythm clock. Repeats share one decision and one note. Rests, ties, and empty-bank steps do not consume chance decisions. An explicit integer seed is required for probability strictly between zero and one and for walks with multiple moves. Probability and walk choices have separate counters; profile name and bank revision also enter each choice. The same profile and input trace therefore reproduce the same decisions regardless of polling intervals. Python snapshots retain these counters and traversal state. The random contract and shared traces specify Python/Rust parity. Probability and walks currently support live held and latched banks; file rendering and recorded history reject them.

Alternating selection walks up and down through the chord in pitch order. With repeat_endpoints = false (the default), three notes play 60, 64, 67, 64, 60, 64…. Setting it to true plays 60, 64, 67, 67, 64, 60, 60…. Equal pitches remain distinct source notes. The first admitted hit starts at the lowest note, moving upward. Ordinary chord edits preserve direction and continue to the nearest note beyond the previous pitch/identity; if none remains, direction reverses. Only a retained endpoint can be repeated. A single-note bank keeps playing that note, and an empty bank resets traversal. retrigger = "bank_edit" restarts at the lowest note. Repeats reuse a selection, and rests, ties, and chance-rejected hits do not advance traversal. Python snapshots retain direction. The shared traces check both engines under irregular polling. Alternating selection currently supports live held and latched banks.

Inside-out selection visits notes from the center outward; outside-in visits the edges first. Use selection = { kind = "inside_out" } or kind = "outside_in". For pitches 60, 64, 67, 72, their repeating orders are respectively 64, 67, 60, 72 and 60, 72, 64, 67. For an odd-sized chord, inside-out starts at the center, then visits the lower side before the upper side at each distance; outside-in always starts on the lower side. Equal pitches are ordered by source identity. Each cycle visits every note once. Chord edits rebuild the order and continue after the selected identity if it survives; removal restarts at the beginning. Bank-edit retrigger, clear, and an empty bank also restart. Single-note banks repeat that note. Rests, ties, chance-rejected hits, and repeated attacks follow the existing selection rules. Both Python and Rust support these orders for live held and latched banks. File rendering and recorded history reject them. The shared traces specify their exact behavior.

Index patterns cycle through explicit chord positions: selection = { kind = "index_pattern", indices = [0, 2, 1, 2] }. Positions start at zero, ordered by ascending pitch and then source identity. boundary = "wrap" is the default: each index is taken modulo the bank size. Set boundary = "rest" to skip a missing position and advance to the next slot. Ordinary chord edits preserve the pattern position; bank-edit retrigger, an empty bank, clear, and stop reset it. Repeats reuse one selection. Rhythm rests, ties, masked steps, and chance-rejected hits leave the position unchanged. Python snapshots retain it, including after a missing-index rest. Both engines support live held and latched banks; file rendering, history, and marked samples reject this selection. Shared traces check the order and both boundary policies under irregular polling.

Shuffle selection visits every note once per cycle. selection = { kind = "shuffle" } shuffles again each cycle; mode = "once" repeats the initial order. An explicit integer seed is required. The default allows the same source note at the end of one cycle and the beginning of the next. Set no_repeat = true to prevent consecutive selections of that source when more than one note is available. A hit's repeats still reuse its selection.

on_edit = "restart" is the default: chord edits start a fresh shuffled cycle. With on_edit = "preserve", surviving notes retain their order, removed notes disappear, and added notes are inserted randomly into the unplayed portion. The updated complete order is reused in once mode. Bank-edit retrigger, an empty bank, clear, and stop discard the order. Rhythm rests, ties, masked steps, and chance-rejected hits leave the queue unchanged. Random counters persist through resets and in Python snapshots. Both engines support live held and latched banks; file rendering, history, and marked samples reject shuffle. Shared traces check both modes and edit policies.

Weighted choice independently selects a note on each admitted hit: selection = { kind = "choice", weights = [4, 1] }. Weights follow ascending pitch/source identity ranks and are positive integers through 4294967295. The default weights [1] make every note equally likely. extend = "ones" supplies weight 1 for unspecified ranks; extend = "repeat" cycles the weight list across the bank. Extra weights beyond the bank are unused. Thus [4, 1] gives three notes weights [4, 1, 1] by default or [4, 1, 4] with repetition. Chord edits update rank weights on the next admitted hit.

An explicit integer seed is required. Repeated selections are allowed by default. no_repeat = true excludes the previous source identity before drawing when another note is available; it preserves the remaining notes' original rank weights. A single-note bank still plays. A hit's repeats reuse its choice, and rhythm rests, ties, masked steps, and chance-rejected hits consume no choice. Bank-edit retrigger, an empty bank, clear, and stop forget the previous identity while retaining the random counter. Python snapshots retain both. Both engines support live held and latched banks; file rendering, history, and marked samples reject choice. Shared traces specify the exact draws.

arpeg/capture.py records a MIDI phrase through MidiCapture.accept and closes it with finish. Its profile declares whether overlapping onsets hand off a monophonic segment or remain independent notes. The original MIDI bytes stay in the captured ledger, including velocity-zero note-on releases. The Python capture fixture checks use the wind and gap traces in conformance/.

The Python gesture renderer reorders completed MIDI notes in source timing or fits their gestures to an output gate. It restores known controller entry values before each onset, retains note-local event times, and allocates a separate channel when gestures overlap. Unowned gap events remain in the captured phrase and are not sent into an unrelated output note. It can instead use live breath and bend, or replay the original MIDI ledger unchanged. For a single-channel destination, overlap="handoff" ends the old output note before initializing the new gesture on channel 1.

arpeg/bank.py records history or phrase takes. Completed history notes wait for the declared capture tail, and replace, overdub, and undo changes become visible at publish_step. Clear empties the published bank immediately. select_step chooses a source note from the published revision; callers can place it through the gesture renderer at an output onset.

arpeg/history.py is the Python reference for live recorded history. LiveHistoryArpeggiator captures channel 1 MIDI, selects completed notes at exact grid steps, and fits their recorded breath and bend gestures to each output gate. It owns one sounding output note at a time: each new step ends the old note before starting the next gesture. clear forgets the history and releases the output note while continuing to capture new input. The Rust event core implements the same step and handoff rules.

arpeg/marked_sample.py turns ordered frame markers into identified SourceNote regions. An explicit marker at frame zero accounts for any prefix, and the last region ends at the asset boundary. Selection keys order regions without claiming an acoustic pitch. The Rust core checks the same region fixture. The enge sample_regions adapter consumes these notes through its prepared sampler; adjacent source regions share a continuous cursor run. Source-rate identity and reordered playback have one-second WAV regressions on both sampler backends.

The standalone arpeg executable validates supported profiles, renders single-track metrical MIDI files containing note and tempo events, and plays classic or recorded-history arpeggios through midir on Linux, Windows, and macOS. The Python arpeg-python host uses mido and its RtMidi backend to run the reference engines with the same presets on those platforms. Both hosts read MIDI channel 1 and output on channel 1. The default clock uses internal BPM; both hosts also accept external MIDI Clock and transport. They timestamp messages when the input callback runs, poll every millisecond, and send events immediately when due. Driver timestamps and future output timestamps are not used yet. Hardware timing and device behavior have not been verified.

Python MIDI port access requires the midi extra. The default package installs the reference engines, mido, and the CLI without the native RtMidi backend:

uv sync --extra midi
uv run --extra midi arpeg-python list-ports
uv run --extra midi arpeg-python play --profile conformance/up.toml --source 0 --destination 1 --bpm 120
uv run --extra midi arpeg-python play --profile conformance/history-wind.toml --source 0 --destination 1 --bpm 120

Use the indices listed by the host you intend to run; Python and Rust port enumeration need not use the same order. Mido's RtMidi backend filters incoming active-sensing messages; received note, controller, bend, clock, and SysEx messages are handled through its portable API. Native driver selection is inside the MIDI libraries, with no OS-specific calls in arpeg.

cargo run -p arpeg-midi -- validate conformance/up.toml
cargo run -p arpeg-midi -- render-file conformance/up.toml input.mid output.mid
cargo run -p arpeg-midi -- list-ports
cargo run -p arpeg-midi -- play conformance/up.toml SOURCE_INDEX DESTINATION_INDEX 120
cargo run -p arpeg-midi -- play conformance/live-latch.toml SOURCE_INDEX DESTINATION_INDEX 120
cargo run -p arpeg-midi -- play conformance/euclidean.toml SOURCE_INDEX DESTINATION_INDEX 120
cargo run -p arpeg-midi -- play conformance/custom-steps.toml SOURCE_INDEX DESTINATION_INDEX 120
cargo run -p arpeg-midi -- play conformance/weighted-walk.toml SOURCE_INDEX DESTINATION_INDEX 120
cargo run -p arpeg-midi -- play conformance/alternating.toml SOURCE_INDEX DESTINATION_INDEX 120
cargo run -p arpeg-midi -- play conformance/history-wind.toml SOURCE_INDEX DESTINATION_INDEX 120

Live clock and transport

For external synchronization, add --clock external to either play command. Clock and transport come from the note input by default. Add --clock-source INDEX to use a separate input; that port accepts only Clock, Start, Continue, Stop and Song Position Pointer, while the note port supplies performance events. Selecting the note port's own index opens it once.

MIDI Clock uses 24 pulses per quarter note. External playback initially waits for Start or Continue, then the first pulse anchors the current beat. The second pulse establishes an interval. Between pulses, the clock interpolates using the latest interval, capped at the next expected pulse. Late pulses never move playback backwards. This conservative estimator can stall with jitter; it does not extrapolate through missing pulses. After --clock-timeout-ms (default 500), it stops and releases owned notes. Fresh pulses alone do not restart it: send Start or Continue to reacquire.

Enter start to restart rhythm and traversal while retaining the source bank; pause releases output and freezes beats and traversal; continue resumes them. The corresponding MIDI transport messages do the same. Song Position Pointer uses sixteenth-note units, releases output, resets traversal, and resumes at the next eligible rhythm boundary. Seeks discard pending repeats and gestures rather than emitting crossed attacks. Random counters remain intact across transport changes. External Continue and seeks wait for a pulse before resuming.

With the internal clock, enter tempo 90 to change BPM without changing beat phase. Recorded source timestamps remain microseconds; fitted gestures and gates follow playback beats, including through tempo changes and pauses. Capture remains available while playback is stopped. Python player snapshots retain both engine and clock state, including clock observations; observation storage is capped at one million messages. Shared transport traces check the same beats and MIDI bytes in both implementations.

Enter clear to empty a latched or history bank and release its owned output notes. Press Enter or Ctrl-C to exit live playback. The default retrigger = "on_empty" continues selection through chord edits; retrigger = "bank_edit" restarts selection at the first note on the next grid step without moving the grid. File rendering rejects bank_edit until it can reproduce the same live decisions from a complete input trace. Recorded history is live only; file rendering rejects its profile. The current history preset captures CC2 breath and pitch bend from channel 1 and fits their recorded note gestures to each step. Channel 1 handoff ends overlapping gestures rather than assigning them independent MIDI channels.

Live expressive performance

Both hosts now follow channel 1 breath (CC2), pitch bend and channel pressure with held or latched notes. Known values are restored before each generated onset, in breath/bend/pressure order; unseen values are left alone. Changes follow the owned sounding note. They are remembered during rests and pauses without being sent to an unrelated voice. Other input channels, sustain pedal, polyphonic pressure and other controllers are not live expression lanes yet.

live-wind latches overlapping keys into a chord and uses live expression. Its gate exceeds the step, demonstrating channel 1 handoff. Scheduled releases belonging to an earlier output occurrence cannot cut off the new one, even when the pitch repeats.

history-live-wind captures recent completed notes and plays their pitches and velocities with your current breath, bend and pressure. The recorded controller ledger remains stored but its gestures are not played in this mode. history-wind instead restores recorded entry values and fits recorded changes, now including channel pressure, to each gate. Controllers arriving outside a completed capture cell remain in the raw ledger and can establish the next note's entry state; they do not extend that cell or become part of its gesture.

Use either preset as the --profile value in Python or the profile argument in Rust. Shared performance traces cover live and recorded expression, handoff, unknown state, pause and clear cleanup. The live hosts still have one independent expression owner on channel 1. MPE and MIDI 2.0 remain later work.

Live phrase capture

Use phrase-wind as the live profile in either Python or Rust. The phrase bank starts empty and listens to channel 1:

  • record starts a new take immediately. The previous committed bank keeps playing.
  • commit ends the take and replaces the bank at the next grid step.
  • overdub ends the take and adds its notes to the bank at the next grid step.
  • undo removes the latest committed take at the next grid step, restoring the preceding bank. An ongoing recording continues.
  • clear discards the bank and any recording and releases owned output immediately.

Committing closes any still-held captured notes. An empty replacement makes subsequent steps silent; an already sounding gesture keeps its scheduled release. Capture controls do not cut off sounding gestures. New notes hand off channel 1 at their scheduled step as usual. The preset restarts traversal on each published bank edit; retrigger = "on_empty" preserves traversal through edits.

Known breath, bend, and channel pressure are sampled at the recording boundary and stored as entry state. Changes within the take retain their MIDI bytes and source timing; playback fits their gestures to the gate. Set expression source = "current" to use live expression instead. Recording works while transport is paused, and commits wait for a step after playback resumes. Input is captured material rather than MIDI through.

The same controls are available through MidiPlayer.capture(time, command) in Python, Rust, and WebAssembly. Python uses nanoseconds; Rust and WebAssembly use microseconds. The native hosts report recording status, published note count, and bank revision after commands. Recording is bounded to one million events between clears and 128 committed takes, including undo history. Undo frees a take slot; clear frees both budgets. Capture identities stay unique after undo and clear. The raw captured source remains in the bank, independent of rendered output. Shared phrase traces cover replacement, overdub, undo, entry state, pause, boundary timing, and cleanup with coarse and frequent polling. Phrase playback currently uses a grid with classic note orders, fit timing, and carried gaps, matching history playback.

Motion control ports

Live MIDI expression has one declared source per lane. For example:

[body.expression]
source = "current"
lanes = { breath = "motion", bend = "recorded" }

Unspecified lanes inherit source; each override accepts current, recorded, or motion. Recorded lanes require a history or phrase bank and the existing timing = "fit", gaps = "carry" playback policy. Held and latched banks support current and Motion lanes. Received MIDI remains captured regardless of ownership.

Motion samples reach the sounding note immediately. Breath and channel pressure use exact values from 0 to 1; bend uses normalized wheel position from -1 to 1, with 0 at center. The synth sets the bend interval. Values round to the nearest MIDI integer, with ties upward. There is no interpolation or source mixing. Samples during rests or pauses are held for the next onset; an unsampled lane has no invented initial value. Current/Motion entry values precede the recorded entry bundle and note-on, preserving recorded event order. Motion samples are not added to the received MIDI recording. Python snapshots retain exact samples.

Use motion-expression with either native host and enter breath 1/2, bend -1/4, or pressure 1. Python uses ArpeggiatorControl with those port names; WebAssembly uses control(time, port, value). These expression controls belong to the MIDI player; the pure step engines and limited file renderer reject them.

Live selection offset chooses another identified note from the current bank, including that note's velocity and recorded expression:

[body.selection_offset]
ranks = 1
boundary = "wrap"

Ranks count upward by source pitch, using source identity to break equal-pitch ties, regardless of the selector's own order. With C–E–G, +1 maps C to E, E to G, and G to C. The selector continues from its original choices, so offset changes preserve ascending, shuffled, and random progression. ranks defaults to 0 and accepts signed 64-bit whole numbers. boundary defaults to wrap; rest skips targets outside the bank while advancing the selector. An empty bank emits rest. A single-note bank wraps to itself; rest requires rank zero. Index-pattern rests are resolved before applying this offset, and density rejections do not advance selection. Transposition applies after choosing the target. Rhythm timing, sounding gestures, and repeated groups remain unchanged.

Enter selection_offset -1 during live playback, or send selection_offset through the Python, Rust, or WebAssembly control API. It publishes at the next step, with the same latest-value, pause, and seek rules as transposition. Python snapshots preserve the original selector cursor and the effective and queued offsets. Nonzero preset offsets require live playback rather than file rendering.

Live transposition changes the selected note's MIDI pitch without changing its source identity, selection order, or recorded controller values. Configure it in the preset:

[body.transposition]
semitones = 12
boundary = "drop"

semitones defaults to 0. boundary defaults to drop: an out-of-range selection advances the order and emits rest. fold moves an out-of-range pitch by the fewest octaves needed to reach MIDI 0–127, leaving in-range pitches alone. error reports an invalid selected pitch; API hosts should stop playback on that error to release their last delivered note. Native hosts do this on exit. No policy clamps pitches. Whole signed 64-bit semitone offsets are supported; fractional offsets are rejected. Nonzero preset transposition requires live playback rather than the limited file renderer.

Enter transposition -12 during live playback, or send the transposition control port from Python, Rust, or WebAssembly. The latest value takes effect at the next step. Sounding notes and pending repeats retain the pitch realized for their original step. Pause retains pending controls; Start and position seeks cancel them, retaining the last published value.

Live held, latched, history, and phrase engines accept two scalar input ports:

  • gate: a nonnegative exact gate fraction. Values above 1 allow overlap.
  • density: hit probability between 0 and 1. It starts at the preset's probability and replaces it when changed. Fractional density requires an explicit preset seed and uses the existing probability draw counter.

Changes publish at the next unprocessed step, including a control delivered before that step at the same time. The latest queued value per port wins. Sounding notes, fitted gestures, tied gates, and pending repeat groups keep their realized parameters. A pause retains queued controls; Start or song-position relocation cancels them. Density rejections leave note selection unchanged.

Use motion-ports in either native MIDI host and enter gate 1/2 or density 2/3. The preset includes the required seed. For a Python host:

from ufor.arpeggiator_ports import ArpeggiatorControl

control = ArpeggiatorControl.model_validate({"port": "density", "value": "2/3"})
messages = player.control(now_ns, control)
messages.extend(player.advance(now_ns))
batch = player.take_events()
for event in batch.events:
    print(event.at, event.port, event.index, event.revision)

Rust accepts player.control(now_us, InputPort::Density, value) and returns the same MIDI output plus a separate take_events() batch. Both pure event engines also expose controls and their ports state. Python snapshots preserve effective values, queued controls, undrained events, and probability counters.

Every opportunity produces step, then hit when a source note was admitted or rest otherwise. Ties produce only step; repeats share one hit. Events carry exact beat time, rhythm index, and bank revision. They describe source admission; a MIDI destination may subsequently hand off an older output note.

History and phrase banks emit capture_ready when a step publishes a changed, nonempty bank, before step and its outcome. The event carries the publication's beat, step index, and bank revision. Several captures published together emit one notification. Replacement, overdub, and undo can notify; unfinished recording, unchanged banks, and empty banks do not. Readiness is independent of density: even a rejected hit can publish usable captured material. Route this event to a Motion that should restart when a new bank becomes eligible.

cycle marks the first step of an authored rhythm loop, including the first loop. Euclidean loops contain steps opportunities; custom patterns loop after their final cell, using the cells' exact durations. Plain grids emit no cycle event. Rotation moves Euclidean hits without moving the cycle boundary. Empty banks, rests, ties, density rejection, and chord edits preserve rhythm phase. Pause preserves phase; Start and seeks relocate it without replaying crossed boundaries. Each cycle event carries its step's beat, index, and bank revision, before step and its outcome. This describes rhythm loops independently of note selector traversal. Hosts can use it to restart a Motion once per rhythm loop.

The MIDI player's batch also has notes: realized note_start and note_end notifications with exact beat at, player-local occurrence, source identity source, delivered key, and attack/release velocity. Held/latched sources use input:ID; captured sources use CAPTURE:NOTE. Repeated attacks and pitches have distinct occurrences; each end retains its start's identity. Handoffs emit old end before new start. Zero gates emit start then end at the same beat. Pause, clear, empty-bank release, and seeks close owned occurrences once; stale scheduled releases produce no duplicate notification. A note end reports a MIDI release, rather than the end of a synth's audible tail. Pure engines leave this list empty. Python snapshots retain sounding and undrained lifecycle state.

Each list holds at most 4096 events. A delivered start reserves a slot for its end. If notes fills, new output attacks and their gestures are suppressed while owned releases continue; draining permits future attacks without replaying skipped ones. batch.exhausted reports either list's exhaustion. Drain both lists using the same take_events() call. This describes returned MIDI output; it does not confirm delivery to hardware or reconcile a restored synth.

The host samples its Motion before the target step and routes collected events to other Motions. Convert sampled scalar values to explicit rational values; there is no hidden float rounding or embedded Motion graph evaluator. Feedback needs an explicit delay of at least one scheduling quantum and a bounded host event budget.

Drain after each operation. The output buffer holds 4096 events and reserves two slots before admitting a step, plus one for each capture_ready or cycle. Notifications, step, and outcome are admitted together. When this decision list fills, new step admissions and their notifications stop while due releases and already realized repeats continue. batch.exhausted reports the condition; draining permits future steps without replaying skipped attacks. Skipped notifications are not replayed; subsequent steps still report the bank revision. Native MIDI hosts drain automatically and print a diagnostic on exhaustion. Shared traces cover selection offsets, target expression, transposition, folding, gate edits, density and seeded decisions, ties, repeats, captured material, pause, seeks, and polling parity.

Preset header defaults

Presets may omit kind; it defaults to "arpeggiator". They may also omit name when loaded from a file: presets/bright.v2.toml becomes "bright.v2". An explicit name takes precedence. In-memory presets still require a name, and invalid explicit names or kinds remain errors.

The Python preset reader is arpeg.profile.parse_profile(text, path=None); pass the source Path when reading a file. Rust's parse_profile(text, path) accepts Some(path) for file content or None for in-memory content. Both live hosts supply the profile path, as do Rust's validation and file-rendering commands. The resulting Python score retains its resolved name and kind when serialized.

For example, up.toml can contain:

title = "Up"

[body]
selection = { kind = "ascending" }
rhythm = { kind = "grid", step = "1/4 beat" }

Development

Building the Rust MIDI host on Linux requires ALSA development headers and pkg-config (libasound2-dev and pkg-config on Debian/Ubuntu). ALSA is also needed at runtime. Windows and macOS builds use their system MIDI services. The pure event core does not open devices.

GitHub Actions builds and tests both implementations on Linux, Windows, and macOS, including Python installation with the MIDI extra. These tests need no physical MIDI device and do not establish hardware latency or USB-driver behavior.

uv sync
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv run ty check arpeg
cargo test --workspace

Metadata

Release files for arpeg 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for arpeg 0.2.0
File Size Uploaded Provenance Actions
arpeg-0.2.0.tar.gz 196.3 kB Without provenance Details for arpeg-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for arpeg 0.2.0
File Interpreter ABI Platform Actions
arpeg-0.2.0-py3-none-any.whl Python 3 none any Details for arpeg-0.2.0-py3-none-any.whl

Total release size: 240.5 kB

Release files / arpeg-0.2.0.tar.gz

Download URL arpeg-0.2.0.tar.gz
Size 196.3 kB
Tags Source
SHA-256 checksum
How to use checksums
12b5d8378c7f666197e02864989005918319785cb1880f464ab93c69aefd08a9
BLAKE2b-256 checksum
How to use checksums
0cf10ce23396d60c5ad64cd4586a80c2b5b4d5b37981db46d6b44280d4ee10cb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / arpeg-0.2.0-py3-none-any.whl

Download URL arpeg-0.2.0-py3-none-any.whl
Size 44.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
85f5fca7ae30fca7a45f33179557992a16fa731cfe3234d48930350b38ed941f
BLAKE2b-256 checksum
How to use checksums
1134509b53b9cd97a1a2d8940c14964d76712de0fca013415c2a59add95b76c5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for arpeg 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page