Recording and playing audio with MediaRecorder and MediaPlayer

Creating the project and UI

  1. Create a new Android App targetting API 21 and later named SoundBoard
  2. Change the Layout in activity_main.xml to use a LinearLayout with vertical orientation
  3. Add a ToggleButton control to the layout, with the id of toggleButton
  4. Set the textOff property to a string resource with the word "Playback" and a corresponding resource of the textOn property of "Record"
  5. As the soundboard will allow us to record a number of different sounds, its simpler to progrmatically generate the grid of buttons. Add the following method to the MainActivity class:
    fun drawBoard(width: Int, height: Int){
        val gridLayout = GridLayout(this)
        gridLayout.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        gridLayout.columnCount = width
        for (i in 0 until (width * height)){
            val button = Button(this)
            button.tag = i
            val buttonNumber = i + 1
            button.setText("${buttonNumber}")
            button.gravity = Gravity.CENTER
            //todo: add touch listener
            gridLayout.addView(button)
            
            val layoutParams = GridLayout.LayoutParams()
            layoutParams.columnSpec = GridLayout.spec(i % width,1f)
            layoutParams.rowSpec = GridLayout.spec(i / width,1f)
            button.layoutParams = layoutParams
        }
        this.layout.addView(gridLayout)
    }
    The GridLayout.spec object defines the weight for each element. Assigning the same weight ensures they are all the same size
  6. Add a call to this method from the onCreate method with values of 3 and 4 for the width and height respectively.
  7. Run the application and verify that you have a toggle button at the top of the UI followed by a grid of 12 buttons

Permissions

Starting with Android Marshmallow (6.0), certain types of permissions require explicit permission from the user, from within the app, in addition to being specified in the AndroidManifest.xml file. This includes accessing the microphone and writing to external storage - both of which this app will need to do

  1. Open the AndroidManifest.xml file and add the following permissions inside the <mainfest> element:
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
  2. Next return to the MainActivity class and add a property to store a request code for the permissions request as follows:
    val PERMISSIONS_REQ = 1
    The actual value of the request is arbitrary
  3. Add the following method to check for each permission, and where permission is not granted, request permission:
    fun requestPermissions(){
        val permissionsRequired = mutableListOf<String>()
        val hasRecordPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
        if (!hasRecordPermission){
            permissionsRequired.add(Manifest.permission.RECORD_AUDIO)
        }
        val hasStoragePermission = ContextCompat.checkSelfPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
        if (!hasStoragePermission){
            permissionsRequired.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
        }
        if (permissionsRequired.isNotEmpty()){
            ActivityCompat.requestPermissions(this, permissionsRequired.toTypedArray(),PERMISSIONS_REQ)
        }
    }
  4. Call this method from the within onCreate method

Playback and recording audio

The application needs to be able to start and stop recording, as well as start and stop playback. Adding all this functionality within the Activity would make it less reuable, so we will create a class with the responsibility for recording and playback

  1. Add a new Kotlin class called AudioManager
  2. Add two properties for a MediaRecorder and MediaPlayer as follows:
    var mediaRecorder : MediaRecorder? = null
    var mediaPlayer : MediaPlayer? = null
  3. When files are saved, we will used an id (corresponding to their position on the soundboard). Add the following method to the AudioManager class to retrieve a file path for a given id as follows:
    private fun filePathForId(id: Int) : String {
        return Environment.getExternalStorageDirectory().absolutePath + "/$id.aac"
    }
  4. Next add a method setup a MediaRecorder object and start recording. Pay attention to the comments in the code which explain what is happening
    fun startRecording(id: Int): Boolean {
        //check the device has a microphone
        if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_MICROPHONE)) {
            
            //create new instance of MediaRecorder
            mediaRecorder = MediaRecorder()
            //specify source of audio (Microphone)
            mediaRecorder?.setAudioSource(MediaRecorder.AudioSource.MIC)
            //specify file type and compression format
            mediaRecorder?.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS)
            mediaRecorder?.setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
            //specify audio sampling rate and encoding bit rate (48kHz and 128kHz respectively)
            mediaRecorder?.setAudioSamplingRate(48000)
            mediaRecorder?.setAudioEncodingBitRate(128000)
            //specify where to save
            val fileLocation = filePathForId(id)
            mediaRecorder?.setOutputFile(fileLocation)
            //record
            mediaRecorder?.prepare()
            mediaRecorder?.start()
            
            return true
        } else {
            return false
        }
    }
    The MediaRecorder class can use a range for formats and file types, detailed here
  5. Stopping the recording is a little easier, add the following method which will do that:
    fun stopRecording() {
        mediaRecorder?.stop()
        mediaRecorder?.release()
        mediaRecorder = null
    }
  6. To playback the audio, we use the MediaPlayer object, but less configuration is required, add the following methods to start and stop playback:
    fun startPlayback(id: Int): Boolean {
        val path = filePathForId(id)
        if (File(path).exists()) {
            mediaPlayer = MediaPlayer()
            mediaPlayer?.setDataSource(path)
            mediaPlayer?.prepare()
            mediaPlayer?.start()
            return true
        }
        return false
    }
    fun stopPlayback() {
        mediaPlayer?.stop()
        mediaPlayer?.release()
        mediaPlayer = null
    }

Handling touch events to start and stop recording and playback

The app is designed to work in such a way that holding down on a button will either play or record audio associated with that button. Whether audio is played or recorded will depend on the state of the ToggleButton.

  1. Add an implementation of the View.OnTouchListener interface as follows:
    val touchListener = View.OnTouchListener { v: View?, event: MotionEvent? -> Boolean
        val id = v?.tag as Int
            if (event?.action == MotionEvent.ACTION_DOWN) {
                if (toggleButton.isChecked){ //recording
                    val isRecording = audioManager.startRecording(id)
                    if (isRecording) {
                        v.background.setColorFilter(Color.RED, PorterDuff.Mode.DARKEN)
                        //PorterDuff is a class with list of blending + compositing modes, named after the authors of a paper on the subje
                    } else {
                        Toast.makeText(this,"Unable to start recording",Toast.LENGTH_LONG).show()
                    }
                } else {
                    if (audioManager.startPlayback(id)){
                        v.background.setColorFilter(Color.GREEN,PorterDuff.Mode.DARKEN)
                    }
                }
                toggleButton.isEnabled = false
                return@OnTouchListener true
            }
            if (event?.action == MotionEvent.ACTION_UP){
                if (toggleButton.isChecked){
                    audioManager.stopRecording()
                } else {
                    audioManager.stopPlayback()
                }
                v.background.clearColorFilter()
                toggleButton.isEnabled = true
                return@OnTouchListener true
            }
         false
    }
  2. Locate the //todo: comment in the drawBoard method, and replace it with the following:
    button.setOnTouchListener(touchListener)
  3. Run the application and make sure it works as expected. Be aware that if you use an emulator the recorded audio may be severely distorted (at the time of writing the documentation states that it does not work). If you are able, test on an Android phone or tablet

A completed version of the project can be found on GitHub

Tasks