Skip to content

Instantly share code, notes, and snippets.

@ryango
Created October 18, 2022 21:55
Show Gist options
  • Save ryango/e2beb62eebbc5deabfe62843ff93e994 to your computer and use it in GitHub Desktop.
Save ryango/e2beb62eebbc5deabfe62843ff93e994 to your computer and use it in GitHub Desktop.
import UIKit
import AVFoundation
class ViewController: UIViewController {
let engine = AVAudioEngine()
override func viewDidLoad() {
super.viewDidLoad()
let outputFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 48000, channels: 2, interleaved: false)!
do {
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, options: [.defaultToSpeaker, .allowBluetooth])
try AVAudioSession.sharedInstance().setPreferredSampleRate(outputFormat.sampleRate)
try engine.inputNode.setVoiceProcessingEnabled(true)
// causes
// ATAudioSessionPropertyManager.mm:71 Invalid input size for property 1684431725
// ATAudioSessionPropertyManager.mm:225 Invalid input size for property 1684431725
} catch let error {
print("Error")
}
let recordingFile = try! AVAudioFile(forWriting: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("test.aac"),
settings: [AVFormatIDKey: kAudioFormatMPEG4AAC, AVNumberOfChannelsKey: 2, AVSampleRateKey: 48000, AVEncoderBitRateKey: 192000])
// Usage:
// toggle useBrokenPath here to check different behavior, tap the screen to stop recording and start playback of what was recorded
//
// The broken path connects the input into the output via the mainMixer, setting the mainMixer output to 0
// The recorded output on iPhone 14 pro has choppy audio, iPhone 13 pro and below do not have this issue.
// This is a legitimate use case. There is observed behavior that all nodes must flow to an output in order to tap them especially when using bluetooth input.
// We have a live streaming app with multiple audio inputs and outputs. A simplified example would be a live streaming tool that allows you to speak over
// music while broadcasting.
// In this example, the mic and music would be sent to a mixer which would be tapped and sent over RTMP to the stream
// The music would also be sent to the speakers. Ideally we could just tap the mixer but we found that if the mixer doesn't flow to an output we get undefined behavior.
// mic \ ___ mixer -> stream
// music /
// \ ___ speakers
// So our solution is
// mic \ _____ mixer (tapped) ____> mixer (0 vol) \
// music / > speaker
// \ _________________________________________/
let useBrokenPath = true
if useBrokenPath {
engine.connect(engine.inputNode, to: engine.mainMixerNode, format: outputFormat)
engine.connect(engine.mainMixerNode, to: engine.outputNode, format: outputFormat)
engine.mainMixerNode.outputVolume = 0
engine.inputNode.installTap(onBus: 0, bufferSize: 480, format: outputFormat) { buffer, time in
try! recordingFile.write(from: buffer)
}
} else {
// Connects an AVAudioPlayerNode in the same way as above but is fed the input node's buffers to demonstrate
// that this isn't as a result of the specific audio flowing through the graph
// Recorded audio does not exhibit the same choppiness
let player = AVAudioPlayerNode()
engine.inputNode.installTap(onBus: 0, bufferSize: 480, format: outputFormat) { buffer, time in
player.scheduleBuffer(buffer)
try! recordingFile.write(from: buffer)
}
engine.attach(player)
engine.connect(player, to: engine.mainMixerNode, format: outputFormat)
engine.connect(engine.mainMixerNode, to: engine.outputNode, format: outputFormat)
engine.mainMixerNode.outputVolume = 0
do {
try engine.start()
} catch let error {
print("error starting")
}
player.play()
}
do {
try engine.start()
} catch let error {
print("error starting")
}
let touch = UITapGestureRecognizer()
touch.addTarget(self, action: #selector(tap))
view.addGestureRecognizer(touch)
}
var player: AVAudioPlayer?
@objc func tap() {
engine.stop()
player = try! AVAudioPlayer(contentsOf: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("test.aac"))
player?.numberOfLoops = -1
player?.play()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment