🐝

ESP32と圧電スピーカ2個で「静かな湖畔」の輪唱(asyncio)

2024/03/22に公開


はじめに

この記事では圧電スピーカ(ピエゾスピーカ)2個をESP32に接続し、「静かな湖畔」 を輪唱する方法について説明しています。
輪唱は非同期で2つのスピーカを同時に制御するので、Micropythonasyncioモジュールを使用しています。

PWMで圧電スピーカから音階を出力する方法についてはESP32でPWM(圧電スピーカ編)を参照下さい。

In English

This article describes how to troll a song, "Shizukana Kohan“ using ESP32 development board, two piezo speakers and Micropython in Japanese.

結線

実際の結線については実行動画を参照して下さい。

輪唱させる楽譜

演奏する「静かな湖畔」のメロディー譜です。
![](https://iot.fever.jp/zenn/ESP32/PWM/cuckoo_score.png =512x)

サンプルコード1

このサンプルコードは一つのスピーカでメロディを演奏するものです。
辞書型変数のSCALEに音階名と周波数をセットしています。
リスト型変数のSCOREにメロディーの音階と長さをセットしています。音階がrは休符を表します。
長さは8部音符を1とし、4部音符は2、付点4部音符は3、2部音符は4、付点2部音符は6としています。この数値は音を出力した後のtime.sleep()の引数として与えます。
play()関数はSCOREと音を出力するスピーカのインスタンスを引数として与えるようにして、SCOREからひとつずつ要素を取り出し、それを音階と長さの変数に分割して代入しています。
一つの音符をスピーカに出力した後、0.01秒程度の短い時間、音を消すようにして、同じ音が連続するような場合でも音が切れて聞こえるようにしています。

troll1.py
# Play "Shizukana Kohan" similar to "The Itsy Bitsy Spider" using piezo speaker
# Mar. 19th 2024 (C) iot101 at zenn.dev

from machine import PWM, Pin
import time

DEBUG = True

TEMPO = 40	# This value is *NOT* an acutual BPM.

# Frequency of the scale which requied to play
# 'g' is 1 octave higher than 'G'
SCALE = {"C":523, "D":587, "E":659, "F":698, "G":783, "g":392}

# 8th note -> 1, quarter note -> 2, dotted quarter note -> 3, half note -> 4, dotted half note -> 6
# 'r' is a rest note
SCORE = ["C3", "C1", "C3", "D1", "E3", "E1", "E3", "E1", "D3", "C1", "D3", "E1", "C4", "g4",
         "E3", "E1", "E3", "F1", "G3", "G1", "G3", "G1", "F3", "E1", "F3", "G1", "E6", "r1", "G1",
         "E6", "r1", "G1", "E6", "r1", "G1", "E3", "G1", "E3", "G1", "E6", "r2"]



# Speaker is connected to GPIO26 pin
SPEAKER = 26

# Set duty 50%
DUTY = int(50 / 100 * 65535)

# Make speakder object
speaker = PWM(Pin(SPEAKER), duty_u16=DUTY)
speaker.duty_u16(0)


# Play scale
def play(s, speaker):
    for score in s:
        scale, note = score
        
        if DEBUG == True:
            print(scale, note)
            
        note_length = float(note) * 0.1 * 60 / TEMPO
        
        if scale != "r":
            speaker.duty_u16(DUTY)
            freq = SCALE[scale]
            speaker.freq(freq)
        
        time.sleep(note_length)
        speaker.duty_u16(0)
        time.sleep(0.01)
    
    speaker.duty_u16(0)


play(SCORE, speaker)

実行動画

ひとつのスピーカ(左側)でメロディを演奏している動画です(34秒、87.2MB)。

サンプルコード2

スピーカを2個使用して輪唱するサンプルコードです。
ふたつ目のスピーカからは1小節遅れてメロディーを演奏するように、1小節分の休みをSCORE2の最初に追加しています。
ひとつ目のスピーカは最後に1小節分の休みをSCORE1に追加しています。
スピーカ1とスピーカ2は非同期で動作するように、asyncioを使用しています。
runplay() という関数を定義してasyncio.wait_for() でタスクを生成し、TIMEOUTで定義される秒を超えても完了しない場合はキャンセルするようにしています。
asyncio.gather() を使ってそれぞれのタスクを同時に実行するようにしています。
asyncioの詳細についてはこちらのサイトを参照して下さい。

troll2.py
# Play "Shizukana Kohan" similar to "The Itsy Bitsy Spider" using piezo speaker
# This is a troll version using 2 piezo speakers.
# The SPEAKER2 starts one bar later.
# Mar. 19th 2024 (C) iot101 at zenn.dev

from machine import PWM, Pin
import time
import asyncio

DEBUG = True

TEMPO = 40	# This value is *NOT* an acutual BPM.
TIMEOUT = 20

# Frequency of the scale
# 'g' is 1 octave higher than 'G'
SCALE = {"C":523, "D":587, "E":659, "F":698, "G":783, "g":392}

# 8th note -> 1, quarter note -> 2, dotted quarter note -> 3, half note -> 4, dotted half note -> 6
# 'r' is a rest note

# Put one bar rest at the end of the score.
SCORE1 = ["C3", "C1", "C3", "D1", "E3", "E1", "E3", "E1", "D3", "C1", "D3", "E1", "C4", "g4",
         "E3", "E1", "E3", "F1", "G3", "G1", "G3", "G1", "F3", "E1", "F3", "G1", "E6", "r1", "G1",
         "E6", "r1", "G1", "E6", "r1", "G1", "E3", "G1", "E3", "G1", "E6", "r2", "r6","r2"]

# Put one bar rest at the start of the score.
SCORE2 = ["r3", "r1", "r3", "r1","C3", "C1", "C3", "D1", "E3", "E1", "E3", "E1", "D3", "C1", "D3", "E1", "C4", "g4",
         "E3", "E1", "E3", "F1", "G3", "G1", "G3", "G1", "F3", "E1", "F3", "G1", "E6", "r1", "G1",
         "E6", "r1", "G1", "E6", "r1", "G1", "E3", "G1", "E3", "G1", "E6", "r2"]
         

# Speaker1 is connected to GPIO26 pin and speaker2 is connected to GPIO27
SPEAKER1 = 26
SPEAKER2 = 27

# Set duty 50%
DUTY = int(50 / 100 * 65535)

# Make speakder objects
speaker1 = PWM(Pin(SPEAKER1), duty_u16=DUTY)
speaker1.duty_u16(0)

speaker2 = PWM(Pin(SPEAKER2), duty_u16=DUTY)
speaker2.duty_u16(0)

# list for asyncio task
tasks = []

# Play scale
async def play(s, speaker):
    # Read from score
    for score in s:
        scale, note = score
        
        if DEBUG == True:
            print(scale, note)
            
        note_length = float(note) * 0.1 * 60 / TEMPO
        
        if scale != "r":
            speaker.duty_u16(DUTY)

            freq = SCALE[scale]
            #print(scale, str(freq))       
            speaker.freq(freq)
        
        await asyncio.sleep(note_length)
        
        # Need small silence to break up the notes
        speaker.duty_u16(0)
        await asyncio.sleep(0.01)	
    
    speaker.duty_u16(0)
    #speaker.deinit()


async def runplay():
    global tasks
    tasks.append(asyncio.wait_for(play(SCORE1, speaker1),TIMEOUT))
    tasks.append(asyncio.wait_for(play(SCORE2, speaker2),TIMEOUT))
    
    try:
        res = await asyncio.gather(*tasks)
        
    except asyncio.TimeoutError:
        print("Timeout, please increase TIMEOUT value.")
        
       
asyncio.run(runplay())
print("Done")
    

実行動画

ふたつのスピーカで輪唱している動画です。右側のスピーカからは1小節分遅れで音が出ています。(18秒、28.7MB)。

Discussion