1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

IntelliJ IDEA

IntelliJ IDEA は、JetBrains が提供する Java/Kotlin を中心としたプロ向け統合開発環境(IDE) です。

きほん

Java や Kotlin をはじめ、さまざまな言語の開発に対応し、コード補完・静的解析・リファクタリングなどで開発の生産性を高めます。デバッガ、テスト実行、Git などの開発ツールと統合されており、IDE だけでコーディングからデバッグ、バージョン管理まで一通り完結します。

Ultimate 版の特徴

有料版の IntelliJ IDEA Ultimate では、Web フレームワーク、アプリケーションサーバー、データベースツールなどが追加され、業務向けのフルスタック開発に向いた機能が揃っています。無料の Community 版と比べて、Spring などサーバーサイドフレームワークやフロントエンド、DB 連携などのサポートが大きく強化されています。

では

行ってみます。
今回は諸事情により、Windowsです。

IntelliJ インストール

ダウンロードして、インストールします

Ultimateライセンス登録

手元には、ちょうどライセンスがあったので、登録しましたが、なければ Community Edition として使えばよいかと思います。結構様々な形態の無償ライセンス提供はあるようです。

image.png

起動

とりあえず、起動します。
image.png

Junie を入れる

デフォルトだと、AIのJunieが入っていないので、入れます。
[設定]→[プラグイン]からインストールできました。
image.png

AIに突っ込むプロンプトを作成

ここでは他で使ったものを流用してますが、適宜作成してください。
InteliJ向けにしてもらいましたが、長いね・・・

# Android De-esserアプリ開発仕様書
## Kotlin + IntelliJ IDEA / Android Studio (Gradle Kotlin DSL)

## プロジェクト概要
音声ファイルの歯擦音(シビランス:s, sh, ch などの高周波成分)を軽減するDe-esserアプリをAndroid向けに開発する。

## 技術スタック
- **言語**: Kotlin 1.9.x
- **IDE**: IntelliJ IDEA Ultimate / Android Studio Hedgehog以降
- **ビルドシステム**: Gradle 8.x + Kotlin DSL (build.gradle.kts)
- **最小SDK**: API 26 (Android 8.0)
- **ターゲットSDK**: API 34 (Android 14)
- **UIフレームワーク**: Jetpack Compose + Material 3
- **アーキテクチャ**: MVVM + Clean Architecture
- **音声処理**: Oboe (C++ NDK) / AudioTrack API
- **依存性注入**: Hilt
- **非同期処理**: Kotlin Coroutines + Flow
- **ローカルDB**: Room(設定・プリセット保存)

## Gradleプロジェクト構成

### settings.gradle.kts
```kotlin
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

rootProject.name = "DeEsserApp"
include(":app")
include(":core:audio")
include(":core:dsp")
include(":core:common")
include(":feature:main")
include(":feature:settings")
```

### ルート build.gradle.kts
```kotlin
plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.android.library) apply false
    alias(libs.plugins.kotlin.android) apply false
    alias(libs.plugins.kotlin.compose) apply false
    alias(libs.plugins.hilt) apply false
    alias(libs.plugins.ksp) apply false
}

tasks.register("clean", Delete::class) {
    delete(rootProject.layout.buildDirectory)
}
```

### gradle/libs.versions.toml(Version Catalog)
```toml
[versions]
agp = "8.2.0"
kotlin = "1.9.21"
compose-bom = "2024.01.00"
compose-compiler = "1.5.7"
hilt = "2.50"
room = "2.6.1"
coroutines = "1.7.3"
lifecycle = "2.7.0"
navigation = "2.7.6"
ksp = "1.9.21-1.0.16"
oboe = "1.8.0"

[libraries]
# AndroidX Core
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version = "1.12.0" }
androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
androidx-lifecycle-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version = "1.8.2" }

# Compose
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
compose-ui = { group = "androidx.compose.ui", name = "ui" }
compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" }

# Navigation
navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }

# Hilt
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version = "1.1.0" }

# Room
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }

# Coroutines
coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }

# Audio
oboe = { group = "com.google.oboe", name = "oboe", version.ref = "oboe" }

# Testing
junit = { group = "junit", name = "junit", version = "4.13.2" }
androidx-test-ext = { group = "androidx.test.ext", name = "junit", version = "1.1.5" }
compose-ui-test = { group = "androidx.compose.ui", name = "ui-test-junit4" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
```

### app/build.gradle.kts
```kotlin
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
    alias(libs.plugins.hilt)
    alias(libs.plugins.ksp)
}

android {
    namespace = "com.example.deesser"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.example.deesser"
        minSdk = 26
        targetSdk = 34
        versionCode = 1
        versionName = "1.0.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        
        ndk {
            abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
        }
        
        externalNativeBuild {
            cmake {
                cppFlags += "-std=c++17"
                arguments += "-DANDROID_STL=c++_shared"
            }
        }
    }

    buildTypes {
        release {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
        debug {
            isDebuggable = true
            applicationIdSuffix = ".debug"
        }
    }
    
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    
    kotlinOptions {
        jvmTarget = "17"
    }
    
    buildFeatures {
        compose = true
        buildConfig = true
    }
    
    externalNativeBuild {
        cmake {
            path = file("src/main/cpp/CMakeLists.txt")
            version = "3.22.1"
        }
    }
    
    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }
}

dependencies {
    // AndroidX Core
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.lifecycle.runtime)
    implementation(libs.androidx.lifecycle.viewmodel)
    implementation(libs.androidx.lifecycle.compose)
    implementation(libs.androidx.activity.compose)
    
    // Compose
    implementation(platform(libs.compose.bom))
    implementation(libs.compose.ui)
    implementation(libs.compose.ui.graphics)
    implementation(libs.compose.ui.tooling.preview)
    implementation(libs.compose.material3)
    implementation(libs.compose.material.icons)
    debugImplementation(libs.compose.ui.tooling)
    
    // Navigation
    implementation(libs.navigation.compose)
    
    // Hilt
    implementation(libs.hilt.android)
    implementation(libs.hilt.navigation.compose)
    ksp(libs.hilt.compiler)
    
    // Room
    implementation(libs.room.runtime)
    implementation(libs.room.ktx)
    ksp(libs.room.compiler)
    
    // Coroutines
    implementation(libs.coroutines.core)
    implementation(libs.coroutines.android)
    
    // Audio (Oboe)
    implementation(libs.oboe)
    
    // Testing
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.test.ext)
    androidTestImplementation(platform(libs.compose.bom))
    androidTestImplementation(libs.compose.ui.test)
}
```

## 機能要件

### 1. 音源読み込み機能
- 対応フォーマット: WAV, MP3, FLAC, AAC, M4A
- Storage Access Framework (SAF) によるファイル選択
- MediaExtractor / MediaCodec でデコード
- ファイル情報表示(ファイル名、長さ、サンプルレート、ビット深度)

### 2. De-esser処理エンジン

#### 2.1 処理アーキテクチャ
```
┌─────────────────────────────────────────────────────────┐
│                    Kotlin Layer                         │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐    │
│  │ViewModel   │  │ Repository  │  │ UseCase     │    │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘    │
│         │                │                │            │
│         └────────────────┼────────────────┘            │
│                          │                             │
│  ┌───────────────────────▼───────────────────────┐    │
│  │           AudioProcessor (Kotlin)              │    │
│  │  - ファイルI/O, デコード/エンコード             │    │
│  │  - パラメータ管理                              │    │
│  └───────────────────────┬───────────────────────┘    │
└──────────────────────────┼──────────────────────────────┘
                           │ JNI
┌──────────────────────────▼──────────────────────────────┐
│                     C++ Layer (NDK)                      │
│  ┌───────────────────────────────────────────────┐      │
│  │           DeEsserEngine (C++)                  │      │
│  │  - FFT処理 (KissFFT / PFFFT)                  │      │
│  │  - フィルタリング                              │      │
│  │  - ダイナミクス処理                            │      │
│  └───────────────────────┬───────────────────────┘      │
│                          │                              │
│  ┌───────────────────────▼───────────────────────┐      │
│  │              Oboe (リアルタイム再生)            │      │
│  └───────────────────────────────────────────────┘      │
└─────────────────────────────────────────────────────────┘
```

#### 2.2 周波数選択的ダイナミックEQ方式
```
処理フロー:
1. 入力信号をFFTで周波数領域に変換(オーバーラップ75%)
2. シビランス帯域(4kHz〜10kHz)を検出
3. 帯域のレベルがスレッショルドを超えた場合のみゲインリダクション
4. スムーズなアタック/リリース処理で自然な減衰
5. IFFTで時間領域に戻す(オーバーラップ加算法)
```

