Pygameサウンドプログラミング: set_endeventのよくあるエラーと解決策

2025-05-31

以下に詳しく説明します。

mixer.Channel.set_endevent() とは?

Pygameのサウンドシステムでは、同時に複数のサウンドを再生するために「チャンネル」という概念を使用します。pygame.mixer.Channelオブジェクトは、個々のサウンド再生を制御するためのものです。

set_endevent() メソッドは、このチャンネルで再生されているサウンドが終了したときに、特定のイベントがPygameのイベントキューにポストされるように設定します。

使い方

set_endevent() メソッドには、引数としてイベントのタイプ(整数値)を渡します。

import pygame

# Pygameの初期化
pygame.init()
pygame.mixer.init()

# 画面設定(例として)
screen = pygame.display.set_mode((600, 400))
pygame.display.set_caption("Channel End Event Example")

# サウンドの読み込み
try:
    sound = pygame.mixer.Sound("your_sound_file.wav") # あなたのサウンドファイルに置き換えてください
except pygame.error as e:
    print(f"サウンドファイルの読み込みに失敗しました: {e}")
    pygame.quit()
    exit()

# カスタムイベントの定義
# pygame.USEREVENT はユーザー定義イベントの開始点です。
# 他のPygameのイベントと衝突しないように、USEREVENTに数値を加算して使います。
SOUND_ENDED_EVENT = pygame.USEREVENT + 1

# サウンドを再生するチャンネルを取得
# find_channel() は利用可能なチャンネルを検索します。
channel = pygame.mixer.find_channel()

if channel:
    # チャンネルに終了イベントを設定
    channel.set_endevent(SOUND_ENDED_EVENT)

    # サウンドを再生
    channel.play(sound)
    print("サウンドが再生を開始しました。")
else:
    print("利用可能なチャンネルがありません。")

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == SOUND_ENDED_EVENT:
            # SOUND_ENDED_EVENTが検出されたら実行されるコード
            print("サウンドの再生が終了しました!")
            # ここで次のサウンドを再生したり、他の処理を行ったりできます
            # 例: 同じサウンドを再度再生 (無限ループになるので注意)
            # channel.play(sound)

    # ゲームの更新や描画(この例では省略)
    # pygame.display.flip()

pygame.quit()

ポイント

  1. イベントタイプの設定: channel.set_endevent(イベントタイプ) のように、整数値を指定します。Pygameの組み込みイベント(pygame.QUITpygame.KEYDOWNなど)と衝突しないように、pygame.USEREVENT をベースにして、それに1以上の数値を加算した値を使用するのが一般的です。

  2. イベントの検知: メインループ内で pygame.event.get() を使ってイベントキューからイベントを取得し、event.type が設定したカスタムイベントタイプと一致するかどうかをチェックします。

  3. イベントが発火するタイミング: 設定されたイベントは、サウンドが自然に最後まで再生されたときに発火します。channel.stop()channel.fadeout() などで明示的に再生を停止した場合には、イベントが発火しないことがあります(ただし、Pygameのバージョンや内部実装によっては発火する場合もあるため、テストで確認することが重要です)。

  4. イベントを停止する: イベントの送信を停止したい場合は、引数なしで channel.set_endevent() を呼び出します。 channel.set_endevent()

pygame.mixer.Channel.set_endevent() と似た機能として、pygame.mixer.music.set_endevent() があります。

  • pygame.mixer.music: 長いBGMやストリーミング形式の音楽ファイル(MP3、OGGなど)の再生に特化しています。通常、一度に一つの音楽ファイルしか再生できません。set_endeventは音楽が終了したときに発火します。
  • pygame.mixer.Channel: 短い効果音や複数のサウンドを同時に再生するのに適しています。それぞれのサウンドは個別のチャンネルで管理され、set_endeventもチャンネルごとに設定されます。


pygame.mixer.init() または pygame.init() の忘れ

