7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Android】AudioRecordで録音機能を実装する(PCM→WAV変換まで)

Last updated at Posted at 2025-09-29

はじめに

前回の記事では、Android標準APIのMediaRecorderを使って手軽に録音する方法を紹介しました。MediaRecorderは録音からエンコード、ファイル保存までしてくれますが、生の音声データを扱うことはできません。

そこで今回は、同じく標準APIであるAudioRecordを使って、マイクからの音声を直接扱う録音処理を実装します。生のPCMデータを保存し、WAV形式に変換して再生できるようにするところまでを解説します。

前回の記事:

前提

検証環境は以下の通りです。

macOS Sequoia 15.6(Apple M4)
Android Studio Narwhal | 2025.1.1
Kotlin 2.0.21

実装していく

Android Studioを起動したらNew Project > EnptyActiviryで新しいプロジェクトを立ち上げます。設定は任意ですがここでは下記の通りとします。

  • Name: MyAudioRecord
  • Minimum SDK: API 24

ここでは以下の要件で録音アプリを作ります。

  • サンプリングレート: 44.1kHz
  • モノラル
  • 16bit PCM
  • WAVファイルに保存

画面作成

録音機能を試すだけなので、画面は「録音開始」と「停止」の2つのボタンだけの最小限にします。MainActivity.ktに下記をコピペでOKです。(UIの解説は割愛します)

package com.example.myaudiorecord

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

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

        setContent {
            SimpleRecorderUI(
                onStart = { /* 後で録音処理を追加する */ },
                onStop = { /* 後で停止処理を追加する */ }
            )
        }
    }
}

@Composable
fun SimpleRecorderUI(onStart: () -> Unit, onStop: () -> Unit) {
    Row(
        modifier = Modifier
            .fillMaxSize()
            .padding(32.dp),
        horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Button(onClick = onStart) { Text("録音開始") }
        Button(onClick = onStop) { Text("停止") }
    }
}

録音機能実装

UIはできたので、次は録音の仕組みを作ります。

権限周り

まずはマイクを使うために使用権限をアプリに追加します。app/src/main/AndroidManifest.xml<manifest>タグの直下に次の1行を追記します。

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

アプリが録音を開始する際に権限があるか確認したいので、録音権限のチェックと、権限リクエストの処理を作っておきます。

import androidx.core.app.ActivityCompat
import android.Manifest
import android.content.pm.PackageManager
// ...

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

        setContent {
            SimpleRecorderUI(
                onStart = {
                    if (checkAudioPermission()) {
                        /* 後で録音処理を追加する */
                    } else {
                        requestAudioPermission()
                    }
                },
                onStop = { /* 後で停止処理を追加する */ }
            )
        }
    }

    // 権限チェック
    private fun checkAudioPermission(): Boolean {
        return ActivityCompat.checkSelfPermission(
            this,
            Manifest.permission.RECORD_AUDIO
        ) == PackageManager.PERMISSION_GRANTED
    }

    // 権限リクエスト
    private fun requestAudioPermission() {
        ActivityCompat.requestPermissions(
            this,
            arrayOf(Manifest.permission.RECORD_AUDIO),
            0 // リクエストコード(今回は特に使わないので0でOK)
        )
    }
}

これで権限がない状態で録音開始ボタンを押すと、アプリに権限を許可するかどうかを確認するダイアログが表示されます。

AudioRecordの初期化

今回の主題である AudioRecord を利用するために、まずは初期化処理を用意します。

import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import androidx.annotation.RequiresPermission
// ...

@RequiresPermission(Manifest.permission.RECORD_AUDIO)
private fun createAudioRecord(): AudioRecord {
    val sampleRate = 44100
    val channelConfig = AudioFormat.CHANNEL_IN_MONO
    val audioFormat = AudioFormat.ENCODING_PCM_16BIT
    val bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)

    return AudioRecord(
        MediaRecorder.AudioSource.MIC,
        sampleRate,
        channelConfig,
        audioFormat,
        bufferSize
    )
}

