プログラミング習得への道のり #4 ドを鳴らす ~PCキーボードで演奏したい~

プログラミング完全初心者の私が、プログラミングを習得するまでの全過程(主に悩み…)を記事にしていきます。

プログラミング学習にあたり、以下の順で進めています。

①アウトプットを決める、②基礎を学習する、③アウトプットを作り始める

アウトプットは PCキーボードで演奏できるアプリケーション に決めました。

前回の記事で、”②基礎を学習する”を紹介しました。今回から”③アウトプットを作り始める”をやっていこうかと思います。

本記事では、 ステップ1:キーボード入力で音を鳴らす のうち、”1つの音(ド)を鳴らす”にチャレンジしました。

PCキーボード演奏アプリの作成ステップ

  1. キーボード入力で音を鳴らす
    1−1.1つの音を鳴らす
    1-2.音階を鳴らす
    1-3.音の長さを自由に変える
    1-4.テンポを決める
  2. 音を制御する
    2-1.波形を変える
    2-2.フィルタリング機能を追加する
    2-3.複数の音を同時に鳴らす
  3. 外部から音を取り込む
    3-1.単音の音源サンプルから音を取り込む(1楽器)
    3-2.音源から楽器を取り出す
  4. 機能を追加する
    4-1.録音&再生する(パターンをつくる)
    4-2.外部と連携する
  5. 曲を演奏する
  6. 曲を作る

☑︎本記事の内容

  • 必要な材料:Pythonで音を鳴らす
  • レシピ①:Scipyとpygameで鳴らす
  • レシピ②:PyAudioで鳴らす
  • まとめと次回予告

☑︎著者の経験

この記事を書いている私は、非情報系の大学院を卒業後、通信関係の企業に3年勤めた後、現在までコンサルティング会社に勤めています。
学生時代〜現職までプログラミングとは縁がなく、前職でルーターの設定を少しかじった程度のプログラミング完全初心者です。
こういった私が、解説していきます。

Twitterもはじめました。

必要な材料:Pythonで音を鳴らす

○ライブラリ

前回の記事で”Pythonの基礎を理解したあとは、作りたいものに応じてライブラリを学んでいく必要があります。“と紹介しました。

“Pythonで音を鳴らす”にあたり、参考になる記事から必要なライブラリを洗い出しました。

音を鳴らすために必要なライブラリ

  • numpy:数値演算用ライブラリ → sin波を作る
  • scipy:科学技術演算用ライブラリ → 波形データをwaveファイルに変換する
  • pygame:ゲーム関連ライブラリ → 音を鳴らす 
  • PyAudio:オーディオ関連ライブラリ → 音を鳴らす
  • pydub:オーディオ関連ライブラリ? → 音を鳴らす

そもそも、”音”とは振動です。様々な波形で振動したものを人間の耳で音として認識しています。

 つまり、全ての音は波形で表すことができます。 

例)様々な波長の波(上:ド/C4 下:ソ/G4)

Pythonで音を鳴らすためには、以下の処理が必要になります。

①波形データをつくる
②PC(あるいはその他ハードウェア)のオーディオに波形データを渡す
③音を鳴らす

それらを実行するためのライブラリが上記になります。なお、音を鳴らすために使えるライブラリは複数種類あるため、用途に応じて選択が必要みたいですが、今のところ違いはわかりません。

ライブラリはお使いの環境に応じて、condaあるいはpipを用いてインストールします。

例)Macのターミナルにてインストール(利用環境:Anaconda)

$ conda install numpy
$ conda install scipy
$ conda install pyaudio

○変数

波形データをつくるために、波について復習します。