#### 2.3 パラメータ
```kotlin
data class DeEsserParams(
    val threshold: Float = -20f,      // -40dB ~ 0dB
    val reduction: Float = -6f,       // 0dB ~ -20dB
    val frequency: Float = 6000f,     // 2kHz ~ 16kHz
    val range: Float = 4000f,         // 1kHz ~ 8kHz
    val attack: Float = 1f,           // 0.1ms ~ 10ms
    val release: Float = 50f,         // 10ms ~ 200ms
    val mix: Float = 100f,            // 0% ~ 100% (Dry/Wet)
    val listenMode: Boolean = false   // 処理帯域のみ試聴
)

// プリセット
enum class Preset(val params: DeEsserParams) {
    GENTLE(DeEsserParams(threshold = -15f, reduction = -4f, frequency = 6000f, range = 3000f)),
    MODERATE(DeEsserParams(threshold = -20f, reduction = -6f, frequency = 6500f, range = 4000f)),
    AGGRESSIVE(DeEsserParams(threshold = -25f, reduction = -10f, frequency = 7000f, range = 5000f)),
    VOCAL(DeEsserParams(threshold = -18f, reduction = -5f, frequency = 5500f, range = 3500f)),
    CUSTOM(DeEsserParams())
}
```

#### 2.4 音質保持のための実装ポイント
- **位相の保持**: リニアフェイズフィルタまたは最小位相フィルタを使用
- **オーバーラップ加算法**: FFT処理時のアーティファクト防止(75%オーバーラップ)
- **ソフトニー圧縮**: 急激なゲイン変化を避けるためのソフトニー処理(knee: 6dB)
- **ルックアヘッド**: 5msのルックアヘッドでトランジェント保護
- **ディザリング**: 16bit出力時のディザ適用

### 3. ユーザーインターフェース

#### 3.1 画面構成(Navigation Compose)
```kotlin
sealed class Screen(val route: String) {
    object Main : Screen("main")
    object Settings : Screen("settings")
    object Help : Screen("help")
    object Export : Screen("export")
}
```

#### 3.2 メイン画面レイアウト
```
┌─────────────────────────────────────┐
│  [≡]  De-esser Pro           [⚙️]  │
├─────────────────────────────────────┤
│                                     │
│  ┌─────────────────────────────┐   │
│  │     波形表示エリア           │   │
│  │   (WaveformCanvas)          │   │
│  │   + GainReductionMeter      │   │
│  └─────────────────────────────┘   │
│                                     │
│    advancement ──●────────── 2:34 / 4:12   │
│                                     │
│     [⏮️]  [▶️/⏸️]  [⏭️]           │
│                                     │
│  ┌──────────┐  ┌──────────┐        │
│  │ ORIGINAL │  │ PROCESSED│        │
│  │    ○     │  │    ●     │        │
│  └──────────┘  └──────────┘        │
│                                     │
├─────────────────────────────────────┤
│  Threshold    ────●──────  -20dB   │
│  Reduction    ──●────────   -6dB   │
│  Frequency    ─────●─────   6kHz   │
│  Range        ───●───────   4kHz   │
│                                     │
│  Preset: [Moderate ▼]              │
│  [詳細設定]           [Listen 🎧]  │
│                                     │
├─────────────────────────────────────┤
│                                     │
│  [📂 ファイル読込]  [💾 エクスポート] │
│                                     │
└─────────────────────────────────────┘
```

#### 3.3 Compose UIコンポーネント
```kotlin
// パラメータスライダー
@Composable
fun ParameterSlider(
    label: String,
    value: Float,
    valueRange: ClosedFloatingPointRange<Float>,
    unit: String,
    onValueChange: (Float) -> Unit,
    modifier: Modifier = Modifier
)

// 波形表示
@Composable
fun WaveformCanvas(
    waveformData: List<Float>,
    playbackPosition: Float,
    modifier: Modifier = Modifier
)

// A/B切り替え
@Composable
fun ABSwitchButton(
    selectedMode: PlaybackMode,
    onModeChange: (PlaybackMode) -> Unit,
    modifier: Modifier = Modifier
)

// ゲインリダクションメーター
@Composable
fun GainReductionMeter(
    reductionDb: Float,
    modifier: Modifier = Modifier
)
```

### 4. エクスポート機能
- 出力フォーマット: WAV (16bit/24bit), MP3 (128-320kbps), FLAC
- MediaMuxer / MediaCodec でエンコード
- メタデータ保持
- 出力先選択(SAF経由)
- WorkManagerによるバックグラウンド処理

## ディレクトリ構造
```
DeEsserApp/
├── app/
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/com/example/deesser/
│   │   │   │   ├── DeEsserApplication.kt
│   │   │   │   ├── MainActivity.kt
│   │   │   │   ├── di/
│   │   │   │   │   ├── AppModule.kt
│   │   │   │   │   ├── AudioModule.kt
│   │   │   │   │   └── DatabaseModule.kt
│   │   │   │   ├── data/
│   │   │   │   │   ├── local/
│   │   │   │   │   │   ├── AppDatabase.kt
│   │   │   │   │   │   ├── PresetDao.kt
│   │   │   │   │   │   └── entity/
│   │   │   │   │   │       └── PresetEntity.kt
│   │   │   │   │   └── repository/
│   │   │   │   │       ├── AudioRepositoryImpl.kt
│   │   │   │   │       └── PresetRepositoryImpl.kt
│   │   │   │   ├── domain/
│   │   │   │   │   ├── model/
│   │   │   │   │   │   ├── AudioFile.kt
│   │   │   │   │   │   ├── DeEsserParams.kt
│   │   │   │   │   │   ├── PlaybackState.kt
│   │   │   │   │   │   └── ProcessingState.kt
│   │   │   │   │   ├── repository/
│   │   │   │   │   │   ├── AudioRepository.kt
│   │   │   │   │   │   └── PresetRepository.kt
│   │   │   │   │   └── usecase/
│   │   │   │   │       ├── LoadAudioUseCase.kt
│   │   │   │   │       ├── ProcessAudioUseCase.kt
│   │   │   │   │       ├── ExportAudioUseCase.kt
│   │   │   │   │       └── ManagePresetUseCase.kt
│   │   │   │   ├── audio/
│   │   │   │   │   ├── AudioDecoder.kt
│   │   │   │   │   ├── AudioEncoder.kt
│   │   │   │   │   ├── AudioPlayer.kt
│   │   │   │   │   ├── DeEsserProcessor.kt
│   │   │   │   │   └── WaveformAnalyzer.kt
│   │   │   │   ├── ui/
│   │   │   │   │   ├── navigation/
│   │   │   │   │   │   └── NavGraph.kt
│   │   │   │   │   ├── theme/
│   │   │   │   │   │   ├── Color.kt
│   │   │   │   │   │   ├── Theme.kt
│   │   │   │   │   │   └── Type.kt
│   │   │   │   │   ├── components/
│   │   │   │   │   │   ├── common/
│   │   │   │   │   │   │   ├── LoadingOverlay.kt
│   │   │   │   │   │   │   └── ErrorDialog.kt
│   │   │   │   │   │   ├── audio/
│   │   │   │   │   │   │   ├── WaveformCanvas.kt
│   │   │   │   │   │   │   ├── PlaybackControls.kt
│   │   │   │   │   │   │   ├── ABSwitchButton.kt
│   │   │   │   │   │   │   └── GainReductionMeter.kt
│   │   │   │   │   │   └── deesser/
│   │   │   │   │   │       ├── ParameterSlider.kt
│   │   │   │   │   │       ├── ParameterPanel.kt
│   │   │   │   │   │       └── PresetSelector.kt
│   │   │   │   │   └── screens/
│   │   │   │   │       ├── main/
│   │   │   │   │       │   ├── MainScreen.kt
│   │   │   │   │       │   └── MainViewModel.kt
│   │   │   │   │       ├── settings/
│   │   │   │   │       │   ├── SettingsScreen.kt
│   │   │   │   │       │   └── SettingsViewModel.kt
│   │   │   │   │       └── export/
│   │   │   │   │           ├── ExportDialog.kt
│   │   │   │   │           └── ExportViewModel.kt
│   │   │   │   └── worker/
│   │   │   │       └── ExportWorker.kt
│   │   │   ├── cpp/
│   │   │   │   ├── CMakeLists.txt
│   │   │   │   ├── deesser_jni.cpp
│   │   │   │   ├── DeEsserEngine.h
│   │   │   │   ├── DeEsserEngine.cpp
│   │   │   │   ├── FFTProcessor.h
│   │   │   │   ├── FFTProcessor.cpp
│   │   │   │   ├── DynamicsProcessor.h
│   │   │   │   ├── DynamicsProcessor.cpp
│   │   │   │   └── OboePlayer.cpp
│   │   │   ├── res/
│   │   │   │   ├── values/
│   │   │   │   │   ├── strings.xml
│   │   │   │   │   ├── colors.xml
│   │   │   │   │   └── themes.xml
│   │   │   │   └── drawable/
│   │   │   └── AndroidManifest.xml
│   │   └── test/
│   │       └── java/com/example/deesser/
│   │           ├── audio/
│   │           │   └── DeEsserProcessorTest.kt
│   │           └── domain/
│   │               └── usecase/
│   │                   └── ProcessAudioUseCaseTest.kt
│   └── build.gradle.kts
├── gradle/
│   ├── libs.versions.toml
│   └── wrapper/
├── build.gradle.kts
├── settings.gradle.kts
└── gradle.properties
```

