NDK學習筆記:一起來變蘿莉音!FMOD學習總結(上)

Mr_Zzr發表於2018-10-14

NDK學習筆記:一起來變蘿莉音!FMOD學習總結(上)

一、不是fmod(),不是/fmod,是FMOD!

關於 FMOD,總感覺全閘道器於它的文章不多,也不夠細,Android平臺上的總結更是如數家珍。可能是FMOD自身沒有中文說明介紹的原因吧,另外一個原因可能就是閉源商用付費吧。我自己是在2016年末,在一個線上教學(TX的動腦學院)接觸使用這個音訊開發庫的,算是接觸NDK音視訊的第一步,在總結編寫這篇文章之前我還連官方網站的連結都記錯(大寫的尷尬)廢話不多,開始進入正題,先來段百度百科的簡介:

Fmod 聲音系統是為遊戲開發者準備的革命性音訊引擎。目前最新版本是Fmod Studio 1.10.08。你也可以在FMOD的官網上下載到FMOD 3。如今採用了FMOD作為音訊引擎的遊戲包括Far Cry(孤島驚魂)、Tom Clancy's Ghost Recon(幽靈行動),甚至著名的World Of Warcraft(魔獸爭霸),QQ變音模組,萬王之王(手遊版魔獸世界,tx爸爸出品)等等。如今Fmod已經完全成為一個多聲道混音器(a full multichannel mixer)。2D聲音可以用5.1甚至7.1的形式播放。聲音可以交換彼此分配到的聲道,舉個例子,一個3D立體聲的左右聲道可以相互交換、混音或是全都通過左揚聲器播放出來。Fmod可以實現這一特性是由於它支援pan matrices(聲像矩陣)。任何輸入聲音訊道都可以被重定向到任意輸出揚聲器,並且支援緊接著的這個百分比層,因此可以說沒有一個絕對的揚聲器分配方案。這句話我不是很理解,原文是:The way this is available is FMOD supports pan matrices. Any input sound channel can be redirected to any output speaker,and on top of this percentages/fractional levels are supported,so there are no absolute speaker assignments.
為了滿足高階音效裝置,Fmod藉助ASIO(Audio Stream Input Output,音訊裝置零延遲)功能,支援16個輸出通道的多通話線路輸出(multichannel output)。更多關於的FMOD的簡介請點選這裡

好了,廢話就到這裡了。現在就讓我開始Fmod的擼碼旅程。

二、準備Fmod

通過百度百科的介紹,我們到Fmod的官方網站,然後註冊登陸之後,點選download跳轉到下載頁面。初步瀏覽一下網頁之後找到FMOD Studio,分頁下包括FMOD Studio Tool / FMOD Studio API。FMOD Studio Tool是一款專業處理音訊的工具軟體,類似會聲會影那一類工具。然後我們的重點就放在FMOD Studio API上,介紹如下:FMOD Studio API is the interface for programmers. There are 2 APIs included. The FMOD Studio API and the Low Level API. For further details, see the revision history. Major version number changes include major updates which may require migration. 1.10 API is not binary compatible with 1.09 API.  重點我都已經幫大家畫出來了,這套API裡面包含了兩套程式碼,然後就是1.10的API並沒有完全相容1.09,啥意思?兩套API的劃分就類似Android的Camera和Camera2,而1.10的API介面和1.09的相比較,可能有更改或刪減的變化,所以為了相容和穩定方面考慮,我們下載1.09.10版本的Android API資料包。

下載解壓之後,我們得到一個fmodstudioapi10910android的資料夾,裡面包含了四個組成部分如下,

plugins是外掛部分,裡面是什麼google_vr,然而我到現在還不知道是啥玩意,忽略。

doc是文件,但文件大部分是說FMOD Sutiod Tool,一些其他描述,並不是API的查考文件,有時間可以大概瀏覽一番。

bin是fsbank部分相關的二進位制檔案。fsbank是FS裡面配合FS Tool 所獨有的一個模組,也不是我們需要關注的重點。適合非常專業的音訊發燒者研究研究。

api才是我們需要關注的,裡面又分為三個部分,分別是fsbank / lowlevel / studio ,fsbank已經在上面bin的介紹過,bin裡面包含的是windows版本的lib和dll,這裡的是標頭檔案和so庫;然後lowlevel、studio分別對應FMOD的兩套APIs,包含了標頭檔案及其庫,還要對應的example例程。然而... 你以為有example就so easy了嗎?呵呵,too young to simple ...

三、閱讀FMOD Example

我們以lowlevel的example開始,探索一下FMOD的example要怎麼搞。首先我們看看example都有些什麼:

一個java資料夾,依次進去這個資料夾你就會立刻發現,這裡面就是Android的MainActivity專案入口檔案;media裡面的是一些使用到的音訊素材,各種格式應有盡有;plugins還是外掛的部分,不懂,忽略;vs2010裡面的是VisualStudio版本的Android工程檔案,也是需要搗鼓一番才能跑起來。剩下的,在example目錄下的 h / cpp檔案就是demo了。沒錯,這些就是demo了。

