What’s a beep?
A beep refers to a sound generated by the sound wave defined by a constant frequency.
This sine wave is the result of an analog or digital oscillation.
Setup the Beep Player
First, let’s define our BeepPlayer.
1import AVFAudio
2
3final class BeepPlayer {
4 // Initialized during configuration
5 private var engine: AVAudioEngine!
6 private var source: AVAudioSourceNode!
7
8 // Control properties
9 private var phase = Float.zero
10 private var amplitude = Float.zero
11
12 // The frequency at which the oscillator
13 // generates soundwaves.
14 var frequency: Float
15
16 init(frequency: Float = 440) {
17 self.frequency = frequency
18 }
19}Prepare the audio engine
We need to initialize an instance of AVAudioEngine and hold a strong reference to it.
Then we create an AVAudioSourceNode with a custom render block, and attach the engine to it.
Then we connect the source node to the main mixer node of the engine with the correct input format.
private func configureEngine() {
engine = AVAudioEngine()
let mixer = engine.mainMixerNode
mixer.outputVolume = 0.5
let output = engine.outputNode
let outputFormat = output.inputFormat(forBus: 0)
let inputFormat = AVAudioFormat(commonFormat: outputFormat.commonFormat,
sampleRate: outputFormat.sampleRate,
channels: 1,
interleaved: outputFormat.isInterleaved)
// TODO: Create and attach audio source node
engine.prepare()
}And call it in the initializer:
init(frequency: Float = 440) {
self.frequency = frequency
configureEngine()
}Implement the render block
We need to implement the render block that AVAudioSourceNode calls on each audio frame
to render the sound buffer.
private func renderBlock(
isSlience: UnsafeMutablePointer<ObjCBool>,
timestamp: UnsafePointer<AudioTimeStamp>,
frameCount: AVAudioFrameCount,
outputData: UnsafeMutablePointer<AudioBufferList>
) -> OSStatus {
// Capture self as a local variable
// to avoid multiple Objective-C
// symbol lookups
let player = self
let sampleRate = Float(player.engine.outputNode.outputFormat(forBus: 0).sampleRate)
// A wrapper for an unsafe mutable pointer to
// an AudioBufferList instance.
let ablPtr = UnsafeMutableAudioBufferListPointer(outputData)
// Phase delta between each sound frame
let phaseDelta = 2 * Float.pi * player.frequency / sampleRate
for frame in 0..<Int(frameCount) {
// Calculate sample for frame based on
// current phase, frame index, and amplitude.
let sample = sin(player.phase) * player.amplitude
// Increment the phase by fixed delta.
player.phase += phaseDelta
// Fract the phase between 0...2*pi
if player.phase > .pi * 2 {
player.phase -= .pi * 2
}
for buffer in ablPtr {
let ptr = buffer.mData!.assumingMemoryBound(to: Float.self)
ptr[frame] = sample
}
}
return noErr
}Now we can create an AVAudioSourceNode by passing the renderBlock method.
Then we attach the engine to it, and then connect the source node to the main mixer node of the engine with an input format identical to the engine’s default output format.
private func configureEngine() {
// ...
// Create source node
source = AVAudioSourceNode(renderBlock: renderBlock)
// Attach source node
engine.attach(source)
engine.connect(source, to: mixer, format: inputFormat)
engine.prepare()
}The engine is now prepared successfully.
Start the audio engine
Now let’s implement the start method.
We safeguard the method to prevent starting a running engine.
func startEngine() {
guard !engine.isRunning else { return }
do {
try engine.start()
} catch {
// Catch the error
print(error.localizedDescription)
}
}After the engine is successfully configured, you should start the engine before using it (ideally in viewDidLoad in UIKit or onAppear in SwiftUI) and leave it running.
Tip
Per Apple’s official documentation, there’s no need to stop an audio engine, and it’s best to leave it running. The system will clean up the resources when not in use.
Destroying and recreating AVAudioEngine nodes are expensive and best not handled manually.
Generate a beep
The engine is now running. To hear a beep you should increase the amplitude.
To cut it off, you reset the amplitude back to zero.
func beep(withFrequency frequency: Float? = nil) {
if let frequency {
self.frequency = frequency
}
amplitude = 0.2
}
func stopBeep() {
amplitude = .zero
}The audio node is now configured and ready to use.
Full Implementation
All of the snippets above put together:
1// BeepPlayer.swift
2
3import AVFAudio
4
5final class BeepPlayer {
6 private var engine: AVAudioEngine!
7 private var source: AVAudioSourceNode!
8
9 private var phase = Float.zero
10 private var amplitude = Float.zero
11
12 var frequency: Float
13
14 init(frequency: Float = 440) {
15 self.frequency = frequency
16 configureEngine()
17 }
18
19 private func configureEngine() {
20 engine = AVAudioEngine()
21 let mixer = engine.mainMixerNode
22 mixer.outputVolume = 0.5
23 let output = engine.outputNode
24
25 let outputFormat = output.inputFormat(forBus: 0)
26 let inputFormat = AVAudioFormat(commonFormat: outputFormat.commonFormat, sampleRate: outputFormat.sampleRate, channels: 1, interleaved: outputFormat.isInterleaved)
27
28 source = AVAudioSourceNode(renderBlock: renderBlock)
29
30 engine.attach(source)
31 engine.connect(source, to: mixer, format: inputFormat)
32
33 engine.prepare()
34 }
35
36 private func renderBlock(
37 isSlience: UnsafeMutablePointer<ObjCBool>,
38 timestamp: UnsafePointer<AudioTimeStamp>,
39 frameCount: AVAudioFrameCount,
40 outputData: UnsafeMutablePointer<AudioBufferList>
41 ) -> OSStatus {
42 let player = self
43
44 let sampleRate = Float(player.engine.outputNode.outputFormat(forBus: 0).sampleRate)
45
46 let ablPtr = UnsafeMutableAudioBufferListPointer(outputData)
47
48 let phaseDelta = 2 * Float.pi * player.frequency / sampleRate
49
50 for frame in 0..<Int(frameCount) {
51 let sample = sin(player.phase) * player.amplitude
52 player.phase += phaseDelta
53
54 if player.phase > .pi * 2 {
55 player.phase -= .pi * 2
56 }
57
58 for buffer in ablPtr {
59 let ptr = buffer.mData!.assumingMemoryBound(to: Float.self)
60 ptr[frame] = sample
61 }
62 }
63
64 return noErr
65 }
66
67 func startEngine() {
68 guard !engine.isRunning else { return }
69 do {
70 try engine.start()
71 } catch {
72 print(error.localizedDescription)
73 }
74 }
75
76 func beep(withFrequency frequency: Float? = nil) {
77 if let frequency {
78 self.frequency = frequency
79 }
80 amplitude = 0.2
81 }
82
83 func stopBeep() {
84 amplitude = .zero
85 }
86}UI Integration
Here’s how to integrate a BeepPlayer instance into your user interface.
Create a beeping button
Let’s create a button that plays a beep when held and stops when released.
This behavior is usually most associated with morse transmitters.
Both SwiftUI and UIKit implementations are provided.
Make a SwiftUI BeepButton
Invoke startEngine in a view’s onAppear.
1// BeepButton.swift
2
3struct BeepButton: View {
4 @State private var beeper = BeepPlayer()
5
6 var body: some View {
7 Circle()
8 .fill(.red)
9 .frame(maxWidth: 200, maxHeight: 200)
10 .onAppear(perform: beeper.startEngine)
11 }
12}Note that we’re decorating the beeper with a @State wrapper to prevent it from being
killed and recreated between state changes.
Now let’s create a DragGesture and register it on our content
to track the user interaction on the view.
1// BeepButton.swift
2
3struct BeepButton: View {
4 @State private var beeper = BeepPlayer()
5 @State private var isPressed = false
6
7 var body: some View {
8 let drag = DragGesture(minimumDistance: .zero)
9 .onChanged { _ in
10 if !isPressed {
11 isPressed = true
12 beeper.beep()
13 }
14 }
15 .onEnded { _ in
16 isPressed = false
17 beeper.stopBeep()
18 }
19
20 Circle()
21 .fill(.red)
22 .frame(maxWidth: 200, maxHeight: 200)
23 .gesture(drag)
24 .onAppear(perform: beeper.startEngine)
25 }
26}We can customize the button label content using generics, and change the opacity upon pressing.
1// BeepButton.swift
2
3struct BeepButton<Label>: View where Label: View {
4 @State private var beeper = BeepPlayer()
5 @State private var isPressed = false
6
7 @ViewBuilder var label: @escaping () -> Label
8
9 var body: some View {
10 let drag = DragGesture(minimumDistance: .zero)
11 .onChanged { _ in
12 if !isPressed {
13 isPressed = true
14 beeper.beep()
15 }
16 }
17 .onEnded { _ in
18 isPressed = false
19 beeper.stopBeep()
20 }
21
22 label()
23 .opacity(isPressed ? 0.85 : 1.0)
24 .gesture(drag)
25 .onAppear(perform: beeper.startEngine)
26 }
27}Now we have a button that beeps.
1struct SomeView: View {
2 var body: some View {
3 BeepButton {
4 Circle()
5 .fill(.red)
6 .frame(maxWidth: 200, maxHeight: 200)
7 }
8 }
9}Make a beeping UIButton
Invoke startEngine in init.
1// BeepButton.swift
2
3final class BeepButton: UIButton {
4 private let beeper = BeepPlayer()
5
6 private var isPressed = false
7
8 override init(frame: CGRect) {
9 super.init(frame: frame)
10 commonInit()
11 }
12
13 required init?(coder: NSCoder) {
14 super.init(coder: coder)
15 commonInit()
16 }
17
18 private func commonInit() {
19 beeper.startEngine()
20 }
21}Logically, we need two targets to detect user press (touchDown and touchUp),
but the use case for the button usually requires a “discard” action as well,
and that’s when the user releases their finger outside of the bounds of the receiver.
So let’s add three targets with this use case in mind.
1 private func commonInit() {
2 beeper.startEngine()
3 setupTargets()
4 }
5
6 private func setupTargets() {
7 addTarget(self, action: #selector(touchDown), for: .touchDown)
8 addTarget(self, action: #selector(touchUp), for: .touchUpInside)
9 addTarget(self, action: #selector(discard), for: .touchUpOutside)
10 }
11
12 @objc private func touchDown() {
13 guard !isPressed else { return }
14 isPressed = true
15 beeper.beep()
16 }
17
18 @objc private func touchUp() {
19 guard isPressed else { return }
20 isPressed = false
21 beeper.stopBeep()
22 }
23
24 @objc private func discard() {
25 // If you want different functionality on discard,
26 // you need to implement this method.
27 touchUp()
28 }Now we have a UIButton that beeps.