iOS 1.0 RC 1: battery and heat work, version label, web player redraw tweaks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0fc35b13e8
commit
c0e33b5e3e
18 files changed
+143
-36
No files matched your search
@@ -27,6 +27,10 @@ final class FloatRing: @unchecked Sendable {
|
||||
private let lock = UnfairLock()
|
||||
private let notFull = DispatchSemaphore(value: 0)
|
||||
private var eosFlag = false
|
||||
/// A writer blocked on a full ring is woken once a quarter of it is free, not after every
|
||||
/// read, so decoders run in bursts and sleep in between.
|
||||
private var writerWaiting = false
|
||||
private var lowWater: Int { cap / 4 }
|
||||
|
||||
init(capacityFrames: Int) {
|
||||
cap = capacityFrames * 2
|
||||
@@ -42,6 +46,7 @@ final class FloatRing: @unchecked Sendable {
|
||||
|
||||
var availableFrames: Int { lock.with { Int((writePos - readPos) / 2) } }
|
||||
var freeFrames: Int { lock.with { Int((Int64(cap) - (writePos - readPos)) / 2) } }
|
||||
var capacityFrames: Int { cap / 2 }
|
||||
|
||||
/// Blocks while full; returns false if [abort] became true first. [len] counts floats.
|
||||
func write(_ src: UnsafePointer<Float>, _ len: Int, abort: () -> Bool) -> Bool {
|
||||
@@ -49,10 +54,12 @@ final class FloatRing: @unchecked Sendable {
|
||||
while done < len {
|
||||
lock.lock()
|
||||
let free = Int(Int64(cap) - (writePos - readPos))
|
||||
if free <= 0 {
|
||||
if free < min(len - done, lowWater) {
|
||||
writerWaiting = true
|
||||
lock.unlock()
|
||||
if abort() { return false }
|
||||
_ = notFull.wait(timeout: .now() + .milliseconds(20))
|
||||
// seek() and release() signal too, so the timeout is only a backstop.
|
||||
_ = notFull.wait(timeout: .now() + .milliseconds(250))
|
||||
continue
|
||||
}
|
||||
lock.unlock()
|
||||
@@ -79,8 +86,10 @@ final class FloatRing: @unchecked Sendable {
|
||||
dst.update(from: buf + r, count: first)
|
||||
if n > first { (dst + first).update(from: buf, count: n - first) }
|
||||
readPos += Int64(n)
|
||||
let wake = writerWaiting && Int(Int64(cap) - (writePos - readPos)) >= lowWater
|
||||
if wake { writerWaiting = false }
|
||||
lock.unlock()
|
||||
if n > 0 { notFull.signal() }
|
||||
if wake { notFull.signal() }
|
||||
return n / 2
|
||||
}
|
||||
|
||||
@@ -88,6 +97,7 @@ final class FloatRing: @unchecked Sendable {
|
||||
lock.with {
|
||||
readPos = writePos
|
||||
eosFlag = false
|
||||
writerWaiting = false
|
||||
}
|
||||
notFull.signal()
|
||||
}
|
||||
@@ -210,7 +220,8 @@ final class StemDecoder: @unchecked Sendable {
|
||||
seek(gen: gen, frame: frame)
|
||||
let t = Thread { [self] in run() }
|
||||
t.name = "stem-\(name)"
|
||||
t.qualityOfService = .userInteractive
|
||||
// Seconds ahead of the mixer, so efficiency cores are fast enough.
|
||||
t.qualityOfService = .userInitiated
|
||||
thread = t
|
||||
t.start()
|
||||
}
|
||||
|
||||
@@ -201,7 +201,11 @@ final class StemEngine: @unchecked Sendable {
|
||||
static let ringSeconds = 3
|
||||
/// Frames each stem must have before (re)starting output.
|
||||
static let prefill = 8192
|
||||
/// Mixed audio kept ahead of the speaker. Short while someone's looking, so faders and EQ
|
||||
/// answer quickly; deep when nobody is, so the engine wakes a few times a second instead of
|
||||
/// hundreds and the CPU can sleep between bursts.
|
||||
static let bufferMs = 120
|
||||
static let backgroundBufferMs = 600
|
||||
|
||||
/// Where playback is; position = frame + (now - nanos) while [running].
|
||||
struct Snapshot {
|
||||
@@ -446,7 +450,8 @@ final class StemEngine: @unchecked Sendable {
|
||||
guard let s = session, wantPlay else {
|
||||
setFlags(false, false)
|
||||
publish()
|
||||
idle(100)
|
||||
// Nothing moves while paused; a command wakes the loop.
|
||||
idle(2000)
|
||||
return
|
||||
}
|
||||
if let e = s.error() {
|
||||
@@ -483,9 +488,13 @@ final class StemEngine: @unchecked Sendable {
|
||||
setFlags(true, false)
|
||||
|
||||
guard let tap else { return }
|
||||
if tap.ring.freeFrames < Self.chunk {
|
||||
let target = min(trackRate * (AppActivity.isActive ? Self.bufferMs : Self.backgroundBufferMs) / 1000, tap.ring.capacityFrames)
|
||||
let buffered = tap.ring.availableFrames
|
||||
if buffered + Self.chunk > target {
|
||||
publish()
|
||||
idle(5)
|
||||
// Sleep until the buffer is half drained, then top it up in one burst.
|
||||
let ms = Double(buffered - target / 2) * 1000 / Double(max(1, trackRate)) / speed
|
||||
idle(max(5, Int(ms)))
|
||||
return
|
||||
}
|
||||
let n = mixChunk(s)
|
||||
@@ -620,7 +629,7 @@ final class StemEngine: @unchecked Sendable {
|
||||
#if os(iOS)
|
||||
try? AVAudioSession.sharedInstance().setPreferredSampleRate(Double(rate))
|
||||
#endif
|
||||
let t = OutputTap(rate: rate, frames: max(4096, rate * Self.bufferMs / 1000))
|
||||
let t = OutputTap(rate: rate, frames: max(4096, rate * Self.backgroundBufferMs / 1000))
|
||||
t.setLatency(Self.outputLatency())
|
||||
t.speed = speed
|
||||
guard let fmt = AVAudioFormat(standardFormatWithSampleRate: Double(rate), channels: 2) else {
|
||||
|
||||
Reference in new issue
Block a user