エラーの原因
サウンドシステムを使用する前に、pygame.mixer.init() または pygame.init() を呼び出してPygameのミキサーモジュールを初期化していない。

よくある症状

  • pygame.mixer.find_channel()None を返す。
  • サウンドが再生されない。
  • pygame.error: mixer not initialized のようなエラーメッセージが表示される。

トラブルシューティング
スクリプトの冒頭、特にサウンドをロードしたり再生したりする前に、必ず pygame.init() または pygame.mixer.init() を呼び出してください。

import pygame

pygame.init() # または pygame.mixer.init()
# その他のコード

サウンドファイルが見つからない、またはサポートされていない形式

エラーの原因
指定されたサウンドファイルが存在しない、パスが間違っている、またはPygameがサポートしていないオーディオ形式(例: MP3をサポートしないPygameのビルド、破損したファイル)。

よくある症状

  • サウンドが再生されない。
  • pygame.mixer.Sound() が例外を発生させる。
  • pygame.error: Couldn't open 'your_sound_file.wav' のようなエラー。

トラブルシューティング

  • ファイルが破損していないか
    他のメディアプレイヤーで再生できるか確認してください。
  • ファイル形式の確認
    PygameはWAV、OGGを安定してサポートしています。MP3は一部のPygameビルドではサポートされていない場合があります。変換ツール(Audacityなど)でWAVやOGGに変換してみてください。
  • ファイル名の確認
    スペルミスがないか確認してください。
  • ファイルパスの確認
    サウンドファイルのパスが正しいことを確認してください。絶対パスを使用するか、スクリプトと同じディレクトリにファイルを置くことを検討してください。

find_channel() が None を返す

エラーの原因
pygame.mixer.get_num_channels() で設定されているチャンネル数よりも多くのサウンドを同時に再生しようとしているか、ミキサーが初期化されていない。

よくある症状

  • サウンドが再生されない。
  • channel.set_endevent() を呼び出す前に channelNone であることに気づかず、AttributeError: 'NoneType' object has no attribute 'set_endevent' が発生する。

トラブルシューティング

  • find_channel() の結果をチェック
    set_endevent を呼び出す前に、find_channel() が有効なチャンネルオブジェクトを返したかどうかを確認してください。
  • チャンネル数の確認
    pygame.mixer.get_num_channels() で現在のチャンネル数を確認し、必要であれば pygame.mixer.set_num_channels(desired_channels) で増やしてください。デフォルトは8チャンネルです。
channel = pygame.mixer.find_channel()
if channel: # channelがNoneでないことを確認
    channel.set_endevent(SOUND_ENDED_EVENT)
    channel.play(sound)
else:
    print("利用可能なチャンネルがありません。")

set_endevent イベントが発火しない

エラーの原因

  • 設定したイベントタイプとイベントループでチェックしているタイプが一致しない。
  • イベントキューが適切に処理されていない。
  • サウンドが最後まで再生されていない(途中で停止した)。

よくある症状

  • サウンドは再生されるが、「サウンド終了」のメッセージやそれに関連する処理が実行されない。

トラブルシューティング

  • イベントタイプの不一致
    set_endevent() に渡したカスタムイベントの定数と、イベントループ内で event.type == YOUR_EVENT_CONSTANT で比較している定数が完全に一致していることを確認してください。pygame.USEREVENT + NN の値に注意してください。
  • イベントループの確認
    pygame.event.get()pygame.event.pump() がメインループ内で定期的に呼び出されていることを確認してください。これらがないと、イベントキューにイベントが蓄積されません。
  • サウンドの長さとループ
    サウンドが最後まで再生されるようにしてください。channel.play(sound) の後のコードで channel.stop()channel.fadeout() をすぐに呼び出していないか確認してください。ループ再生している場合(例: channel.play(sound, -1))は、手動で停止しない限りイベントは発火しません。
# イベントループの例
running = True
while running:
    for event in pygame.event.get(): # これが重要
        if event.type == pygame.QUIT:
            running = False
        elif event.type == SOUND_ENDED_EVENT: # 一致しているか確認
            print("サウンドの再生が終了しました!")

