はじめに

こんにちは。エンジニアの荒井です。
今回はPythonで音を鳴らすコードを書いていこうと思います。

開発環境

ホスト環境

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

docker環境

Python 3.9.16

docker設定

冒頭から少し話が逸れますが、普段ちょっと何か書きたい時などは即席で最小限のPython実行環境を立てて使っています。
ローカルを汚したくないのと、何かに詰まった時に一旦全部リセットするのが簡単だからです。

ディレクトリ構成

quick-dev-environment% tree
.
├── docker-compose.yml
└── python-container
    ├── Dockerfile
    └── test.py

Dockerfileは最小限です。
起動時にExitしないために最後のコマンドを追加しています。

FROM python:3.9
RUN mkdir /app

# 適宜追加
# RUN pip install 

WORKDIR /app

# コンテナが終了しないようにする
CMD tail -f /dev/null

docker-compose.ymlも非常にシンプルです。
逆になぜ用意しているのかというと、

  • docker-compose upを使いたい
  • 複数コンテナ立てている時に起動の管理を一元化したい

という理由です。

version: '3'
services:
  python-container:
    build:
      context: .
      dockerfile: python-container/Dockerfile
    container_name: python-container
    volumes:
      - ./python-container:/app

Githubリポジトリ

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

実践

※音のデジタル表現に関する数学的・物理的な理論は奥が深すぎるため本記事では詳しく解説しません(できません)ので、必要に応じて検索しつつ読み進めていただけますと幸いです。

wavファイルを作る

まず、今回音の出力はwavファイルへ書き出すことにします。
(元の構想ではプログラムから出力デバイスに向けて出力させるつもりだったのに、M1Mac上のdockerからうまく接続ができなかったのでいつかリベンジしたい。。。)

コード

import wave

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

filename = "v0_none.wav"
audio_data = b""

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

解説

インポートするライブラリは1つです。

  • wave
    => waveファイルを扱うために使います。

変数については以下の通りです。

  • SAMPLE_RATE: サンプルレート(サンプル数/秒)
    => 1秒を表現するために何回振幅を測定するかの値です。
    値が大きくなるほど音の解像度が高くなります。
    今回は一般的な44,100 Hzを使います。
  • SAMPLE_WIDTH: サンプルのビット数(バイト数)
    • 1=8ビット, 2=16ビット, 4=32ビット ...
      音の解像度を決める数です。
      => 試しに出力したところ8ビットだと耳が辛く、16ビットなら十分そうだったので今回は2にします。
  • NUM_CHANNELS: チャンネル数
    • 1=モノラル, 2=ステレオ, 3以上=マルチチャンネル
      同じ瞬間に何音鳴らせるか を決める数です。
      => 2以上の場合各チャンネル分のデータを用意しないといけないので、今回は1にします。
  • filename: 出力するwavファイルの名前
  • audio_data: 出力するwavファイルのデータ(バイナリ)

wave.openでファイルの枠を作成後、waveライブラリのメソッドを使って上記の値たちをセットしました。
これで実行すると、空のwavファイルが出力されます。

出力ファイル

以下のリンク先ページにあるView rawからダウンロードできます。
https://github.com/milldea/blog-python-sound-generator/blob/c28e287da1ffb9aebcb12a129c620d9df9ebb18a/v0_none.wav
空ファイルなので、再生はできません。

wavファイルに入れるデータを作る

いよいよ音を作成します。
まずはA4(ド)を一音だけ作成してみます。

コード

import struct
import wave
import math

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

# A4の周波数(Hz)
A4_FREQ = 440
# 音の長さ(秒)
duration = 1.0
# 振幅
amplitude = 0.5

filename = "v1_a4.wav"
audio_data = b""

frames = int(duration * SAMPLE_RATE)
for frame in range(frames):
    time = float(frame) / SAMPLE_RATE
    value = amplitude * math.sin(2.0 * math.pi * A4_FREQ * time)
    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)

解説

まず、インポートするライブラリが増えました。

  • math
    => 音は波なので、正弦波を生成するためにmath.sinメソッドを使います。
  • struct
    => 生成した正弦波の数値をバイナリに変換するために使います。

増えた変数は以下の通り。

  • A4_FREQ: A4(ド)の音の周波数
    => 現代において一般的に使われている周波数の定義から持ってきました。
  • duration: 1音の長さ(秒)
  • amplitude: 振幅
    => 正確な表現ではないですが、ざっくり音の大きさと思っていただいて大丈夫です。
  • frames: 1音の総フレーム数

データの生成箇所では、A4の周波数で計算した総フレーム数分の正弦波を生成し、バイナリにしてaudio_dataに追加していきます。

出力ファイル

以下のリンク先ページにあるView rawからダウンロードできます。
https://github.com/milldea/blog-python-sound-generator/blob/c28e287da1ffb9aebcb12a129c620d9df9ebb18a/v1_a4.wav
1秒間A4の音が鳴るようになりました。

続きます

まずは狙った音が出力されるのを確認したところで小休憩です。
コードは非常に単純ですが、どうしても理論部分が入ってきて小難しい話に見えますね。

後半ではド~シの音と音の長さを指定してメロディーを出力できるようにコードを改修していきます。