pygameで音を鳴らす

来週社内勉強会で発表する機会を先日頂いたので、
Pygameで特に私が興味を持っている音声制御機能pygame.mixerモジュールについて
調べてみて、こういうアプリに使えるんじゃね?という事を、
発表のカンペも兼ねて書いていきます。
なお、作成には
Will McGugan著 杉田臣輔・郷古泰昭 訳「Pythonゲームプログラミング入門」(2011)アスキーメディアワークス
を参考にしてます。


そしてこの記事を、共同で仕事をする約束したが、果たすことなく
先日急逝された高校時代の友人 @8841028 氏に捧げます。


1.Pygameとはなんぞや!?

Pygameの歴史

Pythonゲームプログラミング入門」p68より要約引用。

Pygameは、Simple DirectMedia Layer (SDL) というゲーム作成ライブラリによって構築されています。
SDLはプラットフォーム間でのゲーム移植のタスクを簡易化するために書かれました。
これは、グラフィックスや入力デバイスと複数のプラットフォーム上での表示と同様の処理を作成する一般的な方法を提供します。
1998年のリリース以来、簡単に動作するので非常にゲーム開発者に普及するようになり、趣味から商用のゲームまで多く使われてきました。
SDLC言語で書かれています。C言語は低レベルのハードウェアで動作する能力があるので、一般的にはゲームで使われていました。
しかし、CやC++で開発すると遅い上にエラーしがちです。現在は、他言語で利用出来るように関連付けられているため、
他言語でもSDLが利用出来るようになっています。Pygameに関連付けさせることで、pythonSDLライブラリを使用できます。

つまり、元々はC言語用に提供していたSDLライブラリだったが、
C、C++での開発では処理が遅くエラーが発生しやすいということなので、
提供する機能をそのままに、他の言語でも利用出来るよう関連付けをしたライブラリの一つが
Pygameということです。


2.pygame.mixerモジュール

そもそもミキサー(ミキシング)って?

複数の音源をもとに、ミキシング・コンソールという機材を用いて
音声トラックのバランス、音色などをつくりだす作業です。
そちらの方面はあんまり詳しくはないですが、
自分が作曲したことがある経験から言わせてもらうと、
ミックスした音声の設定をファイルに焼きつけてしまうので
動的に設定を変化させるのはむずかしいと思います。

その音声の設定を動的にプログラム内で行うのがpygame.mixerモジュールです。

pygame.mixerモジュールで使える関数

Channel            Channelオブジェクトを作成します。引数にチャンネル番号を取ります
fadeout            全チャンネルの音量を徐々に0まで減らします。フェード時間(ミリ秒単位)を引数に取ります
find_channel       現在未使用のチャンネルをさがし、そのチャンネル番号を返します
get_busy           任意のチャンネルで音声が再生されてる場合、Trueを返します
get_init           ミキサーが初期化されている場合、Trueを返します
get_num_channels   提供されているチャンネル数を取得します
init               ミキサーモジュールを初期化します。引数には、周波数(整数)、ビットレートサイズ(整数)、ステレオ(1or2)、バッファ(整数)
                   を取ります
pause              すべてのチャンネルの音声を一時停止します
pre_init           pygame.initへの呼び出しによって自動的に初期化する場合、ミキサーのパラメータを設定します
quit               ミキサーを終了します。これはPythonスクリプトの終了時に自動的に行われますが、他のパラメータを使用して
                   ミキサーを再び初期化する場合に呼び出す必要があります
set_num_channels   提供するチャンネルの数を設定します
Sound              Soundオブジェクトを作成します。ファイル名または音声データを含むPythonのファイルのオブジェクトを引数に取ります
stop               全チャンネルの音声の再生をすべて停止します
unpause            一時停止している音声を再開します

モジュールから生成されるオブジェクト

このpygame.mixerから生成されるオブジェクトは2つあります。

Sound
Channel

・Soundオブジェクト

オーディオデータの読み込みと再生のために使用されます。
音楽に例えると、楽譜を持った指揮者のような感じでしょうか。
読み込みに対応するファイル形式は

・WAV
・Ogg

の2つです。

hoge = pygame.mixer.Sound("hoge.WAV")
hoge.play()

と書けば、hogeにオーディオデータを格納したSoundオブジェクトが返され、playメソッドで再生されます。

・Channelオブジェクト

Pygame中で同時に再生できる音源のひとつです。
音楽に例えるとSoundオブジェクトが指揮者に対し、こちらは演奏者に当たると思います。
こちらは

channel = pygame.mixer.Channel(0) 

と呼び出せますし、
先の例に上げたhoge.play()は戻り値にChannelオブジェクトを返すので

channnel = hoge.play()

でも呼び出せます。

channel.set_volume(0.5)

と書けば、再生の仕方を細かく設定することができます。
特に細かく設定する必要がなければ、hoge.play()の戻り値を無視することも可能です。


また、デフォルトで同時発生できるチャンネル番号は0~7の計8つあり
どのチャンネルを鳴らすのかはpygame側に制御を任せるのですが、
特定のチャンネルだけに再生させたいSoundオブジェクトを優先して指定することが可能です。

pygame.mixer.set_reserved(1)
reserved_channel =  pygame.mixer.Channel(0)
reserved_channel.play(hoge)

0番目のチャンネルだけにhogeを鳴らすときはこう書きます。

各オブジェクトが利用出来るメソッド

Soundオブジェクト

fadeout            徐々にすべてのチャンネルのサウンドの音量を低減させます。引数にミリ秒単位の整数を受け取る
get_length         音の長さを秒単位で返します
get_num_channels   何回音声が再生されているかをカウント
get_volume         0.0~1.0の浮動小数点として音声の音量を返します
play               音声を再生します。引数にループ回数とミリ秒単位の最大再生時間を受け取ります
set_volume         再生される音声の音量を設定します。パラメータは0.0~1.0の浮動小数点です         
stop               再生中の音声を停止します

Channelオブジェクト

fadeout            ミリ単位秒で指定された時間で音声をフェードアウトします
get_busy           音声が現在のチャンネルで再生されている場合はTrueを返します
get_endevent       サウンドの再生が終了したときに送信されるイベント、または終了したイベントがない場合はNOEVENTを返します
get_queue          再生のためのキューに入っている任意のサウンド、またはキューに入られた音声がない場合はNoneを返します
get_volume         0.0~1.0の単一の値としてチャンネルの現在の音量を取得します(ステレオ設定時は取得できない)
pause              このチャンネル上の任意の音声の再生を一時停止します
play               特定のチャンネルを再生します。Soundオブジェクトと繰り返し回数と最大再生時間を引数に取ります
queue              現在の音声が終了したときに特定の与えられた音声を再生します。引数にキューに入れるためのSoundオブジェクトを取ります
set_endevent       現在の音声の再生が終了したときにイベントを要求します。送信のためのイベントのidを引数に取ります。
                   idは既存のイベントとの衝突を避けるため、USEREVENT(pygame.localsの定数)以上の値にしなければいけません。
                   引数を指定しない場合は終了イベントの送信は行いません
set_volume         このチャンネルの音量を設定します。1つの値が指定されてる場合は、両方のスピーカに使用されます。
                   2つの値が指定された場合は左右のスピーカの音量が独立して設定されます。どちらも引数に0.0~1.0の値を受け取ります
stop               即座にチャンネル上のあらゆる音声の再生を停止します
unpause            一時停止しているチャンネルの再生を再開します

ステレオ効果を付けた音声再生デモ

※「Pythonゲームプログラミング入門」のp263~266サンプルコードより音声制御部分を引用。

(略)

def stereo_pan(x_coord, screen_width):
    right_volume = float(x_coord) / screen_width #画面の幅に対する現在のx座標位置の割合を、右スピーカのボリュームとして取得
    left_volume = 1.0 - right_volume              #1から右スピーカのボリュームを引いて、左スピーカのボリュームとして取得
    return (left_volume, right_volume)

class Ball(object):
    def __init__(self, position, speed, image, bounce_sound):
        self.position = Vector2(position)
        self.speed = Vector2(speed)
        self.image = image
        self.bounce_sound = bounce_sound
        self.age = 0.0
(略)
    def play_bounce_sound(self):
        channel = self.bounce_sound.play()

        if channel is not None:
            # Get the left and right volumes
            left, right = stereo_pan(self.position.x, SCREEN_SIZE[0])
            channel.set_volume(left, right)        # チャンネルに取得したボリュームを代入
(略)

def run():

    # 44KHz、16ビットのステレオ音声で初期化
    pygame.mixer.pre_init(44100, 16, 2, 1024*4)
    pygame.init()
    pygame.mixer.set_num_channels(8)              # チャンネル数を8に設定

(略)
    # 音声ファイルのロード
    bounce_sound = pygame.mixer.Sound("bounce.wav") # 音声ファイルをロードする
(略)

3.pygame.mixer.musicモジュール

pygame.mixer.musicはオブジェクトを作らない!?

pygame.mixerモジュールでは音楽ファイルを鳴らすことも可能ですが、ファイルを一度全部読み込んでから再生するので、
短いジングル程度ならともかく、BGMとして使うにはコンピュータのリソースに負担をかけてしまう傾向があり、
通常は音楽の再生には向いていません。
BGMの再生にはファイルを少しずつ読み込んで再生(ストリーミング)するpygame.mixer.musicサブモジュールが適しています。
pygame.mixer.musicモジュールはMP3とOgg形式の音楽ファイルを再生できます。
MP3のサポートはプラットフォームに依存するので各プラットフォームで
十分にサポートされてるOggを使用するのがいいでしょう。


音楽ファイルを読み込み、再生するには

pygame.mixer.music.load("music.ogg")
pygame.mixer.music.play()

と書きます。
ここで、pygame.mixerモジュールと違うのは読み込み時にオブジェクトを返さないところです。
複数の音楽ファイルを同時にストリーミングすることができないからです。
ロードしたファイルの操作はpygame.mixer.musicモジュールで行います。

