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