プログラミング習得への道のり #7 アプリの画面をつくる① ~PCキーボードで演奏したい~

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

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

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

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

前回、ステップ1. キーボード入力で音を鳴らすのうち、”音の長さを自由に変える”にチャレンジしました。結果、キーを押している間だけ音を流すことは難しく、BPMとキープレスに応じて音の長さを指定することで音の長さを変えることに成功しました。実際に演奏してみたものが以下です。

名探偵コナンのテーマを演奏してみた

次の課題として、BPMを最初に決めるものの、テンポがわからず演奏しづらいことが挙げられます。そこで、”メトロノーム機能を追加してテンポを分かるようにする”にチャレンジしました。。。が、ネタバレするとメトロノーム機能の実現には新たな壁がたくさんあることが分かりました。結果、本記事で、 “新たにアプリケーション画面(GUI部分)をつくりはじめる” にチャレンジしました。

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

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

☑︎本記事の内容

  • 【序章①】メトロノーム機能をつくる方法を考える
  • 【序章②】アプリ画面をつくる要素を調べる
  • 必要な材料:GUI部分の作成
  • レシピ①:波形をプロットする
  • レシピ②:波形を動的に変化させる
  • まとめと次回予告

☑︎著者の経験

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

Twitterもはじめました。

【序章①】メトロノーム機能をつくる方法を考える

○テンポを知る方法

前回、BPMとキープレスに応じて音の長さを変えるようにしました。ただ、そのままではテンポがわからず思うように演奏できませんでした。そこで、メトロノームのような演奏者にテンポを知らせる機能が必要と考えました。

ここで、テンポをを知る方法は以下の2通りあると考えます。

  • 耳で知る方法
  • 目で知る方法

メトロノームを思い浮かべると「カチッカチッ」という音でテンポがわかるイメージなので、前者で実装を考えました。

○耳で知る方法

Pythonでメトロノームをつくる方法を調べるといくつか出てきました。

Pythonでメトロノームをつくる

https://github.com/Gataromaro/metronome

これを参考にメトロノーム機能をつくろうと考えましたが、ここで問題が発生しました。

それはPC内のスピーカーが1つであることです。

いまつくろうとしているものが音楽演奏アプリであるため、PCに内蔵されているスピーカーは演奏に使われます。そのためそこに重ねる形でメトロノームの音を追加するのは単純な話でないことがわかりました。

複数の音を同時に鳴らす方法としては、あらかじめ音を合成し合成音を出力するという方法もあります。ただし、演奏アプリのようにリアルタイムに音が作られていくなかでメトロノーム音と他の音を合成するのは難しいと考えられます。

したがって、視覚的にテンポを知る方法でメトロノーム機能をつくろうと考えました。

○目で知る方法

視覚的にテンポがわかるように方法を考えると、画面上で点滅するエフェクトを追加する、というのが思い浮かびました。

 これを実現するためには画面(GUI)が必要です。 

まずはPythonで画面をつくるイメージを調べてみました。

(参考①)pygameでメトロノームをつくる

https://www.pygame.org/project-Metronome-1041-.html

(参考②)TkinterでSynthesizerアプリをつくる

Pythonで画面をつくるためのライブラリとして、どうやらpygameTkinterというものがあるようです。

一旦、これらを用いて簡単な画面から作成し、ゆくゆくはメトロノームを作ってみようと思います。

○ステップの見直し

ステップ1. キーボード入力で音を鳴らすを見直し、画面を作成を追加しようと思います。

Before

  1. キーボード入力で音を鳴らす
    1−1.1つの音を鳴らす
    1-2.音階を鳴らす
    1-3.音の長さを自由に変える
    1-4.テンポを決める曲を作る

After

  1. キーボード入力で音を鳴らす
    1−1.1つの音を鳴らす
    1-2.音階を鳴らす
    1-3.音の長さを自由に変える
    1-4.画面をつくるNew
    1-5.メトロノーム機能をつくるNew

【序章②】アプリ画面をつくる要素を調べる

メトロノーム機能をつくる前に画面をつくることにしましたので、メトロノームの他にどんな機能が欲しいか考えてみました。