set_endevent を呼び出しても、即座にイベントがポストされる

エラーの原因
サウンドが非常に短いか、再生速度が速すぎるために、set_endevent が設定された直後にサウンドが終了し、イベントが即座にポストされることがある。

よくある症状

  • サウンドが再生開始された直後に、「サウンド終了」のメッセージが表示される。

トラブルシューティング
これは厳密にはエラーではありませんが、意図しない動作に感じるかもしれません。

  • イベントハンドリングのロジック
    サウンド終了イベントが発生したときに、そのイベントが本当に期待されるタイミングで発生したかどうかを判断するための追加のロジック(例: 再生開始フラグ)を組み込むことを検討してください。
  • サウンドの長さ
    デバッグのために、より長いサウンドファイルでテストしてみてください。

エラーの原因
複数のチャンネルで同じ SOUND_ENDED_EVENT を設定している場合、どのチャンネルのサウンドが終了したのかを判別できない。

よくある症状

  • 特定のサウンドが終了したと期待していたのに、別のサウンドの終了イベントが検出される。
  • チャンネルごとのユニークなイベント
    各チャンネルに対してユニークな USEREVENT を割り当てることで、どのチャンネルのサウンドが終了したかを正確に識別できます。
SOUND1_ENDED_EVENT = pygame.USEREVENT + 1
SOUND2_ENDED_EVENT = pygame.USEREVENT + 2

channel1 = pygame.mixer.find_channel()
if channel1:
    channel1.set_endevent(SOUND1_ENDED_EVENT)
    channel1.play(sound1)

channel2 = pygame.mixer.find_channel()
if channel2:
    channel2.set_endevent(SOUND2_ENDED_EVENT)
    channel2.play(sound2)

# イベントループでevent.typeをチェック
if event.type == SOUND1_ENDED_EVENT:
    print("サウンド1が終了しました!")
elif event.type == SOUND2_ENDED_EVENT:
    print("サウンド2が終了しました!")
  • イベントオブジェクトの channel 属性 (Pygame 2.0.0 以降)
    Pygame 2.0.0以降では、mixer.Channel.set_endevent() でポストされるイベントオブジェクトに、どのチャンネルからイベントが発火したかを示す channel 属性が含まれるようになりました。これを利用することで、同じイベントタイプを使用しても、どのチャンネルのサウンドが終了したかを特定できます。
# pygame.USEREVENT + 1 を両方のチャンネルで設定する例
# channel1.set_endevent(SOUND_ENDED_EVENT)
# channel2.set_endevent(SOUND_ENDED_EVENT)

# イベントループでevent.channelをチェック
if event.type == SOUND_ENDED_EVENT:
    print(f"チャンネル {event.channel.get_id()} のサウンドが終了しました!")
    if event.channel == channel1:
        print("これはchannel1の終了イベントです。")
    elif event.channel == channel2:
        print("これはchannel2の終了イベントです。")


例1:単一のサウンド再生終了イベントの基本

これは最も基本的な使用例で、一つのサウンドが再生を終了したときに特定の処理を実行します。

import pygame
import time # スリープのために使用

# 1. Pygameの初期化
pygame.init()
pygame.mixer.init() # ミキサーモジュールの初期化

# 2. 画面のセットアップ (任意だが、Pygameのイベントループには必要)
screen = pygame.display.set_mode((400, 300))
pygame.display.set_caption("単一サウンド終了イベント")

# 3. サウンドファイルの読み込み
# 実際には存在するWAVファイルに置き換えてください
# 例えば、短い効果音(例:'coin.wav')などが適しています
try:
    sound_file = "C:/Users/user/Desktop/test_sound.wav" # あなたのサウンドファイルのパスに置き換えてください
    sound = pygame.mixer.Sound(sound_file)
    print(f"'{sound_file}' を読み込みました。")
except pygame.error as e:
    print(f"サウンドファイルの読み込みに失敗しました: {e}")
    pygame.quit()
    exit()

# 4. カスタムイベントの定義
# pygame.USEREVENT はユーザー定義イベントの開始点
SOUND_ENDED_EVENT = pygame.USEREVENT + 1
print(f"カスタムイベントタイプ: {SOUND_ENDED_EVENT}")

# 5. サウンドを再生するチャンネルを取得
# find_channel() は利用可能なチャンネルを検索します
channel = pygame.mixer.find_channel()

if channel:
    # 6. チャンネルに終了イベントを設定
    channel.set_endevent(SOUND_ENDED_EVENT)
    print("チャンネルに終了イベントを設定しました。")

    # 7. サウンドの再生
    channel.play(sound)
    print("サウンドの再生を開始しました。再生終了を待ちます...")
else:
    print("利用可能なチャンネルがありません。ミキサーの初期化を確認してください。")
    pygame.quit()
    exit()

# 8. メインゲームループ
running = True
sound_played_message = False # 一度だけメッセージを表示するためのフラグ
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == SOUND_ENDED_EVENT:
            # 9. サウンド終了イベントを検知
            if not sound_played_message:
                print(">>> サウンドの再生が終了しました!イベントを検知しました。 <<<")
                sound_played_message = True
            # ここで次のアクションを実行できます(例:別のサウンドを再生、ゲームの状態を変更など)
            # もし、サウンド終了後に何か特別な処理をしたい場合はここに記述します。

    # イベントループが非常に速く回るため、CPU使用率を抑える
    pygame.time.Clock().tick(60) # 1秒あたり60フレームに制限

# 10. Pygameの終了処理
pygame.quit()
print("Pygameが終了しました。")

説明

  1. 初期化: pygame.init()pygame.mixer.init() は必須です。
  2. サウンド読み込み: pygame.mixer.Sound() でサウンドファイルをロードします。パスを正しく指定してください。
  3. カスタムイベント: pygame.USEREVENT + N で独自のイベントタイプを定義します。他のPygameイベントと衝突しないように、USEREVENT を基点とします。
  4. チャンネルの取得: pygame.mixer.find_channel() で空いているチャンネルを取得します。チャンネルがない場合はエラーハンドリングが必要です。
  5. set_endevent(): 取得したチャンネルに対して channel.set_endevent(SOUND_ENDED_EVENT) を呼び出します。これにより、このチャンネルで再生されているサウンドが終了したときに SOUND_ENDED_EVENT がイベントキューにポストされます。
  6. イベントループ: メインループ内で pygame.event.get() を使ってイベントキューからイベントを取り出し、event.type == SOUND_ENDED_EVENT で終了イベントをチェックします。

例2:複数のサウンドと各チャンネルの終了イベントの区別 (Pygame 2.0.0以降推奨)

複数のサウンドを同時に再生し、どのサウンドが終了したかを識別したい場合、event.channel 属性が非常に便利です。

import pygame
import time

pygame.init()
pygame.mixer.init()
pygame.mixer.set_num_channels(8) # チャンネル数を増やす

screen = pygame.display.set_mode((600, 400))
pygame.display.set_caption("複数サウンド終了イベント (Pygame 2.0+)")

# サウンドファイルの読み込み
# 実際には存在するWAVファイルに置き換えてください
try:
    sound1 = pygame.mixer.Sound("C:/Users/user/Desktop/sound1.wav") # 短い音
    sound2 = pygame.mixer.Sound("C:/Users/user/Desktop/sound2.wav") # やや長い音
    sound3 = pygame.mixer.Sound("C:/Users/user/Desktop/sound3.wav") # もっと長い音
    print("3つのサウンドを読み込みました。")
except pygame.error as e:
    print(f"サウンドファイルの読み込みに失敗しました: {e}")
    pygame.quit()
    exit()

# カスタムイベントの定義 (同じイベントタイプを使用)
SOUND_ENDED_COMMON_EVENT = pygame.USEREVENT + 1
print(f"共通カスタムイベントタイプ: {SOUND_ENDED_COMMON_EVENT}")