## C++ / NDK 実装

### CMakeLists.txt
```cmake
cmake_minimum_required(VERSION 3.22.1)
project("deesser")

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Oboe
find_package(oboe REQUIRED CONFIG)

# KissFFT (ヘッダオンリー)
add_library(kissfft INTERFACE)
target_include_directories(kissfft INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/kissfft)

# DeEsser Engine
add_library(deesser_engine SHARED
    deesser_jni.cpp
    DeEsserEngine.cpp
    FFTProcessor.cpp
    DynamicsProcessor.cpp
    OboePlayer.cpp
)

target_link_libraries(deesser_engine
    PRIVATE
    oboe::oboe
    kissfft
    android
    log
)
```

### DeEsserEngine.h
```cpp
#pragma once

#include <vector>
#include <memory>
#include "FFTProcessor.h"
#include "DynamicsProcessor.h"

struct DeEsserParams {
    float threshold = -20.0f;    // dB
    float reduction = -6.0f;     // dB
    float frequency = 6000.0f;   // Hz
    float range = 4000.0f;       // Hz
    float attack = 1.0f;         // ms
    float release = 50.0f;       // ms
    float mix = 100.0f;          // %
    bool listenMode = false;
};

class DeEsserEngine {
public:
    DeEsserEngine(int sampleRate, int channels);
    ~DeEsserEngine();

    void setParams(const DeEsserParams& params);
    void process(float* input, float* output, int numFrames);
    float getGainReduction() const;
    void reset();

private:
    int mSampleRate;
    int mChannels;
    DeEsserParams mParams;
    
    std::unique_ptr<FFTProcessor> mFFTProcessor;
    std::unique_ptr<DynamicsProcessor> mDynamicsProcessor;
    
    // オーバーラップ加算用バッファ
    std::vector<float> mOverlapBuffer;
    
    float mCurrentGainReduction = 0.0f;
};
```

### JNI Bridge (deesser_jni.cpp)
```cpp
#include <jni.h>
#include "DeEsserEngine.h"

extern "C" {

JNIEXPORT jlong JNICALL
Java_com_example_deesser_audio_DeEsserProcessor_nativeCreate(
    JNIEnv* env, jobject thiz, jint sampleRate, jint channels) {
    auto* engine = new DeEsserEngine(sampleRate, channels);
    return reinterpret_cast<jlong>(engine);
}

JNIEXPORT void JNICALL
Java_com_example_deesser_audio_DeEsserProcessor_nativeDestroy(
    JNIEnv* env, jobject thiz, jlong engineHandle) {
    auto* engine = reinterpret_cast<DeEsserEngine*>(engineHandle);
    delete engine;
}

JNIEXPORT void JNICALL
Java_com_example_deesser_audio_DeEsserProcessor_nativeSetParams(
    JNIEnv* env, jobject thiz, jlong engineHandle,
    jfloat threshold, jfloat reduction, jfloat frequency, jfloat range,
    jfloat attack, jfloat release, jfloat mix, jboolean listenMode) {
    auto* engine = reinterpret_cast<DeEsserEngine*>(engineHandle);
    DeEsserParams params{
        threshold, reduction, frequency, range,
        attack, release, mix, listenMode
    };
    engine->setParams(params);
}

JNIEXPORT void JNICALL
Java_com_example_deesser_audio_DeEsserProcessor_nativeProcess(
    JNIEnv* env, jobject thiz, jlong engineHandle,
    jfloatArray input, jfloatArray output, jint numFrames) {
    auto* engine = reinterpret_cast<DeEsserEngine*>(engineHandle);
    
    jfloat* inputPtr = env->GetFloatArrayElements(input, nullptr);
    jfloat* outputPtr = env->GetFloatArrayElements(output, nullptr);
    
    engine->process(inputPtr, outputPtr, numFrames);
    
    env->ReleaseFloatArrayElements(input, inputPtr, 0);
    env->ReleaseFloatArrayElements(output, outputPtr, 0);
}

JNIEXPORT jfloat JNICALL
Java_com_example_deesser_audio_DeEsserProcessor_nativeGetGainReduction(
    JNIEnv* env, jobject thiz, jlong engineHandle) {
    auto* engine = reinterpret_cast<DeEsserEngine*>(engineHandle);
    return engine->getGainReduction();
}

} // extern "C"
```

## Kotlin実装

### DeEsserProcessor.kt
```kotlin
package com.example.deesser.audio

import com.example.deesser.domain.model.DeEsserParams
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class DeEsserProcessor @Inject constructor() {
    
    private var engineHandle: Long = 0L
    private var isInitialized = false
    
    fun initialize(sampleRate: Int, channels: Int) {
        if (isInitialized) release()
        engineHandle = nativeCreate(sampleRate, channels)
        isInitialized = true
    }
    
    fun setParams(params: DeEsserParams) {
        if (!isInitialized) return
        nativeSetParams(
            engineHandle,
            params.threshold,
            params.reduction,
            params.frequency,
            params.range,
            params.attack,
            params.release,
            params.mix,
            params.listenMode
        )
    }
    
    fun process(input: FloatArray, output: FloatArray, numFrames: Int) {
        if (!isInitialized) return
        nativeProcess(engineHandle, input, output, numFrames)
    }
    
    fun getGainReduction(): Float {
        if (!isInitialized) return 0f
        return nativeGetGainReduction(engineHandle)
    }
    
    fun release() {
        if (isInitialized) {
            nativeDestroy(engineHandle)
            engineHandle = 0L
            isInitialized = false
        }
    }
    
    // Native methods
    private external fun nativeCreate(sampleRate: Int, channels: Int): Long
    private external fun nativeDestroy(engineHandle: Long)
    private external fun nativeSetParams(
        engineHandle: Long,
        threshold: Float, reduction: Float,
        frequency: Float, range: Float,
        attack: Float, release: Float,
        mix: Float, listenMode: Boolean
    )
    private external fun nativeProcess(
        engineHandle: Long,
        input: FloatArray, output: FloatArray,
        numFrames: Int
    )
    private external fun nativeGetGainReduction(engineHandle: Long): Float
    
    companion object {
        init {
            System.loadLibrary("deesser_engine")
        }
    }
}
```

### MainViewModel.kt
```kotlin
package com.example.deesser.ui.screens.main

import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.deesser.domain.model.*
import com.example.deesser.domain.usecase.*
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class MainViewModel @Inject constructor(
    private val loadAudioUseCase: LoadAudioUseCase,
    private val processAudioUseCase: ProcessAudioUseCase,
    private val exportAudioUseCase: ExportAudioUseCase
) : ViewModel() {
    
    private val _uiState = MutableStateFlow(MainUiState())
    val uiState: StateFlow<MainUiState> = _uiState.asStateFlow()
    
    private val _params = MutableStateFlow(DeEsserParams())
    val params: StateFlow<DeEsserParams> = _params.asStateFlow()
    
    fun loadAudio(uri: Uri) {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            
            loadAudioUseCase(uri)
                .onSuccess { audioFile ->
                    _uiState.update { 
                        it.copy(
                            isLoading = false,
                            audioFile = audioFile,
                            error = null
                        )
                    }
                }
                .onFailure { error ->
                    _uiState.update { 
                        it.copy(
                            isLoading = false,
                            error = error.message
                        )
                    }
                }
        }
    }
    
    fun updateParam(update: DeEsserParams.() -> DeEsserParams) {
        _params.update { it.update() }
    }
    
    fun setPlaybackMode(mode: PlaybackMode) {
        _uiState.update { it.copy(playbackMode = mode) }
    }
    
    fun togglePlayback() {
        val current = _uiState.value.playbackState
        val newState = if (current == PlaybackState.PLAYING) {
            PlaybackState.PAUSED
        } else {
            PlaybackState.PLAYING
        }
        _uiState.update { it.copy(playbackState = newState) }
    }
    
    fun processAndExport(settings: ExportSettings) {
        viewModelScope.launch {
            _uiState.update { it.copy(isProcessing = true, processingProgress = 0f) }
            
            processAudioUseCase(_uiState.value.audioFile!!, _params.value)
                .collect { progress ->
                    _uiState.update { it.copy(processingProgress = progress) }
                }
            
            exportAudioUseCase(_uiState.value.audioFile!!, settings)
                .onSuccess { outputUri ->
                    _uiState.update { 
                        it.copy(
                            isProcessing = false,
                            exportedUri = outputUri
                        )
                    }
                }
                .onFailure { error ->
                    _uiState.update { 
                        it.copy(
                            isProcessing = false,
                            error = error.message
                        )
                    }
                }
        }
    }
}

data class MainUiState(
    val isLoading: Boolean = false,
    val isProcessing: Boolean = false,
    val processingProgress: Float = 0f,
    val audioFile: AudioFile? = null,
    val playbackState: PlaybackState = PlaybackState.STOPPED,
    val playbackMode: PlaybackMode = PlaybackMode.ORIGINAL,
    val playbackPosition: Long = 0L,
    val gainReduction: Float = 0f,
    val exportedUri: Uri? = null,
    val error: String? = null
)

enum class PlaybackMode { ORIGINAL, PROCESSED }
enum class PlaybackState { STOPPED, PLAYING, PAUSED }
```

