1 /* 2 * Copyright 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.camera.integration.avsync.model 18 19 import android.content.Context 20 import android.media.AudioAttributes 21 import android.media.AudioFormat 22 import android.media.AudioManager 23 import android.media.AudioTrack 24 import androidx.annotation.VisibleForTesting 25 import androidx.camera.core.Logger 26 import androidx.core.util.Preconditions.checkArgument 27 import androidx.core.util.Preconditions.checkArgumentNonnegative 28 import kotlin.math.pow 29 import kotlin.math.sin 30 import kotlinx.coroutines.Dispatchers 31 import kotlinx.coroutines.withContext 32 33 private const val TAG = "AudioGenerator" 34 private const val DEFAULT_SAMPLE_RATE: Int = 44100 35 private const val SAMPLE_WIDTH: Int = 2 36 private const val MAGNITUDE = 0.5 37 private const val ENCODING: Int = AudioFormat.ENCODING_PCM_16BIT 38 private const val CHANNEL = AudioFormat.CHANNEL_OUT_MONO 39 40 class AudioGenerator(private val isEnabled: Boolean = true) { 41 42 @VisibleForTesting var audioTrack: AudioTrack? = null 43 startnull44 fun start() { 45 if (isEnabled) { 46 startInternal() 47 } else { 48 Logger.i(TAG, "Will not start audio generation, since AudioGenerator is disabled.") 49 } 50 } 51 startInternalnull52 private fun startInternal() { 53 Logger.i(TAG, "start audio generation") 54 audioTrack!!.play() 55 } 56 stopnull57 fun stop() { 58 if (isEnabled) { 59 stopInternal() 60 } else { 61 Logger.i(TAG, "Will not stop audio generation, since AudioGenerator is disabled.") 62 } 63 } 64 stopInternalnull65 private fun stopInternal() { 66 Logger.i(TAG, "stop audio generation") 67 Logger.i(TAG, "playState before stopped: ${audioTrack!!.playState}") 68 Logger.i(TAG, "playbackHeadPosition before stopped: ${audioTrack!!.playbackHeadPosition}") 69 audioTrack!!.stop() 70 audioTrack!!.reloadStaticData() 71 } 72 initialnull73 suspend fun initial( 74 context: Context, 75 frequency: Int, 76 beepLengthInSec: Double, 77 ): Boolean { 78 checkArgumentNonnegative(frequency, "The input frequency should not be negative.") 79 checkArgument(beepLengthInSec >= 0, "The beep length should not be negative.") 80 81 return if (isEnabled) { 82 initAudioTrackInternal(context, frequency, beepLengthInSec) 83 } else { 84 Logger.i(TAG, "Will not initial audio track, since AudioGenerator is disabled.") 85 true 86 } 87 } 88 initAudioTrackInternalnull89 private suspend fun initAudioTrackInternal( 90 context: Context, 91 frequency: Int, 92 beepLengthInSec: Double, 93 ): Boolean { 94 val sampleRate = getOutputSampleRate(context) 95 val samples = generateSineSamples(frequency, beepLengthInSec, SAMPLE_WIDTH, sampleRate) 96 val bufferSize = samples.size 97 98 Logger.i(TAG, "initAudioTrack with sample rate: $sampleRate") 99 Logger.i(TAG, "initAudioTrack with beep frequency: $frequency") 100 Logger.i(TAG, "initAudioTrack with buffer size: $bufferSize") 101 102 val audioAttributes = 103 AudioAttributes.Builder() 104 .setUsage(AudioAttributes.USAGE_MEDIA) 105 .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) 106 .build() 107 val audioFormat = 108 AudioFormat.Builder() 109 .setSampleRate(sampleRate) 110 .setEncoding(ENCODING) 111 .setChannelMask(CHANNEL) 112 .build() 113 114 audioTrack = 115 AudioTrack( 116 audioAttributes, 117 audioFormat, 118 bufferSize, 119 AudioTrack.MODE_STATIC, 120 AudioManager.AUDIO_SESSION_ID_GENERATE 121 ) 122 123 audioTrack!!.write(samples, 0, samples.size) 124 125 return true 126 } 127 getOutputSampleRatenull128 private fun getOutputSampleRate(context: Context): Int { 129 val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager 130 val sampleRate: String? = audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE) 131 132 return sampleRate?.toInt() ?: DEFAULT_SAMPLE_RATE 133 } 134 135 @VisibleForTesting generateSineSamplesnull136 suspend fun generateSineSamples( 137 frequency: Int, 138 beepLengthInSec: Double, 139 sampleWidth: Int, 140 sampleRate: Int 141 ): ByteArray { 142 val waveData = generateSineData(frequency, beepLengthInSec, sampleRate) 143 val samples = toSamples(waveData, sampleWidth) 144 145 return samples.toByteArray() 146 } 147 148 /** magnitude is expected to be from 0 to 1 */ 149 @VisibleForTesting generateSineDatanull150 suspend fun generateSineData( 151 frequency: Int, 152 lengthInSec: Double, 153 sampleRate: Int, 154 magnitude: Double = MAGNITUDE 155 ): List<Double> = 156 withContext(Dispatchers.Default) { 157 val n = (lengthInSec * sampleRate).toInt() 158 val angularFrequency = 2.0 * Math.PI * frequency 159 160 val res = mutableListOf<Double>() 161 for (i in 0 until n) { 162 val x = i * lengthInSec / n 163 val y = magnitude * sin(angularFrequency * x) 164 res.add(y) 165 } 166 167 res 168 } 169 170 @VisibleForTesting toSamplesnull171 suspend fun toSamples(data: List<Double>, sampleWidth: Int): List<Byte> = 172 withContext(Dispatchers.Default) { 173 val scaleFactor = 2.toDouble().pow(8 * sampleWidth - 1) - 1 174 175 data.flatMap { (it * scaleFactor).toInt().toBytes(sampleWidth) } 176 } 177 178 @VisibleForTesting toBytesnull179 fun Int.toBytes(sampleWidth: Int): List<Byte> { 180 val res = mutableListOf<Byte>() 181 for (i in 0 until sampleWidth) { 182 val byteValue = (this shr (8 * i)).toByte() 183 res.add(byteValue) 184 } 185 186 return res 187 } 188 } 189