Android StudioでFluidsynthをビルド

おはようございます。今回は、Android用のlibfluidsynth.soについて、WindowsのAndroidStudioから作成できました。という報告です。ついでに音飛びについても少々。

実行バイナリにこちらを含めるとソースコードを開示しないとダメなルールですので、公式リポジトリから取得したもの限定でアプリ公開を行うほうが、多くの場合セーフでしょう。
実験などの動作確認で、自主ビルドが必要な場合もありそうです。私の場合もそうでした。

Ubuntu+CMakeの記事は後日作成します。

それをしたくなった、理由です。

私のPixel9aは、ある日のOSアップデート以降、以下の条件でノイズが発生するようになってしまいまして、対応を行いました。

  1. libfluidsynth.soの2.5.1を使っている
    一応、2.4.7でも同じでしたので要素ではなさそう
  2. Pixel9aとAndroid15を使う端末だけ発生。14ではあまりなかった気がした
    おそらくどちらかがトリガーかもしれない
  3. oboeのパフォーマンスモードをLowLatencyにした場合に発生する
    Normalでは発生していなかった気がした
  4. 同時に大量の音がなるMIDIファイルを再生した際に、顕著になる

そして、順番に、調査したいものを絞りました。

もしかして、「レンダリングスレッドの処理を時間内に終わらせられていない」のではないか?
それについて、考えられる要因は以下です。

  1. 昔、WindowsでVST関連の処理を記述していたとき、AudioレンダリングThread内で、スレッドを待機させるMutexを使うと起きていたノイズに似ています。Javaでいうところのsynchronizedにあたるものです
  2. あるいは単純に処理が重く、まにあわない頻度でレンダリングがコールされている

1.が怪しかったため、コードをコンパイルして実証しようと思いました。

Fluidsynth公式のコンパイルスクリプトをUbuntu(WSL2.0)で動かしてみました。成功するまで2日くらいかかりました。
(近ううちに成功談の記事を作成します。書式を最新にするのみですが)

この場合、WSLのコンソールと、Windowsを切り替えて作業することになりますから、別の方法が必要だと感じて、リアクションを起こしました。アーキテクチャごとにCMake対応しないといけない。

ひょっとして、AndroidStudioからCMakeを呼べばいいのでは?、、、つまり、可能です。

プロジェクトを作成してみよう

とりあえず、C:\Users\—\AndroidStudioProjects\MidiFluidに、MidiFluidというプロジェクトを、Android Native Libraryとして作ります。
 将来的に、アーキテクチャごとにビルドできるようにしたいです。

C:\Users\—\AndroidStudioProjects\MidiFluid\app\src\main\cpp\fluid_synth_2.5.1
に、fluidsynthの公式GitHubより入手したソースコードZIPを展開します。

https://github.com/FluidSynth/fluidsynth/releases/tag/v2.5.1

ここでは、liboboeのバージョンをそろえるため、fluidsynth-2.5.1-android24.zipもあったほうが、あとで楽ではあります。後述いたします。

C:\Users\—\AndroidStudioProjects\MidiFluid\app\に2つのファイルをおきます。

build.gradle.kts
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    id("com.google.android.gms.oss-licenses-plugin")
}

