4 changed files with 480 additions and 32 deletions
@ -0,0 +1,232 @@ |
|||
import Foundation |
|||
|
|||
class RecordFile { |
|||
static let shared = RecordFile() |
|||
|
|||
private var currentAudioFile: URL? |
|||
private var fileHandle: FileHandle? |
|||
private let writeQueue = DispatchQueue(label: "audio.write.queue") |
|||
private let writeGroup = DispatchGroup() |
|||
private var isWriting = false |
|||
private var dataBuffer = Data() |
|||
private var totalBytesWritten = 0 |
|||
private let sampleRate: UInt32 = 16000 |
|||
private var lastSavedFile: URL? |
|||
var isPause = false |
|||
var fileName = "" |
|||
|
|||
// 创建音频文件并初始化 WAV 头 |
|||
func creatingFiles(atPath path: String) { |
|||
guard fileHandle == nil else { return } |
|||
|
|||
let fileURL = URL(fileURLWithPath: path) |
|||
let directoryURL = fileURL.deletingLastPathComponent() |
|||
|
|||
// 创建目录 |
|||
do { |
|||
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true) |
|||
} catch { |
|||
print("创建目录失败: \(error.localizedDescription)") |
|||
return |
|||
} |
|||
|
|||
// 创建文件 |
|||
FileManager.default.createFile(atPath: path, contents: nil) |
|||
|
|||
do { |
|||
fileHandle = try FileHandle(forWritingTo: fileURL) |
|||
currentAudioFile = fileURL |
|||
|
|||
// 写入初始 WAV 头 |
|||
let header = generateWavHeader(dataLength: 0) |
|||
try fileHandle?.write(contentsOf: header) |
|||
totalBytesWritten = 0 |
|||
} catch { |
|||
print("文件初始化失败: \(error.localizedDescription)") |
|||
fileHandle = nil |
|||
currentAudioFile = nil |
|||
} |
|||
} |
|||
|
|||
// 异步保存音频数据 |
|||
func saveAudioDataToWav(_ data: Data) { |
|||
guard let _ = fileHandle, let _ = currentAudioFile else { return } |
|||
|
|||
writeQueue.async { [weak self] in |
|||
guard let self = self else { return } |
|||
self.dataBuffer.append(data) |
|||
self.totalBytesWritten += data.count |
|||
|
|||
if !self.isWriting { |
|||
self.isWriting = true |
|||
self.writeGroup.enter() |
|||
|
|||
defer { |
|||
self.isWriting = false |
|||
self.writeGroup.leave() |
|||
} |
|||
|
|||
while !self.dataBuffer.isEmpty { |
|||
let chunk: Data |
|||
if self.dataBuffer.count > 8192 { |
|||
chunk = self.dataBuffer.prefix(8192) |
|||
self.dataBuffer.removeFirst(8192) |
|||
} else { |
|||
chunk = self.dataBuffer |
|||
self.dataBuffer.removeAll() |
|||
} |
|||
|
|||
do { |
|||
try self.fileHandle?.seekToEnd() |
|||
self.fileHandle?.write(chunk) |
|||
} catch { |
|||
print("写入数据失败: \(error.localizedDescription)") |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 生成 WAV 文件头 |
|||
private func generateWavHeader(dataLength: Int) -> Data { |
|||
let totalLength = 36 + dataLength |
|||
let byteRate = sampleRate * 2 |
|||
|
|||
var header = Data() |
|||
header.append("RIFF".data(using: .ascii)!) |
|||
header.append(Data(bytes: [ |
|||
UInt8(totalLength & 0xFF), |
|||
UInt8((totalLength >> 8) & 0xFF), |
|||
UInt8((totalLength >> 16) & 0xFF), |
|||
UInt8((totalLength >> 24) & 0xFF) |
|||
])) |
|||
header.append("WAVE".data(using: .ascii)!) |
|||
header.append("fmt ".data(using: .ascii)!) |
|||
header.append(Data(bytes: [16, 0, 0, 0])) // PCM 头长度 |
|||
header.append(Data(bytes: [1, 0])) // PCM 格式 |
|||
header.append(Data(bytes: [1, 0])) // 单声道 |
|||
header.append(Data(bytes: [ |
|||
UInt8(sampleRate & 0xFF), |
|||
UInt8((sampleRate >> 8) & 0xFF), |
|||
UInt8((sampleRate >> 16) & 0xFF), |
|||
UInt8((sampleRate >> 24) & 0xFF) |
|||
])) |
|||
header.append(Data(bytes: [ |
|||
UInt8(byteRate & 0xFF), |
|||
UInt8((byteRate >> 8) & 0xFF), |
|||
UInt8((byteRate >> 16) & 0xFF), |
|||
UInt8((byteRate >> 24) & 0xFF) |
|||
])) |
|||
header.append(Data(bytes: [2, 0])) // 块对齐 |
|||
header.append(Data(bytes: [16, 0])) // 样本位数 |
|||
header.append("data".data(using: .ascii)!) |
|||
header.append(Data(bytes: [ |
|||
UInt8(dataLength & 0xFF), |
|||
UInt8((dataLength >> 8) & 0xFF), |
|||
UInt8((dataLength >> 16) & 0xFF), |
|||
UInt8((dataLength >> 24) & 0xFF) |
|||
])) |
|||
|
|||
return header |
|||
} |
|||
|
|||
// 重命名文件 |
|||
func renameFile(at sourcePath: String, to newName: String) -> Bool { |
|||
let sourceURL = URL(fileURLWithPath: sourcePath) |
|||
|
|||
guard FileManager.default.fileExists(atPath: sourcePath) else { |
|||
print("重命名失败: 文件不存在 \(sourcePath)") |
|||
return false |
|||
} |
|||
|
|||
if let current = currentAudioFile, current == sourceURL { |
|||
print("重命名失败: 不能重命名正在录制的文件") |
|||
return false |
|||
} |
|||
|
|||
let newFileURL = sourceURL |
|||
.deletingLastPathComponent() |
|||
.appendingPathComponent(newName) |
|||
.appendingPathExtension("wav") |
|||
|
|||
do { |
|||
try FileManager.default.moveItem(at: sourceURL, to: newFileURL) |
|||
if lastSavedFile == sourceURL { |
|||
lastSavedFile = newFileURL |
|||
} |
|||
return true |
|||
} catch { |
|||
print("重命名异常: \(error.localizedDescription)") |
|||
return false |
|||
} |
|||
} |
|||
|
|||
// 移动文件 |
|||
func moveFile(from sourcePath: String, to destPath: String) -> Bool { |
|||
let sourceURL = URL(fileURLWithPath: sourcePath) |
|||
let destURL = URL(fileURLWithPath: destPath) |
|||
|
|||
guard FileManager.default.fileExists(atPath: sourcePath) else { |
|||
print("移动失败: 源文件不存在 \(sourcePath)") |
|||
return false |
|||
} |
|||
|
|||
let destDir = destURL.deletingLastPathComponent() |
|||
do { |
|||
try FileManager.default.createDirectory(at: destDir, withIntermediateDirectories: true) |
|||
} catch { |
|||
print("创建目录失败: \(error.localizedDescription)") |
|||
return false |
|||
} |
|||
|
|||
do { |
|||
try FileManager.default.moveItem(at: sourceURL, to: destURL) |
|||
if lastSavedFile == sourceURL { |
|||
lastSavedFile = destURL |
|||
} |
|||
return true |
|||
} catch { |
|||
print("移动文件异常: \(error.localizedDescription)") |
|||
return false |
|||
} |
|||
} |
|||
|
|||
// 关闭文件 |
|||
func closeFile(isSave: Bool) -> Bool { |
|||
defer { |
|||
fileHandle = nil |
|||
currentAudioFile = nil |
|||
dataBuffer.removeAll() |
|||
totalBytesWritten = 0 |
|||
} |
|||
|
|||
guard let fileURL = currentAudioFile, let handle = fileHandle else { |
|||
return false |
|||
} |
|||
|
|||
// 等待写入完成 |
|||
writeGroup.wait() |
|||
|
|||
do { |
|||
try handle.close() |
|||
|
|||
if !isSave || totalBytesWritten == 0 { |
|||
try FileManager.default.removeItem(at: fileURL) |
|||
return false |
|||
} |
|||
|
|||
// 更新 WAV 头 |
|||
let header = generateWavHeader(dataLength: totalBytesWritten) |
|||
let updateHandle = try FileHandle(forUpdating: fileURL) |
|||
try updateHandle.seek(toOffset: 0) |
|||
updateHandle.write(header) |
|||
try updateHandle.close() |
|||
|
|||
lastSavedFile = fileURL |
|||
return true |
|||
} catch { |
|||
print("关闭文件失败: \(error.localizedDescription)") |
|||
return false |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue