明和電機のSUSHI BEATっぽい電子楽器をpythonで作ってみる

# できたもの
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">[記事用動画]<br>明和電機のSUSHI BEATっぽい電子楽器をpythonで作ってみる。<br>キーボードを押すと、予め入力したリズムが再生されます。<br>MuseScoreでmidiファイルを生成しました。 <a href="https://t.co/YvMgbGZWhI">pic.twitter.com/YvMgbGZWhI</a></p>&mdash; zakuzaku-3 (@zakuzaku_3) <a href="https://twitter.com/zakuzaku_3/status/1675511196592795648?ref_src=twsrc%5Etfw">July 2, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# はじめに
みなさんは[SUSHI BEAT](https://www.maywadenki.com/blog/2020/07/03/sushibeat/)をご存知でしょうか？
オタマトーンで有名な明和電機が2020年に発売した寿司型電子楽器です。
１つ１つの寿司に異なる音楽が入っていて、押すとリピート再生されます。
４つの寿司は音程とテンポが合っているので、複数同時に押すと、アンサンブルできます。
なお、動画後ろのロボットが欲しいのですが、どうやら非売品のようです。
<iframe width="560" height="315" src="https://www.youtube.com/embed/z0im2y804Hw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

今回はMIDIとpythonを使って、キーボードでできるSUSHI BEATっぽい電子楽器を作っていきます。
# 実行環境
OS:Ubuntu 22.04 LTS
Python: 3.10.6
# 下準備(作曲)
そのままSUSHI BEATの音を鳴らすわけにはいかないので、自分で音楽を作るところから初めます。
pythonで楽器を鳴らす方法はいくらでもありますが、今回は自分で楽譜を打ち込む必要があったので、MIDIを使用することにしました。
まずは、[MuseScore](https://musescore.org/ja)をインストールします。
それっぽいメロディを打ち込んで行きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287677/6995ca42-7d0e-9381-ae16-44248d696d7f.png)
打ち込み終わったら、楽器毎別々のMIDIファイルに分けて保存します。
以下の４つのファイルを生成しました。
```
* key_beat-Bass_Synthesizer.mid
* key_beat-Drumset.mid
* key_beat-Effect_Synthesizer.mid
* key_beat-Brightness_Synthesizer.mid
```
これをpythonで読み込んで、再生させます。
以下のリンクにMIDIファイルを格納したので、お試ししたい方はダウンロードしてください。
[Google Drive](https://drive.google.com/drive/folders/1SvLIOnj5UJ2PPbDLvqH_NBadSDlCno-y?usp=drive_link)

# コード
外部ライブラリとして、
画面描画・キー入力：pygame
MIDI再生：mido
を使用しています。
```python:main.py
from pygame.locals import *
import pygame
import mido
import threading

#pygameの処理
pygame.init()
screen = pygame.display.set_mode((420, 200))
pygame.display.set_caption("key beat")
font = pygame.font.Font(None, 72)
key_pos = {#各キーのテキストの表示位置
    "a": (50,100),
    "s": (150,100),
    "d": (250,100),
    "f": (350,100),
}

#midoの処理
ports = mido.get_output_names()
port = mido.open_output(ports[2])#数字は環境にあわせる
key_dict = ["a", "s", "d", "f"]# 使用するキー
midi_dict = {# 読み込むMIDIファイルを表記
    "a": mido.MidiFile('key_beat-Drumset.mid'),
    "s": mido.MidiFile('key_beat-Brightness_Synthesizer.mid'),
    "d": mido.MidiFile('key_beat-Effect_Synthesizer.mid'),
    "f": mido.MidiFile('key_beat-Bass_Synthesizer.mid')
}
channel_dict = {# MIDIが使用するチャンネル
    "a": 9,
    "s": 11,
    "d": 2,
    "f": 1
}
key_state = {#各キーのステータス。True:押されている、False:離れている
    "a": False,
    "s": False,
    "d": False,
    "f": False,
}
#MIDIファイルの再生。キーを押したときに呼ぶ関数。非同期にするため、threadingを使用
def play(key):
    mid = midi_dict[key]
    while True:#押されている間は繰り返し
        for msg in mid.play():
            if not key_state[key]:
                return
            print(msg)
            port.send(msg)
thread_dict = {
    "a": threading.Thread(target=play, args="a"),
    "s": threading.Thread(target=play, args="s"),
    "d": threading.Thread(target=play, args="d"),
    "f": threading.Thread(target=play, args="f"),
}

#MIDIファイルの停止時の処理
def stop(channel):
    stop_msg = mido.Message('control_change', control=7, value=0, channel=channel)# チャンネルボリュームを0にする。
    port.send(stop_msg)

#画面描画
def screet_text():
    while True:
        screen.fill((0, 0, 0)) #背景色は黒
        for key in key_dict:
            if key_state[key]:
                if screet_text.light:# 赤LEDを表現するために、チカチカさせている。
                    color = (255, 0, 0)
                else:
                    color = (255, 255, 255)
            else:
                color = (100, 100, 100)
            screen.blit(font.render(key.upper(), True, color), key_pos[key])
        screet_text.light = not screet_text.light
        pygame.display.update()
screet_text.light = True
screen_thread = threading.Thread(target=screet_text)
screen_thread.start()

if __name__ == "__main__":
    while True:
        for event in pygame.event.get():
            if not(event.type == KEYDOWN or event.type == KEYUP):#キー入力以外は無視
                continue
            if event.key == K_ESCAPE:# ESCキーでプログラム終了
                    pygame.quit()
                    exit()
            key = pygame.key.name(event.key)
            if not pygame.key.name(event.key) in key_dict:# 使用していないキーは無視
                continue
            if event.type == KEYDOWN:# キーを押すと、再生させるスレッドを起動する。
                key_state[key] = True
                thread_dict[key].start()
            if event.type == KEYUP:# キーを離すと、再生させるスレッドを停止する。
                key_state[key] = False
                stop(channel_dict[key])
                thread_dict[key].join()
                thread_dict[key] = threading.Thread(target=play, args=key)# 新しいスレッドを作らないと、RuntimeError: threads can only be started onceが発生する。
```

# できたもの
（再掲）
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">[記事用動画]<br>明和電機のSUSHI BEATっぽい電子楽器をpythonで作ってみる。<br>キーボードを押すと、予め入力したリズムが再生されます。<br>MuseScoreでmidiファイルを生成しました。 <a href="https://t.co/YvMgbGZWhI">pic.twitter.com/YvMgbGZWhI</a></p>&mdash; zakuzaku-3 (@zakuzaku_3) <a href="https://twitter.com/zakuzaku_3/status/1675511196592795648?ref_src=twsrc%5Etfw">July 2, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# まとめ
明和電機のSUSHI BEATっぽい電子楽器をpythonで作ってみました。
コードを書くより、MIDIを打ち込む方が時間かかりました。
より、楽曲を磨いていけば、SUSHI BEATみたいにノリノリの音楽が奏でられると思います。

# 参考サイト
大変感謝しております。
* [4つ打ちリズムとは？【DTMを使った作り方も解説】](https://trivisionstudio.com/4-beat/)
* [SUSHI BEATとはなにか？](https://www.maywadenki.com/blog/2020/07/03/sushibeat/)

# 追記
MIDIだと音源の入れ替えが難しいかと思い、mp3バージョンも作成しました。
こちらの方が再生遅延が少ないです。
```python:mp3.py
from pygame.locals import *
import pygame
import threading
import vlc
import time

#pygameの処理
pygame.init()
screen = pygame.display.set_mode((420, 200))
pygame.display.set_caption("key beat")
font = pygame.font.Font(None, 72)
key_pos = {#各キーのテキストの表示位置
    "a": (50,100),
    "s": (150,100),
    "d": (250,100),
    "f": (350,100),
}

key_dict = ["a", "s", "d", "f"]# 使用するキー

mp3_dict = {# 読み込むMIDIファイルを表記
    "a": vlc.MediaPlayer(),
    "s": vlc.MediaPlayer(),
    "d": vlc.MediaPlayer(),
    "f": vlc.MediaPlayer()
}
mp3_dict["a"].set_mrl("key_beat-Drumset.mp3")
mp3_dict["s"].set_mrl("key_beat-Brightness_Synthesizer.mp3")
mp3_dict["d"].set_mrl("key_beat-Effect_Synthesizer.mp3")
mp3_dict["f"].set_mrl("key_beat-Bass_Synthesizer.mp3")

channel_dict = {# MIDIが使用するチャンネル
    "a": 9,
    "s": 11,
    "d": 2,
    "f": 1
}
key_state = {#各キーのステータス。True:押されている、False:離れている
    "a": False,
    "s": False,
    "d": False,
    "f": False,
}
#MIDIファイルの再生。キーを押したときに呼ぶ関数。非同期にするため、threadingを使用
def play(key):
    mp3 = mp3_dict[key]
    mp3.play()
    while True:
        if not key_state[key]:
            mp3.stop()
            return
        time.sleep(0.1)
thread_dict = {
    "a": threading.Thread(target=play, args="a"),
    "s": threading.Thread(target=play, args="s"),
    "d": threading.Thread(target=play, args="d"),
    "f": threading.Thread(target=play, args="f"),
}

#画面描画
def screet_text():
    while True:
        screen.fill((0, 0, 0)) #背景色は黒
        for key in key_dict:
            if key_state[key]:
                if screet_text.light:# 赤LEDを表現するために、チカチカさせている。
                    color = (255, 0, 0)
                else:
                    color = (255, 255, 255)
            else:
                color = (100, 100, 100)
            screen.blit(font.render(key.upper(), True, color), key_pos[key])
        screet_text.light = not screet_text.light
        pygame.display.update()
screet_text.light = True
screen_thread = threading.Thread(target=screet_text)
screen_thread.start()

if __name__ == "__main__":
    while True:
        for event in pygame.event.get():
            if not(event.type == KEYDOWN or event.type == KEYUP):#キー入力以外は無視
                continue
            if event.key == K_ESCAPE:# ESCキーでプログラム終了
                    pygame.quit()
                    exit()
            key = pygame.key.name(event.key)
            if not pygame.key.name(event.key) in key_dict:# 使用していないキーは無視
                continue
            if event.type == KEYDOWN:# キーを押すと、再生させるスレッドを起動する。
                key_state[key] = True
                thread_dict[key].start()
            if event.type == KEYUP:# キーを離すと、再生させるスレッドを停止する。
                key_state[key] = False
                thread_dict[key].join()
                thread_dict[key] = threading.Thread(target=play, args=key)# 新しいスレッドを作らないと、RuntimeError: threads can only be started onceが発生する。
```