Audio Playback Capture in Android X

Learn how to integrate the Android Playback Capture API into your app, allowing you to record and play back audio from other apps. By Evana Margain Puig.

4.8 (4) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Starting the Playback Audio Capture

The first thing you’ll implement in this section is startAudioCapture. Locate it just below onStartCommandand add:

// 1
val config = AudioPlaybackCaptureConfiguration.Builder(mediaProjection!!)
        .addMatchingUsage(AudioAttributes.USAGE_MEDIA)
        .build()

// 2
val audioFormat = AudioFormat.Builder()
        .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
        .setSampleRate(8000)
        .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
        .build()

// 3
audioRecord = AudioRecord.Builder()
        .setAudioFormat(audioFormat)
        .setBufferSizeInBytes(BUFFER_SIZE_IN_BYTES)
        .setAudioPlaybackCaptureConfig(config)
        .build()

audioRecord!!.startRecording()

// 4
audioCaptureThread = thread(start = true) {
  val outputFile = createAudioFile()
  Log.d(LOG_TAG, "Created file for capture target: ${outputFile.absolutePath}")
  writeAudioToFile(outputFile)
}

Again, this is a large function. Here’s what you’re doing with it:

  1. You create a constant that handles the audio capture configuration. The three options available for the Usage type are: USAGE_GAME, USAGE_MEDIA and USAGE_UNKNOWN.
  2. Then you set the values for the recording. The ones in this code are standard, but you may want to modify them.
  3. Here, you set the previous two values into the audio record builder so it can start recording.
  4. Finally, the output of the recording goes through to two of the functions you already have in the service. One creates an audio file, while the other writes it to the phone memory.

If you want to know more about the usage attributes, here’s a brief description of each:

  • USAGE_MEDIA: For media like music or movie soundtracks.
  • USAGE_GAME: For game audio.
  • USAGE_UNKNOWN: Use when you don’t know what type of audio you’ll record.

The function you just created also requires some imports:

import android.media.AudioPlaybackCaptureConfiguration
import android.media.AudioAttributes
import android.media.AudioFormat
import kotlin.concurrent.thread

Build and run to verify everything runs correctly. Click the START AUDIO CAPTURE and STOP AUDIO CAPTURE buttons and you’ll still see the toasts. That’s because the service isn’t attached to RecordingFragment yet.

Stopping the Audio Capture

Great! You can now start the recording and save it to a file in the device. But you still need to be able to stop it — otherwise, it will keep going forever.

To implement this feature, add this code to stopAudioCapture:

// 1
requireNotNull(mediaProjection) { "Tried to stop audio capture, but there was no ongoing capture in place!" }

// 2
audioCaptureThread.interrupt()
audioCaptureThread.join()

// 3
audioRecord!!.stop()
audioRecord!!.release()
audioRecord = null

// 4
mediaProjection!!.stop()
stopSelf()

To stop the audio capture, you need to handle several things. As with the previous functions, here’s a breakdown of the issue to understand it:

  1. You need to ensure an audio capture is really taking place.
  2. Next, you interrupt the audio capture thread. The join is just a method that waits until the thread is fully stopped.
  3. You stop the audio record and release the memory manually.
  4. Finally, you stop the media projection and the service itself.

Great, now your service is finally complete! However, you still have to connect the service to the fragment.

Connecting the Service

Now that your service is complete, you need to connect it to the UI.

Locate onActivityResult in RecordFragment.kt and add the code below inside if (resultCode == Activity.RESULT_OK):

val audioCaptureIntent = Intent(requireContext(), MediaCaptureService::class.java).apply {
  action = MediaCaptureService.ACTION_START
  putExtra(MediaCaptureService.EXTRA_RESULT_DATA, data!!)
}

ContextCompat.startForegroundService(requireContext(), audioCaptureIntent)

setButtonsEnabled(isCapturingAudio = true)

Then, in stopCapturing, you’ll also need to call the stop method:

