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

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

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

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

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

前回の記事から”③アウトプットを作り始める”をはじめ、ステップ1. キーボード入力で音を鳴らすのうち、Pythonで”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. 曲を作る

☑︎本記事の内容

  • 必要な材料:キープレスに応じて音階を鳴らす
  • レシピ①:input関数で音階をで鳴らす
  • レシピ②:pygameでキープレスを検出する
  • まとめと次回予告

☑︎著者の経験

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

Twitterもはじめました。

必要な材料:キープレスに応じて音階を鳴らす

○ライブラリ

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

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

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

前回、numpyでsin波をつくり、PyAudioでPCから音データを再生することに成功しました。

今回は、前回に続きキープレスに応じて音を変えるためにpygameの機能を使います。
※pygameは前回、音を鳴らすライブラリとして紹介しましたが、今回はキープレス検出に使います。

新たに加えるPython基礎要素

  • input関数 → キープレスを取得(レシピ①)
  • リスト → 周波数を変えるための周波数リスト(ド、レ、ミ、、、)(レシピ①)
  • 辞書 → リストに名称を対応させたもの(ド:261.636、、、)(レシピ②)

レシピ①:input関数で音階をで鳴らす

まずは、前回のコードをベースに、キープレスごとに音を変える方法を考えました。

そこで使用したのがinput関数です。play関数内にinput()を配置することでplay関数実行時にキーを入力するようにします。

また、リストを用いて入力キーが”c”のとき周波数freqが”ド”、入力キーが”d”のとき周波数freqが”レ”、入力キーがそれ以外のとき周波数freqが”ミ”になりうようにしました。

○結果

以下のコードで入力するキーによって出力される音が変わりました。

コードはこちら

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

# パラメータ
fs = 44100     # サンプリング周波数fs
f0_scale = [261.626, 293.665, 329.628] # ド レ ミ

# 指定ストリームで、指定周波数のサイン波を、指定秒数再生する関数
def play(s: pyaudio.Stream, duration: float):
    scale = input('Enter scale : ')
    if scale == "c":
        freq = f0_scale[0]
    elif scale == "d":
        freq = f0_scale[1]
    else:
        freq = f0_scale[2]

    # 指定周波数の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, 0.5)
play(stream, 0.5)
play(stream, 0.5)

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

# PyAudio終了
p.terminate()
ド、レ、ミを演奏

以下、メモです。

f0_scaleで周波数を変えるための周波数リストを定義しています。

# パラメータ
f0_scale = [261.626, 293.665, 329.628] # ド レ ミ

scale = input(‘Enter scale : ‘)でキーを入力した値を取得し、値によって周波数$freq$を変えています。

def play(s: pyaudio.Stream, duration: float):
    scale = input('Enter scale : ')
    if scale == "c":
        freq = f0_scale[0]
    elif scale == "d":
        freq = f0_scale[1]
    else:
        freq = f0_scale[2]

○エラーと解決方法

play()関数を前回と同様に定義したはずが下記Typeエラーが発生しました。

def play(s: pyaudio.Stream, freq: float, duration: float):

TypeError: play() missing 1 required positional argument: ‘duration’

音の長さdurationはplay(stream, 0.5)の0.5にあたるはずなのになぜだ。。。と思いましたが、理由はplay()関数内の引数でした。

 1 required positional argumentは引数が足りませんよという意味のようです。 

もともと、play()関数には、3つの引数(s: pyaudio.Stream, freq: float, duration: float)を用意していました。

よって、引数が3つの関数に対し、実行時にplay(stream, 0.5)のように引数を2つしか指定しなかったため発生したエラーです。

解決策として、freqは今回キープレスによって一意に決まる定数となったので、play()関数の引数から除外し、引数を(s: pyaudio.Stream, duration: float)にすることで解消しました。

○問題点

input()を用いることでそれらしい機能になりましたが、厳密にはキープレスを検出しているのではなく、文字入力をしています。なので、文字入力→Enterキー→音声出力になり、リアルタイム性がありません。また、今のコードではplay()を何回も書き込まなければなりません。

これを解決するためには、 キープレスを検出する ことが必要です。

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

レシピ②:pygameでキープレスを検出する

Python キープレス』で検索するとpygameの記事が複数出てきました。特にわかりやかった以下の記事を参考にすると、pygame内のメソッドにevent.typeというものがあり、”event.type == KEYDOWN”とすることでキープレスを検出を検出することができるようです。また、event.keyで任意のキーを指定できるようです。

(参考)【Python】Pygame キーボード入力のイベント操作 – とある …

サンプルコード

コードはこちら

# ライブラリのインポート
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()
            
            else:
                print("押されたキー = " + pygame.key.name(event.key))
        pygame.display.update()

