iOS 1.0 RC 2: Plex streams under upload caps, shuffle and album radio stations, Recently Added list, photo covers, crossfade
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c0e33b5e3e
commit
2fb8040388
17 files changed
+412
-74
No files matched your search
@@ -231,6 +231,13 @@ final class StemEngine: @unchecked Sendable {
|
||||
|
||||
private var session: TrackSession?
|
||||
private var next: TrackSession?
|
||||
/// Crossfade length into [next], in ms (0 = gapless).
|
||||
private var nextFadeMs = 0
|
||||
/// The track fading in over the end of [session], and how far through the fade we are.
|
||||
private var xf: TrackSession?
|
||||
private var xfLen: Int64 = 0
|
||||
private var xfDone: Int64 = 0
|
||||
private var env = [Float](repeating: 0, count: 0)
|
||||
private var wantPlay = false
|
||||
private let av = AVAudioEngine()
|
||||
private var node: AVAudioSourceNode?
|
||||
@@ -319,11 +326,20 @@ final class StemEngine: @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
/// The track to continue into gaplessly when the current one ends (or nil).
|
||||
func queueNext(_ s: TrackSession?) {
|
||||
/// The track to continue into when the current one ends (or nil): gaplessly, or crossfading
|
||||
/// over the last [fadeMs] of the current track.
|
||||
func queueNext(_ s: TrackSession?, fadeMs: Int = 0) {
|
||||
post { [self] in
|
||||
if next !== s { next?.release() }
|
||||
next = s
|
||||
nextFadeMs = s == nil ? 0 : max(0, fadeMs)
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes the fade into the queued track, if it hasn't started.
|
||||
func setNextFade(_ fadeMs: Int) {
|
||||
post { [self] in
|
||||
if next != nil { nextFadeMs = max(0, fadeMs) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,6 +364,21 @@ final class StemEngine: @unchecked Sendable {
|
||||
|
||||
func seekTo(_ ms: Int64) {
|
||||
post { [self] in
|
||||
if let x = xf, let s = session {
|
||||
xf = nil
|
||||
if segments.count > 1 {
|
||||
// The fade hasn't reached the speakers yet, so the seek is in the old track:
|
||||
// put the new one back as next, from its start, to fade in again later.
|
||||
x.gen += 1
|
||||
x.framePos = 0
|
||||
x.decoders.forEach { $0.seek(gen: x.gen, frame: 0) }
|
||||
next?.release()
|
||||
next = x
|
||||
} else {
|
||||
s.release()
|
||||
session = x
|
||||
}
|
||||
}
|
||||
guard let s = session else { return }
|
||||
let frame = min(max(ms * Int64(s.sampleRate) / 1000, 0), max(0, s.durationFrames - 1))
|
||||
s.gen += 1
|
||||
@@ -396,8 +427,10 @@ final class StemEngine: @unchecked Sendable {
|
||||
private func dropAll() {
|
||||
session?.release()
|
||||
next?.release()
|
||||
xf?.release()
|
||||
session = nil
|
||||
next = nil
|
||||
xf = nil
|
||||
drainUntil = -1
|
||||
pauseOutput()
|
||||
tap?.flush()
|
||||
@@ -497,9 +530,10 @@ final class StemEngine: @unchecked Sendable {
|
||||
idle(max(5, Int(ms)))
|
||||
return
|
||||
}
|
||||
let n = mixChunk(s)
|
||||
startFade(s)
|
||||
let n = xf != nil ? mixFade(s) : mixChunk(s)
|
||||
if n == 0 {
|
||||
if s.finished() { endOfTrack(s) } else { idle(2) }
|
||||
if xf == nil, let cur = session, cur.finished() { endOfTrack(cur) } else { idle(2) }
|
||||
return
|
||||
}
|
||||
eq.configure(eqParams, sampleRate: s.sampleRate)
|
||||
@@ -553,20 +587,77 @@ final class StemEngine: @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
private func mixChunk(_ s: TrackSession) -> Int {
|
||||
if s.finished() { return 0 }
|
||||
/// Starts the crossfade into [next] once [s] is within the fade length of its end and the next
|
||||
/// track has buffered. Different sample rates can't share the output, so those stay gapless.
|
||||
private func startFade(_ s: TrackSession) {
|
||||
guard xf == nil, nextFadeMs > 0, let nx = next, nx.sampleRate == s.sampleRate, nx.error() == nil else { return }
|
||||
let dur = s.durationFrames
|
||||
let len = Int64(nextFadeMs) * Int64(s.sampleRate) / 1000
|
||||
guard dur > 0, len > 0, s.framePos >= dur - len, !s.finished(), nx.ready(Self.prefill) || nx.finished() else { return }
|
||||
xf = nx
|
||||
next = nil
|
||||
nextFadeMs = 0
|
||||
xfLen = max(Int64(Self.chunk), min(len, dur - s.framePos))
|
||||
xfDone = 0
|
||||
segments.append(Segment(writtenStart: written, token: nx.token, frameStart: nx.framePos))
|
||||
}
|
||||
|
||||
/// One chunk of the crossfade: [s] fading out under [xf] fading in (equal power). When the fade
|
||||
/// is done, or the old track runs out, the new one becomes the session.
|
||||
private func mixFade(_ s: TrackSession) -> Int {
|
||||
guard let x = xf else { return 0 }
|
||||
let outDone = s.finished()
|
||||
if x.finished() || outDone && xfDone >= xfLen { return endFade(s, x) }
|
||||
var n = available(x)
|
||||
if !outDone { n = min(n, available(s)) }
|
||||
if n <= 0 { return 0 }
|
||||
if env.count < n { env = [Float](repeating: 0, count: n) }
|
||||
out.update(repeating: 0, count: n * 2)
|
||||
let len = Double(xfLen)
|
||||
if !outDone {
|
||||
for f in 0..<n { env[f] = Float(cos(min(1, Double(xfDone + Int64(f)) / len) * .pi / 2)) }
|
||||
env.withUnsafeBufferPointer { mix(s, n, $0.baseAddress!) }
|
||||
}
|
||||
for f in 0..<n { env[f] = Float(sin(min(1, Double(xfDone + Int64(f)) / len) * .pi / 2)) }
|
||||
env.withUnsafeBufferPointer { mix(x, n, $0.baseAddress!) }
|
||||
xfDone += Int64(n)
|
||||
if xfDone >= xfLen { _ = endFade(s, x) }
|
||||
return n
|
||||
}
|
||||
|
||||
private func endFade(_ s: TrackSession, _ x: TrackSession) -> Int {
|
||||
s.release()
|
||||
session = x
|
||||
xf = nil
|
||||
return 0
|
||||
}
|
||||
|
||||
/// Frames every live decoder of [s] can give right now, up to a chunk.
|
||||
private func available(_ s: TrackSession) -> Int {
|
||||
var n = Self.chunk
|
||||
for d in s.decoders {
|
||||
if d.error != nil || d.ring.eos && d.ring.availableFrames == 0 { continue }
|
||||
n = min(n, d.ring.availableFrames)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
private func mixChunk(_ s: TrackSession) -> Int {
|
||||
if s.finished() { return 0 }
|
||||
let n = available(s)
|
||||
if n <= 0 { return 0 }
|
||||
out.update(repeating: 0, count: n * 2)
|
||||
mix(s, n, nil)
|
||||
return n
|
||||
}
|
||||
|
||||
/// Adds [n] frames of [s]'s stems into [out], each at its mix gain (ramped), times [env] when given.
|
||||
private func mix(_ s: TrackSession, _ n: Int, _ env: UnsafePointer<Float>?) {
|
||||
let (gains, version) = shared.with { (_mix, _mixVersion) }
|
||||
if s.mixVersion != version {
|
||||
for i in s.names.indices { s.target[i] = gains[s.names[i]] ?? 1 }
|
||||
s.mixVersion = version
|
||||
}
|
||||
out.update(repeating: 0, count: n * 2)
|
||||
let stride = MeterHistory.maxStems
|
||||
let block = MeterHistory.block
|
||||
let blocks = (n + block - 1) / block
|
||||
@@ -585,8 +676,9 @@ final class StemEngine: @unchecked Sendable {
|
||||
let end = min(n, (b + 1) * block)
|
||||
var sum: Float = 0
|
||||
for f in (b * block)..<end {
|
||||
let l = tmp[f * 2] * g
|
||||
let r = tmp[f * 2 + 1] * g
|
||||
let e = env.map { g * $0[f] } ?? g
|
||||
let l = tmp[f * 2] * e
|
||||
let r = tmp[f * 2 + 1] * e
|
||||
out[f * 2] += l
|
||||
out[f * 2 + 1] += r
|
||||
sum += l * l + r * r
|
||||
@@ -599,7 +691,6 @@ final class StemEngine: @unchecked Sendable {
|
||||
for b in 0..<blocks { meters.add(s.token, s.framePos + Int64(b * block), p.baseAddress! + b * stride, s.names.count) }
|
||||
}
|
||||
s.framePos += Int64(n)
|
||||
return n
|
||||
}
|
||||
|
||||
private func rampGain(_ buf: UnsafeMutablePointer<Float>, _ n: Int, _ from: Float, _ to: Float) {
|
||||
|
||||
Reference in new issue
Block a user