ios 1.0.10: shuffle/repeat on Now Playing, playback speed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cdnutterandClaude Opus 5.5 committed 2026-10-20 11:21:32 -07:00
1 parent 5b767cacc2
commit b0dba777f9
6 files changed
+96 -8

No files matched your search

+29 -5
View File
@@ -104,6 +104,7 @@ private final class OutputTap: @unchecked Sendable {
private var lastNanos: Int64 = 0
private var runningFlag = false
private var latencyNanos: Int64 = 0
private var speedFactor = 1.0
private let scratch: UnsafeMutablePointer<Float>
private static let scratchFrames = 4096
@@ -121,6 +122,12 @@ private final class OutputTap: @unchecked Sendable {
func setLatency(_ seconds: Double) { lock.with { latencyNanos = Int64(seconds * 1e9) } }
/// Playback speed; the time-pitch unit pulls frames this much faster than real time.
var speed: Double {
get { lock.with { speedFactor } }
set { lock.with { speedFactor = newValue } }
}
func flush() {
lock.with {
ring.clear()
@@ -135,7 +142,7 @@ private final class OutputTap: @unchecked Sendable {
lock.with {
if lastNanos == 0 { return 0 }
let elapsed = runningFlag ? max(0, now - lastNanos) : 0
let est = lastStart + elapsed * Int64(rate) / 1_000_000_000 - latencyNanos * Int64(rate) / 1_000_000_000
let est = lastStart + Int64(Double((elapsed - latencyNanos) * Int64(rate)) * speedFactor) / 1_000_000_000
return min(max(0, min(est, consumed)), written)
}
}
@@ -203,9 +210,10 @@ final class StemEngine: @unchecked Sendable {
var rate: Int
var nanos: Int64
var running: Bool
var speed = 1.0
func positionMs(_ now: Int64 = monoNanos()) -> Int64 {
if rate <= 0 { return 0 }
let f = running ? frame + (now - nanos) * Int64(rate) / 1_000_000_000 : frame
let f = running ? frame + Int64(Double((now - nanos) * Int64(rate)) * speed) / 1_000_000_000 : frame
return f * 1000 / Int64(rate)
}
}
@@ -223,6 +231,8 @@ final class StemEngine: @unchecked Sendable {
private let av = AVAudioEngine()
private var node: AVAudioSourceNode?
private var tap: OutputTap?
private let pitch: AVAudioUnitTimePitch = { let p = AVAudioUnitTimePitch(); p.bypass = true; return p }()
private var speed = 1.0
private var trackRate = 0
private var written: Int64 = 0
private var segments: [Segment] = []
@@ -321,6 +331,17 @@ final class StemEngine: @unchecked Sendable {
}
}
/// Playback speed, 0.5 to 2. Pitch stays the same.
func setSpeed(_ v: Double) {
post { [self] in
speed = min(max(v, 0.5), 2)
pitch.rate = Float(speed)
pitch.bypass = speed == 1
tap?.speed = speed
publish()
}
}
func seekTo(_ ms: Int64) {
post { [self] in
guard let s = session else { return }
@@ -601,12 +622,15 @@ final class StemEngine: @unchecked Sendable {
#endif
let t = OutputTap(rate: rate, frames: max(4096, rate * Self.bufferMs / 1000))
t.setLatency(Self.outputLatency())
t.speed = speed
guard let fmt = AVAudioFormat(standardFormatWithSampleRate: Double(rate), channels: 2) else {
throw DecodeError("Unsupported sample rate \(rate)")
}
let n = AVAudioSourceNode(format: fmt) { silence, ts, frames, abl in t.render(silence, ts, frames, abl) }
av.attach(n)
av.connect(n, to: av.mainMixerNode, format: fmt)
if pitch.engine == nil { av.attach(pitch) }
av.connect(n, to: pitch, format: fmt)
av.connect(pitch, to: av.mainMixerNode, format: fmt)
av.prepare()
node = n
tap = t
@@ -655,9 +679,9 @@ final class StemEngine: @unchecked Sendable {
let rate = s.map { $0.decoders.isEmpty ? trackRate : $0.sampleRate } ?? trackRate
if let seg = segments.first {
let running = playing && drainUntil < 0 || drainUntil >= 0 && p < drainUntil
snapshot = Snapshot(token: seg.token, frame: seg.frameStart + max(0, p - seg.writtenStart), rate: rate, nanos: nanos, running: running)
snapshot = Snapshot(token: seg.token, frame: seg.frameStart + max(0, p - seg.writtenStart), rate: rate, nanos: nanos, running: running, speed: speed)
} else {
snapshot = Snapshot(token: s?.token ?? -1, frame: s?.framePos ?? 0, rate: rate, nanos: nanos, running: false)
snapshot = Snapshot(token: s?.token ?? -1, frame: s?.framePos ?? 0, rate: rate, nanos: nanos, running: false, speed: speed)
}
}