まずは今までキープレスに応じてsin波をつくっていましたので、それを視覚的に見えるようにしたいと思います。
完成イメージに近いのはTkinterでSynthesizerアプリをつくるです。

アプリ画面(GUI)イメージ

► Code: https://github.com/denczo/EardrumBlas…

上記のYoutubeには丁寧にGithubのリンクも載っていたためソースコードを閲覧することができました。
まずは、このコードを解読し、見様見真似でアプリ画面を作って見ようと思いました。

が、、、解読するのは難しい。。。詳細をとてもブログに残せそうにありません。。。

やり方としては、「#」を使ってひたすらいらなそうなところを削除し、コードをシンプル化し、機能とコードの対応を確認していくというものです。

色々試してみた結果、画面を加えるにあたり、以下の要素が必要かなと思いました。

必要な要素

  • 機能を分ける
  • クラスを使う

>機能を分ける

参考コードを見てまずわかったことが、アプリケーションを構成しているPythonファイルが階層構造になっていることでした。

EardrumBlaster/synthlogic」のファイル直下には、“main”“interfaces”といったフォルダがあり、さらに階層を進むと“synth.py”“synth_gui.py”“touchpad.py”といったPythonファイルが格納されています。

参考スクショはこちら

synthlogicフォルダ

mainフォルダ

guiフォルダ

このうち、EardrumBlaster/synthlogic/main/synth_gui.py 」を見てみると以下のようなコードがありました。

# === basic window configuration
from synthlogic.main.synth import Synth

また、EardrumBlaster/synthlogic/main/synth.py 」を見てみるとSynthというクラスが定義されていました。

class Synth:
    def __init__(self, rate=44100, chunk_size=256, gain=0.25, fade_seq=256, gui_enabled=False):

ここからわかったのが、機能ごとにPythonファイルをつくった後、以下で呼び出すことができるということです。

 from パス import クラス名 

機能ごとに小分けにPythonファイルをつくる利点は色々あると思いますが、機能の追加や修正をするときに対象の特定をしやすくなることが挙げられます。

サービス開発の手法の1つであるマイクロサービスと同じような考え方なのかなと思います。

(参考)マイクロサービスアーキテクチャの特徴を学ぼう! メリット …

>クラスを使う

いままで、キープレスに応じて音がなる機能を作ってきました。

これにGUIやその他の機能を足していくに際し、上記のような機能の階層分けが有効のようです。そして、そのためには機能ごとにクラスで定義していく方がベターなようです。

なのでこれからはクラスで定義してみようと思います。

class クラス名:

必要な材料:GUI部分の作成

○ライブラリ

今回使用するライブラリは以下です。

使用したライブラリ

  • numpy:数値演算用ライブラリ → sin波を作る
  • PyAudio:オーディオ関連ライブラリ → 音を鳴らす
  • pygame:ゲーム関連ライブラリ → キープレスを検出する
  • matplotlib:グラフ作成ライブラリ(New) → 波形をプロットする(レシピ①、②)

主なメソッド

  • numpy.ones() → 全要素の配列を1で初期化する(レシピ①)
  • numpy.linspace() → 等差数列をつくる(レシピ①)
  • numpy.concatenate() → 要素を足し合わせる(レシピ①)

新たに加えるPython基礎要素

  • クラス:Sinwave_synth → sin波の作成、プロット、音声出力(レシピ②)

レシピ①:波形をプロットする

画面を作成するにあたり、まずはわかりやすいsin波のグラフをつくってみたいと思います。

まずは「EardrumBlaster/tests/FM_Test.py」をそのまま実行したところ、以下のグラフが現れました。なんだこの形は、、、

FM_Test.pyの実行結果

プロットされた式はy = np.sin(2*np.pi*fc*x+2*np.pi*beta*lfo)みたいなので、“beta”“lfo”なるものが波の式に加わりこのような形になっていると推察されます。

次に「EardrumBlaster/tests/EnvelopeTest.py」を実行してみました。
すると、また新しい形のグラフが作成されました。

EnvelopeTest.pyの実行結果

参考にしたYoutubeを見てみると、どうやらsin波のゲイン?を表しているようです。エフェクトとかをかけると音の鳴り始めや鳴り終わりの長さが変わると思いますが、おそらくそれです。