# チャンネルの取得とサウンドの再生
channels = []
sounds = [sound1, sound2, sound3]
channel_ids = {} # チャンネルオブジェクトを識別するための辞書

for i, snd in enumerate(sounds):
    channel = pygame.mixer.find_channel()
    if channel:
        channel.set_endevent(SOUND_ENDED_COMMON_EVENT) # 全てのチャンネルで同じイベントタイプを設定
        channel.play(snd)
        channels.append(channel)
        channel_ids[channel] = f"Sound_{i+1}" # チャンネルオブジェクトと名前を関連付け
        print(f"{channel_ids[channel]} (チャンネルID: {channel.get_id()}) の再生を開始しました。")
    else:
        print(f"サウンド {i+1} のためのチャンネルが見つかりませんでした。")

# メインゲームループ
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == SOUND_ENDED_COMMON_EVENT:
            # Pygame 2.0.0以降では、eventオブジェクトに 'channel' 属性が含まれる
            if hasattr(event, 'channel'):
                terminated_channel = event.channel
                channel_name = channel_ids.get(terminated_channel, "不明なチャンネル")
                print(f">>> {channel_name} (チャンネルID: {terminated_channel.get_id()}) の再生が終了しました! <<<")
                # 必要に応じて、終了したチャンネルを識別して次のアクションを実行
                if terminated_channel == channels[0]: # 例えばsound1のチャンネル
                    print("サウンド1の終了イベントです。")
                elif terminated_channel == channels[1]: # 例えばsound2のチャンネル
                    print("サウンド2の終了イベントです。")
            else:
                print(">>> サウンドの再生が終了しました! (channel属性なし - Pygame 1.x かもしれません) <<<")

    pygame.time.Clock().tick(60)

pygame.quit()
print("Pygameが終了しました。")

説明

  1. 共通イベントタイプ: 全てのチャンネルで同じ SOUND_ENDED_COMMON_EVENT を設定します。
  2. event.channel: Pygame 2.0.0以降のバージョンでは、set_endevent でポストされたイベントオブジェクトには、そのイベントを発生させた Channel オブジェクトが event.channel として含まれます。
  3. チャンネルの識別: event.channel を利用して、どのサウンドが終了したかをプログラム的に識別し、それに応じた処理を行うことができます。channel.get_id() も識別に使えます。
  4. hasattr(event, 'channel'): Pygame 1.x系では event.channel 属性が存在しないため、互換性を保つためにこのチェックを入れると良いでしょう。

サウンドが終了したことをトリガーとして、次のサウンドを自動的に再生する例です。BGMのループや効果音の連鎖などに使えます。

import pygame
import time

pygame.init()
pygame.mixer.init()
pygame.mixer.set_num_channels(2) # 少なくとも2つのチャンネルが必要

screen = pygame.display.set_mode((400, 300))
pygame.display.set_caption("サウンドシーケンス再生")

try:
    intro_sound = pygame.mixer.Sound("C:/Users/user/Desktop/intro_sound.wav") # 短いイントロ音
    main_loop_sound = pygame.mixer.Sound("C:/Users/user/Desktop/main_loop_sound.wav") # 長いループ音
    print("イントロとメインループのサウンドを読み込みました。")
except pygame.error as e:
    print(f"サウンドファイルの読み込みに失敗しました: {e}")
    pygame.quit()
    exit()

SOUND_ENDED_SEQUENCE_EVENT = pygame.USEREVENT + 1
print(f"シーケンス終了イベントタイプ: {SOUND_ENDED_SEQUENCE_EVENT}")

current_channel = None # 現在再生しているチャンネル
is_intro_playing = False
is_main_loop_playing = False

def start_intro_sound():
    global current_channel, is_intro_playing
    channel = pygame.mixer.find_channel()
    if channel:
        channel.set_endevent(SOUND_ENDED_SEQUENCE_EVENT)
        channel.play(intro_sound)
        current_channel = channel
        is_intro_playing = True
        print("イントロサウンドの再生を開始しました。")
    else:
        print("イントロサウンドのためのチャンネルが見つかりませんでした。")

def start_main_loop_sound():
    global current_channel, is_main_loop_playing
    channel = pygame.mixer.find_channel() # 新しいチャンネルでも良いし、同じチャンネルでも良い
    if channel:
        # メインループサウンドは無限ループで再生するため、終了イベントは設定しない(通常)
        # もし特定の回数ループしたい場合は、その回数でplay()を呼び出すか、
        # event.type == SOUND_ENDED_SEQUENCE_EVENT でchannel.get_queue()などを確認して再度再生する
        channel.play(main_loop_sound, loops=-1) # -1 で無限ループ
        current_channel = channel # このチャンネルはメインループサウンド用として保持しても良い
        is_main_loop_playing = True
        print("メインループサウンドの再生を開始しました (無限ループ)。")
    else:
        print("メインループサウンドのためのチャンネルが見つかりませんでした。")

# 最初のサウンドを開始
start_intro_sound()

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == SOUND_ENDED_SEQUENCE_EVENT:
            if is_intro_playing:
                # イントロサウンドが終了した
                print("イントロサウンドが終了しました。メインループサウンドを開始します。")
                is_intro_playing = False
                start_main_loop_sound() # 次のサウンドを開始
            # else: 別のシーケンスサウンドの終了イベントがあればここで処理

    pygame.time.Clock().tick(60)

# メインループサウンドが再生中の場合は停止
if pygame.mixer.get_busy():
    pygame.mixer.stop()

pygame.quit()
print("Pygameが終了しました。")
  1. シーケンスロジック: start_intro_sound() で最初のサウンドを再生し、そのチャンネルに終了イベントを設定します。
  2. イベントトリガー: SOUND_ENDED_SEQUENCE_EVENT が発生すると、それがイントロサウンドの終了であることを確認し、start_main_loop_sound() を呼び出して次のサウンド(メインループサウンド)を開始します。
  3. 無限ループ: メインループサウンドは loops=-1 で無限ループ再生されます。この場合、set_endevent は通常設定しません。なぜなら、自然な終了がないためです。もしBGMを特定回数ループさせたい場合は、その回数をplay()の第二引数に指定し、イベントが発火したら再度play()を呼び出すなどのロジックが必要です。


mixer.Channel.get_busy() と mixer.Channel.get_sound() をポーリングする

最も直接的な代替方法は、ゲームループ内で定期的にチャンネルの状態をチェックすることです。

方法

  1. サウンドを再生する前に、そのサウンドが再生されるチャンネルオブジェクトを保持します。
  2. ゲームループ内で、そのチャンネルの get_busy() メソッドを呼び出し、サウンドが再生中かどうかを確認します。
  3. もし get_busy()False を返したら、サウンドの再生が終了したと判断できます。
  4. (任意)get_sound() を使って、そのチャンネルで最後に再生されたサウンドが期待通りのものであるかを確認できます。

利点

  • Pygameの古いバージョン(event.channel 属性がないなど)でも動作します。
  • イベントシステムを理解する必要がないため、概念的にシンプルです。

欠点

  • 複数の異なるサウンドを同じチャンネルで素早く再生する場合、どのサウンドが終了したのかを正確に追跡するのが難しくなります。
  • 多数のチャンネルをポーリングする場合、コードが冗長になったり、パフォーマンスにわずかな影響を与える可能性があります。
  • イベントドリブンではないため、サウンド終了を即座に検知するのではなく、ループの更新間隔に依存します。

コード例

import pygame
import time

pygame.init()
pygame.mixer.init()
pygame.mixer.set_num_channels(8) # 複数チャンネル

screen = pygame.display.set_mode((400, 300))
pygame.display.set_caption("ポーリングによるサウンド終了検知")

