Visual_Novel_iOS/crush/Crush/Src/Components/Audio/SpeechModel.swift

338 lines
9.6 KiB
Swift
Raw Normal View History

2025-10-09 10:29:35 +00:00
//
// SpeechModel.swift
// Crush
//
// Created by Leon on 2025/8/1.
//
import Foundation
import AVFoundation
// MARK: - Enums
enum SpeechPlayState: Int {
case `default` // Default state
case playing // Playing
case complete // Playback completed
case failed // Playback failed
}
enum SpeechLoadState: Int {
case `default` // Default state
case loading // Loading
case complete // Loading completed
case failed // Loading failed
}
enum EGAudioFileType: Int {
case file // Local audio file
case url // Remote audio file
case base64 // Base64 encoded audio data
}
class SpeechModel: NSObject, AVAudioPlayerDelegate {
// MARK: - Properties
var playState: SpeechPlayState = .default
var loadState: SpeechLoadState = .default
var fileType: EGAudioFileType = .file
var path: String = ""
var canAutoPlay: Bool = false
var stateChangedBlock: ((SpeechModel) -> Void)?
var timerChangedBlock: ((TimeInterval) -> Void)?
var progressChangedBlock: ((TimeInterval, TimeInterval) -> Void)? // block (, )
var audioDuration: TimeInterval = 0.0 //
private var player: AVAudioPlayer?
private var progressTimer: Timer?
// MARK: - Computed Properties
/// x10)
var audioTime: Int {
guard let player = player else { return 0 }
let isOtherPlaying = AVAudioSession.sharedInstance().isOtherAudioPlaying
return Int(ceil(player.duration * 10)) + (isOtherPlaying ? 15 : 5)
}
var isPlaying: Bool {
return player?.isPlaying ?? false
}
var currentTime: TimeInterval {
get { player?.currentTime ?? 0 }
set { player?.currentTime = newValue }
}
class func modelWith(path:String?) -> SpeechModel?{
guard let pathValid = path else{
return nil
}
let model = SpeechModel()
model.path = pathValid
model.fileType = .url
return model
}
class func modelWithBase64String(_ base64String: String?) -> SpeechModel? {
guard let base64Valid = base64String else {
return nil
}
let model = SpeechModel()
model.path = base64Valid
model.fileType = .base64
return model
}
// MARK: - Private Methods
private func loadFileData() {
guard !path.isEmpty else {
dlog("❌ SpeechModel's file path is nil")
setupDefault()
return
}
loadState = .loading
stateChanged()
DispatchQueue.global().async { [weak self] in
guard let self = self else { return }
do {
let audioData = try Data(contentsOf: URL(fileURLWithPath: self.path), options: .mappedIfSafe)
self.player = try AVAudioPlayer(data: audioData)
self.player?.delegate = self
self.player?.prepareToPlay()
self.audioDuration = self.player?.duration ?? 0.0
self.loadState = .complete
self.stateChanged()
} catch {
self.loadState = .failed
self.stateChanged()
}
}
}
private func loadUrlData() {
guard !path.isEmpty else {
dlog("❌ SpeechModel's path is nil")
setupDefault()
return
}
loadState = .loading
stateChanged()
DispatchQueue.global().async { [weak self] in
guard let self = self, let url = URL(string: self.path) else { return }
do {
let audioData = try Data(contentsOf: url, options: .mappedIfSafe)
self.player = try AVAudioPlayer(data: audioData)
self.player?.delegate = self
self.player?.prepareToPlay()
self.audioDuration = self.player?.duration ?? 0.0
self.loadState = .complete
self.stateChanged()
} catch {
self.loadState = .failed
self.stateChanged()
}
}
}
private func loadBase64Data() {
guard !path.isEmpty else {
dlog("❌ SpeechModel's base64 string is nil")
setupDefault()
return
}
loadState = .loading
stateChanged()
DispatchQueue.global().async { [weak self] in
guard let self = self else { return }
do {
guard let audioData = Data(base64Encoded: self.path) else {
DispatchQueue.main.async {
self.loadState = .failed
self.stateChanged()
}
return
}
self.player = try AVAudioPlayer(data: audioData)
self.player?.delegate = self
self.player?.prepareToPlay()
self.audioDuration = self.player?.duration ?? 0.0
self.loadState = .complete
self.stateChanged()
} catch {
self.loadState = .failed
self.stateChanged()
}
}
}
private func stateChanged() {
DispatchQueue.main.async { [weak self] in
guard let self = self, let block = self.stateChangedBlock else { return }
block(self)
}
}
private func checkPlayState() -> Bool {
return player != nil
}
private func setupDefault() {
playState = .default
loadState = .default
audioDuration = 0.0
stateChanged()
}
// MARK: - Progress Update Methods
private func startProgressUpdate() {
stopProgressUpdate()
progressTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
self?.updateProgress()
}
}
private func stopProgressUpdate() {
progressTimer?.invalidate()
progressTimer = nil
}
private func updateProgress() {
guard let player = player, player.isPlaying else { return }
let currentTime = player.currentTime
let duration = player.duration
DispatchQueue.main.async { [weak self] in
guard let self = self, let block = self.progressChangedBlock else { return }
block(currentTime, duration)
}
}
// MARK: - Public Methods
func refreshPath(path: String){
self.path = path
}
func loadSpeechFile() {
guard !path.isEmpty else {
setupDefault()
return
}
guard loadState != .loading else { return }
if player != nil {
loadState = .complete
stateChanged()
return
}
switch fileType {
case .file:
loadFileData()
case .url:
loadUrlData()
case .base64:
loadBase64Data()
}
}
func startPlay() {
guard checkPlayState() else { return }
guard let player = player, !player.isPlaying else { return }
do {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playAndRecord, mode: .default, options: [
.allowBluetooth,
.defaultToSpeaker,
.allowAirPlay,
.duckOthers,
.mixWithOthers
])
try session.setActive(true, options: [])
player.currentTime = 0
player.play()
playState = .playing
startProgressUpdate()
stateChanged()
} catch {
playState = .failed
stateChanged()
}
}
func stopPlay() {
guard checkPlayState() else {
setupDefault()
return
}
if player?.isPlaying == true {
player?.stop()
playState = .complete
stateChanged()
stopProgressUpdate()
//
try? AVAudioSession.sharedInstance().setActive(false, options: [.notifyOthersOnDeactivation])
}
}
func needLoadData() -> Bool {
return player == nil && loadState != .loading
}
// MARK: - AVAudioPlayerDelegate
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
dlog("audioPlayerDidFinishPlaying")
playState = .complete
stateChanged()
stopProgressUpdate()
}
func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
dlog("audioPlayerDecodeErrorDidOccur")
playState = .failed
stateChanged()
stopProgressUpdate()
}
// MARK: - Deinit
deinit {
print("♻️ SpeechModel dealloc")
if player?.isPlaying == true {
player?.stop()
}
stopProgressUpdate()
}
}
/*
使
// SpeechModel
let speechModel = SpeechModel.modelWith(path: "audio_url")!
//
speechModel.progressChangedBlock = { currentTime, totalDuration in
let progress = currentTime / totalDuration
print("播放进度: \(Int(currentTime))s / \(Int(totalDuration))s (\(Int(progress * 100))%)")
}
//
speechModel.loadSpeechFile()
//
speechModel.startPlay()
//
let duration = speechModel.audioDuration
print("音频总时长: \(duration)")
//
speechModel.stopPlay()
*/