-
-
Save hotpaw2/630a466cc830e3d129b9 to your computer and use it in GitHub Desktop.
// | |
// ToneOutputUnit.swift | |
// | |
// This is a Swift 3 class (which should be instantiated as a singleton object) | |
// that can play a single tone of arbitrary tone and frequency on iOS devices | |
// using run-time sinewave synthesis and the Audio Unit v3 API with RemoteIO. | |
// | |
// Created by Ronald Nicholson [email protected] on 2/20/2016. | |
// revised 2016-Sep-08 for Swift 3 | |
// http://www.nicholson.com/rhn/ | |
// Copyright © 2016 Ronald H Nicholson, Jr. All rights reserved. | |
// Distribution: BSD 2-clause license | |
// | |
import Foundation | |
import AudioUnit | |
import AVFoundation | |
final class ToneOutputUnit: NSObject { | |
var auAudioUnit: AUAudioUnit! = nil // placeholder for RemoteIO Audio Unit | |
var avActive = false // AVAudioSession active flag | |
var audioRunning = false // RemoteIO Audio Unit running flag | |
var sampleRate : Double = 44100.0 // typical audio sample rate | |
var f0 = 880.0 // default frequency of tone: 'A' above Concert A | |
var v0 = 16383.0 // default volume of tone: half full scale | |
var toneCount : Int32 = 0 // number of samples of tone to play. 0 for silence | |
private var phY = 0.0 // save phase of sine wave to prevent clicking | |
private var interrupted = false // for restart from audio interruption notification | |
func startToneForDuration(time : Double) { | |
if audioRunning { // make sure to call enableSpeaker() first | |
if toneCount == 0 { // only play a tone if the last tone has stopped | |
toneCount = Int32(round( time / sampleRate )) | |
} | |
} | |
} | |
func setFrequency(freq : Double) { // audio frequencies below 500 Hz may be | |
f0 = freq // hard to hear from a tiny iPhone speaker. | |
} | |
func setToneVolume(vol : Double) { // 0.0 to 1.0 | |
v0 = vol * 32766.0 | |
} | |
func setToneTime(t : Double) { | |
toneCount = Int32(t * sampleRate); | |
} | |
func enableSpeaker() { | |
if audioRunning { return } // return if RemoteIO is already running | |
if (avActive == false) { | |
do { // set and activate Audio Session | |
let audioSession = AVAudioSession.sharedInstance() | |
try audioSession.setCategory(AVAudioSessionCategorySoloAmbient) | |
var preferredIOBufferDuration = 4.0 * 0.0058 // 5.8 milliseconds = 256 samples | |
let hwSRate = audioSession.sampleRate // get native hardware rate | |
if hwSRate == 48000.0 { sampleRate = 48000.0 } // set session to hardware rate | |
if hwSRate == 48000.0 { preferredIOBufferDuration = 4.0 * 0.0053 } | |
let desiredSampleRate = sampleRate | |
try audioSession.setPreferredSampleRate(desiredSampleRate) | |
try audioSession.setPreferredIOBufferDuration(preferredIOBufferDuration) | |
NotificationCenter.default.addObserver( | |
forName: NSNotification.Name.AVAudioSessionInterruption, | |
object: nil, | |
queue: nil, | |
using: myAudioSessionInterruptionHandler ) | |
try audioSession.setActive(true) | |
avActive = true | |
} catch /* let error as NSError */ { | |
// handle error (audio system broken?) | |
} | |
} | |
do { // not running, so start hardware | |
let audioComponentDescription = AudioComponentDescription( | |
componentType: kAudioUnitType_Output, | |
componentSubType: kAudioUnitSubType_RemoteIO, | |
componentManufacturer: kAudioUnitManufacturer_Apple, | |
componentFlags: 0, | |
componentFlagsMask: 0 ) | |
if (auAudioUnit == nil) { | |
try auAudioUnit = AUAudioUnit(componentDescription: audioComponentDescription) | |
let bus0 = auAudioUnit.inputBusses[0] | |
let audioFormat = AVAudioFormat( | |
commonFormat: AVAudioCommonFormat.pcmFormatInt16, // short int samples | |
sampleRate: Double(sampleRate), | |
channels:AVAudioChannelCount(2), | |
interleaved: true ) // interleaved stereo | |
try bus0.setFormat(audioFormat) // for speaker bus | |
auAudioUnit.outputProvider = { ( // AURenderPullInputBlock? | |
actionFlags, | |
timestamp, | |
frameCount, | |
inputBusNumber, | |
inputDataList ) -> AUAudioUnitStatus in | |
self.fillSpeakerBuffer(inputDataList: inputDataList, frameCount: frameCount) | |
return(0) | |
} | |
} | |
auAudioUnit.isOutputEnabled = true | |
toneCount = 0 | |
try auAudioUnit.allocateRenderResources() // v2 AudioUnitInitialize() | |
try auAudioUnit.startHardware() // v2 AudioOutputUnitStart() | |
audioRunning = true | |
} catch /* let error as NSError */ { | |
// handleError(error, functionName: "AUAudioUnit failed") | |
// or assert(false) | |
} | |
} | |
// helper functions | |
private func fillSpeakerBuffer( // process RemoteIO Buffer for output | |
inputDataList : UnsafeMutablePointer<AudioBufferList>, | |
frameCount : UInt32 ) | |
{ | |
let inputDataPtr = UnsafeMutableAudioBufferListPointer(inputDataList) | |
let nBuffers = inputDataPtr.count | |
if (nBuffers > 0) { | |
let mBuffers : AudioBuffer = inputDataPtr[0] | |
let count = Int(frameCount) | |
// Speaker Output == play tone at frequency f0 | |
if ( self.v0 > 0) | |
&& (self.toneCount > 0 ) | |
{ | |
// audioStalled = false | |
var v = self.v0 ; if v > 32767 { v = 32767 } | |
let sz = Int(mBuffers.mDataByteSize) | |
var a = self.phY // capture from object for use inside block | |
let d = 2.0 * M_PI * self.f0 / self.sampleRate // phase delta | |
let bufferPointer = UnsafeMutableRawPointer(mBuffers.mData) | |
if var bptr = bufferPointer { | |
for i in 0..<(count) { | |
let u = sin(a) // create a sinewave | |
a += d ; if (a > 2.0 * M_PI) { a -= 2.0 * M_PI } | |
let x = Int16(v * u + 0.5) // scale & round | |
if (i < (sz / 2)) { | |
bptr.assumingMemoryBound(to: Int16.self).pointee = x | |
bptr += 2 // increment by 2 bytes for next Int16 item | |
bptr.assumingMemoryBound(to: Int16.self).pointee = x | |
bptr += 2 // stereo, so fill both Left & Right channels | |
} | |
} | |
} | |
self.phY = a // save sinewave phase | |
self.toneCount -= Int32(frameCount) // decrement time remaining | |
} else { | |
// audioStalled = true | |
memset(mBuffers.mData, 0, Int(mBuffers.mDataByteSize)) // silence | |
} | |
} | |
} | |
func stop() { | |
if (audioRunning) { | |
auAudioUnit.stopHardware() | |
audioRunning = false | |
} | |
if (avActive) { | |
let audioSession = AVAudioSession.sharedInstance() | |
do { | |
// try audioSession.setActive(false) | |
} catch { | |
} | |
// avActive = false | |
} | |
} | |
private func myAudioSessionInterruptionHandler( notification: Notification ) -> Void { | |
let interuptionDict = notification.userInfo | |
if let interuptionType = interuptionDict?[AVAudioSessionInterruptionTypeKey] { | |
let interuptionVal = AVAudioSessionInterruptionType( | |
rawValue: (interuptionType as AnyObject).uintValue ) | |
if (interuptionVal == AVAudioSessionInterruptionType.began) { | |
if (audioRunning) { | |
auAudioUnit.stopHardware() | |
audioRunning = false | |
interrupted = true | |
} | |
} | |
} | |
} | |
} | |
// end of the ToneOutputUnit class |
I've set the frequency to 1000 and the volume to 1, but there is no sound playing. Am I doing something wrong? The ToneOutputUnit instance is class level so it does not get deallocated, and I am calling startToneForDuration(10)
How do i use it? i mean how can i provide input data in byte to AudioUnit ?? is it possible with avobe mentioned code??
I've found that:
toneOutputUnit.enableSpeaker()
toneOutputUnit.setToneTime(t: 10)
played the standard tone for 10 seconds. Calling
toneOutputUnit.startToneForDuration(time: 10)
does not.
Thanks for the demo :)
I found that tone is not played for first time, it is played second time.
Hello Ronald Nicolson.
I have used this tone generator code and its working fine. One more thing I want to know that can we use this code to differentiate the different frequencies for left and right headphones. My concern to create binaural beat with this code like this:
left freq. 120 hz and right 125 hz then output is 5 hz for listener.
I am trying to use this under Swift 5.0, and I hear nothing... did have to make a few syntax changes and comment out the callback that it couldn't find... but I cannot get it to work... on an iPhone 13 or an iPhone 15 Simulator; do get this error message, which looks ominous to me...
IPCAUClient.cpp:139 IPCAUClient: can't connect to server (18'446'744'073'709'484'868)
Similar here, error message is:
AddInstanceForFactory: No factory registered for id <CFUUID 0x....> F8...-...-...
Pay attention on "audioStalled"; this variable is not declared and then there is a compilation error.