【bevy】mp3の音源を再生できない時の対処法


はじめに

Rust製のゲームエンジンであるbevyを使ってゲームを作っていたのですが、mp3のSEを再生しようと

fn play_se(asset_server: Res<AssetServer>, audio: Res<Audio>) {
    let se_sound: Handle<AudioSource> = asset_server.load(SE_SOUND_PATH);
    audio.play(se_sound);
}

このようなplay_se()関数を定義してSEを再生したいタイミングでこの関数を実行しても音源が再生されず、

WARN bevy_asset::asset_server: no `AssetLoader` found for the following extension: mp3

このようなメッセージがコンソール上に表示されてしまいました。

本記事にはこの問題の対処法を記載しました。

筆者の環境

  • bevy 0.6.1
  • cargo 1.59.0
  • rustc 1.59.0

対処法

[dependencies]
bevy = {version = "0.6.1", features = ["mp3"]}

このようにCargo.tomlでmp3のfeatureを有効化すればOKです。
(しかしAppleStoreの審査でリジェクトされる、wasmでビルドできない、セキュリティに関連した問題が発生する、といったデメリットがあるので注意が必要です)

解説

bevyの0.5->0.6のアップデート時にデフォルトではmp3をサポートするのをやめ、代わりにvorbisをサポートすることにしたそうです。

  • mp3のロード処理はminimp3に依存しているが、このminimp3が内部で使用しているslice_deque内でiOSにおけるメモリアローケーションのための内部的なのapiを使用しているためAppleStoreの審査でリジェクトされてしまう(参考:https://github.com/bevyengine/bevy/issues/3145)
  • minimp3はrodioを通して利用しているが、rodio側にセキュリティに関連した問題やwasmでビルドできないという問題がある

というのが主な理由のようです。

bevyのコードを読んでみる

折角なので今回のエラーがどのようにして起きたのか、bevyのコードに少し潜って理解してみましょう。

bevyのCargo.tomlのfeaturesを確認してみます。

[features]
default = [
  "bevy_audio",
  "bevy_gilrs",
  "bevy_winit",
  "render",
  "png",
  "hdr",
  "vorbis",
  "x11",
  "filesystem_watcher",
]

defaultに"mp3"は含まれておらず、代わりに"vorbis"が含まれていますね。

次にaudio_source.rsのAudioLoader.extensions()を見てみましょう。

fn extensions(&self) -> &[&str] {
    &[
        #[cfg(feature = "mp3")]
        "mp3",
        #[cfg(feature = "flac")]
        "flac",
        #[cfg(feature = "wav")]
        "wav",
        #[cfg(feature = "vorbis")]
        "ogg",
    ]
}

"mp3"は無効になっているのでextensions()の戻り値には"mp3"は含まれないことになります。("vorbis"は有効になっているので戻り値に"ogg"が含まれることになります)

そして次にこのextensions()を呼び出しているasset_server::add_loader()を見てみましょう。

pub fn add_loader<T>(&self, loader: T)
where
    T: AssetLoader,
{
    let mut loaders = self.server.loaders.write();
    let loader_index = loaders.len();
    for extension in loader.extensions().iter() { // "mp3"は来ない
        self.server
            .extension_to_loader_index
            .write()
	    // extension_to_loader_indexに"mp3"はinsertされない
            .insert(extension.to_string(), loader_index); 
    }
    loaders.push(Arc::new(loader));
}

loader.extensions()の戻り値に"mp3"は含まれていないので、extension_to_loader_indexに"mp3"がinsertされません。

よって、asset_server::get_asset_loader()で

fn get_asset_loader(&self, extension: &str) -> Result<Arc<dyn AssetLoader>, AssetServerError> {
    // extensionが"mp3"の時、indexがNONEになる
    let index = {
        // scope map to drop lock as soon as possible
        let map = self.server.extension_to_loader_index.read();
	
	// mapには"mp3"が入っていないのでNONEがreturnされindexにNONEが入る
        map.get(extension).copied()
    };
    index
        .map(|index| self.server.loaders.read()[index].clone())
	// indexはNONEなのでErrになり、AssetServerError::MissingAssetLoaderがreturnされる
        .ok_or_else(|| AssetServerError::MissingAssetLoader {
            extensions: vec![extension.to_string()],
        })
}

この戻り値がAssetServerError::MissingAssetLoaderとなります。
(この後ももう少し続きはあるのですが、大体こんな感じです)

他の対処法

今回はmp3のfeatureを有効化することによって解決しましたが他の方法があります。
それは標準のbevy_audioではなくbevy_kira_audioという外部crateを使う、というものです。
bevy_kira_audioはkiraをbevyで使えるようにするためのcrateで、bevy_audioを置換することを目的としています。
bevy_audioは先述したminimp3に関連した問題の他にもボリュームの調整ができないなど、機能的に不足しているという問題があり、bevy_kira_audioを使えばそれらの問題が解決されるため、本記事で紹介したfeatureにmp3を追加する方法よりもbevy_kira_audioを使う方法の方が推奨されることが多いようです。
(しかし、kiraも3Dオーディオに対応していないという問題があるようです。)

bevy_kira_audioを導入するためには

  • bevy_audioのみを無効にする
  • bevy_kira_audioを有効にする

これらの必要があります。
面倒なのは「bevy_audioのみを無効にする」ことです。
Cargoでは個々のデフォルト機能を無効にすることはできないため、すべてのデフォルトのbevy機能を無効にして、必要な機能を再度有効にする必要があります。
詳しくは https://bevy-cheatbook.github.io/features/audio.html をご覧ください。

参考