Skip to content

Commit ef376d8

Browse files
committed
refactor audio record/playback
1 parent d70eaf7 commit ef376d8

File tree

4 files changed

+239
-77
lines changed

4 files changed

+239
-77
lines changed

app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
buildscript {
1010

1111

12-
ext.PLUGIN_VERSION = "1.1.1"
12+
ext.PLUGIN_VERSION = "1.1.2"
1313
ext.ATAK_VERSION = "4.10.0"
1414

1515
def takdevVersion = '2.+'

app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
tools:ignore="GoogleAppIndexingWarning">
55

66
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
7-
8-
<uses-permission android:name="android.permission.RECORD_AUDIO" />
9-
7+
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
8+
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
109
<application
1110
android:allowBackup="false"
1211
android:icon="@drawable/ic_launcher"

app/src/main/java/com/atakmap/android/meshtastic/MeshtasticDropDownReceiver.java

Lines changed: 155 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static com.atakmap.android.maps.MapView._mapView;
44

55
import android.Manifest;
6+
import android.app.Activity;
67
import android.content.Context;
78
import android.content.Intent;
89
import android.content.SharedPreferences;
@@ -18,7 +19,9 @@
1819
import android.view.View;
1920
import android.widget.Button;
2021
import android.widget.TextView;
22+
import android.widget.Toast;
2123

24+
import androidx.core.app.ActivityCompat;
2225
import androidx.core.content.ContextCompat;
2326

2427
import com.atakmap.android.dropdown.DropDown;
@@ -58,6 +61,8 @@
5861
import java.util.LinkedList;
5962
import java.util.List;
6063
import java.util.Locale;
64+
import java.util.Queue;
65+
import java.util.concurrent.ConcurrentLinkedQueue;
6166
import java.util.concurrent.atomic.AtomicBoolean;
6267

6368
import java.nio.ShortBuffer;
@@ -96,13 +101,17 @@ public class MeshtasticDropDownReceiver extends DropDownReceiver implements
96101
private long c2 = 0;
97102
private int c2FrameSize = 0;
98103
private int samplesBufSize = 0;
104+
private Activity activity;
105+
99106

100107
protected MeshtasticDropDownReceiver(final MapView mapView, final Context context) {
101108
super(mapView);
102109
this.pluginContext = context;
103110
this.appContext = mapView.getContext();
104111
this.mapView = mapView;
105112
this.prefs = PreferenceManager.getDefaultSharedPreferences(mapView.getContext().getApplicationContext());
113+
this.activity = (Activity) mapView.getContext();
114+
106115

107116
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
108117
this.mainView = inflater.inflate(R.layout.main_layout, null);
@@ -126,7 +135,7 @@ protected MeshtasticDropDownReceiver(final MapView mapView, final Context contex
126135
});
127136

128137
// Check if user has given permission to record audio, init the model after permission is granted
129-
int permissionCheck = ContextCompat.checkSelfPermission(mapView.getContext().getApplicationContext(), Manifest.permission.RECORD_AUDIO);
138+
int permissionCheck = ContextCompat.checkSelfPermission(_mapView.getContext().getApplicationContext(), Manifest.permission.RECORD_AUDIO);
130139
if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
131140
Log.d(TAG, "REC AUDIO DENIED");
132141
} else {
@@ -146,6 +155,27 @@ protected MeshtasticDropDownReceiver(final MapView mapView, final Context contex
146155
isRecording.set(false);
147156
talk.setText("All Talk");
148157
Log.d(TAG, "Recording stopped");
158+
/*
159+
if (codec2_chunks.size() > 0) {
160+
new Thread(() -> {
161+
try {
162+
Thread.sleep(500);
163+
byte[] audio = new byte[0];
164+
for (int i = 0; i < codec2_chunks.size(); i++)
165+
audio = append(audio, (byte[]) codec2_chunks.get(i));
166+
Log.d(TAG, "audio total bytes: " + audio.length);
167+
168+
codec2_chunks.clear();
169+
170+
// 0xC2 is my codec2 header
171+
DataPacket dp = new DataPacket(DataPacket.ID_BROADCAST, append(new byte[]{(byte) 0xC2}, audio), Portnums.PortNum.ATAK_FORWARDER_VALUE, DataPacket.ID_LOCAL, System.currentTimeMillis(), 0, MessageStatus.UNKNOWN, MeshtasticReceiver.getHopLimit(), MeshtasticReceiver.getChannelIndex(), false);
172+
MeshtasticMapComponent.sendToMesh(dp);
173+
} catch (InterruptedException e) {
174+
e.printStackTrace();
175+
}
176+
}).start();
177+
}
178+
*/
149179
}
150180
});
151181
} catch (IOException e) {
@@ -190,64 +220,144 @@ protected MeshtasticDropDownReceiver(final MapView mapView, final Context contex
190220
};
191221
mapView.addOnKeyListener(keyListener);
192222

193-
// codec2 recorder/playback
223+
// Codec2 Recorder/Playback
194224
c2 = Codec2.create(Codec2.CODEC2_MODE_700C);
195225
c2FrameSize = Codec2.getBitsSize(c2);
196226
samplesBufSize = Codec2.getSamplesPerFrame(c2);
197-
int minAudioBufSize = AudioRecord.getMinBufferSize(RECORDER_SAMPLERATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
198227
recorderBuf = new short[samplesBufSize];
199-
recorder = new AudioRecord(MediaRecorder.AudioSource.MIC, RECORDER_SAMPLERATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, minAudioBufSize);
200228
}
201229

202-
private final List codec2_chunks = Collections.synchronizedList(new ArrayList<>());
230+
private final Queue<byte[]> recordingQueue = new ConcurrentLinkedQueue<>();
231+
private boolean isRecordingActive = false;
232+
233+
public synchronized void recordVoice(boolean isBroadcast) {
234+
if (isRecordingActive) {
235+
activity.runOnUiThread(() -> {
236+
Toast.makeText(appContext, "Already recording, waiting for previous to finish...", Toast.LENGTH_SHORT).show();
237+
});
238+
return;
239+
}
240+
241+
isRecordingActive = true; // Prevent multiple recordings
242+
243+
new Thread(() -> {
244+
try {
245+
startRecording();
246+
processAudio(isBroadcast);
247+
} finally {
248+
isRecordingActive = false;
249+
}
250+
}).start();
251+
}
203252

204-
public void recordVoice(boolean isBroadcast) {
253+
private void startRecording() {
205254
try {
206-
if (recorder != null && (recorder.getState() == AudioRecord.RECORDSTATE_RECORDING))
255+
if (recorder != null) {
207256
recorder.stop();
257+
recorder.release();
258+
recorder = null;
259+
}
260+
261+
int minAudioBufSize = AudioRecord.getMinBufferSize(
262+
RECORDER_SAMPLERATE,
263+
AudioFormat.CHANNEL_IN_MONO,
264+
AudioFormat.ENCODING_PCM_16BIT
265+
);
266+
267+
if (ActivityCompat.checkSelfPermission(_mapView.getContext(), Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
268+
Log.d(TAG, "Record Audio Permission denied");
269+
return;
270+
}
271+
272+
recorder = new AudioRecord(
273+
MediaRecorder.AudioSource.MIC,
274+
RECORDER_SAMPLERATE,
275+
AudioFormat.CHANNEL_IN_MONO,
276+
AudioFormat.ENCODING_PCM_16BIT,
277+
Math.max(minAudioBufSize, samplesBufSize * 2)
278+
);
279+
280+
if (recorder.getState() != AudioRecord.STATE_INITIALIZED) {
281+
Log.e(TAG, "AudioRecord failed to initialize.");
282+
return;
283+
}
284+
208285
recorder.startRecording();
209-
Log.d(TAG, "Recording...");
210-
// 700C
211-
// minInBufSize: 640 c2FrameSize: 4 samplesBufSize: 320 FrameNum: 2
212-
recordingThread = new Thread(() -> {
213-
encodedBuf = new char[c2FrameSize];
214-
while (isRecording.get()) {
215-
recorder.read(recorderBuf, 0, recorderBuf.length);
216-
Codec2.encode(c2, recorderBuf, encodedBuf);
217-
byte[] frame = charArrayToByteArray(encodedBuf);
218-
219-
//synchronized (codec2_chunks) {
220-
codec2_chunks.add(frame);
221-
//}
222-
223-
if (codec2_chunks.size() > 8) {
224-
// 8 chunks, 4 bytes per chunk
225-
byte[] audio = new byte[0];
226-
for (int i = 0; i < codec2_chunks.size(); i++)
227-
audio = append(audio, (byte[]) codec2_chunks.get(i));
228-
Log.d(TAG, "audio total bytes: " + audio.length);
229-
230-
//synchronized (codec2_chunks) {
231-
232-
codec2_chunks.clear();
233-
//}
234-
235-
if (isBroadcast) {
236-
Log.d(TAG, "Broadcasting voice");
237-
// 0xC2 is my codec2 header
238-
DataPacket dp = new DataPacket(DataPacket.ID_BROADCAST, append(new byte[]{(byte) 0xC2}, audio), Portnums.PortNum.ATAK_FORWARDER_VALUE, DataPacket.ID_LOCAL, System.currentTimeMillis(), 0, MessageStatus.UNKNOWN, MeshtasticReceiver.getHopLimit(), MeshtasticReceiver.getChannelIndex(), false);
239-
MeshtasticMapComponent.sendToMesh(dp);
240-
} else
241-
Log.d(TAG, "Direct voice not implemented");
286+
Log.d(TAG, "Recording started...");
287+
} catch (Exception e) {
288+
Log.e(TAG, "Error initializing AudioRecord: " + e.getMessage());
289+
}
290+
}
242291

243-
}
292+
private void processAudio(boolean isBroadcast) {
293+
byte[] frame;
294+
char[] encodedBuf = new char[c2FrameSize];
295+
296+
while (isRecording.get()) {
297+
int readBytes = recorder.read(recorderBuf, 0, recorderBuf.length);
298+
if (readBytes > 0) {
299+
Codec2.encode(c2, recorderBuf, encodedBuf);
300+
frame = charArrayToByteArray(encodedBuf);
301+
302+
if (frame.length == 0) continue; // Prevent empty frames
303+
304+
synchronized (recordingQueue) {
305+
recordingQueue.add(frame);
244306
}
245-
}, "AudioRecorder Thread");
246-
recordingThread.start();
307+
}
247308

248-
} catch (Exception e) {
249-
e.printStackTrace();
309+
if (recordingQueue.size() > 8) {
310+
sendAudio(isBroadcast);
311+
}
312+
}
313+
314+
sendAudio(isBroadcast); // Flush remaining audio
315+
stopRecording();
316+
}
317+
318+
private void sendAudio(boolean isBroadcast) {
319+
byte[] audio;
320+
synchronized (recordingQueue) {
321+
if (recordingQueue.isEmpty()) return;
322+
323+
int totalSize = recordingQueue.stream().mapToInt(b -> b.length).sum();
324+
audio = new byte[totalSize];
325+
int offset = 0;
326+
327+
for (byte[] chunk : recordingQueue) {
328+
System.arraycopy(chunk, 0, audio, offset, chunk.length);
329+
offset += chunk.length;
330+
}
331+
332+
recordingQueue.clear();
333+
}
334+
335+
Log.d(TAG, "Broadcasting audio: " + audio.length + " bytes");
336+
337+
if (isBroadcast) {
338+
DataPacket dp = new DataPacket(
339+
DataPacket.ID_BROADCAST,
340+
append(new byte[]{(byte) 0xC2}, audio),
341+
Portnums.PortNum.ATAK_FORWARDER_VALUE,
342+
DataPacket.ID_LOCAL,
343+
System.currentTimeMillis(),
344+
0,
345+
MessageStatus.UNKNOWN,
346+
0, // no hops for audio
347+
MeshtasticReceiver.getChannelIndex(),
348+
false
349+
);
350+
MeshtasticMapComponent.sendToMesh(dp);
351+
}
352+
}
353+
354+
private void stopRecording() {
355+
if (recorder != null) {
356+
recorder.stop();
357+
recorder.release();
358+
recorder = null;
250359
}
360+
Log.d(TAG, "Recording stopped");
251361
}
252362

253363
// convert char array to byte array

0 commit comments

Comments
 (0)