Skip to content

Commit

Permalink
AVAudioPCMBuffer resample helper (#498)
Browse files Browse the repository at this point in the history
  • Loading branch information
hiroshihorie authored Oct 7, 2024
1 parent 4051e10 commit 3af61d4
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 0 deletions.
74 changes: 74 additions & 0 deletions Sources/LiveKit/Extensions/AVAudioPCMBuffer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2024 LiveKit
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import AVFoundation

public extension AVAudioPCMBuffer {
func resample(toSampleRate targetSampleRate: Double) -> AVAudioPCMBuffer? {
let sourceFormat = format

if sourceFormat.sampleRate == targetSampleRate {
// Already targetSampleRate.
return self
}

// Define the source format (from the input buffer) and the target format.
guard let targetFormat = AVAudioFormat(commonFormat: sourceFormat.commonFormat,
sampleRate: targetSampleRate,
channels: sourceFormat.channelCount,
interleaved: sourceFormat.isInterleaved)
else {
print("Failed to create target format.")
return nil
}

guard let converter = AVAudioConverter(from: sourceFormat, to: targetFormat) else {
print("Failed to create audio converter.")
return nil
}

let capacity = targetFormat.sampleRate * Double(frameLength) / sourceFormat.sampleRate

guard let convertedBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: AVAudioFrameCount(capacity)) else {
print("Failed to create converted buffer.")
return nil
}

var isDone = false
let inputBlock: AVAudioConverterInputBlock = { _, outStatus in
if isDone {
outStatus.pointee = .noDataNow
return nil
}
outStatus.pointee = .haveData
isDone = true
return self
}

var error: NSError?
let status = converter.convert(to: convertedBuffer, error: &error, withInputFrom: inputBlock)

if status == .error {
print("Conversion failed: \(error?.localizedDescription ?? "Unknown error")")
return nil
}

// Adjust frame length to the actual amount of data written
convertedBuffer.frameLength = convertedBuffer.frameCapacity

return convertedBuffer
}
}
79 changes: 79 additions & 0 deletions Tests/LiveKitTests/Extensions/AVAudioPCMBufferTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2024 LiveKit
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import AVFoundation
@testable import LiveKit
import XCTest

class AVAudioPCMBufferTests: XCTestCase {
func testResample() {
// Test case 1: Resample to a higher sample rate
testResampleHelper(fromSampleRate: 44100, toSampleRate: 48000, expectedSuccess: true)

// Test case 2: Resample to a lower sample rate
testResampleHelper(fromSampleRate: 48000, toSampleRate: 16000, expectedSuccess: true)

// Test case 3: Resample to the same sample rate
testResampleHelper(fromSampleRate: 44100, toSampleRate: 44100, expectedSuccess: true)

// Test case 4: Resample to an invalid sample rate
testResampleHelper(fromSampleRate: 44100, toSampleRate: 0, expectedSuccess: false)
}

private func testResampleHelper(fromSampleRate: Double, toSampleRate: Double, expectedSuccess: Bool) {
// Create a source buffer
guard let format = AVAudioFormat(standardFormatWithSampleRate: fromSampleRate, channels: 2) else {
XCTFail("Failed to create audio format")
return
}

let frameCount = 1000
guard let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: AVAudioFrameCount(frameCount)) else {
XCTFail("Failed to create audio buffer")
return
}

// Fill the buffer with some test data
for frame in 0 ..< frameCount {
let value = sin(Double(frame) * 2 * .pi / 100.0) // Simple sine wave
buffer.floatChannelData?[0][frame] = Float(value)
buffer.floatChannelData?[1][frame] = Float(value)
}
buffer.frameLength = AVAudioFrameCount(frameCount)

// Perform resampling
let resampledBuffer = buffer.resample(toSampleRate: toSampleRate)

if expectedSuccess {
XCTAssertNotNil(resampledBuffer, "Resampling should succeed")

if let sampleRate = resampledBuffer?.format.sampleRate {
XCTAssertTrue(abs(sampleRate - toSampleRate) < 0.001, "Resampled buffer should have the target sample rate")
} else {
XCTFail("Resampled buffer's format or sample rate is nil")
}

let expectedFrameCount = Int(Double(frameCount) * toSampleRate / fromSampleRate)
if let resampledFrameLength = resampledBuffer?.frameLength {
XCTAssertTrue(abs(Int(resampledFrameLength) - expectedFrameCount) <= 1, "Resampled buffer should have the expected frame count")
} else {
XCTFail("Resampled buffer's frame length is nil")
}
} else {
XCTAssertNil(resampledBuffer, "Resampling should fail")
}
}
}

0 comments on commit 3af61d4

Please sign in to comment.