33import static com .atakmap .android .maps .MapView ._mapView ;
44
55import android .Manifest ;
6+ import android .app .Activity ;
67import android .content .Context ;
78import android .content .Intent ;
89import android .content .SharedPreferences ;
1819import android .view .View ;
1920import android .widget .Button ;
2021import android .widget .TextView ;
22+ import android .widget .Toast ;
2123
24+ import androidx .core .app .ActivityCompat ;
2225import androidx .core .content .ContextCompat ;
2326
2427import com .atakmap .android .dropdown .DropDown ;
5861import java .util .LinkedList ;
5962import java .util .List ;
6063import java .util .Locale ;
64+ import java .util .Queue ;
65+ import java .util .concurrent .ConcurrentLinkedQueue ;
6166import java .util .concurrent .atomic .AtomicBoolean ;
6267
6368import 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