いちおくまんえん

最近はcocos2d-xでゲームアプリ「いちおくまんえん」の制作がメインです。 あとアニメも少々・・

cocos2d-xの新しいAudioEngineをluaで使ってみる。

今まではSimpleAudioEngine一択でしたが、V3.3あたりで新しいSimpleがついていないAudioEngineが追加されました。

仕事では自社製のサウンドエンジンを使っていたので、SimpleAudioEngineもあまり良く分かっていません。
個人開発ではなるべくcocos2d-xで用意されてるものを使って作ろうと以前少し触ってみて何となくは使えるかなと思ってました。

過去の記事
タイトル画面作成② 遷移してきた後にBGMを再生 - いちおくまんえん
タイトル画面作成④ タップでSEを鳴らす - いちおくまんえん
cocos2d-xでBGMやSEの音量を調節する - いちおくまんえん


ただ、私の持ってるAndroidの端末ではバックグラウンドに回してから、前面に戻したらレジュームされないとか色々問題があったりしてあまりよい印象はないんですよね(´・ω・`)
今のバージョンではその問題は直ってましたが、今また使ってみると10秒超えるSEが途中で止まるとか他の問題が発生したので、どうせなら新しい方使おうとなったわけです!

一応新しい方なんですがnamespaceがexperimentalとなってて、最初はexperimentalの意味が分からないけどなんか強そう(^q^)と特に調べずに、きっと強化されたすごいやつ!と思って使い始めてました。
この前使ったFastTMXTiledMapとかも同じnamespaceです。ichiokumanyen.hatenablog.jp

で、ある程度AudioEngineの中身を洗って使い始めたので、記事にしようと書き始めたら気になったので調べました。

experimental: 実験の、実験に基づく、実験的な

( ゚д゚)ハッ!まだ安定してないってことなのか!?


・・・。



でもそんなのかんけーねー!!

きっとこっちが標準になるだろうと信じて使います。



前置きが長いですね。はい。


では新しいAudioEngineですが、以前と何が違うかと言うと、全く別物です!

まずAPIが全く一致しません。

で、機能面でどう違うかというと、BGMとかSEとかの区別がありません。
なので何曲でも同時にBGMを鳴らせます。やったね!(違
嬉しい追加機能をあげると以下の点かな

  • 音の長さを取得できる。
  • 再生位置を取得できる。
  • 再生位置を設定できる。
  • 再生完了のコールバックを設定できる。
  • グループ毎に最小ディレイを設定できる。(連続で呼んでも無視してくれる)
  • グループ毎に最大再生数を設定できる。(呼び過ぎても無視してくれる)

逆に困った点

  • preloadがない。
  • pitch, panの設定ができない。

説明は過去記事でしてます。
CocosBuilderでSoundEffectsを設定する - いちおくまんえん

あと今後は3DAudioにも対応してきそう。
今ある再生方法はplay2d()のみで、AudioInfoにis3dAudioとか用意されてるけど使われてない。
3DAudioどういう対応してくるんやろ(´・ω・`)
3次元空間の好きな位置に発声装置を置けて、指向性も設定出来て場所によって聞こえ方変わるよ!とかそういう感じですかね。
カメラの移動で自動でボリュームと左右の音のバランスを変えてくれるとか。
確かに3D対応進めている以上必須な気がしますね。


おっと。話がそれた。


新しいAudioEngineを使うことによって表現の幅が広がりますね!
例えば以下の様なことが出来るようになります。

  • BGMの区間ループ(イントロ、Aメロ、Bメロ、Aメロ、B...みたいな)
  • BGM・SEの部分再生
  • BGMのフェードイン・フェードアウト・クロスフェード
  • ダッキング(特定の音が鳴っている間、他の音を小さくするとか)

今まで出来なかったBGMの区間ループが実装できる様になるのが一番うれしいです。
区間ループ実装するとなると、恐らく毎フレーム現在の再生位置をチェックするのが簡単だと思うので、ここに色々処理を加えていけば、色々出来ます。

とりあえずは、区間ループだけは実装して他はどうしても欲しくなった時に、徐々に実装しようと思います。

