Generar un tono en iOS con PCM de 16 bits, AudioEngine.connect () arroja AUSetFormat: error -10868

Tengo el siguiente código para generar un tono de audio de frecuencia y duración. Está basado libremente en esta respuesta para hacer lo mismo en Android (gracias: @Steve Pomeroy):

https://stackoverflow.com/a/3731075/973364

import Foundation import CoreAudio import AVFoundation import Darwin class AudioUtil { class func play(frequency: Int, durationMs: Int) -> Void { let sampleRateHz: Double = 8000.0 let numberOfSamples = Int((Double(durationMs) / 1000 * sampleRateHz)) let factor: Double = 2 * M_PI / (sampleRateHz/Double(frequency)) // Generate an array of Doubles. var samples = [Double](count: numberOfSamples, repeatedValue: 0.0) for i in 1..<numberOfSamples { let sample = sin(factor * Double(i)) samples[i] = sample } // Convert to a 16 bit PCM sound array. var index = 0 var sound = [Byte](count: 2 * numberOfSamples, repeatedValue: 0) for doubleValue in samples { // Scale to maximum amplitude. Int16.max is 37,767. var value = Int16(doubleValue * Double(Int16.max)) // In a 16 bit wav PCM, first byte is the low order byte. var firstByte = Int16(value & 0x00ff) var secondByteHighOrderBits = Int32(value) & 0xff00 var secondByte = Int16(secondByteHighOrderBits >> 8) // Right shift. // println("\(doubleValue) -> \(value) -> \(firstByte), \(secondByte)") sound[index++] = Byte(firstByte) sound[index++] = Byte(secondByte) } let format = AVAudioFormat(commonFormat: AVAudioCommonFormat.PCMFormatInt16, sampleRate: sampleRateHz, channels:AVAudioChannelCount(1), interleaved: false) let buffer = AudioBuffer(mNumberChannels: 1, mDataByteSize: UInt32(sound.count), mData: &sound) let pcmBuffer = AVAudioPCMBuffer(PCMFormat: format, frameCapacity: AVAudioFrameCount(sound.count)) let audioEngine = AVAudioEngine() let audioPlayer = AVAudioPlayerNode() audioEngine.attachNode(audioPlayer) // Runtime error occurs here: audioEngine.connect(audioPlayer, to: audioEngine.mainMixerNode, format: format) audioEngine.startAndReturnError(nil) audioPlayer.play() audioPlayer.scheduleBuffer(pcmBuffer, atTime: nil, options: nil, completionHandler: nil) } } 

El error que obtengo en time de ejecución al llamar connect () en AVAudioEngine es el siguiente:

 ERROR: [0x3bfcb9dc] AVAudioNode.mm:521: AUSetFormat: error -10868 *** Terminating app due to uncaught exception 'com.apple.coreaudio.avfaudio', reason: 'error -10868' 

¿Lo que estoy generando realmente no es AVAudioCommonFormat.PCMFormatInt16?

[EDITAR]

Aquí hay otro bash más sencillo de usar solo un búfer como PCMFormatFloat32. No hay error, pero tampoco hay sonido.

 import AVFoundation class AudioManager:NSObject { let audioPlayer = AVAudioPlayerNode() lazy var audioEngine: AVAudioEngine = { let engine = AVAudioEngine() // Must happen only once. engine.attachNode(self.audioPlayer) return engine }() func play(frequency: Int, durationMs: Int, completionBlock:dispatch_block_t!) { var error: NSError? var mixer = audioEngine.mainMixerNode var sampleRateHz: Float = Float(mixer.outputFormatForBus(0).sampleRate) var numberOfSamples = AVAudioFrameCount((Float(durationMs) / 1000 * sampleRateHz)) var format = AVAudioFormat(commonFormat: AVAudioCommonFormat.PCMFormatFloat32, sampleRate: Double(sampleRateHz), channels: AVAudioChannelCount(1), interleaved: false) var buffer = AVAudioPCMBuffer(PCMFormat: format, frameCapacity: numberOfSamples) buffer.frameLength = numberOfSamples // Generate sine wave for var i = 0; i < Int(buffer.frameLength); i++ { var val = sinf(Float(frequency) * Float(i) * 2 * Float(M_PI) / sampleRateHz) // log.debug("val: \(val)") buffer.floatChannelData.memory[i] = val * 0.5 } // Audio engine audioEngine.connect(audioPlayer, to: mixer, format: format) log.debug("Sample rate: \(sampleRateHz), samples: \(numberOfSamples), format: \(format)") if !audioEngine.startAndReturnError(&error) { log.debug("Error: \(error)") } // Play player and buffer audioPlayer.play() audioPlayer.scheduleBuffer(buffer, atTime: nil, options: nil, completionHandler: completionBlock) } } 

Gracias: Thomas Royal ( http://www.tmroyal.com/playing-sounds-in-swift-audioengine.html )

El problema era que cuando se caía de la function play (), el jugador estaba siendo limpiado y nunca había completado (o apenas comenzado) a jugar. Aquí hay una solución bastante torpe para eso: dormir durante el time que la muestra antes de regresar de play ().

Aceptaré una mejor respuesta que evite tener que hacer esto si no se limpia el reproductor si alguien quiere publicar uno.

 import AVFoundation class AudioManager: NSObject, AVAudioPlayerDelegate { let audioPlayerNode = AVAudioPlayerNode() var waveAudioPlayer: AVAudioPlayer? var playing: Bool! = false lazy var audioEngine: AVAudioEngine = { let engine = AVAudioEngine() // Must happen only once. engine.attachNode(self.audioPlayerNode) return engine }() func playWaveFromBundle(filename: String, durationInSeconds: NSTimeInterval) -> Void { var error: NSError? var sound = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(filename, ofType: "wav")!) if error != nil { log.error("Error: \(error)") return } self.waveAudioPlayer = AVAudioPlayer(contentsOfURL: sound, error: &error) self.waveAudioPlayer!.delegate = self AVAudioSession.shanetworkingInstance().setCategory(AVAudioSessionCategoryPlayback, error: &error) if error != nil { log.error("Error: \(error)") return } log.verbose("Playing \(sound)") self.waveAudioPlayer!.prepareToPlay() playing = true if !self.waveAudioPlayer!.play() { log.error("Failed to play") } // If we don't block here, the player stops as soon as this function returns. While we'd prefer to wait for audioPlayerDidFinishPlaying() to be called here, it's never called if we block here. Instead, pass in the duration of the wave file and simply sleep for that long. /* while (playing!) { NSThread.sleepForTimeInterval(0.1) // seconds } */ NSThread.sleepForTimeInterval(durationInSeconds) log.verbose("Done") } func play(frequency: Int, durationInMillis: Int, completionBlock:dispatch_block_t!) -> Void { var session = AVAudioSession.shanetworkingInstance() var error: NSError? if !session.setCategory(AVAudioSessionCategoryPlayAndRecord, error: &error) { log.error("Error: \(error)") return } var mixer = audioEngine.mainMixerNode var sampleRateHz: Float = Float(mixer.outputFormatForBus(0).sampleRate) var numberOfSamples = AVAudioFrameCount((Float(durationInMillis) / 1000 * sampleRateHz)) var format = AVAudioFormat(commonFormat: AVAudioCommonFormat.PCMFormatFloat32, sampleRate: Double(sampleRateHz), channels: AVAudioChannelCount(1), interleaved: false) var buffer = AVAudioPCMBuffer(PCMFormat: format, frameCapacity: numberOfSamples) buffer.frameLength = numberOfSamples // Generate sine wave for var i = 0; i < Int(buffer.frameLength); i++ { var val = sinf(Float(frequency) * Float(i) * 2 * Float(M_PI) / sampleRateHz) // log.debug("val: \(val)") buffer.floatChannelData.memory[i] = val * 0.5 } AVAudioSession.shanetworkingInstance().setCategory(AVAudioSessionCategoryPlayback, error: &error) if error != nil { log.error("Error: \(error)") return } // Audio engine audioEngine.connect(audioPlayerNode, to: mixer, format: format) log.debug("Sample rate: \(sampleRateHz), samples: \(numberOfSamples), format: \(format)") if !audioEngine.startAndReturnError(&error) { log.error("Error: \(error)") return } // TODO: Check we're not in the background. Attempting to play audio while in the background throws: // *** Terminating app due to uncaught exception 'com.apple.coreaudio.avfaudio', reason: 'error 561015905' // Play player and schedule buffer audioPlayerNode.play() audioPlayerNode.scheduleBuffer(buffer, atTime: nil, options: nil, completionHandler: completionBlock) // If we don't block here, the player stops as soon as this function returns. NSThread.sleepForTimeInterval(Double(durationInMillis) * 1000.0) // seconds } // MARK: AVAudioPlayerDelegate func audioPlayerDidFinishPlaying(player: AVAudioPlayer!, successfully flag: Bool) { log.verbose("Success: \(flag)") playing = false } func audioPlayerDecodeErrorDidOccur(player: AVAudioPlayer!, error: NSError!) { log.verbose("Error: \(error)") playing = false } // MARK: NSObject overrides deinit { log.verbose("deinit") } } 

Para el context, este AudioManager es una propiedad cargada perezosa en mi AppDelegate:

 lazy var audioManager: AudioManager = { return AudioManager() }() 

Intente configurar su categoría de session en " AVAudioSessionCategoryPlay o AVAudioSessionCategoryPlayAndRecord ". Estoy usando el logging y la reproducción y llamarlo antes de que la grabación funcione bien. Supongo que tiene que ir antes de comenzar a conectar nodos.

  var session = AVAudioSession.shanetworkingInstance() session.setCategory(AVAudioSessionCategoryPlayAndRecord, error: &error) 

Con respecto al problema de no get sonido, incluso cuando se usa PCMFormatFloat32 :

He luchado con el mismo problema por algunos días y finalmente encontré el problema (o al less uno): necesitas configurar manualmente el frameLength del PCM Buffer:

 pcmBuffer.frameLength = AVAudioFrameCount(sound.count/2) 

La split por dos count para los dos bytes por ttwig (16 bits codificados en dos bytes).

Además de eso, otro cambio que hice, y que aún no sé si importa o no, es que hice los miembros AVAudioEngine y AVAudioPlayerNode de la class, para evitar que se destruyan antes de que finalice la reproducción.

He estado encontrando el mismo comportamiento como tú, eso significa que estaba ayudándome con NSThread.sleepForTimeInterval() . En este momento me di count de la solución, que funciona para mí. El punto es que el object AudioEngine() debe inicializarse fuera de la function Play() . Tiene que ser inicializado en el nivel de class, por lo que el motor puede funcionar y reproducir el sonido incluso después de que la function se cierra (lo cual es inmediato). Justo después de mover la línea inicializando AudioEngine, el sonido se puede escuchar incluso sin el "ayudante" que espera. Espero que te ayude.

Para get el número correcto de muestras (numberOfSamples): mixer.outputFormatForBus (0) .sampleRate devuelve 44100.0 Multiplicar por 1000 no es necesario en el segundo ejemplo.

Para mí, la primera llamada play () y luego establecer el progtwigBuffer en playernode no parece lógico. Me gustaría revertir.