Streaming sample source¶
pulp::audio::StreamingSampleSource plays a sequential sample through a
resident preload head and an SPSC tail ring. Its audio-thread pull() call does
not allocate, lock, wait, read a file, or invoke a decoder. One owned, joinable
background thread fills the tail; deterministic tests can disable the thread
and call pump_background() explicitly.
This is a narrow transport primitive, not Pulp's complete polyphonic sampler backend. It supports sequential one-shot playback and fully resident loops. Fractional/pitched one-shots and forward/reverse crossfade loops use the shared page-cache sample stream service. Starvation gain shaping, sampler-owned mip assets, and broader product policy live above this narrow sequential transport primitive; the PulpSampler example integrates those layers. See the sampler playback chooser for resident and instrument-policy alternatives.
File-backed playback¶
make_memory_mapped_frame_reader() adapts a file to the source's FrameReader
contract. Opening retains the original file handle for identity/access-policy
operations, copies exactly the admitted source bytes into an exclusive private
temporary directory, and maps that immutable snapshot for the callback's
lifetime. The copy is bounded by maximum_mapped_bytes, uses disk rather than
decoded-audio RAM, and is removed on close. A later in-place overwrite or
truncate of the source path therefore cannot change playback or fault the
mapping. WAV and uncompressed AIFF/AIFF-C NONE files use bounded ranged decode
from the snapshot bytes; they are not decoded completely before playback.
Concurrent non-RT reads on one reader are serialized internally; open, close,
move, and destruction still require quiescent reads.
The adapter reports supports_ranged_read. A false value means the active file
format uses MemoryMappedAudioReader's decode-once fallback. FLAC currently
falls into that category: the general audio-file layer can decode it, but this
adapter does not claim ranged FLAC reads. Applications with a strict
streaming-memory contract should reject that source transactionally, preserving
the previously published source, or import it into a streamable representation
before note-on. PulpSampler uses that strict policy.
auto file = pulp::audio::make_memory_mapped_frame_reader(path);
if (!file.valid || !file.supports_ranged_read)
return false;
pulp::audio::StreamingSampleSource source;
pulp::audio::StreamingSampleSourceConfig config{
.channels = file.channels,
.total_frames = file.total_frames,
.sample_rate = file.sample_rate,
.preload_frames = 8192,
.ring_capacity_frames = 32768,
.read_chunk_frames = 4096,
};
return source.prepare(config, std::move(file.binding));
prepare() synchronously fills the preload and primes the ring, so it belongs
on a control or loading thread. release() stops and joins the worker before
freeing storage. Destruction calls release(). Legacy FrameReader callbacks
are join-only: teardown waits for an entered call to return. A
FrameReaderBinding marked Cooperative receives a FrameReaderStopToken.
Readers check stop_requested() between bounded read chunks or bounded waits so
teardown can request prompt cooperative exit. The token is callback-scoped and
must not be retained after the reader returns. Decode-once fallbacks remain
honestly marked JoinOnly because their initial full decode cannot be
interrupted.
Underruns¶
pull() never blocks for missing tail data. It writes the available frames,
zero-fills the remaining requested output, and increments underrun_frames.
The source position advances only for frames actually delivered, so recovered
data stays in source order. This preserves source material but stretches wall
clock time during a miss; production sampler recovery must choose and document
its own timeline policy.
Use stats() for telemetry. A no-underrun render can be compared exactly with
resident playback. Once a deliberate underrun occurs, reproducibility requires
the same injected I/O schedule and recovery policy.
Thread contract¶
prepare(),reset(), andrelease(): control/quiescent thread.pull()andfinished(): audio thread after prepare.pump_background(): the owned reader thread, or one deterministic test driver whenstart_background_threadis false.FrameReader: one background caller at a time; it may seek, decode, allocate, and lock, but it must not retain the destination view after returning.FrameReaderBinding: the same single-caller rule plus an explicit join-only or cooperative-stop capability. Cooperative readers must observe their token within a documented bounded interval.
Do not share one StreamingSampleSource between concurrent audio callbacks.