admaDIC App Development & IT Solutions

Create Morse Code and Play it - Morse Code Player

by Annett Schwarze | 2025-08-15

This article is part of a series in which a morse code player is implemented. As a second step, a class is implemented, which prepares the beep sounds and can play them.

The `AVFoundation` framework can be used to generate sounds and play them. The short and long beeps are computed using the sinus function.

The player is kept simple. The pauses are defined by the spaces in the morse code string. This way, the player can loop through the characters and play the appropriate beep or sleep for a pause time.

        
class MorseCodePlayer: ObservableObject {
    static let shared = MorseCodePlayer()

    private var task: Task<Void, Never>?

    private var audioEngine: AVAudioEngine?
    private var playerNode: AVAudioPlayerNode?
    private var isStopped = false

    private var engine: AVAudioEngine?
    private var player: AVAudioPlayerNode?
    private var format: AVAudioFormat?
    private var shortBeep: AVAudioPCMBuffer?
    private var longBeep: AVAudioPCMBuffer?

    typealias UpdateBlock = (_ progress: Double, _ total: Double) -> Void
    typealias FinishBlock = () -> Void

    let dash = "-"
    let dot = "."
    let spc = " "
    let unit: Double = 0.12 // seconds
    let spacePause: Int = 1
    let letterSpacePause: Int = 3
    let wordSpacePause: Int = 7
    let longFactor: Int = 3
    let shortFactor: Int = 1
    var shortDuration: Double { unit * Double(shortFactor) }
    var longDuration: Double { unit * Double(longFactor) }

    private func startup() {
        let engine = AVAudioEngine()
        let player = AVAudioPlayerNode()
        engine.attach(player)
        let format = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 1)!
        engine.connect(player, to: engine.mainMixerNode, format: format)

        self.format = format
        self.engine = engine
        self.player = player

        shortBeep = createBeep(duration: unit * Double(shortFactor))
        longBeep = createBeep(duration: unit * Double(longFactor))

        try? engine.start()
        player.play()
    }

    private func shutdown() {
        player?.stop()
        engine?.stop()

        engine = nil
        player = nil
        format = nil

        shortBeep = nil
        longBeep = nil
    }

    private func createBeep(duration: Double) -> AVAudioPCMBuffer? {
        guard let format else { return nil }

        let frames = UInt32(duration * format.sampleRate)
        let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: frames)!
        buffer.frameLength = frames
        let freq = 900.0
        for i in 0 ..< Int(frames) {
            let t = Double(i) / format.sampleRate
            let val = Float32(sin(2 * .pi * freq * t) * 0.25)
            buffer.floatChannelData!.pointee[i] = val
        }
        return buffer
    }

    func play(morse: String, onProgress: @escaping UpdateBlock, onFinish: @escaping FinishBlock) {
        stop()
        task?.cancel()
        shutdown()

        isStopped = false
        startup()

        task = Task<Void, Never> { [weak self] in
            defer {
                self?.shutdown()
            }
            guard let self else { return }
            do {
                let chars = Array(morse)
                let ticks = chars.map({ [shortFactor, longFactor, dot, dash] in
                    switch String($0) {
                    case dot: return shortFactor
                    case dash: return longFactor
                    default: return shortFactor
                    }
                }).reduce(0, +)
                var tick = 0
                for c in chars where !self.isStopped {
                    if Task.isCancelled {
                        break
                    }
                    switch String(c) {
                    case dot:
                        await beep(buffer: shortBeep, duration: shortDuration, onFinish: {})
                        tick += shortFactor
                    case dash:
                        await beep(buffer: longBeep, duration: longDuration, onFinish: {})
                        tick += longFactor
                    default:
                        try await silence(shortDuration)
                        tick += shortFactor
                    }
                    DispatchQueue.main.async { onProgress(Double(tick), Double(ticks)) }
                }
            } catch {
                print("error: \(error.localizedDescription)")
            }
            self.shutdown()
            DispatchQueue.main.async {
                onFinish()
            }
        }
    }

    private func beep(buffer: AVAudioPCMBuffer?, duration: Double, onFinish: @escaping () -> Void) async {
        guard let buffer else { return }
        guard let player else { return }
        await player.scheduleBuffer(buffer, at: nil, options: .interrupts)
    }

    private func silence(_ duration: Double) async throws {
        try await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000))
    }

    func stop() {
        isStopped = true
        task?.cancel()
    }
}
    
SwiftUI Morse Code Player

 

www.admadic.de | webmaster@admadic.de | Legal Notice and Trademarks | Privacy
© 2005-2007 - admaDIC | All Rights Reserved
All other trademarks and/or registered trademarks are the property of their respective owners
Last Change: Fri Aug 15 07:48:44 2025 GMT