Analog Keys: sequencing, sessions and hardware integration
Connecting the native piano roll, musical services and transport to an explicitly scoped hardware adapter.
Technical research · edition 2 · reviewed 2026-10-24

Prismal Labs native editor research fixture, 10 October 2026. This is an application test image, not a photograph of the hardware. View full-size image
What we found
The sequencing work uncovered a class of bugs that protocol tables cannot prevent: notes can be correctly encoded but released by the wrong owner, tempo can be correct on average but drift across clip boundaries, and a stopped recorder can silently conceal an earlier input gap. We made time, ownership and failure state explicit.
The reusable design is a prepared timeline with stable note identities, integer tempo integration, and bounded cleanup. Device discovery remains separate from scheduling. A model appearing in an adapter registry does not mean its hardware transactions have been implemented or tested.
Integrate musical time once
Compilation follows the session/MIDI rules (SESSION-MIDI.md, retained research): 960 PPQ, independent
clip repetition, note clipping at clip/scene ends, scene and row mutes, row repeats,
and stopping after an end-marked row. Empty songs require an explicit scene ID.
Selecting a scene starts it at tick zero with the session tempo map, matching SMF
scene export. Muted clips occupy time without producing messages.
For absolute tick t, integrate integer tick spans times their tempo in
microseconds per quarter, then multiply by sample rate and divide by
960 * 1,000,000, rounding half upward once. Tempo changes therefore introduce no
per-clip or per-block accumulation error. Meter changes do not change tick duration.
Every note must retain at least one sample; compilation rejects collapsed note
lifetimes and controls rounded outside the final playable sample. It does not
silently lengthen notes or move those controls.
Route indices follow Compiled.device_ids; track indices follow
Compiled.track_ids. Track channel numbers remain 0–15. Device.midi_port is an
SMF label and never opens or selects a native port. Binding stable device IDs to
verified host/device outputs is the future adapter's responsibility. These IDs
must accompany the binary timeline as application metadata; the binary contains
indices only. A track must retain one route/channel throughout a timeline.
Events at one sample are ordered note-offs, controls, then note-ons. Compilation preserves session track order and original event order within a phase. C++ uses a stable phase sort. Note-on velocity zero is rejected; a note lifetime has one nonzero token, one onset and one later matching release. Tokens are unique within the complete expanded timeline. Release velocity is preserved, including during stop, mute, retrigger or loop truncation.
Evidence: Elektron/PLAYBACK.md.
Own note lifetimes, including failure cleanup
| Operation | Behavior |
|---|---|
start(frame) | Drain previous ownership, then start at the requested sample; an enabled loop clamps outside positions to its start |
seek(frame) | Drain old ownership and seek; resume only if already running/restarting |
stop() | Stop new playback and schedule cleanup at the next positive-length callback |
setLoop(true, begin, end) | Use [begin,end) within the timeline; reject empty/out-of-range loops; seek to begin if the current position is outside |
setMuted(track,true) | Drain that track's currently owned gates and last-owned hold controllers before further playback; skip its later onsets and controls |
| Unmute | Future events resume; skipped notes and earlier controls are not reconstructed |
| Same wire pitch retrigger | Explicitly release the previous gate before the new onset; the latest token owns that route/channel/pitch; an older scheduled off cannot kill it |
| Natural/loop end | Emit authored edge releases, then release remaining gates and owned holds; a loop restarts only after this cleanup |
Callbacks use half-open sample ranges. An event exactly at a callback's end is handled at offset zero in the next positive-length callback. A song ending exactly there therefore needs that next callback to release notes and report stopped. Zero-frame callbacks emit nothing and preserve pending work. Timeline rate must match the adapter's sample clock; Player does not resample or follow host tempo.
Seek intentionally performs no note, pedal, program, pitch-bend or controller chase. Recompile/reposition for tempo or sample-rate changes through a future native integration; changing only the callback rate would change musical timing. No existing voice resumes after a newer same-pitch voice ends. Tracks sharing a route/channel necessarily share MIDI pitch and controller state; they are not independent instruments.
Hold controllers 64, 66 and 69 are tracked from accepted messages and explicitly released on stop/seek/end. Their switch numbers and thresholds follow the MIDI Association controller table. The last track to turn a hold on owns its cleanup; muting that track sends the channel-wide reset, which can release other tracks' sustained voices on the same channel. Explicit off values clear the obligation. Generic channel-mode commands are forwarded but do not erase individual note/pedal cleanup obligations: queued delivery alone cannot establish a device's reset implementation.
| Limit | Value |
|---|---|
| Prepared events | 250,000 |
| Devices/routes and tracks | 16 and 128 |
| Sample rate | Integer 8,000–384,000 Hz |
| Timeline duration | 1 through 2^63-1 samples |
| Packet attempts per callback | 2,048, including a rejected attempt |
| Event/loop work units per callback | 4,096, including suppressed events and loop iterations |
| Player storage in tested MSVC x64 build | 328,776 bytes, allocated before processing |
| Dense ownership scan | At most 32,768 addressable route/channel/pitch cells; 16,384 simultaneous gates are reachable with 128 fixed-route tracks |
| Hold scan during cleanup | 256 route/channels × three controls |
| Prepared binary | At most 5,000,032 bytes |
Compilation conservatively bounds repeated-clip expansion before traversing it, including the final partial repeat in the estimate. Timeline validation does not promise that an arbitrary arrangement fits a particular callback or physical port's throughput. The integration layer needs its own routing/admission policy.
Output rejection or a work limit latches a failure, stops new events, cancels a
queued restart and preserves all remaining cleanup obligations. Successful
releases are removed immediately; rejected ones remain owned. Continue callbacks
to drain them. A stop with more than 2,048 outstanding packets drains over multiple
callbacks and can latch the work-limit diagnostic; it never discards the remaining
notes. The diagnostic remains after cleanup, and restarting requires an explicit
start(). Even a one-sample silent loop in a huge callback reaches a bounded limit.
Result.attemptedPackets and Result.workUnits report bounded work, not guaranteed
device delivery. ownedNotes excludes notes whose individual release has been
accepted, although a hold may still sustain them. needsCleanup reports pending
stop/failure/mute cleanup; ordinary active playback also retains ownership. Keep
the output adapter alive through cleanup; destroying a Player does not send MIDI.
Evidence: Elektron/PLAYBACK.md.
Recording must preserve discontinuities
Core/include/elektron/Recording.h owns a preallocated array of up to 65,536
timestamped channel messages. Construct it off the realtime path, then give it
one owner thread. That owner drains an input queue and calls start, push,
advance and stop. It must consume or explicitly discard a stopped take before
starting another: start reuses and clears the retained storage.
Each accepted event contains an integer frame relative to the take's start and the exact channel status/data bytes. Rates are 8,000–384,000 samples/second; duration is bounded to 24 hours. A nonzero generation rejects late messages from an older take. The optional channel filter selects 0–15, or -1 for all channels. Clock, system and SysEx messages are ignored. Notes, poly pressure, ordinary CC, program, channel pressure and pitch bend retain their original bytes.
The core does not read a clock. An input adapter must convert the source's hardware timestamps into the same frame origin/rate as playback and propagate every queue discontinuity. A delayed input packet may precede the latest sender poll, but packets within a take must remain ordered. Backward packet timestamps, invalid packets, an input gap or capacity overflow stop capture and retain the complete accepted prefix with an explicit reason. A later normal Stop cannot erase that reason. No memory allocation/free, file I/O, lock or MIDI output occurs inside capture, advance, stop or storage reuse.
Evidence: Elektron/RECORDING.md.
Device support is an evidence contract
- Pin an official manual/protocol revision and record the exact device firmware. Audit the relevant table columns visually, including repeated addresses, missing entries, mode aliases and channel configuration.
- Establish parameter encodings with documented ranges and independent captures at endpoints and representative intermediate values. Keep contradictory entries disabled. Test synthetic byte fixtures, route collisions, unsupported revisions and partial output failures before any hardware write.
- Verify the declared controls on that exact hardware, with clear expected changes and recoverable settings. Test concurrent notes/clock and unchanged LCD behavior. Generic routing alone is not parameter validation.
- Develop proprietary operations independently: first captured read-only request/reply and lossless parsing, then offline editing with malformed and capacity tests, then verified publication and recovery on actual hardware.
This audit uses manufacturer facts and this project's own contracts. It imports
no reference editor code, controller map or fixture. The Digitakt II manual link
returned by search could not be retrieved in this pass; its cached excerpt was
not used to enable mappings. Other families still need their own revision-pinned
audits and captures. The provenance guide (PROVENANCE.md, retained research) records the existing
implementation and the separately archived reference repositories.
Evidence: Elektron/DEVICE-ADAPTERS.md.
Applying the findings
A useful timing formula is frame = round_half_up(sum(deltaTicks * tempoMicroseconds) * sampleRate / (960 * 1,000,000)). Sum integer tempo spans first and round once. For example, 960 ticks at 500,000 microseconds per quarter produce exactly 24,000 frames at 48 kHz. Meter changes affect musical labels, not the duration of a tick.
Use half-open callback intervals and test an event exactly at the end of a block. Then partition the same timeline into different block sizes and compare the emitted stream. For repeated equal pitches, give each onset its own token: an older scheduled note-off must not kill a newer retrigger. On output rejection, retain the release obligation and keep draining it in later callbacks.
Limits and open questions
The prepared player does not perform controller chase or follow a new host tempo without recompilation/repositioning. Stable device IDs must still be bound to verified outputs by an adapter. Recording tests establish timestamp and editing behavior; they do not prove a physical controller's latency or delivery reliability.
Research illustrations

Prismal Labs native editor capture, illustrating the corresponding software project. View full-size image
Primary references
- Elektron Analog Keys user manual
- MIDI Association: Standard MIDI Files specification
- MIDI Association: MIDI 1.0 Control Change messages
Research provenance
The methods and results above are drawn from these retained research records. Dates and product versions inside the article identify the scope of each experiment; publication review does not mean the hardware or numerical tests were rerun.
Elektron/PLAYBACK.md— SHA-25604cae07568dda096167647d4958620d913b68e7f5956120ac98f35f488e87bf3Elektron/RECORDING.md— SHA-256be03fa3e9205977f0568481df08b4a1322b5210b08999a5401636db08b4887beElektron/DEVICE-ADAPTERS.md— SHA-25610e6c05b799cffe884aad16e72f8e643b60ff91633bbd1e8c8720e87ed763aea
