👌

(Swift)AVAudioFileを利用してwav形式の音声ファイルを保存する

2021/03/26に公開

実装例

例として、/tmp/input.wavを読み込み、同じ内容を/tmp/output.wavとして保存する処理を示します。

import AVFoundation

func readPCMBuffer(url: URL) -> AVAudioPCMBuffer? {
    guard let input = try? AVAudioFile(forReading: url, commonFormat: .pcmFormatInt16, interleaved: false) else {
        return nil
    }
    guard let buffer = AVAudioPCMBuffer(pcmFormat: input.processingFormat, frameCapacity: AVAudioFrameCount(input.length)) else {
        return nil
    }
    do {
        try input.read(into: buffer)
    } catch {
        return nil
    }

    return buffer
}

func writePCMBuffer(url: URL, buffer: AVAudioPCMBuffer) throws {
    let settings: [String: Any] = [
        AVFormatIDKey: buffer.format.settings[AVFormatIDKey] ?? kAudioFormatLinearPCM,
        AVNumberOfChannelsKey: buffer.format.settings[AVNumberOfChannelsKey] ?? 2,
        AVSampleRateKey: buffer.format.settings[AVSampleRateKey] ?? 44100,
        AVLinearPCMBitDepthKey: buffer.format.settings[AVLinearPCMBitDepthKey] ?? 16
    ]

    do {
        let output = try AVAudioFile(forWriting: url, settings: settings, commonFormat: .pcmFormatInt16, interleaved: false)
        try output.write(from: buffer)
    } catch {
        throw error
    }
}

func copyPCMBuffer(from inputPath: String, to outputPath: String) {
    guard let inputBuffer = readPCMBuffer(url: URL(string: inputPath)!) else {
        fatalError("failed to read \(inputPath)")
    }
    guard let outputBuffer = AVAudioPCMBuffer(pcmFormat: inputBuffer.format, frameCapacity: inputBuffer.frameLength) else {
        fatalError("failed to create a buffer for writing")
    }
    guard let inputInt16ChannelData = inputBuffer.int16ChannelData else {
        fatalError("failed to obtain underlying input buffer")
    }
    guard let outputInt16ChannelData = outputBuffer.int16ChannelData else {
        fatalError("failed to obtain underlying output buffer")
    }
    for channel in 0 ..< Int(inputBuffer.format.channelCount) {
        let p1: UnsafeMutablePointer<Int16> = inputInt16ChannelData[channel]
        let p2: UnsafeMutablePointer<Int16> = outputInt16ChannelData[channel]

        for i in 0 ..< Int(inputBuffer.frameLength) {
            p2[i] = p1[i]
        }
    }

    outputBuffer.frameLength = inputBuffer.frameLength

    do {
        try writePCMBuffer(url: URL(string: outputPath)!, buffer: outputBuffer)
    } catch {
        fatalError("failed to write \(outputPath)")
    }
}

copyPCMBuffer(from: "file:///tmp/input.wav", to: "file:///tmp/output.wav")

実装の注意点

以下の2点に注意してください。

  1. AVAudioFileのsettings引数(実装例の28行目)
  2. 書き込み用バッファのframeLength(実装例の57行目)

1.について、WAV形式のファイルを書き込む場合は少なくとも以下の4つのパラメータが必要です。

  • AVFormatIDKey
  • AVNumberOfChannelsKey
  • AVSampleRateKey
  • AVLinearPCMBitDepthKey

なお、settingsパラメータとしてbuffer.format.settingsをそのまま使うこともできますが、実行時に警告が表示されます。AVAudioFileのinterleaved引数とbuffer.format.settings[AVLinearPCMIsNonInterleaved]の値が衝突するためです。

2.についてはバッファに書き込んだフレーム数を設定する必要があります。設定を忘れた場合、すなわちframeLengthが0の場合は空のファイルが作成されます。

今後のアップデートで修正されるかもしれませんが、現状はバッファのframeLengthが0でも書き込みは成功します。エラーは発生しないため注意してください。

参考文献

Discussion