Pythonで演奏してみた【Python】

2020.04.07

2021.08.31

Fabeee社員ブログ

# はじめに

こんにちわ。すぎちゃんです。
世間は今、外出自粛モード真っただ中で、弊社もリモート推奨となっている今日この頃ですが、みなさまいかがお過ごしでしょうか?

「Pythonで卵焼き作ってみた」という若干タイトル詐欺のような記事を書いてから、2年弱の時が過ぎていることに驚きを隠せません。

先日、YouTubeで、ビリーアイリッシュの「Bud Guy」をいろんなものや自分の身体を使って演奏するという動画を見ました。
そして思ったのです。

「Pythonを使って音楽を演奏できないか?」

本日はそんな内容でお送りいたします!(過度な期待は禁物)

# プランニング

さて、Pythonを利用してどうのように演奏を試みましょうかといったところですが

音楽 = 波形(合成波)

とすると、sinやらcosを駆使して好みの波形を作り、それを配列に変換(16bit化)してwavファイルに出力というアプローチが良さそうな気がします。
ただし、楽器の特徴まで再現しようと思うと、とてもではありませんがブログではなく本が書けてしまいそうなので、今回は

「とりあえずメロディーを奏でる」

をゴールにしてみたいと思います。(もう最初よりハードル下がっている…)

# 準備

知っている方もいらっしゃるかもしれませんが、音階というものはヘルツという単位であらわされます。
ヘルツとは周波数とも言い、大雑把に言うと1秒あたりに何回振動するかというものです。

ラ = 440Hz
import numpy as np
import matplotlib.pyplot as plt
sec = 0.01
SAMPLE_RATE = 44100
# 経過時間tに対応するyがnparray形式で取得できる
t = np.arange(0, SAMPLE_RATE * sec)
y = np.sin(2 * np.pi * 440 * t / SAMPLE_RATE)
# グラフにしてみる
plt.plot(t, y)
plt.show()

がおそらく最も有名で、1秒間に440回の振動がラの音に聞こえます。
調べると出てきますが、ド ~ シまでの周波数は以下の通りです。

TONES = {
    'ド': 261.626,
    'レ': 293.665,
    'ミ': 329.628,
    'ファ': 349.228,
    'ソ': 391.995,
    'ラ': 440.000,
    'シ': 493.883
}

これら任意の周波数のsin波をpythonを使って生成し、配列化した後にwavファイルなどに保存出来たら行けそうです!

# 完成

いったい何をすっ飛ばしたら完成なのでしょう。
とりあえず今回作成したコードはこちらです。

import wave
import struct
### 各種定数
VOLUME = 1
SAMPLE_RATE = 44100
TONES = {
    'ド': 261.626,
    'レ': 293.665,
    'ミ': 329.628,
    'ファ': 349.228,
    'ソ': 391.995,
    'ラ': 440.000,
    'シ': 493.883
}
### レコーダークラス
class Recorder:
    melody = []
    def __init__(self, melody):
        # 楽譜格納
        self.melody = melody
    def generate_tone(self, tones, beat, bpm=120):
        """周波数の配列を生成して返却
        Args:
            tones (list): 生成する音の周波数
            beat (float): 再生時間
        Returns:
            list: 生成された波形配列
        """
        sec = bpm / 60 * beat
        t = np.arange(0, SAMPLE_RATE * sec)
        y = None
        for tone in tones:
            # 和音対応(各音の配列を足し合わせると和音になる)
            f = TONES[tone] if tone in TONES else 0
            if y is None:
                y = VOLUME * np.sin(2 * np.pi * f * t / SAMPLE_RATE)
            else:
                y += VOLUME * np.sin(2 * np.pi * f * t / SAMPLE_RATE).tolist()
        return y.tolist()
    def save_as_wave(self, y, filename):
        """waveファイル出力
        Args:
            y (ndarray): wavファイルに出力する波形配列
            filename (str): 出力ファイル名
        Returns:
            None
        """
        max_num = 32767.0 / max(y)
        bit = [int(x * max_num) for x in y]
        waves = struct.pack("h" * len(bit), *bit)
        w = wave.Wave_write(filename)
        w.setparams((1, 2, SAMPLE_RATE, len(waves), 'NONE', 'not compressed'))
        w.writeframes(waves)
        w.close()
    def save(self, filename):
        """セットした楽譜から配列を作成し音声ファイルを出力
        Args:
            filename (str): 出力ファイル名
        """
        song = []
        for note in self.melody:
            song += self.generate_tone(*note)
        self.save_as_wave(np.array(song), filename)