android {
    namespace = "jp.gr.java_conf.---.midifluid"

    defaultConfig {
        applicationId = "jp.gr.java_conf.---.midifluid"
        minSdk = 29
        targetSdk = 35
        compileSdk = 35
        versionCode = 1
        versionName = "1.0"

        versionNameSuffix = "1"

        androidResources.localeFilters += listOf("en", "ja")

        testApplicationId = buildToolsVersion
        externalNativeBuild {
            ndk {
                abiFilters.remove("armelf_linux_eabi");
            }

            cmake {
                arguments.add("-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON")
                arguments.add("-DANDROID_STL=c++_shared")
                arguments.add("-Dosal=cpp11")
                arguments.add("-Denable-libinstpatch=0")

                /*arguments.add("-DANDROID_ARM_NEON=ON")
                cppFlags.add("-fopenmp -static-openmp")*/

                arguments.add("-Denable-libsndfile=0")
                arguments.add("-Denable-opensles=1")
                arguments.add("-Denable-oboe=1")
                arguments.add("-Denable-dbus=0")
                arguments.add("-Denable-oss=0")

                /*
                -DCMAKE_TOOLCHAIN_FILE=${NDK}/build/cmake/android.toolchain.cmake \
                -DANDROID_NATIVE_API_LEVEL=${ANDROID_API} \
                -DANDROID_ABI=${ANDROID_ABI_CMAKE} \
                -DANDROID_TOOLCHAIN=clang \
                -DANDROID_NDK=${NDK} \
                -DCMAKE_INSTALL_PREFIX=${PREFIX} \
                -DCMAKE_VERBOSE_MAKEFILE=1 \
                 */
            }
        }
    }

    buildTypes {
        release {
            isMinifyEnabled = true
        }
        debug {
            isMinifyEnabled = false
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }

    tasks {
        withType<JavaCompile> {
            options.compilerArgs.add("-Xlint:unchecked")
            options.compilerArgs.add("-deprecation")
        }
    }
    externalNativeBuild {
        cmake {
            path("CMakeLists.txt")
        }
    }

    viewBinding {
        enable = true
    }

    dataBinding {
        enable = true
    }
    buildFeatures {
        prefab = true
        viewBinding = true
    }
    buildToolsVersion = "35.0.1"
    kotlinOptions {
        jvmTarget = "1.8"
    }
    dependenciesInfo {
        includeInApk = true
        includeInBundle = true
    }
    ndkVersion = "29.0.14206865"
    compileSdk {
        version = release(35)
    }
}

dependencies {
    implementation("com.google.oboe:oboe:1.10.0")
    implementation(libs.appcompat)
    implementation(libs.activity)
    implementation(libs.material)
    implementation(libs.constraintlayout)
    implementation(libs.lifecycle.livedata.ktx)
    implementation(libs.lifecycle.viewmodel.ktx)
    implementation(libs.legacy.support.v4)
    implementation(libs.core.ktx)
    implementation(libs.recyclerview)
    implementation(libs.navigation.fragment.ktx)
    implementation(libs.navigation.ui.ktx)
    //implementation(libs.oss.licenses.plugin)
    implementation(libs.play.services.oss.licenses)
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)

project(mylib
        VERSION 1.0
        DESCRIPTION "mylib project"
        LANGUAGES CXX
)

add_library(native-lib SHARED src/main/cpp/native-lib.cpp)

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

target_compile_features(native-lib PUBLIC cxx_std_20)

# Create a variable fluidsynth_DIR to specify where the fluidsynth library is located.
set(fluidsynth_DIR C:/github/fluidsynth-2.5.1-android24)

#message(“Architecture1 = ${ANDROID_ABI}")
#message(“Architecture2 = ${CMAKE_ANDROID_ARCH_ABI}")

#find_package(OpenMP REQUIRED)
#set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}")
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_C_FLAGS}")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")

set(NDK_DIR C:/Users/---/AppData/Local/Android/Sdk/ndk/android-ndk-r26d)
set(NDK_SUBDIR ${NDK_DIR}/toolchains/llvm/prebuilt/windows-x86_64/lib/clang/17.0.2/lib/linux)

set(NDK_DIR C:/Users/---/AppData/Local/Android/Sdk/ndk/27.2.12479018)
set(NDK_SUBDIR ${NDK_DIR}/toolchains/llvm/prebuilt/windows-x86_64/lib/clang/18/lib/linux)

set(NDK_DIR C:/Users/---/AppData/Local/Android/Sdk/ndk/28.2.13676358)
set(NDK_SUBDIR ${NDK_DIR}/toolchains/llvm/prebuilt/windows-x86_64/lib/clang/19/lib/linux)

set(NDK_DIR C:/Users/---/AppData/Local/Android/Sdk/ndk/29.0.14206865)
set(NDK_SUBDIR ${NDK_DIR}/toolchains/llvm/prebuilt/windows-x86_64/lib/clang/21/lib/linux)


#add_library(libclang_rt SHARED IMPORTED)

