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.

BeepPlayer.swift
 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.

BeepPlayer.swift
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:

BeepPlayer.swift
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.

BeepPlayer.swift
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.

BeepPlayer.swift
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.

BeepPlayer.swift
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.

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.

BeepPlayer.swift
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:

BeepPlayer.swift
 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.

BeepButton.swift
 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.

BeepButton.swift
 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.

BeepButton.swift
 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.

SomeView.swift
1struct SomeView: View {
2    var body: some View {
3        BeepButton {
4            Circle()
5                .fill(.red)
6                .frame(maxWidth: 200, maxHeight: 200)
7        }
8    }
9}

License

Author: Nozhan Amiri

Link: https://nozhana.github.io/posts/how-to-generate-a-beep-with-swift/

License: CC BY-NC-SA 4.0

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. Please attribute the source, use non-commercially, and maintain the same license.