rec = Recorder([
    [['ド'], 1/4], # [ [音階,], [音の長さ] ]
    [['レ'], 1/4],
    [['ミ'], 1/4],
    [['ファ'], 1/4],
    [['ミ'], 1/4],
    [['レ'], 1/4],
    [['ド'], 2/4],
    [['ミ'], 1/4],
    [['ファ'], 1/4],
    [['ソ'], 1/4],
    [['ラ'], 1/4],
    [['ソ'], 1/4],
    [['ファ'], 1/4],
    [['ミ'], 2/4],
    [['ド'], 2/4],
    [['ド'], 2/4],
    [['ド'], 2/4],
    [['ド'], 2/4],
    [['ド'], 1/8],
    [['ド'], 1/8],
    [['レ'], 1/8],
    [['レ'], 1/8],
    [['ミ'], 1/8],
    [['ミ'], 1/8],
    [['ファ'], 1/8],
    [['ファ'], 1/8],
    [['ミ'], 1/4],
    [['レ'], 1/4],
    [['ド'], 2/4]
])
rec.save('flog_song.wav')

生成した音声ファイルがこちら

そうです。「カエルの歌」です。
さすがに「Bud Guy」とまではいきませんでした。期待してくれていた方、すいません。
ちなみに

rec = Recorder([
    [['ド'], 1/4],
    [['レ'], 1/4],
    [['ミ'], 1/4],
    [['ファ'], 1/4],
    [['ミ'], 1/4],
    [['レ'], 1/4],
    [['ド'], 2/4],
    [['ミ', 'ド'], 1/4],
    [['ファ', 'レ'], 1/4],
    [['ソ', 'ミ'], 1/4],
    [['ラ', 'ファ'], 1/4],
    [['ソ', 'ミ'], 1/4],
    [['ファ', 'レ'], 1/4],
    [['ミ', 'ド'], 2/4],
    [['ド'], 2/4],
    [['ド'], 2/4],
    [['ド'], 2/4],
    [['ド'], 2/4],
    [['ド', 'ミ'], 1/8],
    [['ド', 'ミ'], 1/8],
    [['レ', 'ファ'], 1/8],
    [['レ', 'ファ'], 1/8],
    [['ミ', 'ソ'], 1/8],
    [['ミ', 'ソ'], 1/8],
    [['ファ', 'ラ'], 1/8],
    [['ファ', 'ラ'], 1/8],
    [['ミ', 'ソ'], 1/4],
    [['レ', 'ファ'], 1/4],
    [['ド', 'ミ'], 2/4]
])
rec.save('flog_song2.wav')

のようにすると、ちょっとハモります笑

輪唱をさせたい場合は、各音ごとの長さをそれぞれ調整する必要があるので少し工夫が必要です。
ご興味ある方はぜひチャレンジしてみてください!

# おわりに

なぜか弊社で流行りそうな雰囲気ある「Pythonで○○やってみた」(知らんうちにシリーズ化されている)はいかがでしたか?

Pythonは非常に汎用性が高く、やりたいなと思ったことはアイディア次第で実現できてしまう便利な言語です。
もちろんwebアプリケーションやAIとも相性が良いので、弊社の受託開発ではメイン言語としてPythonを扱っております。

プログラミング初学者の方にもおすすめの言語です!

Python宣伝ブログみたいになってしまいましたが、
また次回、「Pythonで運動能力向上してみた」でお会いしましょう!
最後までお読みいただき、ありがとうございました!!

AIコンサル/SES/受託開発のご依頼についてはこちら

Fabeee編集部

Fabeee編集部

こちらの記事はFabeee編集部が執筆しております。