困った点ですが
preloadがなくなったのはかなり困ります。
あれ?SEが遅れて流れてくるよ(^q^)ってなる気がする。
一度再生するとcacheに貯めていく実装になってたので、音量0で再生させてしまえばpreloadの代わりに出来るはず。
ソースを見たけどストリーミング再生ではなく、ファイル全部メモリ載せてから再生なのでファイルサイズ大きいとかなり遅延しそう。
BGMはサイズでかいからストリーミング再生したいなぁ・・。
pitchとpanは今のとこ特に重要視してなかったのでとりあえず気にしない方向で!


あれ?androidはcacheされてない・・。

間違ったこと言ってないかなと色々見てたらplatformによって実装が違いますね(´・ω・`)
普段cocos2d-xのソース見る時はVisualStudio使ってるので、win32の環境のファイル見てました。androidの実装見てみたら

AudioEngine-inl.h
    void uncache(const std::string& filePath){}
    void uncacheAll(){}

何もしてなかった∑(゚Д゚)ガーン
AndroidだけPlayerはOpenSLES使用してた。
NDKとOpenSLESでガチで書かれてたので読み解くの辛い('A`)
でも気になるので色々調べる。
OpenSL ES と NDK を使って Android オーディオストリーミング - 閉村観光
http://raseene.asablo.jp/blog/cat/mokuji/

なるほど、分からん。

とりあえずAndroidはcacheしない仕組みで、cacheさせる為にplay呼んでしまうと無駄にPlayer作られてしまうだけっぽい。もしかしたら内部で良い感じにやってくれてるかもしれないけど。
たぶん無駄になってる気がするのでAndroidはpreload代わりにplay呼ぶのやめとこう(´・ω・`)


iOS, MACWin32と同じような実装でcacheを利用してメモリに全部載せてから再生してました。


ふぅ。疲れた。

んじゃとりあえず実装してみよう。

sound.lua

local sound = {}

-- 再生失敗した時に返るID
sound.INVALID_ID = -1
-- Androidはキャッシュされない
sound.cacheSupport = device.platform ~= "android"

sound.bgmExtend = require("app.sound.bgm_extend")

-- BGM 1個しか鳴らせない
sound.bgm = {
    DEFAULT_VOLUME = 0.5,
    filePath = "",
    id = sound.INVALID_ID,
    extend = nil, --付加情報
    cache = {}, --キャッシュされてるファイル
    
    -- 再生停止
    stop = function()
        ccexp.AudioEngine:stop(sound.bgm.id)
        sound.bgm.filePath = ""
        sound.bgm.id = sound.INVALID_ID
        sound.bgm.extend = nil
    end,
    
    -- 再生開始。既に再生中のものがあれば止める。同じファイルの時は無視する
    play = function(filePath, loop, volume)
        if sound.bgm.filePath == filePath then
            printf("sound.bgm.play ignore same bgm filePath: %s", filePath)
            return
        end
        -- デフォルトの適用
        if loop == nil then
            loop = true
        end
        if volume == nil then
            volume = sound.bgm.DEFAULT_VOLUME
        end
        -- 付加情報あれば適用
        local extend = sound.bgmExtend[filePath]
        if extend and extend.volume then
            volume = volume * extend.volume
        end
        -- 再生
        local audioID = ccexp.AudioEngine:play2d(filePath,loop,volume)
        if audioID == sound.INVALID_ID then
            printf("sound.bgm.play failed filePath: %s", filePath)
            return
        end
        -- 成功してたら前のBGMを止めて情報を更新
        sound.bgm.stop()
        sound.bgm.filePath = filePath
        sound.bgm.cache[filePath] = true
        sound.bgm.id = audioID
        sound.bgm.extend = extend
    end,
    
    -- プリロード出来ないので無音で再生させてキャッシュに載せる
    preload = function(filePath)
        if not sound.cacheSupport then return end
        local audioID = ccexp.AudioEngine:play2d(filePath, false, 0)
        if audioID == sound.INVALID_ID then
            printf("sound.bgm.preload failed filePath: %s", filePath)
            return
        end
        sound.bgm.cache[filePath] = true
    end,
    
    -- キャッシュクリア
    release = function(filePath)
        if not sound.cacheSupport then return end
        ccexp.AudioEngine:uncache(filePath)
        sound.bgm.cache[filePath] = nil
    end,
    
    -- 毎フレーム呼ばせる。区間ループとかフェードとかダッキングとか実装できる
    step = function(dt)
        --[[printf("sound.bgm.step dt: %0.3f", dt)]]
        local audioID = sound.bgm.id
        -- 再生されてない
        if audioID == sound.INVALID_ID then return end
        
        local extend = sound.bgm.extend
        -- 区間ループ指定あり
        if extend and extend.loopPositionB then
            local currentTime = ccexp.AudioEngine:getCurrentTime(audioID)
            -- printf("sound.bgm.step currentTime: %0.3f, loopPositionB: %0.3f", currentTime, extend.loopPositionB)
            if currentTime >= extend.loopPositionB then
                ccexp.AudioEngine:setCurrentTime(audioID, extend.loopPositionA)
            end
        end
    end
}
cc.Director:getInstance():getScheduler():scheduleScriptFunc(sound.bgm.step, 0, false)

sound.se = {
    DEFAULT_VOLUME = 0.8,
    cache = {},

    -- 再生停止
    stop = function(audioID)
        ccexp.AudioEngine:stop(audioID)
    end,
    
    --再生開始
    play = function(filePath, loop, volume)
        if loop == nil then
            loop = false
        end
        if volume == nil then
            volume = sound.se.DEFAULT_VOLUME
        end
        local audioID = ccexp.AudioEngine:play2d(filePath,loop,volume)
        if audioID == sound.INVALID_ID then
            printf("sound.se.play failed filePath: %s", filePath)
            return sound.INVALID_ID
        end
        sound.se.cache[filePath] = true
        return audioID
    end,
    
    -- プリロード出来ないので無音で再生させてキャッシュに載せる
    preload = function(filePath)
        if not sound.cacheSupport then return end
        local audioID = ccexp.AudioEngine:play2d(filePath, false, 0)
        if audioID == sound.INVALID_ID then
            printf("sound.se.preload failed filePath: %s", filePath)
            return
        end
        sound.se.cache[filePath] = true
    end,

    -- キャッシュクリア
    release = function(filePath)
        if not sound.cacheSupport then return end
        ccexp.AudioEngine:uncache(filePath)
        sound.se.cache[filePath] = nil
    end,
    
    -- 毎フレーム呼ばせる。
    step = function(dt)
        --printf("sound.se.step dt: %0.3f"), dt)
    end
}
cc.Director:getInstance():getScheduler():scheduleScriptFunc(sound.se.step, 0, false)

return sound

bgm_extend.lua

---
-- ファイル毎に追加情報を任意に設定できる
-- 
-- 対応してるパラメータ
-- volume: 補正ボリューム 鳴らそうとしている音量に掛けた値の音量に設定される(1以上も可能だが、掛けた結果が1を超えることはない)
-- loopPositionA: 区間ループの開始秒(Bとセット。0.1秒間隔でしか対応してないっぽい)
-- loopPositionB: 区間ループの終了秒
local bgm_extend = {
    ["sound/bgm/title_01.mp3"] = {
        volume = 0.9,
    },
    ["sound/bgm/school_01.mp3"] = {
        volume = 0.8,
        loopPositionA = 5.2,
        loopPositionB = 7.5,
    },
}

return bgm_extend

使い方

-- どこかの初期化周りで読み込ませる。main.luaのrequire "cocos.init"の下あたりで良いかと
sound = require("app.sound.sound")

-- 遅延させたくない場合は再生するちょっと前にプリロードしとく
sound.bgm.preload("sound/bgm/school_01.mp3")
sound.se.preload("sound/se/school/chime_01.mp3")

-- 再生
sound.bgm.play("sound/bgm/school_01.mp3")
sound.se.play("sound/se/school/chime_01.mp3")

まだ全然デバッグしてないのでちゃんと動かないかもしれません(^q^)
一応区間ループしたかったのと、フリー素材かき集めてるせいで音量ばらばらなので、
毎回音量指定しなくても内部で吸収出来るようにしただけです!

とりあえず私の持ってる実機で、正しく動くようになったのでめでたしめでたしw

いやーluaのお作法がよくわからないです。今度ジュンク堂で良い本ないか見てこよう。