Raspberry Pi OS Bullseyeで圧電ブザーでメロディを鳴らす

## 1. はじめに
Raspberry Pi OS BullseyeでWiring Piが非対応になりwiringpi.softToneWrite()も使えなくなったので代わりのものを作ってみました。

「[Lチカで始めるテスト自動化(18)秋月のIoT学習HATキットの圧電ブザーでテスト終了時にpass/failに応じてメロディを流す](https://qiita.com/pbjpkas/items/43a609581cc68b7ea2fd)」でRaspberry Pi OS Buster + Python + wiringpi.softToneWrite()でメロディを再生していたのをBullseyeに対応させます。

## 2. 設計＆実装
周波数f[Hz]と発音期間duration[sec]を引数で受け取ってGPIOの出力で信号生成を行うモデルを以下に示します。
![Technical Concept](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/9994e963-81b2-5813-aac2-88bec709b365.png)

これを実装したものを以下に示します。1周期の信号波形をGPIO.output()とsleep()で生成しforループで所定の回数繰返します。

```python:softToneWrite()
from time import sleep
import RPi.GPIO as GPIO
R   =   0 #rest

class BEEPSOUND:

    def softToneWrite(self, pin, frequency, duration):
        """! softToneWrite brief.
        generate beepsound using rpi.gpio instead of wiringpi.
        @param pin      : BCM pin number
        @param frequency: frequency[Hz]
        @param durarion : beepsound playback time[sec]
        """
        if frequency != 0:
            half_cycle = 1/(2*frequency)
            loop_num   = int(duration*frequency)
#            print(half_cycle, loop_num)
            for i in range(loop_num):
                GPIO.output(pin, True)
                sleep(half_cycle)
                GPIO.output(pin, False)
                sleep(half_cycle)
        else:
            sleep(duration)
```

## 3. 動作チェック
動作環境は次の通りです。

- Raspberry Pi Zero WH Rev 1.1
- Raspberry Pi OS Lite(32-bit)
  - A port of Debian Bullseye with no desktop environment
  - リリース日時：2022-09-22
- Python 3.9.2(Raspberry Pi OS Liteにプリインストールされているもの)

実際に鳴らしてみたのがこちらの動画です。1周期の時間が微妙に伸びているものの、それっぽく鳴っていることが分かります。

https://twitter.com/pbjpkas/status/1623686692724146177

## 4. プログラムの改修
1周期の時間が微妙に伸びているのが気になって調べたところforループで1周期の信号を生成するのに約0.29msの処理時間が加算されることが分かりました。
![V1-1000Hz-775Hz](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/c6810429-2264-b31a-31f6-a224764add77.png)
![動作特性_V1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/24892d7a-6f4c-e584-97b2-7439f6695e31.png)

もともとテスト終了時にメロディが鳴ればOKで音程の正確さはそれほど求めていないとはいえ1000Hzを鳴らすつもりで775Hzが出てくるのはそのまま放っておくのもなんだかなぁといったトコロです。そこで、Raspberry Pi Zero WHと秋月のIoT学習HATキットの組み合わせで動けばOKと割り切って、上乗せされる処理時間を差し引いてsleep()するように改修します。

```python:softToneWrite()・改修版
from time import sleep
import RPi.GPIO as GPIO
R   =   0 #rest
A0  =  28 #27.5Hz

# on Raspberry Pi Zero WH,
# softToneWrite() processing time to be added is 0.29ms
SOFT_TONE_WRITE_PROCESSING_TIME = 0.00029

class BEEPSOUND:

    def softToneWrite(self, pin, frequency, duration):
        """! softToneWrite brief.
        generate beepsound using rpi.gpio instead of wiringpi.
        @param pin      : BCM pin number
        @param frequency: frequency[Hz]
                          0 means rest.
                          invalid under A0(28Hz).
                          invalid over 1/SOFT_TONE_WRITE_PROCESSING_TIME.
        @param duration : beepsound playback time[sec]
        """
        if frequency == 0:
            sleep(duration)
        elif frequency < A0:
            print("invalid frequency.")
        elif frequency > 1/SOFT_TONE_WRITE_PROCESSING_TIME:
            print("invalid frequency.")
        else:
            half_cycle = 1/(2*frequency) - SOFT_TONE_WRITE_PROCESSING_TIME/2
            loop_num   = int(duration*frequency)
#            print(half_cycle, loop_num)
            for i in range(loop_num):
                GPIO.output(pin, True)
                sleep(half_cycle)
                GPIO.output(pin, False)
                sleep(half_cycle)
```

改修後はほぼほぼ引数で与えた周波数で再生されるようになりました。
![V2-1000Hz-1004Hz](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/5c08eb37-4b39-d62d-01d4-bc0ce86f0e9c.png)
![動作特性_V1V2比較](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/f8590886-bb13-9158-8c12-e6915c50f932.png)

音痴でなくなったように思います。

https://twitter.com/pbjpkas/status/1624426895843680256

## 5. おわりに
- Raspberr Pi OS Bullseyeで圧電ブザーでメロディを鳴らすことができました。
- どこまで作り込むかは目的とかけられるコスト次第と思いました。
  - テスト終了時にメロディを鳴らすぶんには改修前のプログラムでも十分役立ちます。
  - 音程の正確さが必要ならハードウェアで実現することも視野に入れつつ他の手段を検討すると思います。
- 上乗せされる処理時間を差し引く方法は機種依存性があり保守性の点でも悪手ですが、厳密さがそれほど求められずハードウェアも決め打ちならサクッと作って済ますのはありだと思います。

## 付録.ソースコード全文

```python:beepsound_bullseyeV2.py
# -*- coding: utf-8 -*-
# beepsound.py
# python 3.x
#
# 2023-02-09
#   modified to play without wiringpi for Raspberry Pi OS Bullseye.
#
# This software includes the work that is distributed
# in the Apache License 2.0
# ka's@pbjpkas 2021, 2023
#

from time import sleep
import RPi.GPIO as GPIO
#import wiringpi

# Akizuki IoT Learning HAT GPIO Number
BUZZER_PIN = 23 #PIN16, GPIO23

#Note, Frequency
R   =   0 #rest
A0  =  28 #27.5Hz
C3  = 131
CS3 = 139
D3  = 147
DS3 = 156
E3  = 165
F3  = 175
FS3 = 185
G3  = 196
GS3 = 208
A3  = 220
AS3 = 233
B3  = 247
C4  = 262
CS4 = 277
D4  = 294
DS4 = 311
E4  = 330
F4  = 349
FS4 = 370
G4  = 392
GS4 = 415
A4  = 440
AS4 = 466
B4  = 494
C5  = 523


# on Raspberry Pi Zero WH,
# softToneWrite() processing time to be added is 0.29ms
SOFT_TONE_WRITE_PROCESSING_TIME = 0.00029

class BEEPSOUND:
    def __init__(self, tempo=120.0):
        GPIO.setwarnings(False)
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(BUZZER_PIN, GPIO.OUT)
#        wiringpi.wiringPiSetupGpio()
#        wiringpi.softToneCreate(BUZZER_PIN)
        self.tempo = tempo

    def set_tempo(self, tempo):
        self.tempo = float(tempo)

    def softToneWrite(self, pin, frequency, duration):
        """! softToneWrite brief.
        generate beepsound using rpi.gpio instead of wiringpi.
        @param pin      : BCM pin number
        @param frequency: frequency[Hz]
                          0 means rest.
                          invalid under A0(28Hz).
                          invalid over 1/SOFT_TONE_WRITE_PROCESSING_TIME.
        @param duration : beepsound playback time[sec]
        """
        if frequency == 0:
            sleep(duration)
        elif frequency < A0:
            print("invalid frequency.")
        elif frequency > 1/SOFT_TONE_WRITE_PROCESSING_TIME:
            print("invalid frequency.")
        else:
            half_cycle = 1/(2*frequency) - SOFT_TONE_WRITE_PROCESSING_TIME/2
            loop_num   = int(duration*frequency)
#            print(half_cycle, loop_num)
            for i in range(loop_num):
                GPIO.output(pin, True)
                sleep(half_cycle)
                GPIO.output(pin, False)
                sleep(half_cycle)

    def generate(self, frequency, length):
        if float(length) != 0:
            duration = float( (4/float(length)) * (60.0/self.tempo) )
#            wiringpi.softToneWrite(BUZZER_PIN, int(frequency))
#            sleep(duration*0.95)
            self.softToneWrite(BUZZER_PIN, frequency, duration*0.95)
#            wiringpi.softToneWrite(BUZZER_PIN, 0)
            sleep(duration*0.05)

    # Für Elise - Ludwig van Beethoven
    # https://ja.wikipedia.org/wiki/エリーゼのために
    def FurElise(self):
        self.generate(E4, 16)
        self.generate(DS4,16)
        
        self.generate(E4, 16)
        self.generate(DS4,16)
        self.generate(E4, 16)
        self.generate(B3, 16)
        self.generate(D4, 16)
        self.generate(C4, 16)
        
        self.generate(A3,  8)
        self.generate(R,  16)
        self.generate(C3, 24)
        self.generate(E3, 24)
        self.generate(A3, 24)
        
        self.generate(B3,  8)
        self.generate(R,  16)
        self.generate(E3, 24)
        self.generate(GS3,24)
        self.generate(B3, 24)
        
        self.generate(C4,  8)
        self.generate(R,  16)

    # Menuet - Christian Petzold
    # https://ja.wikipedia.org/wiki/メヌエット (ペツォールト)
    def Menuet(self):
        self.generate(D4,  4)
        self.generate(G3,  8)
        self.generate(A3,  8)
        self.generate(B3,  8)
        self.generate(C4,  8)
        self.generate(D4,  4)
        self.generate(G3,  4)
        self.generate(G3,  4)
        
        self.generate(E4,  4)
        self.generate(C4,  8)
        self.generate(D4,  8)
        self.generate(E4,  8)
        self.generate(FS4, 8)
        self.generate(G4,  4)
        self.generate(G3,  4)
        self.generate(G3,  4)

    # Toccata und Fuge in d-Moll BWV565 - Johann Sebastian Bach
    # https://www.gmajormusictheory.org/Freebies/Level2/2ToccataFugueDm/2ToccataFugueDm.pdf
    def ToccataUndFugeInDMoll(self):
        self.generate(A4, 16)
        self.generate(G4, 16)
        #self.generate(A4,  8)
        self.generate(A4,  2)
        self.generate(G4, 16)
        self.generate(F4, 16)
        self.generate(E4, 16)
        self.generate(D4, 16)
        self.generate(CS4, 4)
        self.generate(D4,  2)
        self.generate(R ,  4)


if __name__ == '__main__':
    beepsound = BEEPSOUND()
#    print("1000Hz")
#    beepsound.softToneWrite(BUZZER_PIN, 1000, 5.0)
#    exit()
    beepsound.set_tempo(60)
    beepsound.FurElise()

    beepsound.set_tempo(120)
    beepsound.Menuet()
    beepsound.ToccataUndFugeInDMoll()
```