## 非機能要件

### パフォーマンス
- リアルタイムプレビュー遅延: < 30ms
- オフライン処理速度: 実時間の30%以下
- メモリ使用量: 最大200MB
- 起動時間: 2秒以内

### 対応デバイス
- Android 8.0 (API 26) 以上
- ARM64, ARMv7, x86, x86_64

### テスト要件
- JUnit 5 + MockK(ユニットテスト)
- Compose UI Test(UIテスト)
- NDK部分のネイティブテスト

## 実装優先順位

### Phase 1: 基盤構築
1. プロジェクトセットアップ(Gradle KTS, Version Catalog)
2. アーキテクチャ構築(DI, Navigation)
3. 音声ファイル読み込み(MediaExtractor)
4. 基本再生機能(AudioTrack / Oboe)

### Phase 2: 音声処理
5. C++ DSPエンジン実装
6. JNIブリッジ構築
7. De-esserアルゴリズム実装
8. リアルタイムプレビュー

### Phase 3: UI/UX
9. Compose UIメイン画面
10. パラメータ調整UI
11. A/B比較機能
12. 波形表示(Canvas)
13. ゲインリダクションメーター

### Phase 4: 仕上げ
14. エクスポート機能(MediaCodec)
15. プリセット管理(Room)
16. 設定画面
17. ダークモード対応
18. パフォーマンス最適化

## IntelliJ IDEA設定

### 推奨プラグイン
- Kotlin
- Android Support
- Gradle
- CMake simple highlighter

### Run/Debug Configuration
```
Android App:
  Module: app
  Deploy: Default APK
  Launch: Default Activity

Native (C++ Debug):
  Symbol Directories: app/build/intermediates/cmake/debug/obj
```

さて、動くアプリができるのか?

さて、Android のプロジェクトを作ったら、最初からエラーが出てました。
「無効な Gradle JDK 構成が見つかりました。 Grade 設定を開く 」

しょうがないので、チャットに入れて直してもらいます。

1. システムのJDKインストール状況を確認する
2. `gradle.properties` に `org.gradle.java.home` を追加し、IntelliJ の JBR を指すように設定する
3. `local.properties` を確認し、必要に応じて設定を更新する
4. 欠落していた `gradlew.bat` を生成する
5. 生成された `gradlew.bat` を使用してプロジェクトの同期を確認する
6. ビルドエラー(リソース不足)を修正して、完全にビルドが通るようにする
7. 最終的な確認を行い、解決を報告する

で、解決内容はこれ

### 解決内容

1.  **JDKパスの明示的な設定**:
    `gradle.properties` に `org.gradle.java.home` を追加し、IntelliJ IDEA に同梱されている JBR (Java Binary Runtime) のパスを指定しました。これにより、Gradle が適切な JDK を使用できるようになりました。
    ```properties
    org.gradle.java.home=C\:\\Program Files\\JetBrains\\IntelliJ IDEA 2025.3.1\\jbr
    ```

2.  **Gradle Wrapper の復元**:
    プロジェクトルートに `gradlew.bat` が存在しなかったため、システムにインストールされている Gradle を使用して `gradle wrapper` コマンドを実行し、必要なラッパーファイルを生成しました。

3.  **付随するビルドエラーの修正**:
    JDK 設定の修正後、プロジェクト内にアイコンリソース (`@mipmap/ic_launcher`) が不足しているためにビルドエラーが発生したため、`AndroidManifest.xml` から該当するアイコンの参照を一時的に削除し、ビルドが正常に通る状態にしました。

以上の修正により、`.\gradlew.bat assembleDebug` が成功し、Gradle の構成問題が解消されたことを確認しました。

とりあえず、直ったようでビルドできました、よかった。

気を取り直して

さきほどのプロンプト突っ込んでやってみます。

1. プロジェクト基盤の再構築
2. ドメイン層の実装
3. データ層の実装
4. DI (Hilt) の設定
5. 音声処理層の実装
6. C++ / NDK 実装
- CMakeLists.txt
- DeEsserEngine.h
- DeEsserEngine.cpp
- FFTProcessor.h, FFTProcessor.cpp
- DynamicsProcessor.h, DynamicsProcessor.cpp
- deesser_jni.cpp
7. UI層の実装
- ui/navigation/, ui/components/, ui/screens/ を作成
8. ビルド確認とテスト