try:
    sound_a = pygame.mixer.Sound("C:/Users/user/Desktop/sound_a.wav") # 短い音
    sound_b = pygame.mixer.Sound("C:/Users/user/Desktop/sound_b.wav") # やや長い音
except pygame.error as e:
    print(f"サウンドファイルの読み込みに失敗しました: {e}")
    pygame.quit()
    exit()

# 再生中のサウンドとそのチャンネルを追跡するための辞書
# key: channelオブジェクト, value: サウンドの名前や関連情報
active_channels = {}

def play_sound_and_track(sound_obj, name):
    channel = pygame.mixer.find_channel()
    if channel:
        channel.play(sound_obj)
        active_channels[channel] = {'name': name, 'sound_obj': sound_obj}
        print(f"'{name}' の再生を開始しました。")
    else:
        print(f"'{name}' のためのチャンネルが見つかりませんでした。")

# サウンドの再生を開始
play_sound_and_track(sound_a, "サウンドA")
# 少し遅れて別のサウンドを開始
pygame.time.wait(500)
play_sound_and_track(sound_b, "サウンドB")

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # ポーリングによるサウンド終了検知
    # active_channels のコピーをイテレートすることで、ループ中に要素を削除できるようにする
    channels_to_remove = []
    for channel, info in active_channels.items():
        if not channel.get_busy(): # サウンドが再生中ではない
            # 再生が終了したことを確認
            # channel.get_sound() が None か、または再生していたサウンドと同じかを確認
            if channel.get_sound() is None or channel.get_sound() == info['sound_obj']:
                 print(f"--- ポーリング: '{info['name']}' の再生が終了しました! ---")
                 channels_to_remove.append(channel)

    for channel in channels_to_remove:
        del active_channels[channel]

    # すべてのサウンドが終了したら終了
    if not active_channels and not pygame.mixer.get_busy():
        print("全てのサウンドの再生が終了しました。")
        running = False # 全てのサウンドが終了したらループを抜ける

    pygame.time.Clock().tick(60)

pygame.quit()
print("Pygameが終了しました。")

pygame.mixer.music の使用(BGMなど、単一の長いサウンドの場合)

ゲームのBGMなど、一度に1つの長いサウンドファイルだけを再生する場合には、pygame.mixer.music モジュールが適しています。これには独自の set_endevent() があります。

方法

  1. pygame.mixer.music.load() で音楽ファイルをロードします。
  2. pygame.mixer.music.play() で再生を開始します。
  3. pygame.mixer.music.set_endevent() でイベントを設定し、メインループで検知します。

利点

  • 専用の set_endevent() があるため、Channel を管理する必要がありません。
  • BGM再生に特化しており、よりメモリ効率が良い場合があります。

欠点

  • 一度に1つの音楽ファイルしか再生できません。複数の効果音やBGMのレイヤーには不向きです。

コード例

import pygame
import time

pygame.init()
pygame.mixer.init()

screen = pygame.display.set_mode((400, 300))
pygame.display.set_caption("Musicモジュールによる終了検知")

try:
    # 音楽ファイルはWAV, OGG, MP3など(Pygameのビルドによる)
    music_file = "C:/Users/user/Desktop/background_music.ogg" # あなたの音楽ファイルのパスに置き換えてください
    pygame.mixer.music.load(music_file)
    print(f"'{music_file}' を読み込みました。")
except pygame.error as e:
    print(f"音楽ファイルの読み込みに失敗しました: {e}")
    pygame.quit()
    exit()

MUSIC_ENDED_EVENT = pygame.USEREVENT + 2 # 別のカスタムイベントタイプ
pygame.mixer.music.set_endevent(MUSIC_ENDED_EVENT)
print(f"音楽終了イベントタイプ: {MUSIC_ENDED_EVENT}")

