大阪市中央区 システムソフトウェア開発会社

営業時間:平日09:15〜18:15
MENU

【Android】メトロノームアプリの作成【Kotlin】

著者:北本 敦
公開日:2021/06/10
最終更新日:2021/06/10
カテゴリー:技術情報
タグ:

北本です。

最近、Androidプログラミングの勉強を始めました。
Android StudioでKotlinを使ってのプログラミングに挑戦しており、『はじめてのAndroidプログラミング 第5版』(金田浩明著, SBクリエイティブ)を参考書にしています。

練習がてら非常に簡単なアプリを作成してみたので紹介します。最低限の機能のみを備えたメトロノームです。

 

仕様

画面は上掲画像のようなものとします。

入力欄にBPM値(一分間の拍数)をキー入力し、「START」ボタンをタップすると、再生状態になり指定したテンポでの拍に合わせて音が鳴ります。再生状態になるとボタンのテキストが「START」から「STOP」に変化し、それをタップすると再生状態から抜けます。

設定可能なBPM値は、1~300の整数とし、異常値が設定された状態で「START」ボタンをタップした場合は、エラーメッセージを表示し再生状態に遷移しないものとします。

再生状態中は、入力欄が使用不可となりBPM値の変更ができないものとします。

また、メトロノームは再生しながら楽器を弾くような使い方が想定されるため、再生状態中は操作せずに放置してもスリープ状態にならないようにします。

 

下準備

Android Studioで、Empty Activityを選択し、名前を「metronome」、言語を「Kotlin」としてプロジェクトを作成します。

非常に短い音声ファイルbeat.oggを用意し、app/resにrawフォルダを作成し、その中に配置します。

ビューバインディングを有効にするため、build.gradle (Module: metronome.app)のandroid{}内に以下のように追記し、Sync Nowで変更を適用します。

android {
    // 中略 //
    buildFeatures{
        viewBinding = true
    }
}

 

レイアウト

「BPM」のテキスト表示をTextViewとして、BPM値の入力欄をNumber (Signed)として、「START」ボタンをButtonとして配置します。idは以下のように設定することとします。

ビューid
TextViewtextViewBPM
Number (Signed)editTextBPM
ButtonbuttonStart

レイアウトの詳細については説明を割愛しますが、以下のようなxmlファイルになっています。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textViewBPM"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="BPM"
        android:textSize="34sp"
        app:layout_constraintBottom_toTopOf="@+id/buttonStart"
        app:layout_constraintEnd_toStartOf="@+id/editTextBPM"
        app:layout_constraintHorizontal_bias="0.521"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/editTextBPM"
        android:layout_width="129dp"
        android:layout_height="83dp"
        android:ems="10"
        android:inputType="numberSigned"
        android:maxLength="3"
        android:text="60"
        android:textSize="60sp"
        app:layout_constraintBottom_toBottomOf="@+id/textViewBPM"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/textViewBPM"
        app:layout_constraintTop_toTopOf="@+id/textViewBPM" />

    <Button
        android:id="@+id/buttonStart"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="START"
        android:textSize="34sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/editTextBPM" />
</androidx.constraintlayout.widget.ConstraintLayout>

 

実装

肝要となるのは、
・Handlerを使ったタイマー処理
・SoundPoolを使った音声再生
・FLAG_KEEP_SCREEN_ONのON/OFFによるアイドル状態での自動スリープの無効/有効化
です。

MainActivity.kt

package com.example.metronome

import android.media.AudioAttributes
import android.media.SoundPool
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import com.example.metronome.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private lateinit var soundPool: SoundPool
    private val handler = Handler(Looper.getMainLooper())
    private var isRunning: Boolean = false // 再生状態であるか
    private var interval: Long = 0 // 拍間のミリ秒数
    private var soundResId = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.buttonStart.setOnClickListener{
            onStartButtonTapped()
        }
    }

    override fun onResume() {
        super.onResume()
        // SoundPoolのインスタンスを生成しリソースを読み込み
        soundPool =
            SoundPool.Builder().run{
                val audioAttributes = AudioAttributes.Builder().run{
                    setUsage(AudioAttributes.USAGE_ALARM)
                    build()
                }
                setMaxStreams(1)
                setAudioAttributes(audioAttributes)
                build()
            }
        soundResId = soundPool.load(this, R.raw.beat,1)
    }

    override fun onPause(){
        // アクティビティ非表示時はSoundPoolのリソースを解放
        super.onPause()
        soundPool.release()
    }

    private fun onStartButtonTapped(){
        if(isRunning){
            stop();
        }
        else{
            start();
        }
    }

  // 「START」ボタン押下時の処理
    private fun start(){
        var bpm: Long?;
        bpm = binding.editTextBPM.text.toString().toLongOrNull()

        if(bpm == null){
            val dialog = AlertDialog.Builder(this)
            dialog.setMessage("BPM値の入力が正しくありません。")
            dialog.setPositiveButton("OK", null)
            dialog.show()
            return
        }

        bpm?.let{
            if(it <= 0 || 300 < it){
                val dialog = AlertDialog.Builder(this)
                dialog.setMessage("BPM値は1~300の範囲で入力してください。")
                dialog.setPositiveButton("OK", null)
                dialog.show()
                return
            }
        }
        interval = 60000 / bpm // 1分( = 60000ms) / BPMで拍間のミリ秒数を求める

        isRunning = true
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) // 再生中はスリープ状態にならないようにFLAG_KEEP_SCREEN_ONをONにする
        handler.post(runnable) // Handlerにコールバック関数を渡す
        binding.buttonStart.setText("STOP")
        binding.editTextBPM.isEnabled = false
    }

  // 「STOP」ボタン押下時の処理
    private fun stop(){
        isRunning = false
        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) // 非再生時はスリープ状態になってもよいのでFLAG_KEEP_SCREEN_ONをOFFにする
        handler.removeCallbacks(runnable) // Handlerからコールバック関数を削除
        binding.buttonStart.setText("START")
        binding.editTextBPM.isEnabled = true
    }

    // 音声を再生するコールバック関数
    private val runnable = object: Runnable {
        override fun run(){
            soundPool.play(soundResId, 1.0f, 1.0f, 100, 0, 1.0f) // SoundPoolでの音声再生
            handler.postDelayed(this, interval) // intervalに設定したミリ秒後にコールバックを呼び出す
        }
    }
}

 

以下は、アプリをエミュレータで動作させた様子の動画です。
音声をONにしてご覧ください。

    上に戻る