以下はメモです。

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

pygameを使用するには少なくとも1つの画面が必要なようです。

これをなくすと”error: video system not initialized ”というエラーが出てきました。

#構文
while True:
    screen.fill((0, 0, 0)) 
    for event in pygame.event.get():
・
・
・
        pygame.display.update()

pygameを使うときの構文のようです。whileで処理をループさせています。

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

キープレスの取得部分です。他にも、これを使った例として以下も参考にしました。

(参考)Python keydown combinations (ctrl + key or shift + key) – Stack …

ESCAPEキーが押されたときに終了するよう記述されています。例えば、”a”が押されたときに何らかの処理を行うには”K_a”を記述すればよいみたいです。

○結果

下記のコードでキー入力に応じてリアルタイムに音を鳴らすことに成功しました。

コードはこちら

# ライブラリのインポート
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:
                if pygame.key.get_mods() & pygame.KMOD_CTRL:
                    freq = f0_scale['ド/C5']
                else:
                    freq = f0_scale['ド/C4']
            elif event.key == K_s:
                freq = f0_scale['レ/D4']
            elif event.key == K_d:
                freq = f0_scale['ミ/E4']
            elif event.key == K_f:
                if pygame.key.get_mods() & pygame.KMOD_SHIFT:
                    freq = f0_scale['ファ#/F#4']
                else:
                    freq = f0_scale['ファ/F4']
            elif event.key == K_w:
                if pygame.key.get_mods() & pygame.KMOD_SHIFT:
                    freq = f0_scale['ソ#/G#4']
                else:
                    freq = f0_scale['ソ/G4']
            elif event.key == K_e:
                if pygame.key.get_mods() & pygame.KMOD_SHIFT:
                    freq = f0_scale['ラ#/A#4']
                else:
                    freq = f0_scale['ラ/A4']
            elif event.key == K_r:
                freq = f0_scale['シ/B4']
            else:
                continue
                
            #再生
            print("押されたキー = " + pygame.key.name(event.key))
            play(stream, 0.5)
        pygame.display.update()
キープレス

なんかエラーぽいの出たけどなんとかできました。エラーの理由は誰か教えてください。。。

以下、メモです。

音のバリエーションを対応をわかりやすくするためにf0_scaleをリスト型から辞書型へ変更しました

# パラメータ
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,
}

escapeキーを押すとpygameが終了します。

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

入力されたキーに応じてf0_scaleから周波数freqをもってきます。

pygame.key.get_mods() & pygame.KMOD_XXXは同時押しの場合を定義しています。

        if event.type == KEYDOWN:  # キーを押したとき
・
・
・
            # キーに応じて周波数変化
            elif event.key == K_a:
                if pygame.key.get_mods() & pygame.KMOD_CTRL:
                    freq = f0_scale['ド/C5']
                else:
                    freq = f0_scale['ド/C4']
            elif event.key == K_s:
                freq = f0_scale['レ/D4']
            elif event.key == K_d:
                freq = f0_scale['ミ/E4']
            elif event.key == K_f:
                if pygame.key.get_mods() & pygame.KMOD_SHIFT:
                    freq = f0_scale['ファ#/F#4']
                else:
                    freq = f0_scale['ファ/F4']
            elif event.key == K_w:
                if pygame.key.get_mods() & pygame.KMOD_SHIFT:
                    freq = f0_scale['ソ#/G#4']
                else:
                    freq = f0_scale['ソ/G4']
            elif event.key == K_e:
                if pygame.key.get_mods() & pygame.KMOD_SHIFT:
                    freq = f0_scale['ラ#/A#4']
                else:
                    freq = f0_scale['ラ/A4']
            elif event.key == K_r:
                freq = f0_scale['シ/B4']
            else:
                continue

○問題点

音を鳴らすことはできましたが、音の長さを自由に変えられません。
今は、play()関数内の引数durationで指定秒数ならすようにしています。

次回は、音の長さを任意に変える方法を探したいと思います。

まとめ&次回予告

○まとめ

pygameを用いてPCのキープレスに応じて音を変えることに成功しました。

使用したライブラリ

  • numpy
  • PyAudio
  • pygame(New)
○今日のエラーと解決方法

エラー

  1. TypeError: play() missing 1 required positional argument: ‘duration’
  2. error: video system not initialized

解決方法

  1. play()関数の引数に定数(freq)が含まれていたため、play()関数の引数から除外することで解消しました。
  2. pygameを使用するには少なくとも1つの画面が必要なため、画面を定義することで解消しました。
○次回予告

次回は音の長さを自由に変える方法を探したいと思います。

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

Leave a Reply

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