なので、ステップ2. 音を制御するで使いそうだなと推察できます。

Youtubeより抜粋

上記から、 波を表すグラフとして、sin波とゲインの2つプロットしていきたいと思います。 

○結果

参考コードを取捨選択して、波形をプロットしてみました。

上:ゲイン(変化なし) 下:サイン波(ド)

コードはこちら

# ライブラリのインポート
import numpy as np
import matplotlib.pyplot as plt

# ドの条件
rate = 44100
duration = 0.5 
freq = 261.626

# 波の式
t = np.arange(0, duration * rate) / rate
sin_t = np.sin(2 * np.pi * freq * t)
envRange = np.ones(int(duration * rate)) # 1字配列

# グラフのプロット
fig = plt.figure()
fig_envRange = fig.add_subplot(2, 1, 1) # 2行1列の1番目(1,1)に表示
fig_wave = fig.add_subplot(2, 1, 2)     # 2行1列の2番目(2,1)に表示
fig_envRange.plot(t, envRange) #2DLine
fig_wave.plot(sin_t[:500])     #2DLine

上:ゲイン(変化あり) 下:サイン波(ド)

コードはこちら

# ライブラリのインポート
import numpy as np
import matplotlib.pyplot as plt

# ドの条件
rate = 44100
duration = 0.5
freq = 261.626

# 波の式
t = np.arange(0, duration * rate) / rate
sin_t = np.sin(2 * np.pi * freq * t)
envRange = np.ones(int(duration * rate)) # 1字配列

phaseRange = int(len(envRange)/4) # len関数で引数の要素数を取得し、attack, decay, sustain, release用に4分割

# ゲインの定義
attack = np.linspace(0, 1, phaseRange) # linspaceで等差数列を生成(0→1へ要素数phaseRangeで変化)
decay = np.linspace(1, 0.5, phaseRange) # linspaceで等差数列を生成(1→0.5へ要素数phaseRangeで変化)
sustain = np.empty(phaseRange)
sustain.fill(0.5)
release = np.linspace(0.5, 0, len(envRange) - 3 * phaseRange) # linspaceで等差数列を生成(0.5→0へ変化 要素数は割り切れない場合を加味)

envelope = np.concatenate((attack, decay, sustain, release)) # concatenateで要素を結合
envRange *= envelope # ゲインを整形

# グラフのプロット
fig = plt.figure()
fig_envRange = fig.add_subplot(2, 1, 1) # 2行1列の1番目(1,1)に表示
fig_wave = fig.add_subplot(2, 1, 2)     # 2行1列の2番目(2,1)に表示
fig_envRange.plot(t, envRange) #2DLine
# ライブラリのインポート
import numpy as np
import matplotlib.pyplot as plt

# ドの条件
rate = 44100
duration = 0.5
freq = 261.626

# 波の式
t = np.arange(0, duration * rate) / rate
sin_t = np.sin(2 * np.pi * freq * t)
envRange = np.ones(int(duration * rate)) # 1字配列

phaseRange = int(len(envRange)/4) # len関数で引数の要素数を取得し、attack, decay, sustain, release用に4分割

# ゲインの定義
attack = np.linspace(0, 1, phaseRange) # linspaceで等差数列を生成(0→1へ要素数phaseRangeで変化)
decay = np.linspace(1, 0.5, phaseRange) # linspaceで等差数列を生成(1→0.5へ要素数phaseRangeで変化)
sustain = np.empty(phaseRange)
sustain.fill(0.5)
release = np.linspace(0.5, 0, len(envRange) - 3 * phaseRange) # linspaceで等差数列を生成(0.5→0へ変化 要素数は割り切れない場合を加味)

envelope = np.concatenate((attack, decay, sustain, release)) # concatenateで要素を結合
envRange *= envelope # ゲインを整形

# グラフのプロット
fig = plt.figure()
fig_envRange = fig.add_subplot(2, 1, 1) # 2行1列の1番目(1,1)に表示
fig_wave = fig.add_subplot(2, 1, 2)     # 2行1列の2番目(2,1)に表示
fig_envRange.plot(t, envRange) #2DLine
fig_wave.plot(sin_t[:500])     #2DLine