if (${ANDROID_ABI} STREQUAL "arm64-v8a")
    set(lib_omp_DIR ${NDK_SUBDIR}/aarch64)
    #set(lib_c_DIR ${NDK_DIR}/toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/lib/aarch64-linux-android)
    #set_target_properties(libclang_rt PROPERTIES IMPORTED_LOCATION ${NDK_DIR}/toolchains/llvm/prebuilt/windows-x86_64/lib/clang/18/lib/linux/libclang_rt.asan-aarch64-android.so)
elseif (${ANDROID_ABI} STREQUAL "armeabi-v7a")
    set(lib_omp_DIR ${NDK_SUBDIR}/arm)
    #set(lib_c_DIR ${NDK_DIR}/toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/lib/arm-linux-androideabi)
    #set_target_properties(libclang_rt PROPERTIES IMPORTED_LOCATION ${NDK_DIR}/toolchains/llvm/prebuilt/windows-x86_64/lib/clang/18/lib/linux/libclang_rt.asan-arm-android.so)
elseif (${ANDROID_ABI} STREQUAL "x86_64")
    set(lib_omp_DIR ${NDK_SUBDIR}/x86_64)
    #set(lib_c_DIR ${NDK_DIR}/toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/lib/x86_64-linux-android)
    #set_target_properties(libclang_rt PROPERTIES IMPORTED_LOCATION ${NDK_DIR}/toolchains/llvm/prebuilt/windows-x86_64/lib/clang/18/lib/linux/libclang_rt.asan-x86_64-android.so)
elseif (${ANDROID_ABI} STREQUAL "x86")
    set(lib_omp_DIR ${NDK_SUBDIR}/i386)
    #set(lib_c_DIR ${NDK_DIR}/toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/lib/i686-linux-android)
    #set_target_properties(libclang_rt PROPERTIES IMPORTED_LOCATION ${NDK_DIR}/toolchains/llvm/prebuilt/windows-x86_64/lib/clang/18/lib/linux/libclang_rt.asan-i686-android.so)
endif ()

# Create a variable lib_other_DIR to specify where the other non-fluidsynth libraries is located.
#set(lib_c_DIR C:/github/android-ndk-r27c/toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/lib/aarch64-linux-android/)
# set(lib_c_DIR C:/github/back/MixAndCC/app/build/intermediates/cxx/Debug/5ea1y5rp/obj/arm64-v8a)
# Fluidsynth library code will be calling some non-fluidsynth functions which are not part of
# default NDK, so we add the binaries as dependencies of our code.

#add_library(libc++_shared SHARED IMPORTED)
#set_target_properties(libc++_shared PROPERTIES IMPORTED_LOCATION ${lib_c_DIR}/libc++_shared.so)

#add_library(libomp SHARED IMPORTED)
#set_target_properties(libomp PROPERTIES IMPORTED_LOCATION ${lib_omp_DIR}/libomp.so)

# Our code (native-lib.cpp) will be calling fluidsynth functions, so adding the fluidsynth binaries as dependencies.

add_library(libFLAC SHARED IMPORTED)
set_target_properties(libFLAC PROPERTIES IMPORTED_LOCATION ${fluidsynth_DIR}/lib/${ANDROID_ABI}/libFLAC.so)

#add_library(libfluidsynth SHARED IMPORTED)
#set_target_properties(libfluidsynth PROPERTIES IMPORTED_LOCATION ${fluidsynth_DIR}/lib/${ANDROID_ABI}/libfluidsynth.so)
#set_target_properties(libfluidsynth PROPERTIES IMPORTED_LOCATION C:/Users/---/AndroidStudioProjects/MidiFluid/app/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/${ANDROID_ABI}/libfluidsynth.so)
add_library(libfluidsynth-assetloader SHARED IMPORTED)
set_target_properties(libfluidsynth-assetloader PROPERTIES IMPORTED_LOCATION ${fluidsynth_DIR}/lib/${ANDROID_ABI}/libfluidsynth-assetloader.so)

#add_library(libgio-2.0 SHARED IMPORTED)
#set_target_properties(libgio-2.0 PROPERTIES IMPORTED_LOCATION ${fluidsynth_DIR}/lib/${ANDROID_ABI}/libgio-2.0.so)

#add_library(libglib-2.0 SHARED IMPORTED)
#set_target_properties(libglib-2.0 PROPERTIES IMPORTED_LOCATION ${fluidsynth_DIR}/lib/${ANDROID_ABI}/libglib-2.0.so)

