電子工作で何か作るとき、ユーザーインターフェースとして音は重要ですよね。

Arduinoに音を鳴らすメソッドtone()があります。それから名前をもらって指定された周波数の音を鳴らすドライバーを作ってみました。

方式としては、PWM機能を使用して実装しているのですが、MCUによりPWMのAPIが違っているので、ドライバーの読み込み時にMCUの種類を判別し、それぞれに適したTONEの定義を読み込むようにしています。特にSTM32は、PWM自体が実装されていないので、PWM自体も独自に定義したものを使用しています。


TONEドライバー

Arduinoのtone()と利用法が少々異なりますが、オブジェクト指向的にはこちらの方が正統派かな?

"""
TONE: Tone driver
for MicroPython
Version 1.1.0

Copyright 2023 K.Kakizaki
"""

import sys

__pitch = {'C1':32.7, 'C#1':34.6, 'D1':36.7, 'D#1':38.9, 'E1':41.2, 'F1':43.7, 'F#1':46.2, 'G1':49.0, 'G#1':51.9, 'A1':55.0, 'A#1':58.3, 'B1':61.7,
        'C2':65.4, 'C#2':69.3, 'D2':73.4, 'D#2':77.8, 'E2':82.4, 'F2':87.3, 'F#2':92.5, 'G2':98.0, 'G#2':103.8, 'A2':110.0, 'A#2':116.5, 'B2':123.5,
        'C3':130.8, 'C#3':138.6, 'D3':146.8, 'D#3':155.6, 'E3':164.8, 'F3':174.6, 'F#3':185.0, 'G3':196.0, 'G#3':207.7, 'A3':220.0, 'A#3':233.1, 'B3':246.9,
        'C4':261.6, 'C#4':277.2, 'D4':293.7, 'D#4':311.1, 'E4':329.6, 'F4':349.2, 'F#4':370.0, 'G4':392.0, 'G#4':415.3, 'A4':440.0, 'A#4':466.2, 'B4':493.9,
        'C5':523.2, 'C#5':554.3, 'D5':587.3, 'D#5':622.3, 'E5':659.3, 'F5':698.4, 'F#5':740.0, 'G5':784.0, 'G#5':831.0, 'A5':880.0, 'A#5':932.3, 'B5':987.8,
        'C6':1046.5, 'C#6':1108.7, 'D6':1174.7, 'D#6':1244.5, 'E6':1318.5, 'F6':1396.9, 'F#6':1480.0, 'G6':1568.0, 'G#6':1661.2, 'A6':1760.0, 'A#6':1864.7, 'B6':1975.5,
        'C7':2093.0, 'C#7':2217.5, 'D7':2349.3, 'D#7':2489.0, 'E7':2637.0, 'F7':2793.8, 'F#7':2960.0, 'G7':3136.0, 'G#7':3322.4, 'A7':3520.0, 'A#7':3729.3, 'B7':3951.0,
        'C8':4186.0,
        '休符':0, '無音':0, 'ストップ':0}

__jtoa = {'ド':'C', 'ド#':'C#', 'レ':'D', 'レ#':'D#', 'ミ':'E', 'ファ':'F', 'ファ#':'F#', 'ソ':'G', 'ソ#':'G#', 'ラ':'A', 'ラ#':'A#', 'シ':'B'}

if sys.platform.startswith('esp32'):
    from machine import Pin, PWM
    class TONE:
        _pitch = __pitch
        _jtoa = __jtoa

        def __init__(self, pin, duty=50):
            self._tone = PWM(Pin(pin), freq=400, duty=0)
            self._duty = duty

        def duty(self, v):
            self._duty = v
            
        def on(self, hz):
            if isinstance(hz, str):
                if hz in TONE._pitch:
                    self.on(int(TONE._pitch[hz]))
                elif hz[:-1] in TONE._jtoa:
                    self.on(int(TONE._pitch[TONE._jtoa[hz[:-1]] + hz[-1:]]))
            else:
                if hz == 0:
                    self._tone.duty(0)
                else:
                    self._tone.freq(int(hz))
                    self._tone.duty(int(1023*self._duty/100))

        def off(self):
            self._tone.duty(0)