以下、メモです。

こちら

# ライブラリのインポート
import matplotlib.pyplot as plt

# グラフのプロット
fig = plt.figure()
fig_envRange = fig.add_subplot(2, 1, 1) # 2行1列の1番目(1,1)に表示
fig_wave = fig.add_subplot(2, 1, 2)     # 2行1列の2番目(2,1)に表示
fig_envRange.plot(t, envRange) #2DLine
fig_wave.plot(sin_t[:500])     #2DLine

グラフの作成に使用するmatplotlibをインポートし、”plt“という名前で扱うようにしています。
fig = plt.figure()でグラフの作成エリアを作り、fig.add_subplot(x, y, n)で(x, y)行列からなるエリアのn番目にグラフを配置するよう記述しています。
そして、.plot()でグラフをプロットしています。
なお、[:500]はx 軸方向の表示範囲を指定する書き方のようです。

# 波の式
envRange = np.ones(int(duration * rate)) # 1字配列

np.onesで配列の全要素を1で初期化してます。([1 1 1 1 … 1 1]みたいな感じ)

(参考)【Numpy入門 np.ones】配列の全要素を1で初期化するones …

# ドの条件
rate = 44100
duration = 0.5
freq = 261.626

# 波の式
envRange = np.ones(int(duration * rate)) # 1字配列

phaseRange = int(len(envRange)/4) # len関数で引数の要素数を取得し、attack, decay, sustain, release用に4分割

print('要素数 enRange:', len(envRange)) # 追加
print('要素数 phaseRange:', phaseRange) # 追加

このあと、ゲインを要素数の1/4ずつ変化させるため、phaseRangeを定義しています。
ここで注意点として、割り切れなかった場合の小数点以下は切り捨てのようです。
実際に、print関数で要素数を見てみるとphaseRangeを4倍してもenRangeの要素数にならないことがわかります。

なお、要素数を表すためにlen関数を使っています。

(参考)len関数の使い方(オブジェクトの長さや要素数を取得する)

# ゲインの定義
attack = np.linspace(0, 1, phaseRange) # linspaceで等差数列を生成(0→1へ要素数phaseRangeで変化)
decay = np.linspace(1, 0.5, phaseRange) # linspaceで等差数列を生成(1→0.5へ要素数phaseRangeで変化)
sustain = np.empty(phaseRange)
sustain.fill(0.5)
release = np.linspace(0.5, 0, len(envRange) - 3 * phaseRange) # linspaceで等差数列を生成(0.5→0へ変化 要素数は割り切れない場合を加味)
print('要素数 attack:', len(attack)) # 追加
print(attack) # 追加
print('要素数 decay:', len(decay)) # 追加
print(decay) # 追加
print('要素数 sustain:', len(sustain)) # 追加
print(sustain) # 追加
print('要素数 release:', len(release)) # 追加
print(release) # 追加

envelope = np.concatenate((attack, decay, sustain, release)) # concatenateで要素を結合
print('要素数 envelope:', len(envelope)) # 追加

np.linspace(start, end, 要素数)で等差数列をつくり、attack, decay, sustain, releaseの4つの変化でゲインを表そうとしています。

(参考)NumPyのarange, linspaceの使い方(連番や等差数列を生成)

また、np.concatenate()で要素数を足し合わせています。

(参考)PythonのNumPyのconcatenate関数の使い方について現役 …

ここでポイントとなるのが、releaseの要素数をphaseRangeではないことです。問題点のところで詳しく説明しますが、配列同士の演算では要素数を合わせる必要があるためです。
 要素数envelopeの値が要素数enRangeと一致していることがわかります。 

envRange *= envelope # ゲインを整形

最後に、np.onesで要素がすべて1だったenvRangeにゲインの変化情報をかけることで整形しました。

○エラーと解決方法

releaseの要素数をphaseRangeにし、実行すると”ValueError”というエラーが出てきました。