ContextCompat.startForegroundService(requireContext(), Intent(requireContext(), MediaCaptureService::class.java).apply {
  action = MediaCaptureService.ACTION_STOP
})

setButtonsEnabled(isCapturingAudio = false)

You used setButtonsEnabled in both the start and stop methods above. This method will enable and disable the play buttons.

Next, implement this method:

private fun setButtonsEnabled(isCapturingAudio: Boolean) {
  button_start_recording.isEnabled = !isCapturingAudio
  button_stop_recording.isEnabled = isCapturingAudio
}

Android Studio will also ask you to import the service. Add it at the top of the file:

import com.raywenderlich.android.cataudio.service.MediaCaptureService

Build and run and… the app still doesn’t work. For now, just verify your app is running and works as it did before. The only notable difference now is that one of the buttons is disabled whenever the other is enabled.

Record Cat Sounds screen with a toast displayed

One more thing, services have to be declared in the Manifest just like activities. You’ll do that next:

Adding Your Service to the Android Manifest

Open AndroidManifest.xml and, inside the application tags, add:

<service
  android:name=".service.MediaCaptureService"
  android:enabled="true"
  android:exported="false"
  android:foregroundServiceType="mediaProjection"
  tools:targetApi="q" />

Build and run. It’s finally working! You’ll notice a red icon in the top-right corner of the phone, close to where you find the clock. This icon indicates that casting is taking place.

Audio Recording in progress

Now, go to another app and capture Cat Sounds!

Disabling Audio Playback Capture

Something important to consider is that some apps disable Audio Playback Capture. Apps that target Android 28 need to manually opt-in for audio capture for your app to use it.

If you have an app with content that you don’t want others to record, you can use two methods to restrict it:

  1. Add the following code to AndroidManifest.xml: android:allowAudioPlaybackCapture="false".
  2. If you have specific audio you don’t want other apps to capture, set its capture policy to AudioManager.setAllowedCapturePolicy(ALLOW_CAPTURE_BY_SYSTEM) before playing it.

Listening to Recorded Audio

Great! Now that your app captures playback audio, the only step you’re missing is being able to listen to the audio content you saved.

You’ll do this in the other tab at the bottom of the app, List of Audios.

Getting the Files From Memory

You need to get the captured files from your device’s memory.

Open RecordingListFragment.kt located in com.raywenderlich.android.cataudio/UI. Next, locate a TODO prompting you to create a function for this and add:

 private fun createItems() {
    // 1
    val files = File(context?.getExternalFilesDir(null), "/AudioCaptures")
    items.clear()

    if (files.listFiles() != null) {
      val file : Array<File> = files.listFiles()!!

      // 2
      for (i in file.indices) {
        items.add(Pair(file[i], i))
      }
    } else {
      Log.d("Files", "No files")
    }
  }

Now, review what the code above does:

  1. First, you retrieve the directory where the files are.
  2. Then you add them to an array called items.

You also need to add a call in resetUI. Look for the TODO indicating where you should add:

createItems()

The items array doesn’t exist yet. You’ll find a TODO indicating where to do it. Add this:

private var items: MutableList<Pair<File, Int>> = ArrayList()

Lastly, you need to display the items in your recycler view. Locate the last TODO item inside onCreateView and add a call to createItems:

createItems()

Just below the code you just added, substitute DummyContent.ITEMS for your items variable.

Now that you’ve made this change, you’ll get an error in the adapter.

To fix this, open MyItemRecyclerViewAdapter and change values‘ type from List of DummyItems to List of File. The code should look like this:

 private val values: List<Pair<File, Int>>)

Add the following import as well:

import java.io.File

Now, onBindViewHolder is complaining because the properties in the files are different than they were with the dummy items. Fix this by replacing the two lines with an error inside onBindViewHolder with:

holder.idView.text = (position + 1).toString()
holder.contentView.text = item.first.name

With that, the errors in RecordListFragment should be gone.

Build and run. Go to the List of Audios tab and you’ll see your audios:

List of recorded audio clips

Finally, you need to change the UI to actually play your recordings.