pygame.mixer.musicモジュールで利用出来るメソッド

get_busy           現在音楽が再生されている場合にTrueを返します
fadeout            一定時間音量を減らします。引数にミリ単位秒の整数を取ります
get_endevent       送信された終了イベントを返します。またはイベントがないときは0を返します
get_volume         音楽の音量を返します。
load               再生する音楽ファイルを指定します。引数にオーディオファイルを取ります
play               loadで指定された音楽ファイルの再生を開始します。引数にループ回数と再生開始位置(秒単位)を取ります。
                   ループ回数に-1を指定した場合stopを呼び出すまでループし続けます
rewind             引数で指定した開始位置(秒単位)から音楽ファイルを再生します
set_endevent       音楽の再生が終了したときに送信できるイベントを要求します。送信するイベントのidを引数に取ります。
                   既存のイベントとの競合を避けるため、USEREVENT(pygame.localsの定数)以上でなければいけません。
                   引数がない場合、終了イベントを送信しません
set_volume         音楽の音量を設定します。0.0~1.0の値を引数に取ります。新しい音楽が読み込まれると、1.0にリセットされます
stop               音楽の再生を停止します。
unpause            一時停止している音楽を再生します
get_pos            再生している音楽の再生時間をミリ単位秒で返します
pause              音楽の再生を一時停止します
queue              再生中の音楽が終了したときに再生される曲を指定します。引数に再生したい音楽ファイルを取ります

BGM再生デモサンプル

※「Pythonゲームプログラミング入門」のp269~273サンプルコードより音楽制御部分を引用。

(略)
def run():
    # 44KHz、16ビットのステレオ音声で初期化
    pygame.mixer.pre_init(44100, 16, 2, 1024*4)
    pygame.init()

(略)
    # 最初のトラックをロードする
    pygame.mixer.music.load(music_filenames[current_track])

    clock = pygame.time.Clock()
    
    playing = False
    paused = False

    # 音楽のトラックが終わったときにこのイベントが送信される
    TRACK_END = USEREVENT + 1
    pygame.mixer.music.set_endevent(TRACK_END)

    while True:
    
        button_pressed = None

        for event in pygame.event.get():
        
            if event.type == QUIT:
                return 

            if event.type == MOUSEBUTTONDOWN:
                
                # 押下されたボタンを探す
                for button_name, button in buttons.iteritems():
                    if button.is_over(event.pos):
                        print button_name, "pressed"
                        button_pressed = button_name
                        break

            
            if event.type == TRACK_END:
                # 曲が終了したとき、'次へ(next)'ボタンの押下をシミュレートする
                button_pressed = "next"

        if button_pressed is not None:
        
            # '次へ(next)'ボタンが押下されたとき、次の曲へ進む
            if button_pressed == "next":
                current_track = (current_track + 1) % max_tracks
                pygame.mixer.music.load(music_filenames[current_track])
                if playing:
                    pygame.mixer.music.play()

            # '前へ(prev)'ボタンが押下されたとき、前の曲に戻る
            elif button_pressed == "prev":

                # 曲が3秒より長く再生されたときは巻き戻し
                # そうでなければ、前の曲を選択する
                if pygame.mixer.music.get_pos() > 3000:
                    pygame.mixer.music.stop()
                    pygame.mixer.music.play()
                else:
                    current_track = (current_track - 1) % max_tracks
                    pygame.mixer.music.load(music_filenames[current_track])
                    if playing:
                        pygame.mixer.music.play()

            # '一時停止(pause)'ボタンが押下されたとき、一時停止。もう一度押下されたら、停止位置から再生
            elif button_pressed == "pause":
                if paused:
                    pygame.mixer.music.unpause()
                    paused = False
                else:
                    pygame.mixer.music.pause()
                    paused = True
            
            # '停止(stop)'ボタンが押下されたとき、曲の再生を止める
            elif button_pressed == "stop":
                pygame.mixer.music.stop()
                playing = False

            # '再生(play)'ボタンが押下されたとき、曲を再生する
            elif button_pressed == "play":
                if paused:
                    pygame.mixer.music.unpause()
                    paused = False

                else:
                    pygame.mixer.music.play()
                    playing = True

(略)

4.今後の展望

こんなアプリが作れるかも

音声を読み込んで再生させるだけなら、アドベンチャーゲーム等のは作れるでしょう。
しかし、ただ再生させるだけでなく、どう再生させるかで音声再生の用途の幅は増えていきます。
Pygameだからといって、ゲームにしか使えないというわけではなく、ゲーム以外でも活躍はできると思います。


pygame.mixerモジュールを使えば例えば、
使用者の向いている方向を計算し正しい経路のある方向から音が鳴るという、
視覚障害の方のための音声案内ナビゲーションアプリが作れるのではないでしょうか。


また、そのナビゲーションアプリをゲーム側にフィードバックすれば、
目標物を発生する音を頼りに探す、宝探しゲーム等も作れそうです。