Avaudioengine Downsample Issue

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



Leave a reply



Submit