# ゲインの定義
attack = np.linspace(0, 1, phaseRange) # linspaceで等差数列を生成(0→1へ要素数phaseRangeで変化)
decay = np.linspace(1, 0.5, phaseRange) # linspaceで等差数列を生成(1→0.5へ要素数phaseRangeで変化)
sustain = np.empty(phaseRange)
sustain.fill(0.5)
release = np.linspace(0.5, 0, phaseRange) # linspaceで等差数列を生成(0.5→0へ要素数phaseRangeで変化)

envelope = np.concatenate((attack, decay, sustain, release)) # concatenateで要素を結合

これは先に上げたとおり要素数があっていないからです。phaseRangeは元の変数enRangeの要素を1/4していますが割り切れないため、✕4をしても元に戻りません。

エラー画面から、要素数enRangeは22050であるのに対し、要素数envelopeは22048です。よって、envRange *= envelopで要素数のミスマッチが起こっています。

解消方法として、先にあげたようにreleaseの要素数を調整しました。

なお、要素数の考え方は以下を参考にしました。

(参考)覚えておくべきNumPy配列のブロードキャストのルール

○まとめ

sin波およびゲインのプロット方法がわかりました。ゲインの設定については、ステップ2. 音を制御するで詳しく考えたいとため、ステップ1では一旦ゲインの変化はなしで進めていきたいと思います。

レシピ②:波形を動的に変化させる

グラフのプロット方法がわかりましたので、キープレスに応じて波形が変化するか確かめてみました。

○結果

以下のコードでキープレスに応じてグラフが作成できました。

コードはこちら

# ライブラリのインポート
import numpy as np  # sin波
import pyaudio  # メモリ上の音楽を再生
from pygame.locals import *  # キープレス
import matplotlib.pyplot as plt
import pygame
import sys


# クラスの定義
class Sinwave_synth:
    def __init__(self, gain=0.25, rate=44100, chunk_size=1024):
        self.gain = gain  # "A"
        self.rate = rate  # サンプリング周波数"fs":44100
        self.chunk_size = chunk_size  # 音源から1回読み込むときのデータサイズ。1024(=2の10乗) とする場合が多い
        self.p = pyaudio.PyAudio()  # pyaudioの開始
        self.start_up() # 初期値

    # 初期値
    def start_up(self):
        global duration # 初期値のためにglobal変数が必要
        duration = DURATION['L4']
    
    # sin波の作成
    def create_sinwave(self, duration, freq):
        self.duration = duration
        self.freq = freq

        # 指定周波数のsin波を指定秒数生成
        self.t = np.arange(0, duration * self.rate) / self.rate
        self.sin_t = np.sin(2 * np.pi * freq * self.t)
        self.envRange = np.ones(int(duration * self.rate))  # 1字配列

        # グラフのプロット
        fig = plt.figure()
        fig_envRange = fig.add_subplot(2, 1, 1) # 2行1列の1番目(1,1)に表示
        fig_wave = fig.add_subplot(2, 1, 2)     # 2行1列の2番目(2,1)に表示        
        fig_envRange.plot(self.t, self.envRange)
        fig_wave.plot(self.sin_t[:500])
        plt.show

    # ストリームに渡して再生
    def play(self):
        self.stream = self.p.open(format=pyaudio.paFloat32,
                                  channels=1,
                                  rate=self.rate,
                                  frames_per_buffer=self.chunk_size,
                                  output=True)
        self.stream.write(self.sin_t.astype(np.float32).tobytes())
        self.stream.close()


# パラメータ
bpm = int(input('BPMを入力してください : '))
DURATION = {
    'L1': (60 / bpm * 4),      # 全音符
    'L2': (60 / bpm * 4) / 2,  # 二分音符
    'L4': (60 / bpm * 4) / 4,  # 四分音符
    'L8': (60 / bpm * 4) / 8,  # 八分音符
}
FREQ_SCALE = {  # 周波数f0のスケール
    'ド/C4': 261.626,
    'レ/D4': 293.665,
    'ミ/E4': 329.628,
    'ファ/F4': 349.228,
    'ファ#/F#4': 369.994,
    'ソ/G4': 391.995,
    'ソ#/G#4': 415.305,
    'ラ/A4': 440.000,
    'ラ#/A#4': 466.164,
    'シ/B4': 493.883,
    'ド/C5': 523.251,
    'レ/D5': 587.330,
    'ミ/E5': 659.255,
}

