snapshot current state before gitea sync
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import Darwin
|
||||
|
||||
final class AudioPlayerController: NSObject, ObservableObject {
|
||||
@Published private(set) var isPlaying: Bool = false
|
||||
@Published private(set) var playbackPosition: TimeInterval = 0
|
||||
|
||||
private let engine = AVAudioEngine()
|
||||
private let playerNode = AVAudioPlayerNode()
|
||||
private var audioFile: AVAudioFile?
|
||||
private var progressTimer: Timer?
|
||||
private var didConfigureSession = false
|
||||
|
||||
private var currentTrack: LocalTrack?
|
||||
private var scheduleStartPosition: TimeInterval = 0
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
engine.attach(playerNode)
|
||||
engine.connect(playerNode, to: engine.mainMixerNode, format: nil)
|
||||
}
|
||||
|
||||
func play(track: LocalTrack, atUptime startUptime: TimeInterval, startPosition: TimeInterval) {
|
||||
configureAudioSessionIfNeeded()
|
||||
|
||||
do {
|
||||
let file = try AVAudioFile(forReading: track.url)
|
||||
audioFile = file
|
||||
currentTrack = track
|
||||
scheduleStartPosition = startPosition
|
||||
|
||||
let startFrame = AVAudioFramePosition(startPosition * file.fileFormat.sampleRate)
|
||||
let totalFrames = file.length
|
||||
let framesLeft = max(AVAudioFrameCount(totalFrames - startFrame), 0)
|
||||
|
||||
if !engine.isRunning {
|
||||
try engine.start()
|
||||
}
|
||||
|
||||
playerNode.stop()
|
||||
playerNode.scheduleSegment(
|
||||
file,
|
||||
startingFrame: startFrame,
|
||||
frameCount: framesLeft,
|
||||
at: AVAudioTime(hostTime: hostTime(forUptime: startUptime))
|
||||
) { [weak self] in
|
||||
DispatchQueue.main.async {
|
||||
self?.isPlaying = false
|
||||
self?.stopProgressTimer()
|
||||
}
|
||||
}
|
||||
|
||||
if !playerNode.isPlaying {
|
||||
playerNode.play()
|
||||
}
|
||||
|
||||
isPlaying = true
|
||||
startProgressTimer()
|
||||
} catch {
|
||||
isPlaying = false
|
||||
}
|
||||
}
|
||||
|
||||
func pause() {
|
||||
playerNode.pause()
|
||||
isPlaying = false
|
||||
stopProgressTimer()
|
||||
}
|
||||
|
||||
func stop() {
|
||||
playerNode.stop()
|
||||
isPlaying = false
|
||||
playbackPosition = 0
|
||||
stopProgressTimer()
|
||||
}
|
||||
|
||||
func seek(to position: TimeInterval) {
|
||||
guard let track = currentTrack else { return }
|
||||
play(track: track, atUptime: SyncClock.uptime() + 0.1, startPosition: position)
|
||||
}
|
||||
|
||||
func correctDrift(targetPosition: TimeInterval, hostUptime: TimeInterval) {
|
||||
guard let track = currentTrack else { return }
|
||||
let targetNow = SyncClock.convert(hostUptime: hostUptime)
|
||||
let expectedPosition = targetPosition + (SyncClock.uptime() - targetNow)
|
||||
let drift = expectedPosition - playbackPosition
|
||||
if abs(drift) > 0.15 {
|
||||
play(track: track, atUptime: SyncClock.uptime() + 0.1, startPosition: expectedPosition)
|
||||
}
|
||||
}
|
||||
|
||||
private func startProgressTimer() {
|
||||
stopProgressTimer()
|
||||
progressTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
if let nodeTime = self.playerNode.lastRenderTime,
|
||||
let playerTime = self.playerNode.playerTime(forNodeTime: nodeTime) {
|
||||
let seconds = Double(playerTime.sampleTime) / playerTime.sampleRate
|
||||
self.playbackPosition = self.scheduleStartPosition + seconds
|
||||
self.isPlaying = self.playerNode.isPlaying
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopProgressTimer() {
|
||||
progressTimer?.invalidate()
|
||||
progressTimer = nil
|
||||
}
|
||||
|
||||
private func configureAudioSessionIfNeeded() {
|
||||
guard !didConfigureSession else { return }
|
||||
do {
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
try session.setCategory(.playback, mode: .default)
|
||||
try session.setActive(true)
|
||||
didConfigureSession = true
|
||||
} catch {
|
||||
didConfigureSession = false
|
||||
}
|
||||
}
|
||||
|
||||
private func hostTime(forUptime startUptime: TimeInterval) -> UInt64 {
|
||||
let nowUptime = SyncClock.uptime()
|
||||
let delay = max(0, startUptime - nowUptime)
|
||||
let hostDelay = AVAudioTime.hostTime(forSeconds: delay)
|
||||
return mach_absolute_time() + hostDelay
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user