我們找一個簡單的感興趣的play_sound.cpp開始,開啟檔案就一個FMOD_Main,然後例程程式碼如下:

#include "fmod.hpp"
#include "common.h"

int FMOD_Main()
{
    FMOD::System     *system;
    FMOD::Sound      *sound1, *sound2, *sound3;
    FMOD::Channel    *channel = 0;
    FMOD_RESULT       result;
    unsigned int      version;
    void             *extradriverdata = 0;
    
    Common_Init(&extradriverdata);

    /*
        Create a System object and initialize
    */
    result = FMOD::System_Create(&system);
    ERRCHECK(result);

    result = system->getVersion(&version);
    ERRCHECK(result);

    if (version < FMOD_VERSION)
    {
        Common_Fatal("FMOD lib version %08x doesn't match header version %08x", version, FMOD_VERSION);
    }

    result = system->init(32, FMOD_INIT_NORMAL, extradriverdata);
    ERRCHECK(result);

    result = system->createSound(Common_MediaPath("drumloop.wav"), FMOD_DEFAULT, 0, &sound1);
    ERRCHECK(result);

    result = sound1->setMode(FMOD_LOOP_OFF);    /* drumloop.wav has embedded loop points which automatically makes looping turn on, */
    ERRCHECK(result);                           /* so turn it off here.  We could have also just put FMOD_LOOP_OFF in the above CreateSound call. */

    result = system->createSound(Common_MediaPath("jaguar.wav"), FMOD_DEFAULT, 0, &sound2);
    ERRCHECK(result);

    result = system->createSound(Common_MediaPath("swish.wav"), FMOD_DEFAULT, 0, &sound3);
    ERRCHECK(result);

    ... ...
}

初步瀏覽程式碼我們可以知道,Common系列的API很重要。然後我們以Common_MediaPath這個API為例,看看play_sound的依賴關係,它一開始包含了兩個標頭檔案,一個fmod.hpp,另外一個是common.h,從命名規則考慮,我們可以發現Common_MediaPath的定義就在common.h,然而你到common.cpp是找不到Common_MediaPath的實現的,哈哈哈哈!why?

因為 common.h 又包含了兩個標頭檔案 common_platform.h / fmod.h,又是命名規則出發,到common_platform.cpp,這下我們就找到了Common_MediaPath的實現了,我們看看原始碼:

const char *Common_MediaPath(const char *fileName)
{
    char *filePath = (char *)calloc(256, sizeof(char));

    strcat(filePath, "file:///android_asset/");
    strcat(filePath, fileName);
    gPathList.push_back(filePath);

    return filePath;
}

從原始碼我們可以知道,Common_MediaPath都是基於asset的目錄下尋找資原始檔,而且檔案命名長度不能超過256-"file:///android_asset/".length 個字元。

(如果一些檔案不好找在哪個目錄,建議大家在電腦上裝一個軟體,everything,全域性搜尋賊快。)

好的,c++程式碼的閱讀方式就是這麼一個大概的流程。我們回到example/java目錄,快速瀏覽一下Android的入口:

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
    	super.onCreate(savedInstanceState);

        org.fmod.FMOD.init(this);
        
        mThread = new Thread(this, "Example Main");
        mThread.start();
        
        setStateCreate(); // native
    }

邏輯我們先不著急,從onCreate出發,我們要知道需要引入org.fmod.FMOD.jar,還有相對應的so庫,另外還有一個工作執行緒。一切準備就緒之後,我們就開始真正在AndroidStudio上跑一個play_sound的例程。

四、FMOD For Android

首先我們還是基於BlogApp的專案工程,這個AS工程當初一開始建立的時候就已經新增了c++的支援。(AS版本2.3、外掛版本2.2.2、Gradle version 2.14.1 都是比較老的版本,提高大家的適應性)接著我就要發車了,抓緊時間上車。

第一步:我們把需要的demo檔案全部都拷貝到工程對應的位置,如上圖,然後我們把 \fmodstudioapi10910android\api\lowlevel\examples\java\org\fmod\example\MainActivity 複製到org.zzrblog.fmod.FmodActivity。把相關的cpp檔案拷貝到cpp/fmod下,\api\lowlevel\inc目錄下都是一些標頭檔案,我們把整個inc都拷貝下來。

第二步:開啟FmodActivity,修改一些包名路徑之後,移動檔案的最下面位置的靜態程式碼塊。去除fmodstudio、fmodstudioL、fmodstudioD的System.loadLibrary,因為我們只需用到Fmod的API,並不需要fmodstudio的功能。然後把System.loadLibrary("stlport_shared"); System.loadLibrary("example"); 暫時都先登出。

