はじめに
Androidで簡易的な録音アプリの作り方を解説します。
録音機能を実装するには、Androidの標準クラスであるMediaRecorderかAudioRecordを利用します。
MediaRecorderは録音からファイル保存までをまとめて処理できる高レベルAPIで、シンプルな録音アプリを素早く作りたいときに便利です。一方、AudioRecordはマイク入力の生データを直接扱える低レベルAPIで、波形解析や音声認識など高度な処理に適しています。
APIリファレンス:
今回は、より簡単に始められるMediaRecorderを使って録音アプリを実装していきます。
前提
検証環境は以下の通りです。
- macOS Sequoia 15.6(Apple M4)
- Android Studio Narwhal | 2025.1.1
- Kotlin 2.0.21
実装していく
さっそく録音アプリを実装していきます。
権限の設定
まずは、録音を行うためにマイクの使用権限をアプリに追加します。
権限の追加は app/src/main/AndroidManifest.xml
に次の1行を追記します。
<uses-permission android:name="android.permission.RECORD_AUDIO" />
この行は <manifest>
タグの直下に追加します。
<?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" />
<application
android:allowBackup="true"
...
これは「このアプリは録音機能を使う予定がある」とAndroidに知らせる宣言のようなものです。あとでAndroidの設定画面からアプリの権限設定でマイクの許可をオンにしますが、この宣言をしておかないとマイクの権限設定ができません。
画面周り
次に録音操作用の画面を作ります。今回は録音機能が主役なので、UIは録音の開始と停止を切り替えるだけの最小構成にします。下記のコードをMainActivity
にそのままコピペして使えます。UIの詳細な説明は省きますが、興味があればJetpack Composeについて調べてみてください。
package com.example.myrecording // 適宜修正
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 起動時点で録音権限をチェック
val granted = ContextCompat.checkSelfPermission(
this,
Manifest.permission.RECORD_AUDIO
) == PackageManager.PERMISSION_GRANTED
setContent {
var isRecording by remember { mutableStateOf(false) }
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Button(onClick = {
if (!isRecording) {
if (granted) {
// startRecording() は後で実装
isRecording = true
} else {
// 権限がない場合はトースト表示
Toast.makeText(
this@MainActivity,
"録音権限がありません",
Toast.LENGTH_SHORT
).show()
}
} else {
// stopRecording() は後で実装
isRecording = false
}
}) {
Text(if (isRecording) "Stop" else "Start")
}
}
}
}
}
冒頭のこのコードは、アプリがマイクの録音権限(RECORD_AUDIO
)をすでに持っているかどうかを判定するためのチェック処理です。
val granted = ContextCompat.checkSelfPermission(
this,
Manifest.permission.RECORD_AUDIO
) == PackageManager.PERMISSION_GRANTED
MediaRecorderを実装
まずMediaRecorderのインスタンスを作成したいのですが、従来のコンストラクタが非推奨になっており、APIレベル31から追加されたコンストラクタと使い分ける必要があります。そのため、次のようにインスタンスを生成する関数を作ります。
import android.media.MediaRecorder
import android.os.Build
// ...
private fun createMediaRecorder(): MediaRecorder {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// API 31以上は新コンストラクタ
MediaRecorder(this)
} else {
// API 30以下は旧コンストラクタ(非推奨だが互換性のため必要)
@Suppress("DEPRECATION")
MediaRecorder()
}
}
録音開始処理
次にMediaRecorder
インスタンスを使って録音処理を開始するstartRecording()
メソッドを作ります。(冒頭setContent内のコメントアウト解除をお忘れなく)
import java.io.File
// ...
private var recorder: MediaRecorder? = null
private fun startRecording() {
if (recorder != null) return
val file = File(getExternalFilesDir(null)!!, "rec_${System.currentTimeMillis()}.m4a")
recorder = createMediaRecorder().apply {
setAudioSource(MediaRecorder.AudioSource.MIC) // マイク入力
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) // コンテナ形式
setAudioEncoder(MediaRecorder.AudioEncoder.AAC) // 音声コーデック
setAudioEncodingBitRate(128_000) // ビットレート
setAudioSamplingRate(44_100) // サンプリングレート
setOutputFile(file.absolutePath) // 保存先
prepare() // 準備
start() // 録音開始
}
}
ここでは MediaRecorder に対して必要なパラメータを順番に設定しています。
-
setAudioSource(...)
録音の入力元を指定する。マイク入力を使いたいのでMediaRecorder.AudioSource.MIC
を指定します。 -
setOutputFormat(...)
録音データをどのファイル形式で保存するかを決める。ここでは MediaRecorder.OutputFormat.MPEG_4 を指定して MP4 コンテナ形式にしています。 -
setAudioEncoder(...)
録音した音声をどの方式で圧縮するかを決める。 -
setAudioEncodingBitRate(...)
ビットレート(音声データの情報量)を指定する。ここでは128000(=128kbps)とする。 -
setAudioSamplingRate(...)
サンプリングレートを指定する。ここでは44100Hzに設定し、CD音質相当で録音できるようにしています。 -
setOutputFile(...)
実際に書き込むファイルのパスを指定します。ここではgetExternalFilesDir(null)
以下に .m4a ファイルとして保存します。
録音停止処理
録音を終えるときは、ファイルを書き終えてリソースを解放します。画面を閉じるときに録音が走っていない保証はないので、onDestroyでも保険としてリリースしておきます。
private fun stopRecording() {
recorder?.let {
try {
it.stop()
} catch (_: IllegalStateException) {
}
it.release()
}
recorder = null
}
override fun onDestroy() {
super.onDestroy()
recorder?.release()
recorder = null
}
音声ファイル形式について
今回指定している拡張子.m4a
とはなにか?
→.m4a
はMPEG-4 Audioの略で、もともと動画ファイルとしてよく使われるMP4を「音声だけに使ったときの拡張子」です。動画の部分を省いて、音声だけを保存するための形式だと思えばOKです。中身は主に「AAC」(Advanced Audio Coding) という方式で圧縮されていて、スマホやPCなど多くの環境でそのまま再生できます。
AndroidのMediaRecorderではMP3を直接保存することができません。
その代わりに、録音した音声は標準的にAACをm4aファイルとして保存する形になります。
MediaRecorderは録音した音声をリアルタイムでエンコード(圧縮変換)しながら保存 する仕組みであるため、OutputFormat
で選べる形式は「圧縮済みのコンテナ形式」に限られているようです。(つまり、非圧縮のPCMやRAWは選べない)
録音してみる
手元に実機がなかったのでエミュレータで録音してみました。
エミュレータで録音するにはPCのマイクに入力した音を認識させる必要があるため、次のようにエミュレータのマイク設定をしてください。
- エミュレータを起動したら「Running Devices」を開き三点アイコンを押して「Extended Controls」を開く(
Shift
2回押しして「Extended Controls」と入力して検索してもOK) - 左のメニューから「Microphone」を選択
- 「Virtual microphone uses host audio input」をオンにする
設定が完了したらエミュレータを起動し、Androidの設定画面からアプリにマイクの権限を与えてからアプリを起動して録音してみてください。
録音したファイルを取り出す
録音した音声をPCで再生するには、エミュレータ(または実機)の内部に保存されたファイルをPCにコピーします。保存先は、今回のコードの場合 アプリ専用の外部ストレージ領域 です。
保存パスの例:
/sdcard/Android/data/<パッケージ名>/files/
(例:/sdcard/Android/data/com.example.myrecording/files/rec_XXXX.m4a
)
ターミナルから以下のコマンドを実行してデスクトップにコピーします。(ファイル名はadb shell ls
で確認してください)
adb pull /sdcard/Android/data/com.example.myrecording/files/<ファイル名>.m4a ~/Desktop/
Android StudioのDevice Explorerを使えばGUI操作で取り出すことも可能です。(説明は割愛)
PCにコピーした.m4a
ファイルはPC標準のメディアプレーヤーでそのまま再生できます。
完成コード
最後に完成系のコードを貼っておきます。
package com.example.myrecording
import android.Manifest
import android.content.pm.PackageManager
import android.media.MediaRecorder
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat
import java.io.File
class MainActivity : ComponentActivity() {
private var recorder: MediaRecorder? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 起動時点で録音権限をチェック
val granted = ContextCompat.checkSelfPermission(
this,
Manifest.permission.RECORD_AUDIO
) == PackageManager.PERMISSION_GRANTED
setContent {
var isRecording by remember { mutableStateOf(false) }
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Button(onClick = {
if (!isRecording) {
if (granted) {
startRecording()
isRecording = true
} else {
Toast.makeText(
this@MainActivity,
"録音権限がありません",
Toast.LENGTH_SHORT
).show()
}
} else {
stopRecording()
isRecording = false
}
}) {
Text(if (isRecording) "Stop" else "Start")
}
}
}
}
private fun createMediaRecorder(): MediaRecorder {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// API 31以上は新コンストラクタ
MediaRecorder(this)
} else {
// API 30以下は旧コンストラクタ(非推奨だが互換性のため必要)
@Suppress("DEPRECATION")
MediaRecorder()
}
}
private fun startRecording() {
if (recorder != null) return
val file = File(getExternalFilesDir(null)!!, "rec_${System.currentTimeMillis()}.m4a")
recorder = createMediaRecorder().apply {
setAudioSource(MediaRecorder.AudioSource.MIC) // マイク入力
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) // コンテナ形式
setAudioEncoder(MediaRecorder.AudioEncoder.AAC) // 音声コーデック
setAudioEncodingBitRate(128_000) // ビットレート
setAudioSamplingRate(44_100) // サンプリングレート
setOutputFile(file.absolutePath) // 保存先
prepare() // 準備
start() // 録音開始
}
}
private fun stopRecording() {
recorder?.let {
try {
it.stop()
} catch (_: IllegalStateException) {
}
it.release()
}
recorder = null
}
override fun onDestroy() {
super.onDestroy()
recorder?.release()
recorder = null
}
}
今回の記事は以上です。ありがとうございました!