はじめに

こんにちは。エンジニアの荒井です。
今回はPythonで音を鳴らすコードを書く後半です。
前半の記事はこちら
後半では、ド~シの音と音の長さを指定してメロディーを出力できるようにコードを改修していきます。
最後にちょっとだけおまけも。

開発環境

ホスト環境

macOS 12.5.1
Docker version 20.10.17, build 100c701
Docker Compose version v2.10.2

docker環境

Python 3.9.16

Githubリポジトリ

この記事で解説するコードおよび出力ファイルは以下のリポジトリに格納しております。
https://github.com/milldea/blog-python-sound-generator/tree/0f0109

実践

音階を用意する

次のようにdict型で音階と周波数の対応を定義します。
これで、音名でメロディーを作成する準備ができます。

コード

# 音階と周波数の対応
NOTE_FREQS = {
    "C": 130.81,
    "C#": 138.59,
    "D": 146.83,
    "D#": 155.56,
    "E": 164.81,
    "F": 174.61,
    "F#": 185.00,
    "G": 196.00,
    "G#": 207.65,
    "A": 220.00,
    "A#": 233.08,
    "B": 246.94,
}

メロディーを音源化する

用意した音階を使用したメロディーの定義配列を追加して、for文で音を繋げていきます。

コード

import struct
import wave
import math

# 出力するwavファイルの設定値
# サンプルレート(Hz)
SAMPLE_RATE = 44100
# サンプル幅(ビット)
SAMPLE_WIDTH = 2
# チャンネル数
NUM_CHANNELS = 1

# 音階と周波数の対応
NOTE_FREQS = {
    "C": 130.81,
    "C#": 138.59,
    "D": 146.83,
    "D#": 155.56,
    "E": 164.81,
    "F": 174.61,
    "F#": 185.00,
    "G": 196.00,
    "G#": 207.65,
    "A": 220.00,
    "A#": 233.08,
    "B": 246.94,
}
# 音の長さ(秒)
duration = 1.0
# 振幅
amplitude = 0.5

# 演奏する音階の指定
notes = ["C", "C", "G", "G", "A", "A", "G", "F", "F", "E", "E", "D", "D", "C"]
filename = "v2_melody.wav"
audio_data = b""

for note in notes:
    freq = NOTE_FREQS[note] * 4
    num_frames = int(duration * SAMPLE_RATE)
    for frame in range(num_frames):
        t = float(frame) / SAMPLE_RATE
        value = amplitude * math.sin(2.0 * math.pi * freq * t)
        packed_value = struct.pack("<h", int(value * 32767.0))
        audio_data += packed_value

with wave.open(filename, "wb") as f:
    f.setnchannels(NUM_CHANNELS)
    f.setsampwidth(SAMPLE_WIDTH)
    f.setframerate(SAMPLE_RATE)
    f.writeframes(audio_data)

解説

notes に定義した音階の配列で、ドドソソララソファファミミレレドと指定してみました。
(シャープの周波数も定義したのに使わなかった。。)
あとは notes をfor文で回しながら、各音の周波数で計算した総フレーム数分の正弦波を生成し、バイナリにしてaudio_dataに繋げていきます。
(NOTE_FREQS[note] * 4とさりげなく各音の周波数を4倍していますが、実際作って聴いてみたら音が低く重い印象だったので、これでオクターブ上の音に変更しています。)

出力ファイル

以下のリンク先ページにあるView rawからダウンロードできます。
※注意 音が大きい, 割れている可能性があるので音量小さめでの再生を推奨します
https://github.com/milldea/blog-python-sound-generator/blob/main/v2_melody.wav

(おまけ)キラキラさせてみた

ちょっと味気ないので、音楽っぽさを出せないか考えてみました。
DTM界隈ではエンベロープ(封筒じゃないよ)という概念があります。
単一の音に対して時間の経過とともに変化を持たせることで色をつけるようなものです。
(詳しくは エンベロープ 音楽 で検索してください。。)

コード

# 音の揺らぎ感を追加
envelope = amplitude * math.sin(10.0 * math.pi * t)
value *= envelope

出力ファイル

以下のリンク先ページにあるView rawからダウンロードできます。
※注意 音が大きい, 割れている可能性があるので音量小さめでの再生を推奨します
https://github.com/milldea/blog-python-sound-generator/blob/main/v3_envelope.wav

解説

上記のコードをvalue =...とpacked_value =...の間に入れます。
これにより、元の正弦波に対して、時間tに応じて取る値の変わるenvelopeが追加されます。
難しい話はしない(分からない)ので簡単に言うと、valueは1フレームごとの音の粒です。
そこに1フレームごとに少しずつ違う値をかけることによって、直線的な音の波を細かくぐにゃぐにゃにしているようなイメージです。
少しだけ音楽っぽくなった気がするのでよしとします。

おわりに

これで簡単に自分の考えたメロディーのwavファイルが作れるようになりましたね。
で、これが一体何に使えるんだ?と言うと難しいんですが、個人的には

  • 自作のアプリで使う
  • 非音声データを何らかのロジックで数値化して音源化する

あたりがパッと思いつきました。
実際に私は自作アプリの検知音にしてみたところ、結構違和感なく使えました。(このアプリについてはまた後日記事にする予定です)
あとは非音声データを音源化するのはスパイ映画みたいでちょっとワクワクしますよね。もう世の中にはたくさんそれができるコードがあると思いますが、自分で作るところにワクワクがありそうです。(そういうの車輪の再開発って言うんだよ って言わないで。。)

長らく間が開いてしまいましたが、Pythonでメロディーを奏でてみる は完結です。
ありがとうございました。