FmodActivity的修改還沒結束,因為FmodActivity中的native方法還是紅色錯誤狀態的,這是因為AS編譯器還找不到java層的native方法的cpp實現。我們開啟cpp/fmod/common_platform.cpp 也是移動到最下方的位置,可以看到extern "C" 的各種jni介面,我們把原來的Java_org_fmod_example_MainActivity_methodName改為現在的包名Java_org_zzrblog_fmod_FmodActivity修改之後,AS編譯器就能找到java層native方法的函式實現了。

第三步:我們依次開啟cpp/fmod/下的所有h / cpp檔案,注意上方include標頭檔案的路徑,因為我們把共用的標頭檔案都放到了cpp/fmod/inc下,所以我們要手動的新增標頭檔案的路徑 inc/fmod.h 

第四步:我們到\fmodstudioapi10910android\api\lowlevel\lib下找出需要預載入的so動態庫,我們把要使用到的armeabi和armeabi-v7a資料夾下libfmod.so / libfmodL.so,都複製拷貝到AS工程下cpp/fmod/prebuild/資料夾內。

第四步:這一步比較複雜而且是挺重要的,編寫我們的CMakeList檔案 及其 相關配置。

(1)首先我們到app下的build.gradle配置ANDROID_ABI的平臺,通過abiFilters指定:

android {
    ... ...
    defaultConfig {
        applicationId "org.zzrblog.blogapp"
        externalNativeBuild {
            cmake {
                cppFlags ""
                abiFilters 'armeabi','armeabi-v7a'
            }
        }
    }
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

(2)之後我們開始編寫CMakeLists.txt 的指令碼檔案。

cmake_minimum_required(VERSION 3.4.1)

# 設定生成的so動態庫最後輸出的路徑
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI})

add_library( # 生成動態庫的名稱
             fmod-voice-lib
             # 指定是動態庫SO
             SHARED
             # 編譯庫的原始碼檔案
             src/main/cpp/fmod/play_sound.cpp
             src/main/cpp/fmod/common.cpp
             src/main/cpp/fmod/common_platform.cpp)
             
# 引入外部so fmod 供原始檔編譯
add_library(fmod
            SHARED
            IMPORTED )
set_target_properties(fmod PROPERTIES IMPORTED_LOCATION
            ${PROJECT_SOURCE_DIR}/src/main/cpp/fmod/prebuild/${ANDROID_ABI}/libfmod.so)
set_target_properties(fmod PROPERTIES LINKER_LANGUAGE CXX)

# 引入外部so fmodL 供原始檔編譯
add_library(fmodL
            SHARED
            IMPORTED )
set_target_properties(fmodL PROPERTIES IMPORTED_LOCATION
            ${PROJECT_SOURCE_DIR}/src/main/cpp/fmod/prebuild/${ANDROID_ABI}/libfmodL.so)
set_target_properties(fmodL PROPERTIES LINKER_LANGUAGE CXX)

# 在系統找出預編譯的log-lib庫,指定在CMake指令碼下的別名為log
find_library( log-lib log )

target_link_libraries( # 指定目標連結庫
                       fmod-voice-lib
                       # 新增預編譯庫到目標連結庫中
                       ${log-lib}
                       fmod
                       fmodL)

CMakeLists.txt的編譯指令碼其實不難寫,已經附帶了些註釋。重要一點就是搞清楚依賴關係,確認好Cmake的語法。

現在來解析解析這個CMake指令碼。首先我們設定CMAKE_LIBRARY_OUTPUT_DIRECTORY 動態庫輸出路徑為${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}。PROJECT_SOURCE_DIR和ANDROID_ABI是AS版CMake指令碼的內建變數,為啥我要指定這個路徑呢?這是因為這個目錄就是AS預設載入SO的目錄。(並不是${PROJECT_SOURCE_DIR}/libs 嘿嘿嘿)... ...隨後我們按照示例,用add_library指定原始檔編譯生成動態庫,然而只有原始檔還是不夠的,我們需要fmod和fmodL兩個動態庫,所以我們還是通過add_library將這兩個動態庫引入編譯鏈。而且需要指明其路徑和連結引數。... ...最後我們還是按照示例,從系統路徑下找到log庫,把所有的依賴關係理順之後,我們通過target_link_libraries進行目標庫的連結編譯。That is all.

第五步:編寫完CMake指令碼之後,我們需要sync同步一下專案工程。然後rebuild-project。如無意外你會發現src/main之下就會出現jniLibs/ANDROID_ABI/生成 libfmod-voice-lib.so 然而先不要急著執行demo,我們需要在FmodActivity的靜態程式碼塊中載入libfmod-voice-lib.so。我們還需要手動的把libfmod.so和libfmodL.so拷貝對應的ANDROID_ABI目錄下。最後的最後,我們到fmodstudioapi10910android\api\lowlevel\examples\media資料夾下把demo使用到的媒體檔案,放到專案的assets資原始檔夾。附帶工程目錄如下:

 

執行FmodActivity吧。

相關文章