Files
transui/macos/Sources/Transui/MP3Encoder.swift
T
jourintownandintelligence 64b9d704fb feat:mp3 (#3)
---------

Co-authored-by: intelligence <intelligence@debian.local>
Reviewed-on: #3
2026-08-29 23:55:02 +02:00

91 lines
3.0 KiB
Swift

import Foundation
enum MP3EncodingError: LocalizedError {
case invalidRecording
case encodingFailed
var errorDescription: String? {
switch self {
case .invalidRecording: "The recording could not be read."
case .encodingFailed: "The recording could not be compressed."
}
}
}
enum MP3Encoder {
static func encode(wav url: URL) throws -> URL {
let wav = try Data(contentsOf: url, options: .mappedIfSafe)
let pcm = try pcmData(fromWAV: wav)
var encodedBytes: UnsafeMutablePointer<UInt8>?
var encodedSize = 0
let result = pcm.withUnsafeBytes { bytes in
transui_mp3_encode_mono_16k_32kbps(
bytes.bindMemory(to: UInt8.self).baseAddress,
bytes.count,
&encodedBytes,
&encodedSize
)
}
guard result == TRANSUI_MP3_OK, let encodedBytes, encodedSize > 0 else {
transui_mp3_free(encodedBytes)
throw MP3EncodingError.encodingFailed
}
defer { transui_mp3_free(encodedBytes) }
let output = FileManager.default.temporaryDirectory
.appendingPathComponent("transui-\(UUID().uuidString).mp3")
try Data(bytes: encodedBytes, count: encodedSize).write(to: output, options: .atomic)
return output
}
static func pcmData(fromWAV wav: Data) throws -> Data {
guard wav.count >= 12,
String(decoding: wav[0..<4], as: UTF8.self) == "RIFF",
String(decoding: wav[8..<12], as: UTF8.self) == "WAVE"
else { throw MP3EncodingError.invalidRecording }
var formatIsSupported = false
var audio: Data?
var offset = 12
while offset <= wav.count - 8 {
let chunkName = String(decoding: wav[offset..<(offset + 4)], as: UTF8.self)
let chunkSize = Int(readUInt32(wav, at: offset + 4))
let contentOffset = offset + 8
guard chunkSize >= 0, contentOffset <= wav.count,
chunkSize <= wav.count - contentOffset
else { throw MP3EncodingError.invalidRecording }
if chunkName == "fmt ", chunkSize >= 16 {
formatIsSupported = readUInt16(wav, at: contentOffset) == 1
&& readUInt16(wav, at: contentOffset + 2) == 1
&& readUInt32(wav, at: contentOffset + 4) == 16_000
&& readUInt16(wav, at: contentOffset + 14) == 16
} else if chunkName == "data" {
audio = wav.subdata(in: contentOffset..<(contentOffset + chunkSize))
}
let paddedSize = chunkSize + (chunkSize & 1)
guard paddedSize <= Int.max - contentOffset else {
throw MP3EncodingError.invalidRecording
}
offset = contentOffset + paddedSize
}
guard formatIsSupported, let audio, !audio.isEmpty, audio.count.isMultiple(of: 2) else {
throw MP3EncodingError.invalidRecording
}
return audio
}
private static func readUInt16(_ data: Data, at offset: Int) -> UInt16 {
UInt16(data[offset]) | UInt16(data[offset + 1]) << 8
}
private static func readUInt32(_ data: Data, at offset: Int) -> UInt32 {
UInt32(data[offset])
| UInt32(data[offset + 1]) << 8
| UInt32(data[offset + 2]) << 16
| UInt32(data[offset + 3]) << 24
}
}