AVAudioEngine downsample issue
An other way to do it , with AVAudioConverter
in Swift 5
let engine = AVAudioEngine()
func setup() {
let input = engine.inputNode
let bus = 0
let inputFormat = input.outputFormat(forBus: bus )
guard let outputFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 8000, channels: 1, interleaved: true), let converter = AVAudioConverter(from: inputFormat, to: outputFormat) else{
return
}
input.installTap(onBus: bus, bufferSize: 1024, format: inputFormat) { (buffer, time) -> Void in
var newBufferAvailable = true
let inputCallback: AVAudioConverterInputBlock = { inNumPackets, outStatus in
if newBufferAvailable {
outStatus.pointee = .haveData
newBufferAvailable = false
return buffer
} else {
outStatus.pointee = .noDataNow
return nil
}
}
if let convertedBuffer = AVAudioPCMBuffer(pcmFormat: outputFormat, frameCapacity: AVAudioFrameCount(outputFormat.sampleRate) * buffer.frameLength / AVAudioFrameCount(buffer.format.sampleRate)){
var error: NSError?
let status = converter.convert(to: convertedBuffer, error: &error, withInputFrom: inputCallback)
assert(status != .error)
// 8kHz buffers
print(convertedBuffer.format)
}
}
do {
try engine.start()
} catch { print(error) }
}
AudioKit down sample audio
Essentially, I had to create my own "tap" to tap into the data
First, I had a "converter". This basically takes audio coming from another mixer (via a "tap") converts it to a target format and writes it out to an audio file
class TapConverter: NodeTapperDelegate {
let audioConfig: AudioConfig
internal var inputFormat: AVAudioFormat?
internal var converter: AVAudioConverter?
var onError: ((Error) -> Void)?
init(audioConfig: AudioConfig) {
self.audioConfig = audioConfig
}
func open(format: AVAudioFormat) throws {
inputFormat = format
converter = AVAudioConverter(from: format, to: audioConfig.audioFormat)
}
func drip(buffer: AVAudioPCMBuffer, time: AVAudioTime) {
guard let converter = converter else {
return
}
guard let inputFormat = inputFormat else {
return
}
let inputBufferSize = inputFormat.sampleRate
let sampleRateRatio = inputBufferSize / audioConfig.audioFormat.sampleRate
let capacity = Int(Double(buffer.frameCapacity) / sampleRateRatio)
let bufferPCM16 = AVAudioPCMBuffer(pcmFormat: audioConfig.audioFormat, frameCapacity: AVAudioFrameCount(capacity))!
var error: NSError? = nil
converter.convert(to: bufferPCM16, error: &error) { inNumPackets, outStatus in
outStatus.pointee = AVAudioConverterInputStatus.haveData
return buffer
}
if let error = error {
// Handle error in someway
} else {
let audioFile = audioConfig.audioFile
do {
log(debug: "Write buffer")
try audioFile.write(from: bufferPCM16)
} catch let error {
log(error: "Failed to write buffer to audio file: \(error)")
onError?(error)
}
}
}
func close() {
converter = nil
inputFormat = nil
// we close the audio file
}
}
AudioConfig
is just a basic placeholder, it contains the audioFile
which is been written to (must already be created) and the target AVAudioFormat
struct AudioConfig {
let url: URL
let audioFile: AVAudioFile
let audioFormat: AVAudioFormat
}
Creation might look something like...
let settings: [String: Any] = [
AVFormatIDKey: NSNumber(value: kAudioFormatMPEG4AAC),
AVSampleRateKey: NSNumber(value: 8000),
AVNumberOfChannelsKey: NSNumber(value: 1),
AVEncoderBitRatePerChannelKey: NSNumber(value: 16),
AVEncoderAudioQualityKey: NSNumber(value: AVAudioQuality.min.rawValue)
]
let audioFile = try AVAudioFile(forWriting: sourceURL, settings: settings)
let audioConfig = AudioConfig(url: sourceURL, audioFile: audioFile, audioFormat: audioFormat)
From there, I needed a way to tap the node (get it's data) and pass it onto my converter, for that, I used something like...
import Foundation
import AudioKit
protocol NodeTapperDelegate: class {
func open(format: AVAudioFormat) throws
func drip(buffer: AVAudioPCMBuffer, time: AVAudioTime)
func close()
}
class NodeTapper: NSObject {
// MARK: - Properties
// The node we record from
private(set) var node: AKNode?
/// True if we are recording.
@objc private(set) dynamic var isTapping = false
/// The bus to install the recording tap on. Default is 0.
private var bus: Int = 0
/// Used for fixing recordings being truncated
private var recordBufferDuration: Double = 16_384 / AKSettings.sampleRate
weak var delegate: NodeTapperDelegate?
// MARK: - Initialization
/// Initialize the node recorder
///
/// Recording buffer size is defaulted to be AKSettings.bufferLength
/// You can set a different value by setting an AKSettings.recordingBufferLength
///
/// - Parameters:
/// - node: Node to record from
/// - bus: Integer index of the bus to use
///
@objc init(node: AKNode? = AKManager.output,
bus: Int = 0) throws {
self.bus = bus
self.node = node
}
// MARK: - Methods
/// Start recording
@objc func start() throws {
if isTapping == true {
return
}
guard let node = node else {
return
}
guard let delegate = delegate else {
return
}
let bufferLength: AVAudioFrameCount = AKSettings.recordingBufferLength.samplesCount
isTapping = true
// Note: if you install a tap on a bus that already has a tap it will crash your application.
let nodeFormat = node.avAudioNode.outputFormat(forBus: 0)
try delegate.open(format: nodeFormat)
// note, format should be nil as per the documentation for installTap:
// "If non-nil, attempts to apply this as the format of the specified output bus. This should
// only be done when attaching to an output bus which is not connected to another node"
// In most cases AudioKit nodes will be attached to something else.
node.avAudioUnitOrNode.installTap(onBus: bus,
bufferSize: bufferLength,
format: nil, // Might need to the input node's format :/
block: process(buffer:time:))
}
private func process(buffer: AVAudioPCMBuffer, time: AVAudioTime) {
guard let sink = delegate else { return }
sink.drip(buffer: buffer, time: time)
}
/// Stop recording
@objc func stop() {
if isTapping == false {
return
}
isTapping = false
if AKSettings.fixTruncatedRecordings {
// delay before stopping so the recording is not truncated.
let delay = UInt32(recordBufferDuration * 1_000_000)
usleep(delay)
}
node?.avAudioUnitOrNode.removeTap(onBus: bus)
delegate?.close()
}
}
And then, somehow, bind it altogether
let microphone = AKMicrophone()
microphone?.volume = 10 * volume
let monoToStereo = AKStereoFieldLimiter(microphone, amount: 1)
let microphoneMixer = AKMixer(monoToStereo)
// This is where we're converting the audio from
// the microphone and dripping it into the audio file
let converter = TapConverter(audioConfig: audioConfig)
// handleError is basically just a func in this case
converter.onError = handleError
// Here we tap the mixer/node and output to the converter
let tapper = try NodeTapper(node: microphoneMixer)
tapper.delegate = converter
// Silence the output from the microphone, so it's not
// fed back into the microphone
let silence = AKMixer(microphoneMixer)
silence.volume = 0
self.microphoneMixer = microphoneMixer
self.converter = converter
self.tapper = tapper
self.microphone = microphone
self.silence = silence
AKManager.output = silence
log(debug: "Start")
try AKManager.start()
log(debug: "Record")
try tapper.start()
So much of this came from scraps of different ideas from different posts around the web, so is it the best option? I don't know, but it does what I need it to do
How can I specify the format of AVAudioEngine Mic-Input?
You cannot change audio format directly on input nor output nodes. In the case of the microphone, the format will always be 44KHz, 1 channel, 32bits. To do so, you need to insert a mixer in between. Then when you connect inputNode > changeformatMixer > mainEngineMixer, you can specify the details of the format you want.
Something like:
var inputNode = audioEngine.inputNode
var downMixer = AVAudioMixerNode()
//I think you the engine's I/O nodes are already attached to itself by default, so we attach only the downMixer here:
audioEngine.attachNode(downMixer)
//You can tap the downMixer to intercept the audio and do something with it:
downMixer.installTapOnBus(0, bufferSize: 2048, format: downMixer.outputFormatForBus(0), block: //originally 1024
{ (buffer: AVAudioPCMBuffer!, time: AVAudioTime!) -> Void in
print(NSString(string: "downMixer Tap"))
do{
print("Downmixer Tap Format: "+self.downMixer.outputFormatForBus(0).description)//buffer.audioBufferList.debugDescription)
})
//let's get the input audio format right as it is
let format = inputNode.inputFormatForBus(0)
//I initialize a 16KHz format I need:
let format16KHzMono = AVAudioFormat.init(commonFormat: AVAudioCommonFormat.PCMFormatInt16, sampleRate: 11050.0, channels: 1, interleaved: true)
//connect the nodes inside the engine:
//INPUT NODE --format-> downMixer --16Kformat--> mainMixer
//as you can see I m downsampling the default 44khz we get in the input to the 16Khz I want
audioEngine.connect(inputNode, to: downMixer, format: format)//use default input format
audioEngine.connect(downMixer, to: audioEngine.outputNode, format: format16KHzMono)//use new audio format
//run the engine
audioEngine.prepare()
try! audioEngine.start()
I would recommend using an open framework such as EZAudio, instead, though.
Error when installing a tap on audio engine input node
This is a sample rate mismatch.
The input node's format can't be changed so you need to match it. installTap
listens to the output of a node, so use inputNode's output format.
inputNode.installTap(onBus: 0, bufferSize: 1024, format: inputNode.outputFormat(forBus: 0))
Another option is to connect the input to a mixer, then tap the mixer using the preferred recording format. Mixers perform implicit sample rate conversions between their inputs and output.
How do I change the tap frequency on the input bus?
I took a break from this problem and returned to it today with XCode 8.1
The above code just works now and I have no idea why. Bug in earlier version of AVAudioSession?
Related Topics
Swift - Increment Label With Stepper in Tableview Cell
Swift 5: What's 'Escaping Closure Captures Mutating 'Self' Parameter' and How to Fix It
Reference to Property in Closure Requires Explicit 'Self.' to Make Capture Semantics Explicit
Why .Pch File Not Available in Swift
Why Are Doubles Printed Differently in Dictionaries
Reading Data into a Struct in Swift
How to Get User Home Directory Path (Users/"User Name") Without Knowing the Username in Swift3
Function with Datatask Returning a Value
Reasons to Include Function in Protocol Definition VS. Only Defining It in the Extension
Aws Cognito Swift Credentials Provider "Logins Is Deprecated: Use Awsidentityprovidermanager"
Break a Number Up to an Array of Individual Digits
Unowned Vs. Weak. Why We Should Prefer Unowned
Pass Variables from One Viewcontroller to Another in Swift
Finish Asynchronous Task in Firebase With Swift
Swift: How to Detect Linear Type Barcodes
Linker Command Failed with Exit Code 1 After Installing Cocoapods and Firebase Pod