From 0ae53e2e7c57176ac4f8503c3358e6fb07fa3991 Mon Sep 17 00:00:00 2001 From: hiroshihorie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 3 Oct 2024 00:41:12 +0900 Subject: [PATCH 1/3] impl1 --- .../LiveKit/Extensions/AVAudioPCMBuffer.swift | 70 ++++++++++++++++ .../Extensions/AVAudioPCMBufferTests.swift | 79 +++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 Sources/LiveKit/Extensions/AVAudioPCMBuffer.swift create mode 100644 Tests/LiveKitTests/Extensions/AVAudioPCMBufferTests.swift diff --git a/Sources/LiveKit/Extensions/AVAudioPCMBuffer.swift b/Sources/LiveKit/Extensions/AVAudioPCMBuffer.swift new file mode 100644 index 000000000..8df8a0edf --- /dev/null +++ b/Sources/LiveKit/Extensions/AVAudioPCMBuffer.swift @@ -0,0 +1,70 @@ +/* + * 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 + +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 + } + + // Create the AVAudioConverter for resampling. + guard let converter = AVAudioConverter(from: sourceFormat, to: targetFormat) else { + print("Failed to create audio converter.") + return nil + } + + // Calculate the frame capacity for the converted buffer. + let ratio = targetFormat.sampleRate / sourceFormat.sampleRate + let convertedFrameCapacity = AVAudioFrameCount(Double(frameCapacity) * ratio) + + // Create a buffer to hold the converted data. + guard let convertedBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: convertedFrameCapacity) else { + print("Failed to create converted buffer.") + return nil + } + + // Perform the conversion. + let inputBlock: AVAudioConverterInputBlock = { _, outStatus in + outStatus.pointee = .haveData + 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 + } + + return convertedBuffer + } +} diff --git a/Tests/LiveKitTests/Extensions/AVAudioPCMBufferTests.swift b/Tests/LiveKitTests/Extensions/AVAudioPCMBufferTests.swift new file mode 100644 index 000000000..4464cb685 --- /dev/null +++ b/Tests/LiveKitTests/Extensions/AVAudioPCMBufferTests.swift @@ -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") + } + } +} From a3ba36dff5a2d855b20601aaf8b43c0485722606 Mon Sep 17 00:00:00 2001 From: hiroshihorie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 3 Oct 2024 00:54:11 +0900 Subject: [PATCH 2/3] make public --- Sources/LiveKit/Extensions/AVAudioPCMBuffer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/LiveKit/Extensions/AVAudioPCMBuffer.swift b/Sources/LiveKit/Extensions/AVAudioPCMBuffer.swift index 8df8a0edf..d7ae3e523 100644 --- a/Sources/LiveKit/Extensions/AVAudioPCMBuffer.swift +++ b/Sources/LiveKit/Extensions/AVAudioPCMBuffer.swift @@ -16,7 +16,7 @@ import AVFoundation -extension AVAudioPCMBuffer { +public extension AVAudioPCMBuffer { func resample(toSampleRate targetSampleRate: Double) -> AVAudioPCMBuffer? { let sourceFormat = format From 895b37c500f4eca506014613b247a426bb382fb0 Mon Sep 17 00:00:00 2001 From: hiroshihorie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 3 Oct 2024 03:56:09 +0900 Subject: [PATCH 3/3] Minor fix --- .../LiveKit/Extensions/AVAudioPCMBuffer.swift | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Sources/LiveKit/Extensions/AVAudioPCMBuffer.swift b/Sources/LiveKit/Extensions/AVAudioPCMBuffer.swift index d7ae3e523..96eeab168 100644 --- a/Sources/LiveKit/Extensions/AVAudioPCMBuffer.swift +++ b/Sources/LiveKit/Extensions/AVAudioPCMBuffer.swift @@ -35,25 +35,26 @@ public extension AVAudioPCMBuffer { return nil } - // Create the AVAudioConverter for resampling. guard let converter = AVAudioConverter(from: sourceFormat, to: targetFormat) else { print("Failed to create audio converter.") return nil } - // Calculate the frame capacity for the converted buffer. - let ratio = targetFormat.sampleRate / sourceFormat.sampleRate - let convertedFrameCapacity = AVAudioFrameCount(Double(frameCapacity) * ratio) + let capacity = targetFormat.sampleRate * Double(frameLength) / sourceFormat.sampleRate - // Create a buffer to hold the converted data. - guard let convertedBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: convertedFrameCapacity) else { + guard let convertedBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: AVAudioFrameCount(capacity)) else { print("Failed to create converted buffer.") return nil } - // Perform the conversion. + var isDone = false let inputBlock: AVAudioConverterInputBlock = { _, outStatus in + if isDone { + outStatus.pointee = .noDataNow + return nil + } outStatus.pointee = .haveData + isDone = true return self } @@ -65,6 +66,9 @@ public extension AVAudioPCMBuffer { return nil } + // Adjust frame length to the actual amount of data written + convertedBuffer.frameLength = convertedBuffer.frameCapacity + return convertedBuffer } }