Files
mediaplayer/ios/OralTrainer/Services/DubbingCompositor.swift
2026-08-26 18:56:04 +08:00

155 lines
6.3 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import AVFoundation
import Foundation
enum DubbingCompositorError: LocalizedError {
case inputReadFailed(String?)
case outputWriteFailed(String?)
case convertFailed(String?)
var errorDescription: String? {
switch self {
case .inputReadFailed(let detail):
return detail.map { "无法读取录音音频(\($0)" } ?? "无法读取录音音频"
case .outputWriteFailed(let detail):
return detail.map { "无法写入合成音频(\($0)" } ?? "无法写入合成音频"
case .convertFailed(let detail):
return detail.map { "无法转换音频格式(\($0)" } ?? "无法转换音频格式"
}
}
}
enum DubbingCompositor {
/// 16kHz AACm4a
///
/// 使 AVMutableComposition + AVAssetExportSessioniOS 26 / macOS 26
/// AVAssetExportSession AVError -11800 'fmt?'
/// AVAudioFile AAC CheckClientFormatSet / 'fmt?'
/// PCMcaf AAC
///
static func merge(urls: [URL], outputURL: URL) async throws {
do {
try mergeSync(urls: urls, outputURL: outputURL)
} catch {
try? FileManager.default.removeItem(at: outputURL)
throw error
}
}
private static func mergeSync(urls: [URL], outputURL: URL) throws {
try? FileManager.default.removeItem(at: outputURL)
let settings: [String: Any] = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVSampleRateKey: 16_000,
AVNumberOfChannelsKey: 1,
]
let outputFile: AVAudioFile
do {
outputFile = try AVAudioFile(forWriting: outputURL, settings: settings)
} catch {
throw DubbingCompositorError.outputWriteFailed(error.localizedDescription)
}
let targetFormat = outputFile.processingFormat
for url in urls {
let inputFile: AVAudioFile
do {
inputFile = try AVAudioFile(forReading: url)
} catch {
throw DubbingCompositorError.inputReadFailed(error.localizedDescription)
}
guard inputFile.length > 0 else {
throw DubbingCompositorError.inputReadFailed("录音为空")
}
let inputFormat = inputFile.processingFormat
if inputFormat.sampleRate == targetFormat.sampleRate,
inputFormat.channelCount == targetFormat.channelCount {
try appendStream(from: inputFile, to: outputFile)
} else {
try appendConverted(from: inputFile, to: outputFile, targetFormat: targetFormat)
}
}
}
private static func appendStream(from inputFile: AVAudioFile, to outputFile: AVAudioFile) throws {
var remaining = inputFile.length
while remaining > 0 {
let frames = AVAudioFrameCount(min(remaining, 8192))
guard let buffer = AVAudioPCMBuffer(pcmFormat: inputFile.processingFormat, frameCapacity: frames) else {
throw DubbingCompositorError.convertFailed("无法分配解码缓冲")
}
do {
try inputFile.read(into: buffer)
} catch {
throw DubbingCompositorError.inputReadFailed(error.localizedDescription)
}
guard buffer.frameLength > 0 else {
throw DubbingCompositorError.inputReadFailed("录音文件读取出错(读取到空帧)")
}
do {
try outputFile.write(from: buffer)
} catch {
throw DubbingCompositorError.outputWriteFailed(error.localizedDescription)
}
remaining -= AVAudioFramePosition(buffer.frameLength)
}
}
private static func appendConverted(
from inputFile: AVAudioFile,
to outputFile: AVAudioFile,
targetFormat: AVAudioFormat
) throws {
guard let converter = AVAudioConverter(from: inputFile.processingFormat, to: targetFormat) else {
throw DubbingCompositorError.convertFailed("无法创建格式转换器")
}
let inputBlock: AVAudioConverterInputBlock = { inNumPackets, outStatus in
if inputFile.framePosition >= inputFile.length {
outStatus.pointee = .endOfStream
return nil
}
let remaining = inputFile.length - inputFile.framePosition
let frames = AVAudioFrameCount(min(inNumPackets, AVAudioFrameCount(remaining)))
guard frames > 0,
let buffer = AVAudioPCMBuffer(pcmFormat: inputFile.processingFormat, frameCapacity: frames) else {
outStatus.pointee = .noDataNow
return nil
}
do {
try inputFile.read(into: buffer)
guard buffer.frameLength > 0 else {
outStatus.pointee = .endOfStream
return nil
}
outStatus.pointee = .haveData
return buffer
} catch {
outStatus.pointee = .noDataNow
return nil
}
}
guard let outputBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: 8192) else {
throw DubbingCompositorError.convertFailed("无法分配转码缓冲")
}
while true {
var error: NSError?
let status = converter.convert(to: outputBuffer, error: &error, withInputFrom: inputBlock)
switch status {
case .haveData:
do {
try outputFile.write(from: outputBuffer)
} catch {
throw DubbingCompositorError.outputWriteFailed(error.localizedDescription)
}
case .inputRanDry, .endOfStream:
return
case .error:
if let error {
throw DubbingCompositorError.convertFailed(error.localizedDescription)
}
throw DubbingCompositorError.convertFailed(nil)
default:
return
}
}
}
}