# 実装
Sinwave = Sinwave_synth()

# 画面作成
pygame.init()  # Pygameを初期化
screen = pygame.display.set_mode((400, 330))  # 画面を作成
pygame.display.set_caption("keyboard event")  # タイトルを作成

# キープレスに応じて音を変える
while True:
    screen.fill((0, 0, 0))
    for event in pygame.event.get():

        if event.type == QUIT:
            pygame.quit()
            sys.exit()
        if event.type == KEYDOWN:  # キーを押したとき
            # ESCキーならスクリプトを終了
            if event.key == K_ESCAPE:
                pygame.quit()
                sys.exit()

            # キーに応じて音の長さ変化
            elif event.key == K_j:
                duration = DURATION['L8']
            elif event.key == K_k:
                duration = DURATION['L4']
            elif event.key == K_l:
                duration = DURATION['L2']
            elif event.key == K_SEMICOLON:
                duration = DURATION['L1']
                
            # キーに応じて周波数変化
            if event.key == K_a:
                if pygame.key.get_mods() & pygame.KMOD_CTRL:
                    freq = FREQ_SCALE['ド/C5']
                else:
                    freq = FREQ_SCALE['ド/C4']
            elif event.key == K_s:
                if pygame.key.get_mods() & pygame.KMOD_CTRL:
                    freq = FREQ_SCALE['レ/D5']
                else:
                    freq = FREQ_SCALE['レ/D4']
            elif event.key == K_d:
                if pygame.key.get_mods() & pygame.KMOD_CTRL:
                    freq = FREQ_SCALE['ミ/E5']
                else:
                    freq = FREQ_SCALE['ミ/E4']
            elif event.key == K_f:
                if pygame.key.get_mods() & pygame.KMOD_SHIFT:
                    freq = FREQ_SCALE['ファ#/F#4']
                else:
                    freq = FREQ_SCALE['ファ/F4']
            elif event.key == K_w:
                if pygame.key.get_mods() & pygame.KMOD_SHIFT:
                    freq = FREQ_SCALE['ソ#/G#4']
                else:
                    freq = FREQ_SCALE['ソ/G4']
            elif event.key == K_e:
                if pygame.key.get_mods() & pygame.KMOD_SHIFT:
                    freq = FREQ_SCALE['ラ#/A#4']
                else:
                    freq = FREQ_SCALE['ラ/A4']
            elif event.key == K_r:
                freq = FREQ_SCALE['シ/B4']
            else:
                continue
            
            # sin波の作成 および 音声出力
            print("押されたキー = " + pygame.key.name(event.key))
            Sinwave.create_sinwave(duration, freq)
            Sinwave.play()

        pygame.display.update()

以下、メモです。

コードはこちら

# クラスの定義
class Sinwave_synth:
    def __init__(self, gain=0.25, rate=44100, chunk_size=1024):
        self.gain = gain  # "A"
        self.rate = rate  # サンプリング周波数"fs":44100
        self.chunk_size = chunk_size  # 音源から1回読み込むときのデータサイズ。1024(=2の10乗) とする場合が多い
        self.p = pyaudio.PyAudio()  # pyaudioの開始
        self.start_up() # 初期値
・
・
・

【序章②】アプリ画面をつくる要素を調べるに記載したとおり、sin波をつくるところをクラスSinwave_synthで表しました。

クラスの考え方は以下のYoutubeがすごくわかりやすかったです。def __init__()はコンストラクタといってクラス実装時に実行されるコードです。

# クラスの定義
class Sinwave_synth:
・
・
・
    # 初期値
    def start_up(self):
        global duration # 初期値のためにglobal変数が必要
        duration = DURATION['L4']

start_up()関数はdurationの初期値を定義する関数です。
Sinwave_synthが実装された段階でdef __init__()内のself.start_upが実行され、初期値が代入されます。

また、durationをglobal関数として定義しています。これをしないとエラーが発生します。詳しくはエラーのところで記載します。

# クラスの定義
class Sinwave_synth:
・
・
・    
    # sin波の作成
    def create_sinwave(self, duration, freq):
        self.duration = duration
        self.freq = freq

        # 指定周波数のsin波を指定秒数生成
        self.t = np.arange(0, duration * self.rate) / self.rate
        self.sin_t = np.sin(2 * np.pi * freq * self.t)
        self.envRange = np.ones(int(duration * self.rate))  # 1字配列

        # グラフのプロット
        fig = plt.figure()
        fig_envRange = fig.add_subplot(2, 1, 1) # 2行1列の1番目(1,1)に表示
        fig_wave = fig.add_subplot(2, 1, 2)     # 2行1列の2番目(2,1)に表示        
        fig_envRange.plot(self.t, self.envRange)
        fig_wave.plot(self.sin_t[:500])
        plt.show
・
・
・

create_sinwave()関数でsin波を作成し、グラフにプロットします。変数はdurationとfreqの2つです。

グラフへのプロット部分が今回新たに加えたところです。

# クラスの定義
class Sinwave_synth:
・
・
・
    # ストリームに渡して再生
    def play(self):
        self.stream = self.p.open(format=pyaudio.paFloat32,
                                  channels=1,
                                  rate=self.rate,
                                  frames_per_buffer=self.chunk_size,
                                  output=True)
        self.stream.write(self.sin_t.astype(np.float32).tobytes())
        self.stream.close()

play()関数で音声を出力させます。

# 実装
Sinwave = Sinwave_synth()
・
・
・
# キープレスに応じて音を変える
while True:
    screen.fill((0, 0, 0))
    for event in pygame.event.get():
・
・
・
        if event.type == KEYDOWN:  # キーを押したとき
・
・
・            
            # sin波の作成 および 音声出力
            Sinwave.create_sinwave(duration, freq)
            Sinwave.play()

Sinwave = Sinwave_synth()でクラスを実装します。

そして、キープレスに応じてdurationおよびfreqが決まったのち、nwave.create_sinwave(duration, freq)でsin波がつくられる&プロットされ、Sinwave.play()で音声が出力されます。

○エラーと解決方法

durationをglobal関数で指定しないと”NameError”が出ました。

NameError: name 'duration' is not defined

変数には「グローバル変数」「インスタンス変数」「ローカル変数」の3種類あり、それぞれで扱えるスコープが異なります。

  • グローバル変数:プログラムのどこからでも使える変数
  • インスタンス変数:クラス内でのみ使える変数
  • ローカル変数:メソッド内でのみ使える変数

(参考)インスタンス変数、ローカル変数のスコープをざっくり理解する

今回、ローカル変数として定義されてしまっていたため、クラス内のstart_up()関数で定義されたduration = DURATION[‘L4’]が初期値として代入されずエラーになりました。

したがって、durationをグローバル変数として定義することで解消しました。

        global duration # 初期値のためにglobal変数が必要

○問題点

キープレスによって波形を変化させることができましたが、プログラムの終了後に履歴のように波形がまとめて表示されてしまっています。なので、リアルタイムで波形を変化させたいです。

まとめ&次回予告

○まとめ

PCのキープレスによってsin波の周波数を変えたのち、グラフにプロットすることができました(レシピ②)。
一方、キーを押した瞬間にグラフは出現せず、リアルタイム性がない状況です。

使用したライブラリ(レシピ②)

  • numpy
  • PyAudio
  • pygame
  • matplotlib

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

エラー

  1. ValueError: operands could not be broadcast together with shaps (22050,) (22048,) (22050,)
  2. NameError: name ‘duration’ is not defined

解決方法

  1. 配列内で要素数のミスマッチが生じて起こったエラーでした。要素数を揃えることで解消しました。
  2. classを用いたことで、初期値を設定する際に変数durationをglobal変数にする必要がありました。

○次回予告

リアルタイムに波形が変わる画面を作成したいと思います。

  1. キーボード入力で音を鳴らす
    ☑1−1.1つの音を鳴らす
    ☑1-2.音階を鳴らす
    ☑1-3.音の長さを自由に変える
    1-4. 画面をつくるNew
    1-5.メトロノーム機能をつくる(New

Leave a Reply

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