Create Morse Code and Play it - Morse Code Player
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()
}
}