Skip to main content

03 SEとBGMを別扱いに(なんで?)

第3回 効果音とBGMを分ける

目標

この回では、Audio機能を次の2種類に分けます。

SE  : 効果音。短い音。何度も鳴る。
BGM : 背景音楽。長い音。基本的に1曲を流し続ける。

最終的に、次のように使える形にします。

file_path shotPath = "Assets/Audio/SE/shot.wav";
file_path stagePath = "Assets/Audio/BGM/stage.wav";

Audio::LoadSE("shot", shotPath);
Audio::LoadBGM("stage", stagePath);

Audio::PlaySE("shot");
Audio::PlayBGM("stage", true);

今回変更するファイル

Engine/Audio.h
Engine/Audio.cpp
TestScene.cpp

使用する音声ファイルを次の場所に用意してください。

Assets/Audio/SE/shot.wav
Assets/Audio/SE/damage.wav
Assets/Audio/BGM/stage.wav

ファイル名は、資料内のコードと合わせてください。


1. Engine/Audio.h を変更する

Engine/Audio.h を次のように変更します。

#pragma once
#include <string>
#include <filesystem>

using file_path = std::filesystem::path;

namespace Audio
{
    bool Initialize();
    void Update();
    void Release();

    bool LoadSE(const std::string& name, file_path& filepath);
    void PlaySE(const std::string& name);

    bool LoadBGM(const std::string& name, file_path& filepath);
    void PlayBGM(const std::string& name, bool loop = true);
    void StopBGM();
}

file_path の使い方は第2回と同じです。

file_path shotPath = "Assets/Audio/SE/shot.wav";
Audio::LoadSE("shot", shotPath);

LoadSE("shot", "Assets/Audio/SE/shot.wav") のように直接文字列を渡すのではなく、いったん file_path 変数に入れてから渡します。

第2回で作った LoadPlay は、今回から使いません。

Load    → LoadSE / LoadBGM に分ける
Play    → PlaySE / PlayBGM に分ける

2. Engine/Audio.cpp を変更する

Engine/Audio.cpp を次のように変更します。

#include "Audio.h"
#include <Audio.h>
#include <memory>
#include <unordered_map>

namespace
{
    std::unique_ptr<DirectX::AudioEngine> audioEngine_;

    std::unordered_map<std::string, std::unique_ptr<DirectX::SoundEffect>> seSounds_;
    std::unordered_map<std::string, std::unique_ptr<DirectX::SoundEffect>> bgmSounds_;

    std::unique_ptr<DirectX::SoundEffectInstance> currentBGM_;
    std::string currentBGMName_;
}

bool Audio::Initialize()
{
    audioEngine_ = std::make_unique<DirectX::AudioEngine>();
    return true;
}

void Audio::Update()
{
    if (audioEngine_ != nullptr)
    {
        audioEngine_->Update();
    }
}

void Audio::Release()
{
    currentBGM_.reset();
    bgmSounds_.clear();
    seSounds_.clear();
    audioEngine_.reset();
}

bool Audio::LoadSE(const std::string& name, file_path& filepath)
{
    if (audioEngine_ == nullptr)
    {
        return false;
    }

    seSounds_[name] = std::make_unique<DirectX::SoundEffect>(
        audioEngine_.get(),
        filepath.c_str()
    );

    return true;
}

void Audio::PlaySE(const std::string& name)
{
    auto it = seSounds_.find(name);
    if (it == seSounds_.end())
    {
        return;
    }

    it->second->Play();
}

bool Audio::LoadBGM(const std::string& name, file_path& filepath)
{
    if (audioEngine_ == nullptr)
    {
        return false;
    }

    bgmSounds_[name] = std::make_unique<DirectX::SoundEffect>(
        audioEngine_.get(),
        filepath.c_str()
    );

    return true;
}

void Audio::PlayBGM(const std::string& name, bool loop)
{
    auto it = bgmSounds_.find(name);
    if (it == bgmSounds_.end())
    {
        return;
    }

    if (currentBGM_ != nullptr)
    {
        currentBGM_->Stop(true);
        currentBGM_.reset();
    }

    currentBGM_ = it->second->CreateInstance();
    currentBGM_->Play(loop);
    currentBGMName_ = name;
}

void Audio::StopBGM()
{
    if (currentBGM_ != nullptr)
    {
        currentBGM_->Stop(true);
        currentBGM_.reset();
        currentBGMName_.clear();
    }
}

3. SoundEffect と SoundEffectInstance の違い

今回から SoundEffectInstance が出てきます。

SoundEffect          : 音声ファイルのデータ
SoundEffectInstance  : 実際に再生している音

効果音は、単純に PlaySE で鳴らします。

Audio::PlaySE("shot");

BGMは、再生中の音を止めたり、ループしたりする必要があります。 そのため、SoundEffectInstance を使います。

currentBGM_ = it->second->CreateInstance();
currentBGM_->Play(loop);

4. TestScene.cpp で使う

TestScene.cppInitialize() を次のように変更します。

void TestScene::Initialize()
{
    Instantiate<Stage>(this);

    file_path shotPath = "Assets/Audio/SE/shot.wav";
    file_path damagePath = "Assets/Audio/SE/damage.wav";
    file_path stagePath = "Assets/Audio/BGM/stage.wav";

    Audio::LoadSE("shot", shotPath);
    Audio::LoadSE("damage", damagePath);
    Audio::LoadBGM("stage", stagePath);

    Audio::PlayBGM("stage", true);
}

Update() を次のように変更します。

void TestScene::Update()
{
    if (Input::IsKeyDown(DIK_SPACE))
    {
        Audio::PlaySE("shot");
    }

    if (Input::IsKeyDown(DIK_D))
    {
        Audio::PlaySE("damage");
    }

    if (Input::IsKeyDown(DIK_B))
    {
        Audio::StopBGM();
    }
}

5. ビルドして実行する

確認すること:

起動するとBGMがループ再生される
スペースキーでshot.wavが鳴る
Dキーでdamage.wavが鳴る
BキーでBGMが止まる

よくある失敗

BGMが鳴らない

ファイルパスを確認してください。

"Assets/Audio/BGM/stage.wav"

BGMが一瞬で止まる

currentBGM_ をローカル変数にしていると、関数終了時に消えてしまいます。 今回のコードでは、ファイル上部の namespace 内に置いています。

std::unique_ptr<DirectX::SoundEffectInstance> currentBGM_;

効果音を連打すると音が重なる

これは正常です。 効果音は、短い音を何度も鳴らす用途なので、同じ音が重なって鳴っても問題ありません。


今回の完成状態

この回で、Audio機能は次の形になりました。

file_path shotPath = "Assets/Audio/SE/shot.wav";
file_path stagePath = "Assets/Audio/BGM/stage.wav";

Audio::LoadSE("shot", shotPath);
Audio::PlaySE("shot");

Audio::LoadBGM("stage", stagePath);
Audio::PlayBGM("stage", true);
Audio::StopBGM();

次回は、マスター音量、SE音量、BGM音量を分けて管理します。