コード解説:

  • @RequiresPermission(Manifest.permission.RECORD_AUDIO)
    このアノテーションは「この関数を呼ぶには録音権限が必要」という情報を付与します。呼び出し側で権限チェックを忘れるとIDEやLintが警告を出してくれるため、権限周りの不備を防ぐのに役立ちます。

AudioRecordについて、コンストラクタは次のようになっています。

AudioRecord(
    audioSource: Int,
    sampleRateInHz: Int,
    channelConfig: Int,
    audioFormat: Int,
    bufferSizeInBytes: Int
)

それぞれの引数について、ざっくり説明するとこんな感じです。

  • audioSource
    どのマイクを使うかを選びます。
    今回はシンプルに MediaRecorder.AudioSource.MIC を指定して、端末の普通のマイクを使います。
  • sampleRateInHz
    サンプリングレート。「1秒間に音を何回切り取るか」の数値。
    よく使われるのは 44,100Hz(44.1kHz)で、CD音質と同じ値なのでとりあえずこれでOK。
  • channelConfig
    モノラルにするかステレオにするかを決めます。
    今回はシンプルに AudioFormat.CHANNEL_IN_MONO(モノラル)を指定しています。
  • audioFormat
    音声データの形式です。
    AudioFormat.ENCODING_PCM_16BIT を選んでおけば、非圧縮の16bit PCMで扱いやすく、標準的なフォーマットになります。
  • bufferSizeInBytes
     録音中にオーディオデータが書き込まれるバッファの合計サイズのこと。(マイクから流れてくる音を一時的にためておく入れ物の大きさみたいなもの)
    AudioRecord.getMinBufferSize() を使うと、AudioRecordオブジェクトを正常に作成するために必要な最小バッファサイズをバイト単位で返してくれるためこれを使います。

参考:

録音開始処理

次は録音を始める処理を追加します。

録音はメインスレッドでやるとアプリが止まってしまうので、別スレッドで実行するようにしています。

import java.io.File
import java.io.FileOutputStream
// ...

    private var pcmFile: File? = null
    private var isRecording = false
    private lateinit var audioRecord: AudioRecord

    @RequiresPermission(Manifest.permission.RECORD_AUDIO)
    private fun startRecording() {
        pcmFile = File(getExternalFilesDir(null), "record_${System.currentTimeMillis()}.pcm")

        audioRecord = createAudioRecord()
        audioRecord.startRecording()
        isRecording = true


        Thread {
            val buffer = ByteArray(1024)
            FileOutputStream(pcmFile!!).use { outputStream ->
                while (isRecording) {
                    val read = audioRecord.read(buffer, 0, buffer.size)
                    if (read > 0) {
                        outputStream.write(buffer, 0, read)
                    }
                }
            }
        }.start()
    }

コードの説明:

  • audioRecord.startRecording()
    実際にマイク入力を開始するメソッド。
  • Thread { ... }.start()
    録音処理を別スレッドで動かすため。
  • ByteArray(1024)
    読み取る一時バッファ。ここでは1024バイト単位でデータを読み取っています。
  • audioRecord.read()
    マイクからバッファに音声データを読み込む。戻り値は実際に読み込んだデータサイズ(バイト数)で、これが0より大きければファイルに書き込まれる。
  • FileOutputStream(pcmFile!!).use { ... }
    指定したファイルに録音データを書き込んでいます。.use { ... }にすることで自動的にクローズされます。

録音停止処理

録音を終了する時は、isRecordingフラグをfalseに更新して、AudioRecordを正しく停止・解放します。

private fun stopRecording() {
    if (this::audioRecord.isInitialized && isRecording) {
        isRecording = false
        audioRecord.stop()
        audioRecord.release()
    }
}

録音してみる

録音開始と停止の処理ができたため、未実装だったボタン押下時の処理を追加します。

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

    setContent {
        SimpleRecorderUI(
            onStart = {
                if (checkAudioPermission()) {
                    startRecording() // ★追加
                } else {
                    requestAudioPermission()
                }
            },
            onStop = {
                stopRecording() // ★追加
            }
        )
    }
}