一般的にsin波は以下の式で表すことができます。
\begin{eqnarray}
\left\{
\begin{array}{l}
\sin (t) = A\sin (2{\pi}f_{ 0 }t) \\
t=n/fs
\end{array}
\right.
\end{eqnarray}

変数

  • $A$:振幅
  • $f_{ 0 }$[1/s]:生成するサイン波の周波数
  • $t$[s]:再生時間
  • $n$:サンプル数 → 1回の取得でいくつのデータを取得したか
  • $f_{ s }$[1/s]:サンプリング周波数 → 1秒間に何回データを取得しているか

ここで、ドを鳴らすためには、変数は以下になります。

$f_{ 0 } = 261.626$

$f_{ s } = 44100$

また、振幅$A$はとりあえず1にしました。

レシピ①:Scipyとpygameで鳴らす

まずは、参考記事Pythonと音楽と…(1)音を鳴らすを真似てコードを書いてみました。

①numpyでsin波をつくり、②scipyでwaveファイルに変換し、③pygameで再生するという流れです。

ここで、sin波はnumpy.sin()で指定します。
また、サンプル数nは間隔(公差)を指定するnumpy.arange()で指定します。

○結果

以下のコードでPCからドを流すことができました。

# ライブラリのインポート
import numpy as np              # sin波作成用
from scipy.io import wavfile    # waveファイル変換
from pygame import mixer        # 音楽再生
import time

# パラメータ
f0_do = 261.626       # 生成するサイン波の周波数f0(note#60 C4 ド)
fs = 44100     # サンプリング周波数fs

# sin波の式
# sin_wave = A * np.sin(2*np.pi*f0*t)
# t = n / fs
#   = np.arange(start:0, stop:duration * fs) / fs
# duration = 1
t = np.arange(0, fs) / fs
sin_wave_do = np.sin(2 * np.pi * f0_do * t)

# 16bitのwaveファイルを作成
#サイン波を-32768から32767の#16bit符号付き整数に変換(signed 16bit pcmへ)
wavfile.write("do.wav", fs,(sin_wave_do * 32767.0).astype(np.int16))

# wavファイルをロードして再生
mixer.init()  # mixerを初期化
mixer.music.load("do.wav")  # wavをロード
mixer.music.play(1)  # wavを1回再生

# 1秒(音がおわるまで)待つ
time.sleep(1)

※コード上に”#”を打つことでコメントとなります。

do_wavファイルが作成されている

補足ですが波の式は以下の部分です。

# sin波の式
t = np.arange(0, fs) / fs
sin_wave_do = np.sin(2 * np.pi * f0_do * t)

○エラーと解決方法

以下の順で記述をすると”NameError”というエラーが出ました。

# sin波の式
sin_wave_do = np.sin(2 * np.pi * f0_do * t)
t = np.arange(0, fs) / fs
エラー画面

“name ‘t’ is not defined”というエラーメッセージを調べたところ、プログラミング言語は上から順にコンピュータが処理をするため、変数sin_wave内のtが定義されていませんというエラーのようです。

解決策はそのまんま、変数tを先に定義します。t内のサンプリング周波数fsは定数なので、エラーが出なくなりました。

○問題点

Pythonで一つの音(ド)を流すことに成功しましたが、問題が見つかりました。

scipyとpygameの組み合わせだとwavファイルを作成・保存→ロード→実行が必要です。
そのため、音を鳴らすたびにwavファイルが作られてしまいます。

# 16bitのwaveファイルを作成
#サイン波を-32768から32767の#16bit符号付き整数に変換(signed 16bit pcmへ)
wavfile.write("do.wav", fs,(sin_wave_do * 32767.0).astype(np.int16))

# wavファイルをロードして再生
mixer.init()  # mixerを初期化
mixer.music.load("do.wav")  # wavをロード
mixer.music.play(1)  # wavを1回再生

キープレスに応じて音を鳴らすためには楽器のようなリアルタイム性が必要であり、wavファイル作成・保存→ロード→実行だと時間がかかってしまいそうです。

これを解決するためには、 メモリ上の音を直接鳴らす 必要があると考えました。

この方法を調べた結果、PyAudioを利用することに行き着きました。

レシピ②:PyAudioで鳴らす

こちらも参考記事Pythonと音楽と…(1)音を鳴らすを真似てコードを書いてみました。

まず、numpyでつくったsin波をPyAudioを用いてメモリ上で再生します。

次に、PyAudioで以下の順で実行することでwavファイルに保存することなくメモリ上の音データを鳴らすことができるようです。

①PyAudioインスタンスを作成
②ストリームを開く
③ストリームにデータを書き込み&再生する
④ストリームを閉じる&インスタンスを破棄する

下記のサンプルでは、一つの音を生成&再生するための関数 play() を定義しています。
play()の変数としてstream、周波数f0、音の長さ(秒数)を用意し、play()関数実行時に指定周波数のsin波を指定秒数分生成、ストリームにデータを渡します。

○結果

下記のコードでwavファイルに保存することなくメモリ上の音データを鳴らすことができました。

# ライブラリのインポート
import numpy as np              # sin波
import pyaudio                  # メモリ上の音楽を再生

# パラメータ
f0_do = 261.626       # 生成するサイン波の周波数f0(note#60 C4 ド)
f0_mi = 329.628       # 生成するサイン波の周波数f0(note#64 E4 ミ)
f0_so = 391.995       # 生成するサイン波の周波数f0(note#67 G4 ソ)
fs = 44100     # サンプリング周波数fs

# 指定ストリームで、指定周波数のサイン波を、指定秒数再生する関数
def play(s: pyaudio.Stream, freq: float, duration: float):
    # 指定周波数のsin波を指定秒数生成
    # sin_wave = A * np.sin(2*np.pi*f0*t)
    # t = n / fs
    #   = np.arange(start:0, stop:duration * fs) / fs
    t = np.arange(0, duration * fs) / fs
    sin_wave = np.sin(2 * np.pi * freq * t)

    # ストリームに渡して再生
    s.write(sin_wave.astype(np.float32).tobytes())

# PyAudioを開始
p = pyaudio.PyAudio()

# PyAudioを使うには、ストリームをopenし、writeし、close
# ストリームを開く
stream = p.open(format=pyaudio.paFloat32,
                channels=1,
                rate=fs,
                frames_per_buffer=1024,
                output=True)

# ドミソドーを再生
play(stream, f0_do, 0.3)  # note#60 C4 ド
play(stream, f0_mi, 0.3)  # note#64 E4 ミ
play(stream, f0_so, 0.3)  # note#67 G4 ソ
play(stream, f0_do, 0.6)  # note#72 C5 ド

# ストリームを閉じる
stream.close()

# PyAudio終了
p.terminate()

○エラーと解決方法

参考記事のとおり記述すると以下のエラーが出ました。

"DeprecationWarning: tostring() is deprecated. Use tobytes() instead."
# ストリームに渡して再生
s.write(sin_wave.astype(np.float32).tostring())

意味は正直よくわかってませんが、 Pythonのバージョン3.2から明確化のためtostring() の名前が tobytes() に変更されたようです。 

まとめ&次回予告

○まとめ

Pythonを用いてPCから音を出力することに成功しました。

方法としては2通り考えられましたが、PCキーボードで演奏できるアプリケーションの作成に向け、今後は方法②で進めていきたいと思います。

方法①:numpyでsin波をつくり、scipyでwaveファイルに変換し、pygameで再生する
 方法②:numpyでsin波をつくり、PyAudioでメモリ上のsin波を直接再生する 

使用したライブラリ(方法②)

  • numpy
  • PyAudio

○今日のエラーと解決方法

エラー

  • NameError : name ‘t’ is not defined

解決方法

変数sin_wave内のtが定義されていなかったため、変数tを先に定義しました。

t内のサンプリング周波数fsは定数なので、エラーが出なくなりました。

○次回予告

次回実現したいこととして、キーボード入力によって出力される音を変える方法を探したいと思います。(cでド、dでレ、eでミ、、、みたいな感じ)

  1. キーボード入力で音を鳴らす
    1−1.1つの音を鳴らす

Leave a Reply

Your email address will not be published. Required fields are marked *