#add_library(libgmodule-2.0 SHARED IMPORTED)
#set_target_properties(libgmodule-2.0 PROPERTIES IMPORTED_LOCATION ${fluidsynth_DIR}/lib/${ANDROID_ABI}/libgmodule-2.0.so)

#add_library(libgobject-2.0 SHARED IMPORTED)
#set_target_properties(libgobject-2.0 PROPERTIES IMPORTED_LOCATION ${fluidsynth_DIR}/lib/${ANDROID_ABI}/libgobject-2.0.so)

#add_library(libgthread-2.0 SHARED IMPORTED)
#set_target_properties(libgthread-2.0 PROPERTIES IMPORTED_LOCATION ${fluidsynth_DIR}/lib/${ANDROID_ABI}/libgthread-2.0.so)

#add_library(libinstpatch-1.0 SHARED IMPORTED)
#set_target_properties(libinstpatch-1.0 PROPERTIES IMPORTED_LOCATION ${fluidsynth_DIR}/lib/${ANDROID_ABI}/libinstpatch-1.0.so)

#find_package(PkgConfig REQUIRED)
#pkg_search_module(GLIB REQUIRED glib-2.0)

#target_include_directories(native-lib PRIVATE ${GLIB_INCLUDE_DIRS})
#target_link_libraries(native-lib INTERFACE ${GLIB_LDFLAGS})

#add_library(liboboe SHARED IMPORTED)
#set_target_properties(liboboe PROPERTIES IMPORTED_LOCATION ${fluidsynth_DIR}/lib/${ANDROID_ABI}/liboboe.so)

add_library(libogg SHARED IMPORTED)
set_target_properties(libogg PROPERTIES IMPORTED_LOCATION ${fluidsynth_DIR}/lib/${ANDROID_ABI}/libogg.so)

add_library(libopus SHARED IMPORTED)
set_target_properties(libopus PROPERTIES IMPORTED_LOCATION ${fluidsynth_DIR}/lib/${ANDROID_ABI}/libopus.so)

add_library(libpcre SHARED IMPORTED)
set_target_properties(libpcre PROPERTIES IMPORTED_LOCATION ${fluidsynth_DIR}/lib/${ANDROID_ABI}/libpcre.so)

add_library(libsndfile SHARED IMPORTED)
set_target_properties(libsndfile PROPERTIES IMPORTED_LOCATION ${fluidsynth_DIR}/lib/${ANDROID_ABI}/libsndfile.so)

add_library(libvorbis SHARED IMPORTED)
set_target_properties(libvorbis PROPERTIES IMPORTED_LOCATION ${fluidsynth_DIR}/lib/${ANDROID_ABI}/libvorbis.so)

add_library(libvorbisenc SHARED IMPORTED)
set_target_properties(libvorbisenc PROPERTIES IMPORTED_LOCATION ${fluidsynth_DIR}/lib/${ANDROID_ABI}/libvorbisenc.so)

add_library(libvorbisfile SHARED IMPORTED)
set_target_properties(libvorbisfile PROPERTIES IMPORTED_LOCATION ${fluidsynth_DIR}/lib/${ANDROID_ABI}/libvorbisfile.so)

# Specifies the directory where the C or C++ source code will look the #include <yourlibrary.h> header files
target_include_directories(native-lib PRIVATE ${fluidsynth_DIR}/include)

find_library(ANDROID_LIB android)
list(APPEND PAG_SHARED_LIBS ${ANDROID_LIB})

find_package (oboe REQUIRED CONFIG)

add_subdirectory("src/main/cpp/fluid_synth_2.5.1")
# Link everything all together. Notice that native-lib should be the first element in the list.
target_link_libraries(
        native-lib
        android
        #libclang_rt
        # Non-fluidsynth binaries
        #libomp
        #libc++_shared
        log

        #Gnu Package
        #libgmodule-2.0
        #libglib-2.0
        #libgobject-2.0
        #libgthread-2.0
        #libgio-2.0
        #libinstpatch-1.0

        # fluidsynth binaries
        libFLAC
        libfluidsynth
        libfluidsynth-assetloader
        #liboboe
        libogg
        libopus
        libpcre
        libsndfile
        libvorbis
        libvorbisenc
        libvorbisfile
        oboe::oboe
)