これでボタンを押したら録音開始・停止できるようになったため、アプリを起動して実際に録音してみてください。

録音データを確認

録音ファイルはアプリ専用のストレージに保存されます。
今回のコードでは以下のパスに出力されます。

/sdcard/Android/data/com.example.myaudiorecord/files

ファイル名はrecord_数字.pcmのような形で生成されます。

Android StudioのDevice Explorerを使うか、adbコマンドを使ってファイルをPCに取り出せます。ただ、生成されるのは生のPCMデータなので、通常の音楽プレイヤーでは再生できません。

次の章で、PCやスマホの通常の音楽プレイヤーでも再生可能なWAV形式に変換します。

PCM→WAV変換

まず「PCM」と「WAV」の違いを簡単に整理しました。

  • PCM(Pulse Code Modulation)
    マイクから取り込んだ音を、そのまま数値として並べただけのデータ。
    → 生の波形データなので、再生するには追加の情報が必要

  • WAV(Waveform Audio File Format)
    PCMデータの先頭に「サンプリングレート」「チャンネル数」「ビット深度」などの情報(ヘッダ)を付けたもの。
    → 音楽プレイヤーはヘッダを読んで正しく再生できる

ということで、WAVに変換する理由がわかったので実装を進めていきます。

import java.nio.ByteBuffer
import java.nio.ByteOrder
// ...

private fun pcmToWav(pcmFile: File, wavFile: File) {
    val sampleRate = 44100
    val channels = 1
    val bitsPerSample = 16

    val pcmSize = pcmFile.length().toInt()
    val totalDataLen = pcmSize + 36
    val byteRate = sampleRate * channels * bitsPerSample / 8

    FileOutputStream(wavFile).use { out ->
        val header = ByteBuffer.allocate(44).order(ByteOrder.LITTLE_ENDIAN)

        // RIFFヘッダ
        header.put("RIFF".toByteArray(Charsets.US_ASCII))
        header.putInt(totalDataLen)
        header.put("WAVE".toByteArray())

        // fmtチャンク
        header.put("fmt ".toByteArray())
        header.putInt(16) // fmt chunk size
        header.putShort(1) // PCMフォーマット
        header.putShort(channels.toShort())
        header.putInt(sampleRate)
        header.putInt(byteRate)
        header.putShort((channels * bitsPerSample / 8).toShort())
        header.putShort(bitsPerSample.toShort())

        // dataチャンク
        header.put("data".toByteArray())
        header.putInt(pcmSize)

        out.write(header.array(), 0, 44)

        // PCM本体コピー
        pcmFile.inputStream().use { it.copyTo(out) }
    }
}

次に、開始時にWAVファイル出力用のwavFileを初期化し、停止時にPCMからWAVに変換して書き込みます。

private var wavFile: File? = null // ★追加

@RequiresPermission(Manifest.permission.RECORD_AUDIO)
private fun startRecording() {
    // ★以下のように書き換え
    val baseName = "record_${System.currentTimeMillis()}"
    pcmFile = File(getExternalFilesDir(null), "$baseName.pcm")
    wavFile = File(getExternalFilesDir(null), "$baseName.wav")
    
    // ...
}

private fun stopRecording() {
    if (this::audioRecord.isInitialized && isRecording) {
        isRecording = false
        audioRecord.stop()
        audioRecord.release()

        // ★追加
        pcmFile?.let { pcm ->
            wavFile?.let { wav ->
                pcmToWav(pcm, wav)
            }
        }
    }
}

ここまでできたらもう一度録音してみます。PCMファイルと同時にWAVファイルも生成されていると思います。WAVファイルを取り出して、通常の音楽プレイヤーでも再生できますので確認してみてください。

完成コード

以上で完成です。最後に完成系のコードを載せておきます。

// MainActivity.kt

package com.example.myaudiorecord

import android.Manifest
import android.content.pm.PackageManager
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.RequiresPermission
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
import java.io.File
import java.io.FileOutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder

class MainActivity : ComponentActivity() {
    private var pcmFile: File? = null
    private var wavFile: File? = null
    private var isRecording = false
    private lateinit var audioRecord: AudioRecord

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

        setContent {
            SimpleRecorderUI(
                onStart = {
                    if (checkAudioPermission()) {
                        startRecording()
                    } else {
                        requestAudioPermission()
                    }
                },
                onStop = {
                    stopRecording()
                }
            )
        }
    }
    private fun checkAudioPermission(): Boolean {
        return ActivityCompat.checkSelfPermission(
            this,
            Manifest.permission.RECORD_AUDIO
        ) == PackageManager.PERMISSION_GRANTED
    }

    private fun requestAudioPermission() {
        ActivityCompat.requestPermissions(
            this,
            arrayOf(Manifest.permission.RECORD_AUDIO),
            0
        )
    }

    @RequiresPermission(Manifest.permission.RECORD_AUDIO)
    private fun createAudioRecord(): AudioRecord {
        val sampleRate = 44100
        val channelConfig = AudioFormat.CHANNEL_IN_MONO
        val audioFormat = AudioFormat.ENCODING_PCM_16BIT
        val bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)

        return AudioRecord(
            MediaRecorder.AudioSource.MIC,
            sampleRate,
            channelConfig,
            audioFormat,
            bufferSize
        )
    }

    @RequiresPermission(Manifest.permission.RECORD_AUDIO)
    private fun startRecording() {
        val baseName = "record_${System.currentTimeMillis()}"
        pcmFile = File(getExternalFilesDir(null), "$baseName.pcm")
        wavFile = File(getExternalFilesDir(null), "$baseName.wav")

        audioRecord = createAudioRecord()
        audioRecord.startRecording()
        isRecording = true


        Thread {
            val buffer = ByteArray(1024)
            FileOutputStream(pcmFile!!).use { outputStream ->
                while (isRecording) {
                    val read = audioRecord.read(buffer, 0, buffer.size)
                    if (read > 0) {
                        outputStream.write(buffer, 0, read)
                    }
                }
            }
        }.start()
    }

    private fun stopRecording() {
        if (this::audioRecord.isInitialized && isRecording) {
            isRecording = false
            audioRecord.stop()
            audioRecord.release()

            pcmFile?.let { pcm ->
                wavFile?.let { wav ->
                    pcmToWav(pcm, wav)
                }
            }
        }
    }

    private fun pcmToWav(pcmFile: File, wavFile: File) {
        val sampleRate = 44100
        val channels = 1
        val bitsPerSample = 16

        val pcmSize = pcmFile.length().toInt()
        val totalDataLen = pcmSize + 36
        val byteRate = sampleRate * channels * bitsPerSample / 8

        FileOutputStream(wavFile).use { out ->
            val header = ByteBuffer.allocate(44).order(ByteOrder.LITTLE_ENDIAN)

            // RIFFヘッダ
            header.put("RIFF".toByteArray(Charsets.US_ASCII))
            header.putInt(totalDataLen)
            header.put("WAVE".toByteArray())

            // fmtチャンク
            header.put("fmt ".toByteArray())
            header.putInt(16) // fmt chunk size
            header.putShort(1) // PCMフォーマット
            header.putShort(channels.toShort())
            header.putInt(sampleRate)
            header.putInt(byteRate)
            header.putShort((channels * bitsPerSample / 8).toShort())
            header.putShort(bitsPerSample.toShort())

            // dataチャンク
            header.put("data".toByteArray())
            header.putInt(pcmSize)

            out.write(header.array(), 0, 44)

            // PCM本体コピー
            pcmFile.inputStream().use { it.copyTo(out) }
        }
    }
}

@Composable
fun SimpleRecorderUI(onStart: () -> Unit, onStop: () -> Unit) {
    Row(
        modifier = Modifier
            .fillMaxSize()
            .padding(32.dp),
        horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Button(onClick = onStart) { Text("録音開始") }
        Button(onClick = onStop) { Text("停止") }
    }
}
7
5
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
7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?