1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Swift] DTMFクラス(プッシュ信号音再生)

Last updated at Posted at 2024-10-27

DTMFとは、Dual-Tone Multi-Frequencyの略で、プッシュホン回線で信号を送信する音声信号のこと。

高音と低音の4種類ずつの周波数の音を組み合わせて、下表に示す16種類の符号を表現する。

低音 \ 高音 1209Hz 1336Hz 1477Hz 1633Hz
697Hz 1 2 3 A
770Hz 4 5 6 B
852Hz 7 8 9 C
941Hz * 0 # D

・Pythonによる実装については、次のサイトで詳しく説明されています。



・Swiftでの実装は、次のサイトに公開されています。これを参考に、次の DTMFクラス を作りました。

DTMFクラス
import AVFoundation

class DTMF {
    private let sampleRate = Float32(48000)
    private let engine: AVAudioEngine!
    private let player: AVAudioPlayerNode!
    private let mixer: AVAudioMixerNode!

    private typealias MarkSpaceType = (Float32, Float32)
    private typealias DTMFType = (Float32, Float32)

    init() {
        engine = AVAudioEngine()
        player = AVAudioPlayerNode()
        mixer = engine.mainMixerNode
    }
    
    func play(for phoneNumber: String) {
        if player.isPlaying { player.stop() }
        
        if let tones = tonesFor(string: phoneNumber) {
            let audioFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: Double(sampleRate), channels: 2, interleaved: false)!
            
            // fill up the buffer with some samples
            var allSamples = [Float32]()
            for tone in tones {
                let samples = generateDTMF(tone)
                allSamples.append(contentsOf: samples)
            }
            
            let frameCount = AVAudioFrameCount(allSamples.count)
            guard let buffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: frameCount) else { return }
            
            buffer.frameLength = frameCount
            let channelMemory = buffer.floatChannelData!
            for channelIndex in 0 ..< Int(audioFormat.channelCount) {
                let frameMemory = channelMemory[channelIndex]
                memcpy(frameMemory, allSamples, Int(frameCount) * MemoryLayout<Float32>.size)
            }
            
            engine.attach(player)
            engine.connect(player, to: mixer, format: audioFormat)
            mixer.outputVolume = 1.0
            
            do {
                try engine.start()
            } catch let error as NSError {
                print("Engine start failed - \(error)")
            }
            
            player.scheduleBuffer(buffer)
            player.play()
        }
    }

    /**
     Generates a series of Float32 samples representing a DTMF tone with a given mark and space.
     
     - parameter DTMF: takes a DTMFType comprised of two floats that represent the desired tone frequencies in Hz.
     - parameter markSpace: takes a MarkSpaceType comprised of two floats representing the duration of each in milliseconds. The mark represents the length of the tone and space the silence.
     - parameter sampleRate: the number of samples per second (Hz) desired.
     - returns: An array of Float32 that contains the Linear PCM samples that can be fed to AVAudio.
     */
    private func generateDTMF(_ dtmfType: DTMFType, markSpace: MarkSpaceType = MarkSpaceType(100.0, 100.0), sampleRate: Float32 = 48000) -> [Float32] {
        let toneLengthInSamples = 10e-4 * markSpace.0 * sampleRate
        let silenceLengthInSamples = 10e-4 * markSpace.1 * sampleRate
        
        var sound = [Float32](repeating: 0, count: Int(toneLengthInSamples + silenceLengthInSamples))
        let twoPI = Float32.pi * 2
        
        for n in 0 ..< Int(toneLengthInSamples) {
            let sample1 = 0.5 * sin(Float32(n) * twoPI / (sampleRate / dtmfType.0));
            let sample2 = 0.5 * sin(Float32(n) * twoPI / (sampleRate / dtmfType.1));
            sound[n] = sample1 + sample2
        }
        
        return sound
    }
    
    private func tonesFor(string dial: String) -> [DTMFType]? {
        let toneDict: [Character: DTMFType] = [
            "1": (697, 1209),
            "2": (697, 1336),
            "3": (697, 1477),
            "4": (770, 1209),
            "5": (770, 1336),
            "6": (770, 1477),
            "7": (852, 1209),
            "8": (852, 1336),
            "9": (852, 1477),
            "*": (941, 1209),
            "0": (941, 1336),
            "#": (941, 1477),
            "A": (697, 1633),
            "B": (770, 1633),
            "C": (852, 1633),
            "D": (941, 1633),
            " ": (  0,    0), //本来は不要
        ]
        let tones = dial.uppercased().compactMap { toneDict[$0] }
        return tones.count > 0 ? tones : nil
    }
}
  • 使い方
let dtmf = DTMF()
dtmf.play(for: "01-2345-6789")
Thread.sleep(forTimeInterval: 3) //for REPL
  • ミッキーマウスのテーマ(に聞こえる?) 再生
dtmf.play(for: "66666666 96321 666 666 #6936")
Thread.sleep(forTimeInterval: 6) //for REPL



  • SwiftUIでの使用例
import SwiftUI

struct ContentView: View {
    let dtmf = DTMF()
    @State var phoneNumber: String = "01-2345-6789"
    var body: some View {
        VStack {
            TextField(text: $phoneNumber, label: { Text("Phone number") })
            
            Button("Tone", action: {
                if phoneNumber.isEmpty { return }
                dtmf.play(for: phoneNumber)
            })
        }
        .padding()
    }
}

相手方の電話番号を入力して、電話の受話器に向かって音声信号を聞かせると、実際にダイヤルすることができるはずです。

 

以上

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?