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