pygame.mixer.music.play() # 音楽の再生を開始
print("音楽の再生を開始しました。終了を待ちます...")

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == MUSIC_ENDED_EVENT:
            print(">>> 音楽の再生が終了しました!イベントを検知しました。 <<<")
            # 必要であれば、ここで次の音楽をロードして再生する
            # pygame.mixer.music.load("next_music.ogg")
            # pygame.mixer.music.play()
            running = False # デモのため、終了イベントでループを抜ける

    pygame.time.Clock().tick(60)

pygame.quit()
print("Pygameが終了しました。")

サウンド再生後にカスタムタイマーを設定する

サウンドの再生時間がおおよそ分かっている場合、pygame.time.set_timer() を使用して、サウンド終了が予想される時刻にカスタムイベントを発火させる方法です。

方法

  1. サウンドの長さを事前に把握します(pygame.mixer.Sound.get_length() で取得できます)。
  2. サウンドを再生した後、pygame.time.set_timer() を使って、そのサウンドの長さ(ミリ秒)後にカスタムイベントを発生させるように設定します。

利点

  • set_endevent() を使えない状況や、サウンドの長さに基づいて他のゲームイベントを同期させたい場合に便利です。

欠点

  • サウンドが途中で停止された場合でもイベントは発火します。
  • これは「サウンドが終了した」ことを直接検知するのではなく、「サウンドが終了するであろう時間」に基づいてイベントを発生させるため、厳密な同期はできません。サウンドの再生速度が変更されたり、システム負荷が高い場合、実際の終了時間とずれる可能性があります。
import pygame
import time

pygame.init()
pygame.mixer.init()

screen = pygame.display.set_mode((400, 300))
pygame.display.set_caption("タイマーによるサウンド終了検知")

try:
    sound_fx = pygame.mixer.Sound("C:/Users/user/Desktop/short_effect.wav")
    print("サウンドエフェクトを読み込みました。")
except pygame.error as e:
    print(f"サウンドファイルの読み込みに失敗しました: {e}")
    pygame.quit()
    exit()

SOUND_FX_ENDED_TIMER_EVENT = pygame.USEREVENT + 3

# サウンドの長さを取得 (秒単位)
sound_length_seconds = sound_fx.get_length()
sound_length_ms = int(sound_length_seconds * 1000)
print(f"サウンドの長さ: {sound_length_seconds:.2f}秒 ({sound_length_ms}ms)")

# サウンドを再生
channel = pygame.mixer.find_channel()
if channel:
    channel.play(sound_fx)
    print("サウンドエフェクトの再生を開始しました。")
    # 再生時間後にカスタムイベントを発生させるタイマーを設定
    pygame.time.set_timer(SOUND_FX_ENDED_TIMER_EVENT, sound_length_ms)
    print(f"{sound_length_ms}ms後にタイマーイベントが発生します。")
else:
    print("チャンネルが見つかりませんでした。")
    pygame.quit()
    exit()

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == SOUND_FX_ENDED_TIMER_EVENT:
            print(">>> タイマーイベントが発生しました! (サウンド終了を想定) <<<")
            pygame.time.set_timer(SOUND_FX_ENDED_TIMER_EVENT, 0) # タイマーを停止
            running = False # デモのため、終了イベントでループを抜ける

    pygame.time.Clock().tick(60)

pygame.quit()
print("Pygameが終了しました。")
  • pygame.time.set_timer(): サウンドの終了をゲーム内の他のイベントと同期させたいが、厳密なサウンド終了検知は不要な場合(例:アニメーションの開始など)に補助的に使用できます。
  • mixer.music.set_endevent(): ゲームのBGMなど、一度に1つの音楽ファイルだけを再生する場合に最適です。
  • mixer.Channel.get_busy() のポーリング: シンプルなスクリプトや、厳密な終了検知が不要な場合に利用できます。しかし、多数のチャンネルや頻繁なチェックが必要な場合は非効率になる可能性があります。
  • mixer.Channel.set_endevent(): 最も推奨される方法です。サウンドの終了を正確かつ効率的にイベントドリブンで検知できます。特に効果音など、複数の独立したサウンドの終了を追跡する場合に最適です。