Haiku BMidiStore: Sequence Capture, Event Ordering, and MIDI File Limits
Use Haiku BMidiStore for offline MIDI event capture and file exchange, accounting for mixed time units, tempo conversion, sorting, and export limits.
BMidiStore is a BMidi implementation that collects MIDI events into an in-memory sequence and can import or export MIDI files. It is useful for recording, inspecting, or transforming events before playback. It is not the same as connecting live MIDI producers and consumers through the MIDI roster. The storage object has sequence ordering and file-format behavior that should be understood separately from endpoint routing.
The API accepts the familiar BMidi event calls, such as NoteOn(), ControlChange(), and TempoChange(). It also exposes event count, current event position, sorting, tempo, import/export, and playback-related methods inherited from BMidi. A practical implementation must preserve time-unit meaning and verify the target format rather than assuming every MIDI file feature is round-tripped.
Record events with one clear time base
The BMidi methods use a uint32 time argument. B_NOW is defined as system_time() / 1000, so live timestamps default to milliseconds. MIDI-file events, however, are represented in ticks when imported. The current BMidiStore implementation tracks whether an event’s time is ticks or milliseconds internally. Do not compare event timestamps from those origins without normalizing them through the store’s timing rules.
BMidiStore store;
store.NoteOn(0, 60, 100, B_NOW);
store.NoteOff(0, 60, 0, B_NOW + 250);
store.SortEvents(true);
uint32 eventCount = store.CountEvents();
uint32 startTime = store.BeginTime();
This is a minimal sequence-capture sketch, not a file export. B_NOW + 250 is only meaningful if the producer uses the same millisecond time domain and the arithmetic does not wrap. For a long-running process, uint32 wraparound is possible; normalize or segment capture before timestamps become ambiguous. For live sources, capture arrival time consistently and avoid mixing an absolute event clock with a relative delta from the first event.
The current implementation’s DeltaOfEvent() has a source comment explaining that imported MIDI-file events return file-style delta timing, while events captured from other BMidi objects can return absolute timestamps. That behavior is subtle and easy to misread. Write tests for each event origin and do not label every returned value “delta” without checking the source and target SDK behavior.
Sort and inspect without losing ordering intent
BMidiStore can collect events from multiple callbacks. SortEvents() orders them, and CountEvents() exposes the current number. For events sharing the same timestamp, define a deterministic tie policy in your application if note-off/note-on ordering or controller precedence matters. Do not assume a time sort resolves semantically conflicting simultaneous messages.
CurrentEvent() and SetCurrentEvent() expose a positional cursor. A numeric index is not a stable event identity after sorting or mutation. If a UI lets a user select an event while another thread appends or sorts, serialize access and restore the selection by an application-level key rather than reusing the old index. Keep the store’s event mutations on one owner thread unless your synchronization model proves otherwise.
Tempo changes affect how ticks map to elapsed time. Set a known tempo policy for captured sequences and record tempo events when they are part of the performance. SetTempo() changes the store’s tempo value; it should not be treated as rewriting every existing event’s timestamp in place. Verify playback timing against a metronome or independent expected schedule.
Import with explicit format expectations
Import(entry_ref*) opens a referenced file and parses MIDI file chunks. Check the returned status and only expose the sequence when import succeeded. The current source accepts Standard MIDI File header data, tracks events, and sorts the result. It does not support SMPTE time-code division as a faithful time base: the implementation detects the high-bit form and substitutes a ticks-per-beat value. Treat SMPTE-division inputs as a compatibility limitation and reject or warn when exact timing preservation matters.
Parsing external files is a trust boundary. Test truncated headers, malformed chunk lengths, missing tracks, oversized SysEx data, invalid variable-length quantities, and incomplete events. The public interface returns status_t, but a robust application should also constrain maximum file size and event count before rendering a large sequence in a UI.
The current public interface has no general-purpose Clear() method. The implementation’s Import() path reads and adds events to the store rather than establishing a documented replacement transaction. Use a fresh BMidiStore for each import unless the exact target implementation provides another verified reset path. Otherwise a retry after a recoverable error can mix old and new sequence content in ways that look like a valid but duplicated song.
Keep the original source file if import is part of a conversion workflow. Store the chosen tempo interpretation and format provenance with the converted sequence. A successful import means the implementation accepted the file; it does not mean every vendor-specific metadata chunk or track arrangement was preserved.
Export only what the implementation actually writes
Export(entry_ref*, int32 format) takes a format argument in the header, but the current implementation writes a single-track format-0 file and ignores the supplied format when writing the header. Do not promise format 1 or 2 based on the method signature. If track separation matters, verify the actual implementation for the target Haiku revision or use a different exporter that provides the required format.
Before exporting, sort the events, validate the destination, and write to a temporary file when practical. Check the return status and then reopen the output with an independent parser. Compare event counts, timing, note pairing, controller values, and tempo information. The current implementation’s ability to write an SMF does not establish lossless round-trip preservation for every MIDI file feature.
If the target path already exists, define whether it may be replaced and preserve the old file until the new output is complete. The export call’s success should not be followed by assumptions about filesystem durability or user-visible publication; handle close and rename failures in the surrounding file workflow.
The current implementation opens its destination with B_READ_WRITE; it does not visibly request erase/truncate mode in that call. For repeatable output, export to a fresh temporary file or explicitly prepare a destination with the desired truncation policy before invoking the store. Reopen the final file and verify its length and header so stale trailing bytes from a previous, longer export cannot be mistaken for valid output.
Playback and application responsibilities
BMidiStore derives from BMidi and includes a Run() implementation, so it can participate in event playback and connections. Keep playback control separate from storage. Use the appropriate MIDI synth or endpoint for output, and stop the object before destroying it or replacing its data. Do not use a store’s event index as a substitute for an endpoint connection state.
For sequencing UI, make tempo, event time origin, current cursor, and source format visible in diagnostics. A sequence imported from file ticks and a live capture recorded in milliseconds should not be silently concatenated unless you convert both into a common timeline. Preserve original timing where the format requires it, and avoid quantizing during import just to make a display grid look tidy.
Failure-oriented verification
Test live event capture, out-of-order callbacks, equal timestamps, tempo changes, import of format-0 and format-1 files, SMPTE-division input, SysEx, empty files, invalid chunks, and export with each advertised format argument. Verify the actual output header and track count instead of trusting a UI label. Reopen exported files using an independent MIDI parser and compare the semantic event timeline.
Test capture near the uint32 timestamp wrap boundary and confirm that the app handles it without a negative-looking duration or reordered event sequence. Include cancellation during import/export and ensure temporary files are cleaned without overwriting the last good project.
Acceptance criteria
Accept a BMidiStore workflow when live millisecond times and imported ticks are never confused, sorting and cursor state are synchronized, SMPTE-based input is handled as a documented limitation, and export claims match the implementation’s actual single-track format. Verify outputs independently.
BMidiStore is a useful sequence container and adapter. It does not make all MIDI timing bases equivalent or guarantee that every input file feature survives a round trip.
Related:
- Haiku MIDI Kit: Roster-Based Endpoints, Connections, and Timestamped Events
- Haiku BTimeCode: Frame Labels, Drop-Frame Arithmetic, and Clock Boundaries
Sources: