Skip to content

Releases: timusus/AudioPlaybackKit

0.20.2

Choose a tag to compare

@timusus timusus released this 24 Oct 22:35
  • Render: cache the output latency instead of querying AVAudioSession on every position read (a blocking IPC that hung the main thread); refresh it on route change, media-services reset, rebuild and playback start.

0.20.1

Choose a tag to compare

@timusus timusus released this 24 Oct 12:27

Fixes

  • Streaming: time the process spends suspended no longer counts against the 30 s link-down window, so a load started just before the app was backgrounded is no longer failed with no_response_s on return.

0.20.0

Choose a tag to compare

@timusus timusus released this 23 Oct 12:03

AudioPlaybackKit 0.20.0 reads position on demand instead of pushing it every 100 ms, so a playing transport
wakes only for work and for the observers an app registers (#189,
ADR-0031).

This is a minor bump at 0.x: it changes public API (below). Decode and the PCM played are unchanged.

What changed

  • Position is computed when read, as media3 Player.getCurrentPosition and AVPlayer currentTime().
    Transport.position (a TransportPosition: item, seconds) maps the output clock through a snapshot
    under the transport's lock, on the caller's thread with no hop to its queue. It is nil while a
    setCurrent or seek is called but not taken, and within one item and one call it never goes back.
    Player.position reads it once every call is answered and the heard item is the current one; until then
    it holds the value set with the last discontinuityCount change.
  • Observers, named as AVPlayer's. addPeriodicObserver(interval:using:) fires every interval of
    output time while the clock runs (wall time at any speed). addBoundaryObserver(times:using:) fires once
    when playback crosses a time in the heard item; a load or seek landing past it does not. Both are on
    Transport (callbacks on its queue) and Player (callbacks on the main actor), and return a
    TimeObserverToken that removes the observer on cancel() or deinit.
  • Events. TransportEvent.landed(item:seconds:) is the first position of a setCurrent or seek,
    once its first audio is written; seekLanded and seekAdjusted come from it.
    TransportEvent.durationChanged(item:duration:isSeekable:) is sent when the heard item's duration or
    seekability changes. firstAudioRendered is sent on .status(.playing) after a landing, or on a landing
    or .crossed while playing.
  • NowPlayingSession still writes elapsed time only on a jump, a play or pause, a rate or item change,
    or drift. After a write while playing it asks the player for one periodic tick, so a play heard late is
    rewritten once heard; a paused or held player wakes nothing.
  • Test support. ManualPumpTimer.advance moves the clock in 1/64 s steps, so an output's ready wake
    lands as it would on a real clock.

Breaking changes

  • TransportEvent.position(item:seconds:duration:isSeekable:) is removed, replaced by .landed and
    .durationChanged: code matching it, or an exhaustive switch, no longer compiles.
  • Player.position is computed, so SwiftUI and withObservationTracking no longer see it move between
    discontinuities. A view that showed it by observation stops updating.
  • NowPlayingControllable requires addPeriodicObserver(interval:using:). A
    ForwardingNowPlayingControllable wrapper gets it forwarded; a direct conformer must forward it to its player.

Additive: Transport.position, TransportPosition, TimeObserverToken, and both observers on Transport
and Player.

Upgrading

  • A progress bar or elapsed label subscribes with player.addPeriodicObserver(interval:using:), keeps the
    token, and reads player.position on each fire.
  • Ad and silence skips and a sleep timer at an item time use addBoundaryObserver(times:using:) in place of
    comparing each .position.
  • Code that read duration or seekability from .position reads .durationChanged, or Player.duration
    and isSeekable.

0.19.0

Choose a tag to compare

@timusus timusus released this 21 Oct 06:37

Added

  • Player.startGateEnded: reports why a start gate ended (.intentWithdrawn or .superseded) and for which item, synchronously at the moment it ends. StartGateEnd has a public initialiser for tests.

Fixed

  • A gate cancelled before its task first ran no longer calls startGate.
  • A failure of the gated item, or a source that fails to load, now ends the gate and reports it.

0.18.1

Choose a tag to compare

@timusus timusus released this 21 Oct 03:40

Fixes the start gate: a jump to the armed next item during the gate now gates the new item and plays, and a finished gate no longer calls play() on an inactive session.

0.18.0

Choose a tag to compare

@timusus timusus released this 21 Oct 03:20

Adds Player.startGate, an optional async hook awaited after an item is prepared and before it first plays.

Pause, stop or replacing the item cancels the wait; resuming and gapless joins are not gated. With no gate set, behaviour is unchanged.

0.17.0

Choose a tag to compare

@timusus timusus released this 20 Oct 00:00

Changed

  • Playback speed is now a pitch-preserving time-stretch (Apple's AUNewTimePitch) applied before enqueue, and the renderer stays at rate 1. A speed change goes through the gapless processor re-feed path, so it no longer briefly mutes on the iPhone speaker. A new speed is heard about half a second after the call (#188).

Breaking

  • timePitchAlgorithm is removed from SampleBufferAudioOutput.init, the Transport init and the Player convenience init. Drop the argument; see docs/migrating.md.

0.16.1

Choose a tag to compare

@timusus timusus released this 19 Oct 20:48

Fixed

  • Live EQ and processor changes no longer cut out on shallow output queues, such as the iPhone speaker. If the renderer refuses a partial flush, the new chain is appended at the end of the queued audio instead of restarting playback (#187).

0.16.0

Choose a tag to compare

@timusus timusus released this 19 Oct 12:47

Fixed

  • Choppy AirPlay playback (timusus/Shuttle2#1064). The feeder kept a fixed lead of about two seconds of audio. AirPlay receivers buffer more than the latency they report, so the renderer ran dry and audio was dropped silently, with no stall reported. The feeder now writes while the renderer is ready for more media data, woken by requestMediaDataWhenReady, and keeps at most 30 s of media written ahead. That cap is a time bound: about 11 MB at 48 kHz stereo Float32, and more at higher sample rates or channel counts.

Changed

  • Start and rebuffer depth. Playback starts once 0.5 s (scaled by rate) is queued. After an underrun it waits for 2 s, or starts as soon as the output reports full.
  • Effect changes flush. With up to 30 s queued ahead, draining the old chain would delay a change too long. setProcessors now flushes and re-feeds from the frame being heard, which shows as a brief buffering, as a seek does. This doesn't happen after .ended.
  • New processorsChanged() on Transport and Player (public API). Call it after changing a live processor parameter, such as an EQ gain or a Voice Boost amount, so the change is heard at once. It does nothing while the transport is failed or ended.
  • Playback rate is still applied by the synchronizer, so speed changes take effect at once with no flush.

Migrating from 0.15.0

  • If your processors read parameters that change while playing, call player.processorsChanged() after each change. Without it, the change is heard only after the audio already queued (up to 30 s) has played.
  • Code that relied on setProcessors never flushing will now see a short buffering state.

0.15.0

Choose a tag to compare

@timusus timusus released this 15 Oct 18:40

Render

  • Underruns and resumes are now judged against the renderer clock, not the heard playhead (#183). On a high-latency route such as AirPlay (about 2 s), audio written after an upstream stall used to land behind the running clock and be dropped silently, with status still .playing. Now the output pauses and reports .buffering. Likely cause of timusus/Shuttle2#1064.
  • Through an underrun the reported position holds at the last heard frame, instead of jumping ahead by the route latency.
  • After an underrun, or after the renderer itself runs dry, playback resumes only once the full feed depth (2 s at 1x) is written, as media3's bufferForPlaybackAfterRebufferMs does. This gives one longer pause instead of repeated short stalls. The first start, a seek, a route change and play after a pause still start at the 0.5 s resume depth.
  • A next item that resolves just after the end of a stream is no longer written behind the clock, whether playing or paused.
  • Pausing in the last seconds of a stream on a high-latency route and pressing play now finishes the item instead of staying in .buffering.

Tooling

  • The release caches the FFmpeg source per tag and retries its fetch, alternating git.ffmpeg.org and the GitHub mirror (#186). The source tarball now also contains scripts/lib/fetch-ffmpeg.sh, which build-ffmpeg.sh sources.

No public API change. Decoded PCM is unchanged; what moved is buffering timing (.buffering now appears on AirPlay stalls, and resumes after a stall are later).