【Android】TarsosDSP を 2024.02 時点の Android 開発環境で使ってみる

## はじめに

[ピッチ(音高)](https://www.sii.co.jp/music/try/tuner/01.html)を測るチューナーアプリを作るためライブラリを探していたら [TarsosDSP](https://github.com/JorenSix/TarsosDSP) というものを見つけた。 Android でも使用できるみたいだがかなり古い。TarsosDSP を使用した Android プロジェクトの GitHub リポジトリを見つけたがこれらも古い。

- [mikkipastel/pitchDetector](https://github.com/mikkipastel/pitchDetector)
- [project-spectra/SpectraPitchDetectorAndroid](https://github.com/project-spectra/SpectraPitchDetectorAndroid)

記事を書いている 2024.02 時点の最新の環境でも TarsosDSP を使えるのか検証する。

## 環境

- Android Studio 
    Android Studio Hedgehog | 2023.1.1 Patch 1
- Target API Level 34
- Empty Activity テンプレート (= Jetpack Compose)
    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/8994e7da-4cd5-1385-25f7-7613a516485d.png" alt="" width="400">


## 1. Android プロジェクトを作成する

Empty Activity テンプレートでAndroid プロジェクトを新規作成する。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/3d0537cf-3705-f64e-afb9-19fc27817241.png" alt="" width="500">


## 2. TarsosDSP のセットアップ

[TarsosDSP の GitHub リポジトリ](https://github.com/JorenSix/TarsosDSP)の README に記載されたリンク 「TarsosDSP release directory」をクリックする。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/0ef68e90-481a-33f0-e80c-a616282cedb3.png" alt="" width="600">
	
 TarsosDSP-Android-latest.jar をダウンロードする。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/68a1aec6-3224-3b6b-1389-99d0f2994823.png" alt="" width="600">

app/libs/TarsosDSP-Android-latest.jar として配置する。 
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/a356e1f1-3092-cbc1-fe46-c3fea07933dd.png" alt="" width="300">

app モジュールから TarsosDSP-Android-latest.jar を参照できるようにする。

```kotlin:app/build.gradle.kts
dependencies {
+   implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
}
```

## 3. AndroidManifest.xml にパーミッションを追加する

マイクのパーミッション `RECORD_AUDIO` を追加する。

```diff:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

+   <uses-permission android:name="android.permission.RECORD_AUDIO" />
...
```

## 4. TarsosDSP API を使ってコーディングする

Empty Activity テンプレートで作られた MainActivity を利用して TarsosDSP API を使ったコーディングをする。
マイクから入力した音のピッチが測れるよう `AudioDispatcherFactory.fromDefaultMicrophone`! を使用する。

```kotlin:MainActivity.kt
class MainActivity : ComponentActivity() {

    private val pitchInHzStateFlow = MutableStateFlow(0.0f)
    private val noteStateFlow = MutableStateFlow("")

    private val requestPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { granted ->
        if (granted) {
            initTarsosDsp()
        } else {
            //TODO: エラーダイアログを表示する
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        requestPermissionLauncher.launch(android.Manifest.permission.RECORD_AUDIO)

        setContent {
            val pitchInHzState by pitchInHzStateFlow.collectAsState()
            val noteState by noteStateFlow.collectAsState()

            HelloTunerTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting(hz = pitchInHzState, note = noteState)
                }
            }
        }
    }

    private fun initTarsosDsp() {
        val dispatcher = AudioDispatcherFactory.fromDefaultMicrophone(22050, 1024, 0)

        val pdh = PitchDetectionHandler { res, e ->
            val pitchInHz = res.pitch
            runOnUiThread { processPitch(pitchInHz) }
        }
        val pitchProcessor: AudioProcessor =
            PitchProcessor(PitchEstimationAlgorithm.FFT_YIN, 22050f, 1024, pdh)
        dispatcher.addAudioProcessor(pitchProcessor)

        val audioThread = Thread(dispatcher, "Audio Thread")
        audioThread.start()
    }

    private fun processPitch(pitchInHz: Float) {
        pitchInHzStateFlow.value = pitchInHz
        if(pitchInHz >= 110 && pitchInHz < 123.47) {
            //A
            noteStateFlow.value = "A"
        }
        else if(pitchInHz >= 123.47 && pitchInHz < 130.81) {
            //B
            noteStateFlow.value = "B"
        }
        else if(pitchInHz >= 130.81 && pitchInHz < 146.83) {
            //C
            noteStateFlow.value = "C"
        }
        else if(pitchInHz >= 146.83 && pitchInHz < 164.81) {
            //D
            noteStateFlow.value = "D"
        }
        else if(pitchInHz >= 164.81 && pitchInHz < 174.61) {
            //E
            noteStateFlow.value = "E"
        }
        else if(pitchInHz >= 174.61 && pitchInHz < 185) {
            //F
            noteStateFlow.value = "F"
        }
        else if(pitchInHz >= 185 && pitchInHz < 196) {
            //G
            noteStateFlow.value = "G"
        }
    }
}

@Composable
fun Greeting(
    modifier: Modifier = Modifier,
    hz: Float,
    note: String
) {
    Column(
        modifier = modifier,
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Row {
            Text(text = "ノート：")
            Text(text = "${note}")
        }
        Row {
            Text(text = "周波数：")
            Text(text = "${hz.toString()}")
        }
    }
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    HelloTunerTheme {
        Greeting(hz = 130.81f, note = "A")
    }
}
```

## 完成

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/412b03a7-a850-1676-711b-4f5fe73d75ae.png" alt="" width="250">

## 補足

[TarsosDSP の GitHub リポジトリ](https://github.com/JorenSix/TarsosDSP)の README のとおり、以下のように `dependencies` にパッケージを追加したり、

```
repositories {
    maven {
        name = "TarsosDSP repository"
        url = "https://mvn.0110.be/releases"
    }
}
dependencies {
    implementation 'be.tarsos.dsp:core:2.5'
    implementation 'be.tarsos.dsp:jvm:2.5'
}
```

ソースコードをビルドしてできた `jar` を使うと、

```
git clone --depth 1 https://JorenSix@github.com/JorenSix/TarsosDSP.git
cd TarsosDSP
./gradlew build
```

`javax.sound.sampled.AudioRecord` を参照する箇所でビルドエラーとなるはず。理由は、`javax.sound.sampled` パッケージは Android SDK に含まれてないからだ。 Android 用にビルドされた TarsosDSP-Android-latest.jar は `javax.sound.sampled.AudioRecord` の代替として `android.media.AudioRecord` を使用しているのでどうエラーは発生しない。

## 参考

https://medium.com/@juniorbump/pitch-detection-in-android-using-tarsosdsp-a2dd4a3f04e9

https://0110.be/tags/TarsosDSP