Haiku BTimeSource: Media Performance Time and Clock Drift
Coordinate Haiku media nodes with BTimeSource performance time, real-time conversion, start latency, time warps, and measurable drift behavior.
BTimeSource is the Media Kit’s clock abstraction for coordinating media performance. It lets nodes reason about when media events should happen in the graph, and it provides conversions between performance time and real time. It is not a general-purpose replacement for wall-clock APIs, a calendar timestamp, or an assumption that every device clock runs at exactly the same rate.
That separation is fundamental. An audio device may run at a rate that differs slightly from another clock; video frames may arrive late; a graph can be started, stopped, sought, or moved onto a new time base. A media node needs to schedule work relative to the graph’s performance timeline, while the time source relates that timeline to real-time scheduling. Treating timestamps as interchangeable integers leads to drift, incorrect ordering, and synchronization bugs that grow over long playback.
Performance time is a media coordinate
BTimeSource::Now() returns the current performance time according to that source. PerformanceTimeFor(realTime) and RealTimeFor(performanceTime, latency) convert between the two domains. The conversion is meaningful only in the context of the selected time source and its current state. Do not store one conversion forever and assume that it remains valid after a time warp, seek, source switch, or other graph operation.
Media timestamps should be interpreted according to the node protocol and negotiated format. A producer typically associates a time with a buffer so downstream nodes can present or process it at the right point. A consumer that handles the buffer immediately without considering its start time can exhibit jitter or drift even if its callback is fast. Conversely, adding a fixed sleep based on an assumed frame period may duplicate scheduling already managed by the Media Kit.
The static BTimeSource::RealTime() helper is not the same thing as media performance time. It represents the API’s real-time clock domain. Use the source’s conversion functions when the question is “when should this media event occur?” Use ordinary calendar/time-zone APIs only for user-facing date and time. A log may record both domains, but label them explicitly.
Mapping a deadline with latency in mind
RealTimeFor() accepts a performance time and a latency argument. The latency parameter exists because the operation may need to account for a known delay when translating a media event into real time. Do not pass a made-up constant or copy a value from another node. Measure or obtain the relevant latency using the graph’s API and include the components that the node is responsible for.
GetStartLatency() exposes the time source’s start latency. This contributes to coordinated startup, but it is not a universal statement about end-to-end output latency. Device buffering, conversion, scheduling, and downstream nodes can add further delay. For a synchronization claim, inspect the actual graph path and measure output behavior rather than summing one field and calling it total latency.
When a node has a timestamped buffer, make the scheduling decision in the selected performance-time domain. If a deadline is already late, use an explicit policy: process immediately, drop stale data, or report lateness according to the node’s role. Do not sleep for a negative duration or let a late item block newer work. For live streams, bounded lateness handling is often more useful than attempting to recover every old frame.
Drift, time warps, and changing clocks
GetTime() reports performance time, real time, and drift. The drift estimate is an important signal: two clocks can be close but not identical, and their difference accumulates. A playback engine that compares only the first few buffers may appear synchronized before the error becomes visible. Long-run tests should chart drift and observe whether the chosen node or time source corrects it as expected.
BroadcastTimeWarp() exists for a time source to announce a mapping change at a real-time point to a new performance-time value. A node must not treat the old linear mapping as permanent after a warp. Seek and start/stop operations also alter the relationship between media position and system scheduling. Implement the relevant TimeSourceOp() behavior if implementing a time source; do not announce a warp unless the source can honor the new mapping.
A media time source may be slaved to a physical clock or serve as the master for a graph. Those roles have different responsibilities. A source that follows incoming hardware timestamps needs a policy for discontinuities and missing samples. A master needs a monotonic, stable progression while running and a coherent response to control operations. Verify whether the application is consuming a system source or implementing a custom one before diagnosing drift.
Run modes and offline processing
Media nodes can operate under different run modes. In real-time playback, wall-clock scheduling matters. Offline rendering can be driven as quickly as data can be produced, so assuming a frame must correspond to a fixed sleep is incorrect. BTimeSource has interfaces for run-mode changes and a source operation hook; a custom source must define behavior consistent with the graph mode it supports.
Do not build a CPU-burning loop around Now() to wait for each event. Use the Media Kit’s scheduling facilities and queue semantics, and allow the event machinery to wake at the appropriate deadline. Polling adds load and can still miss timing due to scheduler jitter. For offline work, remove real-time sleeps but preserve media ordering, timestamps, and completion signaling.
Timestamp units and arithmetic
Haiku media APIs represent time with bigtime_t. Keep values in their documented units and use checked arithmetic when adding durations or converting rates. A sample count multiplied by frame duration can overflow if dimensions or rates are untrusted. Validate ranges, perform conversion in sufficiently wide types, and avoid truncation when converting between integer clock values and floating point.
For periodic media, derive timestamps from a stable sequence or source clock, not repeatedly from the last rounded duration. If a frame period is fractional in the chosen unit, accumulating a rounded value each iteration can create systematic drift. Keep a rational or higher-precision accumulator where necessary, then convert at the API boundary. Document whether timestamps represent the beginning or another point in the media interval according to the format and producer contract.
Observability for clock bugs
Log the node ID, selected time-source ID, performance timestamp, corresponding real-time value, drift estimate, run mode, event lateness, start latency, and buffer duration when diagnosing a timing issue. Avoid combining numbers from different domains into one unlabeled “timestamp.” Capture enough samples to identify whether error grows linearly, jumps at a seek, or begins after switching devices.
In tests, compare two nodes against a known stimulus and measure both short-term jitter and long-term accumulated offset. A one-second success does not demonstrate hour-long synchronization. Exercise pause, stop, restart, seek, time-source replacement, a delayed consumer, and a simulated clock-rate mismatch. Record the Haiku revision and hardware path, since scheduling and audio-device behavior are environmental.
If drift appears, first verify the source selection and timestamp domain. Then check whether the producer marks buffers consistently, the consumer honors event times, and the nodes report latency accurately. Only after those contracts are verified should the time source itself be suspected. A graph-level timing problem can be caused by any node that changes, drops, or mislabels timestamps.
Implementing a custom source safely
Subclassing BTimeSource is an advanced operation. The public header leaves TimeSourceOp() as a pure virtual hook for source operations, and exposes PublishTime() and BroadcastTimeWarp() to update the graph’s mapping. The implementation must handle start, stop, stop-immediately, and seek coherently; return valid conversions; and synchronize its clock state. If the node cannot provide the guarantees its consumers need, use a system-provided source instead of advertising a fragile custom clock.
Keep clock state protected when it is read by several threads. Publish a coherent tuple of performance time, real time, and drift rather than updating fields independently so readers can observe a torn mapping. Define behavior before start and after stop. Handle signals, device loss, and discontinuities without blocking the media control thread indefinitely.
Acceptance checks
A correct time-source integration has documented domains, correct conversion at representative points, measured drift under sustained load, bounded handling of late events, and predictable behavior across start/stop/seek. It reports latency based on the actual node path and does not substitute wall-clock strings for media timestamps. It also remains correct in the selected offline or real-time run mode.
Use tests that can fail: inject a controlled drift, delay one consumer, perform repeated seeks, and compare against a reference clock. Verify values before and after time warps. Confirm that the UI displays wall time only for human calendar context, while media scheduling stays in performance time. This makes synchronization bugs reproducible instead of anecdotal.
BTimeSource is the clock contract of a media graph. Treating its domains and transitions explicitly allows the Media Kit to coordinate audio, video, and other timed events without confusing device clocks, wall time, and playback position.
Related:
- The Media Kit: Real-Time Audio and Video in Haiku
- Haiku Time Settings: Time Zones, RTC Mode, and NTP Sync
Sources: