プログラミング習得への道のり #6 音の長さを変える ~PCキーボードで演奏したい~

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

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

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

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

前回、ステップ1. キーボード入力で音を鳴らすのうち、Pythonで”キープレスに応じて音階を鳴らす”ことができました。一方、現状はプログラム内で再生時間を指定しており、音の長さを自由に変えられません。

そこで本記事では、 ステップ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. 曲を作る

☑︎本記事の内容

  • 必要な材料:音の長さを変える
  • レシピ①:pygame.time.get_ticks()メソッドを使用する
  • レシピ②:pygame.key.get_pressed()メソッドを使用する
  • レシピ③:辞書であらかじめ音の長さを定義する
  • まとめと次回予告

☑︎著者の経験

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

Twitterもはじめました。

必要な材料:音の長さを変える

○ライブラリ

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

使用したライブラリ

  • numpy:数値演算用ライブラリ → sin波を作る
  • PyAudio:オーディオ関連ライブラリ → 音を鳴らす
  • pygame:ゲーム関連ライブラリ → キープレスを検出する

主なメソッド

  • pygame.event.get() → イベントキューからイベントを取得する
  • pygame.key.name() → キーの名前を取得する
  • pygame.time.get_ticks()(New) → 時間をミリ秒単位で取得する(レシピ①)
  • pygame.key.get_pressed()(New) → キーボードボタンの状態を取得する(レシピ②)

使用したライブラリは前回同様です。

前回までにpygameの機能を使ってキープレスを検出し、キーによって波長を変えることに成功しました。そしてnumpyでsin波をつくり、PyAudioでPCから音データを再生できました。

今回は、前回に続き出力される音の長さを変えるためにpygameのメソッドを色々と試してみました。

新たに加えるPython基礎要素

  • input関数 → BPMを指定(レシピ③)
  • 辞書:DURATION → 音の長さを指定(レシピ③)

レシピ①:pygame.time.get_ticks()メソッドを使用する

前回のコードをベースに、音の長さを変える方法を考えました。

最初に思いついたのは、キーを押した瞬間の時間と話した瞬間の時間を取得し、その差分を出力時間にする方法です。このキーを押すあるいは離す時間の取得はpygameのメソッドにありました。

  • pygame.event.get() → イベントキューからイベントを取得する

ちなみに参考にしたサイトは以下です。

(参考)Pygame – pygame.time – pygameの時間はミリ秒(1/1000秒 …

これをもとのコードと組み合わせて

        if event.type == KEYDOWN:  # キーを押したとき
            if event.key == K_a:           #  そのキーが”a”のとき
                start_time = pygame.time.get_ticks() / 1000

とすることで、キーを押したときの時間を取得できると考えました。         

更に

        if event.type == KEYUP:  # キーを離したとき
            if event.key == K_a:     # そのキーが”a”のとき 
                return_time = pygame.time.get_ticks() / 1000

とすることで、キーを離したときの時間を習得することができると考えました。

そして

press_time = return_time – start_time

とすることで、 キーを押している間の時間を取得 できるはずです。

試しに、「入力されたキー」、「キーを押した時間」、「キーを離した時間」、「キーを押している間の時間」をprintするコードを書いてみました。

サンプルコード

コードはこちら

from pygame.locals import *
import pygame
import sys

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_a:
                start_time = pygame.time.get_ticks() / 1000
                print("押されたキー = " + pygame.key.name(event.key))  # 入力されたキー
                print("開始時間 = " + str(start_time))                # キーを押した時間
                
        if event.type == KEYUP:  # キーを離したとき
            if event.key == K_a:
                return_time = pygame.time.get_ticks() / 1000
                press_time = return_time - start_time
                print("終了時間 = " + str(return_time))    # キーを離した時間
                print("押された時間 = " + str(press_time))  # キーを押している間の時間 
        pygame.display.update()

実行結果

それぞれ正しく取得できましたので、これを用いて音を鳴らしてみます。

まずは実験なので、音は”ド”だけでコードを書きました。

Warning画面

ちなみに、実行はできましたが上記のWarning画面が出ました。この意味はわかりませんのでわかったら加筆します!

○結果

以下のコードでキーを押している時間分、音が鳴りました。

コードはこちら

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

# パラメータ
fs = 44100     # サンプリング周波数fs
f0_scale = {
    'ド/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,
}

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

# 指定ストリームで、指定周波数のサイン波を、指定秒数再生する関数
def play(s: pyaudio.Stream, 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)

# キープレスに応じて音を変える
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_a:
                freq = f0_scale['ド/C4']
                start_time = pygame.time.get_ticks() / 1000
                print("押されたキー = " + pygame.key.name(event.key)) # 入力されたキー
                print("開始時間 = " + str(start_time))               # キーを押した時間

            else:
                continue
            
        if event.type == KEYUP:  # キーを離したとき
            if event.key == K_a:
                return_time = pygame.time.get_ticks() / 1000
                press_time = return_time - start_time
                print("終了時間 = " + str(return_time))    # キーを離した時間
                print("押された時間 = " + str(press_time))  # キーを押している間の時間


                
            #再生
            play(stream, press_time)
        pygame.display.update()
キーを押している時間だけ音が鳴る

○問題点

やってみて気づきましたが、play()関数の引数durationにpress_timeを渡しています。これが
t = np.arange(0, duration * fs) / fs
に渡されるわけです。

何が言いたいかというと、 キーを離してはじめて再生時間が決定し、sin波が作られます。 

よって、押している時間分の音は鳴りましたが、キーを離してから音が鳴りはじめてしまうため演奏には使えません。。。

レシピ②:pygame.key.get_pressed()メソッドを使用する

押している間だけ処理が進む方法がないか考えてみました。

Pygameはもともとゲームを作る際に使用されるライブラリなので、例えば”矢印キーを入力している間ある物体を動かす”ような処理は容易のはずだと考えました。いつもの如く調べた結果、pygame.key.get_pressed()メソッドというものを使用しているようです。

参考にしたサイトは以下です。

(参考)【Pygame】キーイベントでキャラクター移動(長押し対応版)https://algorithm.joho.info › プログラミング › Python

(参考)Pygame – pygame.key – このモジュールには、キーボードを …

サンプルコード

コードはこちら

    while (1):
        # キーイベント処理(キャラクタ画像の移動)
        pressed_key = pygame.key.get_pressed()
        if pressed_key[K_LEFT]:
            rect.move_ip(-1, 0)
        if pressed_key[K_RIGHT]:
            rect.move_ip(1, 0)
        if pressed_key[K_UP]:
            rect.move_ip(0, -1)
        if pressed_key[K_DOWN]:
            rect.move_ip(0, 1)

        pygame.display.update()     # 画面更新
        pygame.time.wait(30)        # 更新時間間隔
        screen.fill((0, 20, 0, 0))  # 画面の背景色
        screen.blit(im, rect)       # 画像の描画
        # 終了用のイベント処理
        for event in pygame.event.get():
            if event.type == QUIT:          # 閉じるボタンが押されたとき
                pygame.quit()
                sys.exit()
            if event.type == KEYDOWN:       # キーを押したとき
                if event.key == K_ESCAPE:   # Escキーが押されたとき
                    pygame.quit()
                    sys.exit()

これを用いてコードを書いてみました。

○結果

キーを長押しても音はなりませんでした。。。

失敗コード

コードはこちら

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

# パラメータ
fs = 44100     # サンプリング周波数fs
f0_scale = {
    'ド/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,
}

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

# 指定ストリームで、指定周波数のサイン波を、指定秒数再生する関数
def play(s: pyaudio.Stream):
    # 指定周波数の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)

#初期値
duration = 0.1

# キープレスに応じて音を変える
while True:
    screen.fill((0, 0, 0))
    pressed_key = pygame.key.get_pressed()
    if pressed_key[K_a]:
        duration += 1
    
    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_a:
                freq = f0_scale['ド/C4']

            else:
                continue
            
            #再生
            play(stream)
        pygame.display.update()

○エラー

OverflowError: size does not fit in an int

○問題点

上記エラーの内容もわかりませんが、確かにキーを押す時間に沿ってsin波を時間軸方向に足していくというのは簡単な話じゃない気がします。。。

# キープレスに応じて音を変える
while True:
    screen.fill((0, 0, 0))
    pressed_key = pygame.key.get_pressed()
    if pressed_key[K_a]:
        duration += 1

なので、これをやろうとすると、今の波の式を用いて①波を作る→②渡す→③再生するの手順で音を鳴らすことは難しく、波の作り方から見直しが必要と考えられます。

\begin{eqnarray}
\left\{
\begin{array}{l}
\sin (t) = A\sin (2{\pi}f_{ 0 }t) \\
t=n/fs
\end{array}
\right.
\end{eqnarray}

レシピ③:辞書であらかじめ音の長さを定義する

本記事の目的は音の長さを変えるということで、キーを押している間音を鳴らそうとしたところうまくいきませんでした。

次のステップとして2案あると考えます。

①波の作り方を見直す
②押している間音を鳴らすという考えを見直す

①は、今の知識ではできそうにありません。

また、改めてPCキーボード演奏アプリを作るモチベーションに立ち返ってみると

  • 演奏できない人でも簡単に演奏できたらなぁ
  • 新しいスタイルで演奏できたらカッコいいなぁ
  • 好きにアレンジしたり作曲できたら尚更よいなぁ

でした。

“カタカタ仕事しているようで実は演奏している”という新しいスタイルで演奏する方が新しいし面白いなと思いました。

そもそも普段PCを使うとき、キーを長押しすることってあまりないのでは?とも思いました。(PCゲームはやっていないので分かりませんが)。

 よって、②側で進めようという考えに至りました。 

ではどうするかというと、楽器の要素を残しつつカタカタ演奏するため、あるキーを押すと全音符〜四分音符の間で長さが変わるようにするのはどうかと考えました。

イメージとして、日本語を打つとき例えば「は」と打つには「h」のあとに「a」と打ちますが、その要領で「h」を打つと全音符の長さになり、そのあと「a」を押すとドが全音符分鳴る、みたいなことです。

キーボード配置

○結果

以下のコードで音の長さを変えることができました。

作成したコード

コードはこちら

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

# パラメータ
fs = 44100     # サンプリング周波数fs
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,
}

# 初期値
duration = DURATION['L4']

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

# 指定ストリームで、指定周波数のサイン波を、指定秒数再生する関数
def play(s: pyaudio.Stream):
    # 指定周波数の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)

# キープレスに応じて音を変える
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:  # キーを押したとき
            print("押されたキー = " + pygame.key.name(event.key))

            # 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
                
            #再生
            play(stream)
        pygame.display.update()
様々な長さのドを鳴らしてみた

以下、メモです。

音の長さdurationをピアノのように全音符〜8分音符で表し、辞書型DURATIONで定義しました。
このとき、音の長さはBPMで決まるため、input()関数でBPMを先に定義するようにしました。

# パラメータ
bpm = int(input('BPMを入力してください : '))
DURATION = {
    'L1': (60 / bpm * 4),        # 全音符
    'L2': (60 / bpm * 4) / 2,    # 二分音符
    'L4': (60 / bpm * 4) / 4,    # 四分音符
    'L8': (60 / bpm * 4) / 8,    # 八分音符

前回の記事で、while分の”event.type == KEYDOWN”とすることでキープレスを検出し、event.keyで任意のキーを指定できるとお伝えしました。これを用いて、音の長さdurationをキーによって変えるよにしました。

# キープレスに応じて音を変える
while True:
…
        if event.type == KEYDOWN:  # キーを押したとき
…
            # キーに応じて音の長さ変化
            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']               
…

○エラーと解決方法

プログラム実行後、最初に”a”を押すと以下エラーが出てきました。

NameError: name ‘duration’ is not defined

エラー画面

これは、while文のなかで”周波数が決まったときにsin波をつくる”ようコードが書かれているため、音の長さを決めるキーを押すより前に、周波数を決めるキーを押してしまうとsin波が作られずエラーが起こるとわかりました。

したがって、以下のように初期値を先に定義することで解消しました。

# 初期値
duration = DURATION['L4']

まとめ&次回予告

○まとめ

PCのキープレスの種類に応じて音を長さを変えることに成功しました(レシピ③)。
一方、キープレスの長さに応じて音の長さを変えることには失敗しました(レシピ①、②)。

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

  • numpy
  • PyAudio
  • pygame

使用したメソッド(レシピ③)

  • pygame.event.get() → イベントキューからイベントを取得する
  • pygame.key.name() → キーの名前を取得する

最後に、作ったコードで遊んでみました。

一応演奏はできましたが、各音の長さを決めるテンポが分かりづらいと感じましたので、
メトロノームのような機能が必要かなと思いました。

名探偵コナンのOPを演奏してみた

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

エラー

  1. NameError: name ‘duration’ is not defined

解決方法

  1. 初期値を設定することでwhile文内で起こるエラーを解消しました。

○次回予告

メトロノームのような機能を追加し、テンポが分かるようにしたいと思います。

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

Leave a Reply

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