FluidsynthのCMakeを、このプロジェクトのCMakeを使い呼び出す形です。メニューのビルドや、構成の変更を用いることができます。

このときの、CMakeの指示ファイルには、oboe::oboeをつかうようにしていますが。前述のZIPから展開した、liboboe.soを用いたほうが、そのZIPを用いているプロジェクトと親和性が高いです。(構造体が変更されてて、ビルドできても、実行時にフィールドが異なってしまう)

使ってるSDKのバージョンによっては、このURLのままでは警告やエラーがありますので、上の2つのファイルと見比べるといいかもしれません。Anrdoidの世界では、新しいが絶対的な正義ではありませんので、両方残します。

自分のアプリだけで、oboeを用いるなら、下記の対応で十分です。FluidSynth自体が、オーディオドライバーとして、Oboeをサポートしています。モジュールがあります。アクティベートするには、CMake時のフラグを変更して、ライブラリをリンクするだけです。find_packageではなくライブラリを直リンクする場合には、強制的にフラグをたてる変更をCMakeにほどこさないと、見つからないことになってしまいます。(後述)

Oboe 向けにビルド設定を更新する
https://developer.android.com/games/sdk/oboe/update-build-settings?hl=ja

関連しますが、fluid_synth_2.5.1\CMakeLists.txtを編集しました。

706:# find_package ( oboe )
707: find_package (oboe REQUIRED CONFIG)

oboeをいうパッケージ検索するさい、ローカルではなく、特定のリポジトリから見つけるためです、コマンドの意味は、上記のURLをご参照ください。リポジトリは標準のものではありません。

結局ここまでコンパイルして実行して、Glibを無効にしたりしても、音飛びが発生しましたので、ライブラリや、FluidSynth自体の構造的な問題ではなさそうです。(つづく)

広告を表示しています。

(つづき)すると、考えられる要因の2番目です。

「単純に処理が重い」のかもしれない

FluidSynthには、さまざまなパフォーマンス用のパラメータがあります。

設定方法はこちらです。
https://www.fluidsynth.org/api/CreatingSettings.html
設定可能な一覧がこちらです。
https://www.fluidsynth.org/api/fluidsettings.html > Synthesizer settings

fluid_settings_setint(handle->settings, "synth.polyphony", 180); //256
fluid_settings_setint(handle->settings, "synth.note-cut", 2); //0
fluid_settings_setnum(handle->settings, "synth.overflow.sustained", 0); //-1000
fluid_settings_setnum(handle->settings, "synth.overflow.age", -1000); //1000

//の右が、デフォルト値です。

synth.polyphone (256 → 180)

発音数を指定します。おそらくですが、ステレオは2づつ、レイヤーの場合もその分も消費されていると思います。

synth-note-cut (0 → 2)

あるキーがなっているとき、そのキーのNoteOnが再び送られたときの処理。これカットするようにしたところ、とてもDTM音源ぽい響きになりましたね。逆にいうと、DTM音源ぽさをなくすには、この逆の対応がよさそうです。ただ、元のままだと、シンセエンジンの消費するポリ数は、それなりに増加するようです。

synth.overflow.sustained (-1000 → 0) // -が消音しやすいぽい

サスティーンされているノートが発音数たりなくなったとき、消音する優先度だと思っています。
ポリ数を減らしたところ、サスティーンされている重要な音が消されてしまうので。消失しない優先度に変更しました。

synth.overflow.age (1000 → -1000) // -が消音しやすいぽい

古いキーの音符というだけでは、消失の対象になりにくいという設定でした。一般的なシンセメーカーに合わせて、消失させるようにしました。この修正が、DTM音源ぽさがました原因です。でも、発音数を減らすためには、、、

大きなMIDIファイルを再生するとき、この設定のほうが、負荷がさがるので、音飛びは防ぎやすいです。でも、鑑賞目的であれば、デフォルトの設定のほうがいいかもしれません。

端末が過剰なチューニングをしている端末や、バックグランドタスクが増えると、また音割れはおこるかもしれない。なので、オプションを追加する予定です。3択くらいの最大発音数、2択くらいの優先順位。クリスマスまでには間に合わせたいです!