elif sys.platform.startswith('rp2'):
    from machine import Pin, PWM
    class TONE:
        _pitch = __pitch
        _jtoa = __jtoa

        def __init__(self, pin, duty=50):
            self._tone = PWM(Pin(pin), freq=400, duty_u16=0)
            self._duty = duty

        def duty(self, v):
            self._duty = v
            
        def on(self, hz):
            if isinstance(hz, str):
                if hz in TONE._pitch:
                    self.on(int(TONE._pitch[hz]))
                elif hz[:-1] in TONE._jtoa:
                    self.on(int(TONE._pitch[TONE._jtoa[hz[:-1]] + hz[-1:]]))
            else:
                if hz == 0:
                    self._tone.duty_u16(0)
                else:
                    self._tone.freq(int(hz))
                    self._tone.duty_u16(int(65535*self._duty/100))

        def off(self):
            self._tone.duty_u16(0)
elif sys.platform.startswith('stm32'):
    from machine import Pin
    from stm32pwm import PWM
    class TONE:
        _pitch = __pitch
        _jtoa = __jtoa

        def __init__(self, pin, duty=50):
            self._tone = PWM(pin, freq=400, duty_percent=0)
            self._duty = duty

        def duty(self, v):
            self._duty = v
            
        def on(self, hz):
            if isinstance(hz, str):
                if hz in TONE._pitch:
                    self.on(TONE._pitch[hz])
                elif hz[:-1] in TONE._jtoa:
                    self.on(TONE._pitch[TONE._jtoa[hz[:-1]] + hz[-1:]])
            else:
                if hz == 0:
                    self._tone.duty_percent(0)
                else:
                    self._tone.freq(hz)
                    self._tone.duty_percent(int(self._duty))

        def off(self):
            self._tone.duty_percent(0)

del __pitch
del __jtoa

on()メソッドでは、音の周波数の他に’C4’、’ド4'などのように、音の名前を指定することもできます。


TONEの使用例

異なる周波数の音の出力

周波数が100Hzから1000Hzまで、100Hzごとに周波数を上げて音を出力するプログラム例を示します。(実際に出力される音(の高さ)は、圧電スピーカーの特性によるのか、ちょっと違和感があります。)

マイクロファンの開発ボードでは、圧電スピーカーが組み込まれている場合には、その制御に割り当てられている端子は'SND'で指定できます。

from tone import TONE
import time

snd = TONE('SND')

for hz in range(100,1001,100): # 100Hz -1000Hz
    snd.on(hz)
    time.sleep_ms(500)
    
snd.off()

カエルの歌

TONEでは、音の周波数だけでなく音名を指定することができます。

皆様ご存じカエルの歌です。お楽しみください。

以下のサイトの音符を利用させていただきました。

from tone import TONE
import time

snd=TONE('SND',50)

kaeru = [ 'ド4', 'レ4', 'ミ4', 'ファ4', 'ミ4', 'レ4', 'ド4', 'ド4',
'ミ4', 'ファ4', 'ソ4', 'ラ4', 'ソ4', 'ファ4', 'ミ4', 'ミ4',
'ド4', '休符', 'ド4', '休符', 'ド4', '休符', 'ド4', '休符', 
'ド4', 'レ4', 'ミ4', 'ファ4', 'ミ4', 'レ4', 'ド4', 'ド4']

for s in kaeru:
    snd.on(s)
    time.sleep_ms(400)

snd.off()

圧電スピーカーの周波数特性なのか、方形波の高調波成分のせいなのか、そもそも、周波数リストの数値誤りなのか、ちょっと違う感じもありますが、楽しめましたか?

それから、音符の長さを一定にしている都合で、あからさまに表現が異なるところがありますがご容赦ください。:-P