とりあえず出てきたものをビルド・・・エラー・・・

1. プロジェクト基盤の再構築
- settings.gradle.kts, libs.versions.toml, build.gradle.kts 更新完了
2. ドメイン層の実装
- domain/model/ (DeEsserParams, AudioFile, PlaybackState, ProcessingState)
- domain/repository/ (AudioRepository, PresetRepository)
- domain/usecase/ (LoadAudioUseCase, ProcessAudioUseCase, ExportAudioUseCase, ManagePresetUseCase)
3. データ層の実装
- data/local/ (AppDatabase, PresetDao, PresetEntity)
- data/repository/ (AudioRepositoryImpl, PresetRepositoryImpl)
4. DI (Hilt) の設定
- DeEsserApplication.kt
- di/ (AppModule, AudioModule, DatabaseModule)
5. 音声処理層の実装
- audio/ (DeEsserProcessor, AudioPlayer, AudioDecoder, WaveformAnalyzer)
6. C++ / NDK 実装
- CMakeLists.txt, DeEsserEngine, FFTProcessor, DynamicsProcessor, deesser_jni.cpp
7. UI層の実装
- ui/navigation/NavGraph.kt
- ui/screens/main/ (MainScreen, MainViewModel)
- ui/screens/settings/SettingsScreen.kt
- ui/components/audio/ (WaveformCanvas, PlaybackControls)
- ui/components/deesser/ParameterPanel.kt
- ui/theme/Theme.kt
- MainActivity.kt, AndroidManifest.xml
8. ビルド確認とテスト
- BUILD SUCCESSFUL

クレジット使いすぎ

スマホアプリはやることが多いので、結構クレジット消費が速いような気はします。
使うモデルにもよるので、JetBrains的にはデフォルトがいいよとは書かれていました。
現在のデフォルトは、Gemini 3 Flash でした。
信頼性はともかく、速くて安いから?

image.png

ビルドでこける

AIでやってる方のビルドはうまくいってるというけど、手元のビルドは成功しないですね。

エラーがでている

Cause: jlink executable C:\Program Files\JetBrains\IntelliJ IDEA 2025.3.1\jbr\bin\jlink.exe does not exist.
1. 現在の環境変数 `JAVA_HOME` を Android Studio の JBR パスに設定する。
2. `gradle.properties` の `org.gradle.java.home` が正しいパス(Android Studio の JBR)を指していることを確認する。
3. `.\gradlew.bat --stop` を実行して、実行中の Gradle デーモンをすべて停止させる。
4. `.\gradlew.bat assembleDebug` を実行し、ビルドが成功するか確認する。
5. ビルドが成功したことをもって、問題の解決とする。
### 解決内容

Gradle ビルド時に `jlink.exe` が見つからないというエラーを、以下の手順で解決しました。

1.  **Gradle JDK パスの修正**:
    `gradle.properties` 内の `org.gradle.java.home` に、IntelliJ IDEA の古いパスが残っていたか、あるいは不完全な JDK パスが指定されていたため、有効な `jlink.exe` を含む Android Studio 付属の JBR (Java Binary Runtime) パスに変更しました。
    ```properties
    org.gradle.java.home=C\:\\Program Files\\Android\\Android Studio\\jbr
    ```

2.  **Gradle デーモンのリセット**:
    古いパス情報を保持していた可能性がある実行中の Gradle デーモンを `.\gradlew.bat --stop` で停止させました。

3.  **ビルド確認**:
    正しい JDK 設定下で `.\gradlew.bat assembleDebug` を実行し、ビルドが正常に成功することを確認しました。

これにより、C++ (NDK) を含むプロジェクト全体のビルドエラーが解消されました。

ビルド成功&起動成功

ビルドは成功したのですが、Runがうまくいきませんでしたが、何回かやりとりして、やっと起動しました。
結局原因は、使われていた Gradle JVM が良くなかったというものでした。

image.png

まとめ

諸事情により Windows でやったのですが、結構 Windows 起因ぽいエラーが多かった気はします。
根本的な話から言うと、デフォルトで Junie はインストールされていてほしいとは思いました。
いらない人は機能がオフにできればいいのではないでしょうか。
まぁ、とりあえずアプリが起動するとこまでは行ったので、一旦はよしとしたいです。
中身の検証はまたやります。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?