Forked from rgcottrell/gist:5b876d9c5eea4c9e411c
Last active
October 8, 2017 19:54
-
-
Save matej-io/7003cc754fb4b358446528cc15e672c7 to your computer and use it in GitHub Desktop.
An FM Synthesizer in Swift using AVAudioEngine
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//Swift 4 | |
import AVFoundation | |
import Foundation | |
// The maximum number of audio buffers in flight. Setting to two allows one | |
// buffer to be played while the next is being written. | |
private let kInFlightAudioBuffers: Int = 2 | |
// The number of audio samples per buffer. A lower value reduces latency for | |
// changes but requires more processing but increases the risk of being unable | |
// to fill the buffers in time. A setting of 1024 represents about 23ms of | |
// samples. | |
private let kSamplesPerBuffer: AVAudioFrameCount = 1024 | |
public class FMSynthesizer { | |
// The audio engine manages the sound system. | |
private let engine = AVAudioEngine() | |
// The player node schedules the playback of the audio buffers. | |
private let playerNode = AVAudioPlayerNode() | |
// Use standard non-interleaved PCM audio. | |
let audioFormat = AVAudioFormat(standardFormatWithSampleRate: 44100.0, channels: 2)! | |
// A circular queue of audio buffers. | |
private var audioBuffers: [AVAudioPCMBuffer] = [] | |
// The index of the next buffer to fill. | |
private var bufferIndex = 0 | |
// The dispatch queue to render audio samples. | |
private let audioQueue = DispatchQueue(label: "FMSynthesizerQueue", attributes: []) | |
// A semaphore to gate the number of buffers processed. | |
private let audioSemaphore = DispatchSemaphore(value: kInFlightAudioBuffers) | |
public static let shared = FMSynthesizer() | |
private init() { | |
// Create a pool of audio buffers. | |
for _ in 0 ..< kInFlightAudioBuffers { | |
let audioBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: kSamplesPerBuffer)! | |
audioBuffers.append(audioBuffer) | |
} | |
// Attach and connect the player node. | |
engine.attach(playerNode) | |
engine.connect(playerNode, to: engine.mainMixerNode, format: audioFormat) | |
do { | |
try engine.start() | |
} catch let error as NSError { | |
print("Error starting audio engine: \(error)") | |
} | |
NotificationCenter.default.addObserver(self, selector: #selector(audioEngineConfigurationChange(notification:)), name: NSNotification.Name.AVAudioEngineConfigurationChange, object: engine) | |
} | |
public func play(carrierFrequency: Float32, modulatorFrequency: Float32, modulatorAmplitude: Float32) { | |
let unitVelocity = Float32(2.0 * Double.pi / audioFormat.sampleRate) | |
let carrierVelocity = carrierFrequency * unitVelocity | |
let modulatorVelocity = modulatorFrequency * unitVelocity | |
audioQueue.async { | |
var sampleTime: Float32 = 0 | |
while true { | |
// Wait for a buffer to become available. | |
_ = self.audioSemaphore.wait(timeout: DispatchTime.distantFuture) | |
// Fill the buffer with new samples. | |
let audioBuffer = self.audioBuffers[self.bufferIndex] | |
let floatChannelData = audioBuffer.floatChannelData! | |
let leftChannel = floatChannelData[0] | |
let rightChannel = floatChannelData[1] | |
for sampleIndex in 0 ..< Int(kSamplesPerBuffer) { | |
let sample = sin(carrierVelocity * sampleTime + modulatorAmplitude * sin(modulatorVelocity * sampleTime)) | |
leftChannel[sampleIndex] = sample | |
rightChannel[sampleIndex] = sample | |
sampleTime += 1 | |
} | |
audioBuffer.frameLength = kSamplesPerBuffer | |
// Schedule the buffer for playback and release it for reuse after | |
// playback has finished. | |
self.playerNode.scheduleBuffer(audioBuffer) { | |
self.audioSemaphore.signal() | |
return | |
} | |
self.bufferIndex = (self.bufferIndex + 1) % self.audioBuffers.count | |
} | |
} | |
playerNode.pan = 0.8 | |
playerNode.play() | |
} | |
@objc func audioEngineConfigurationChange(notification: NSNotification) -> Void { | |
NSLog("Audio engine configuration change: \(notification)") | |
} | |
} | |
FMSynthesizer.shared.play(carrierFrequency: 440.0, modulatorFrequency: 679.0, modulatorAmplitude: 0.8) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment