Files
HaishinKit.swift/HaishinKit/Sources/Stream/AudioPlayerNode.swift
2025-07-21 19:45:37 +09:00

108 lines
3.0 KiB
Swift

@preconcurrency import AVFoundation
import Foundation
final actor AudioPlayerNode {
static let bufferCounts: Int = 10
var currentTime: TimeInterval {
if playerNode.isPlaying {
guard
let nodeTime = playerNode.lastRenderTime,
let playerTime = playerNode.playerTime(forNodeTime: nodeTime) else {
return 0.0
}
return TimeInterval(playerTime.sampleTime) / playerTime.sampleRate
}
return 0.0
}
private(set) var isPaused = false
private(set) var isRunning = false
private(set) var soundTransfrom = SoundTransform()
private let playerNode: AVAudioPlayerNode
private var audioTime = AudioTime()
private var scheduledAudioBuffers: Int = 0
private var isBuffering = true
private weak var player: AudioPlayer?
private var format: AVAudioFormat? {
didSet {
guard format != oldValue else {
return
}
Task { [format] in
await player?.connect(self, format: format)
}
}
}
init(player: AudioPlayer, playerNode: AVAudioPlayerNode) {
self.player = player
self.playerNode = playerNode
}
func setSoundTransfrom(_ soundTransfrom: SoundTransform) {
soundTransfrom.apply(playerNode)
}
func enqueue(_ audioBuffer: AVAudioBuffer, when: AVAudioTime) async {
format = audioBuffer.format
guard let audioBuffer = audioBuffer as? AVAudioPCMBuffer, await player?.isConnected(self) == true else {
return
}
if !audioTime.hasAnchor {
audioTime.anchor(playerNode.lastRenderTime ?? AVAudioTime(hostTime: 0))
}
scheduledAudioBuffers += 1
if !isPaused && !playerNode.isPlaying && Self.bufferCounts <= scheduledAudioBuffers {
playerNode.play()
}
Task {
audioTime.advanced(Int64(audioBuffer.frameLength))
await playerNode.scheduleBuffer(audioBuffer, at: audioTime.at)
scheduledAudioBuffers -= 1
if scheduledAudioBuffers == 0 {
isBuffering = true
}
}
}
func detach() async {
stopRunning()
await player?.detach(self)
}
}
extension AudioPlayerNode: AsyncRunner {
// MARK: AsyncRunner
func startRunning() {
guard !isRunning else {
return
}
scheduledAudioBuffers = 0
isRunning = true
}
func stopRunning() {
guard isRunning else {
return
}
if playerNode.isPlaying {
playerNode.stop()
playerNode.reset()
}
audioTime.reset()
format = nil
isRunning = false
}
}
extension AudioPlayerNode: Hashable {
// MARK: Hashable
nonisolated public static func == (lhs: AudioPlayerNode, rhs: AudioPlayerNode) -> Bool {
lhs === rhs
}
nonisolated public func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
}