1 /*
2  * Copyright (C) 2020 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 com.google.android.car.kitchensink.audio;
18 
19 import static android.media.AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY;
20 import static android.media.AudioFormat.CHANNEL_OUT_MONO;
21 import static android.media.AudioFormat.ENCODING_PCM_16BIT;
22 import static android.media.AudioTrack.MODE_STREAM;
23 
24 import static com.google.android.car.kitchensink.audio.AudioTestFragment.getAudioLogTag;
25 
26 import android.media.AudioAttributes;
27 import android.media.AudioFormat;
28 import android.media.AudioTrack;
29 import android.util.Log;
30 
31 import com.android.internal.annotations.GuardedBy;
32 import com.android.internal.util.Preconditions;
33 
34 final class AudioTrackPlayer {
35 
36     private static final String TAG = getAudioLogTag(AudioTrackPlayer.class);
37     private static final boolean DEBUG = true;
38     private static final int SAMPLE_RATE = 48_000;
39     private static final int MS_TO_SECONDS = 1_000;
40     private static final double DURATION_MS = 100;
41     private static final int WAIT_TIME_MS = 10;
42     private static final int AUDIBLE_RANGE_BOTTOM = 20;
43     private static final int AUDIBLE_RANGE_TOP = 20_000;
44 
45     private static final AudioAttributes AUDIO_ATTRIBUTES = new AudioAttributes.Builder()
46             .setUsage(USAGE_ASSISTANCE_ACCESSIBILITY)
47             .build();
48     private static final AudioFormat AUDIO_FORMAT = new AudioFormat.Builder()
49             .setChannelMask(CHANNEL_OUT_MONO)
50             .setEncoding(ENCODING_PCM_16BIT)
51             .setSampleRate(SAMPLE_RATE)
52             .build();
53 
54     private final int mToneFreq;
55     private final Object mLock = new Object();
56     @GuardedBy("mLock")
57     private Thread mThread;
58     @GuardedBy("mLock")
59     private boolean mRunning = false;
60 
AudioTrackPlayer(int toneFreq)61     AudioTrackPlayer(int toneFreq) {
62         Preconditions.checkArgumentInRange(toneFreq, AUDIBLE_RANGE_BOTTOM, AUDIBLE_RANGE_TOP,
63                 "toneFreq");
64         mToneFreq = toneFreq;
65     }
66 
start()67     void start() {
68         if (DEBUG) {
69             Log.d(TAG, "Starting!");
70         }
71         synchronized (mLock) {
72             if (!mRunning) {
73                 mRunning = true;
74                 startTrackLocked();
75             }
76         }
77     }
78 
stop()79     void stop() {
80         if (DEBUG) {
81             Log.d(TAG, "Stopping!");
82         }
83         synchronized (mLock) {
84             mRunning = false;
85             if (mThread != null) {
86                 try {
87                     mThread.join(WAIT_TIME_MS);
88                     mThread = null;
89                 } catch (InterruptedException e) {
90                     Thread.currentThread().interrupt();
91                     Log.w(TAG, "Failed to stop ", e);
92                 }
93             }
94         }
95     }
96 
startTrackLocked()97     private void startTrackLocked() {
98         mThread = new Thread(() -> {
99             AudioTrack track = new AudioTrack.Builder()
100                     .setAudioAttributes(AUDIO_ATTRIBUTES)
101                     .setAudioFormat(AUDIO_FORMAT)
102                     .setTransferMode(MODE_STREAM)
103                     .build();
104             track.flush();
105             track.play();
106             short[] buffer = getBuffer();
107             while (true) {
108                 synchronized (mLock) {
109                     if (!mRunning) break;
110                 }
111                 track.write(buffer, 0, buffer.length);
112             }
113         }, TAG + ":PlaybackThread");
114         mThread.start();
115     }
116 
getBuffer()117     private short[] getBuffer() {
118         double periodMs = MS_TO_SECONDS / ((double) mToneFreq);
119         double samplesInCycle =   ((double) SAMPLE_RATE) / ((double) mToneFreq);
120         int samplesInDuration = (int) ((DURATION_MS / periodMs) * samplesInCycle);
121         short[] buffer = new short[samplesInDuration];
122         double kw = 2 * Math.PI  / (SAMPLE_RATE / mToneFreq);
123         for (int i = 0; i < samplesInDuration; ++i) {
124             // Higher amplitude increases volume
125             buffer[i] = (short) (Math.sin(kw * i) * Short.MAX_VALUE);
126         }
127         return buffer;
128     }
129 
isPlaying()130     public boolean isPlaying() {
131         synchronized (mLock) {
132             return mRunning;
133         }
134     }
135 }
136