From 8325abf6a287b7c4f8154dd69be763e3a4d973a5 Mon Sep 17 00:00:00 2001 From: Ahmet Inan Date: Thu, 25 Apr 2024 17:47:06 +0200 Subject: [PATCH] added menu with audio and night mode settings --- app/src/main/java/xdsopl/robot36/Decoder.java | 9 +- .../main/java/xdsopl/robot36/Demodulator.java | 23 +- .../java/xdsopl/robot36/MainActivity.java | 264 ++++++++++++++++-- app/src/main/res/layout/activity_main.xml | 8 - app/src/main/res/menu/menu_main.xml | 82 ++++++ app/src/main/res/values-night/themes.xml | 2 +- app/src/main/res/values/strings.xml | 34 ++- app/src/main/res/values/themes.xml | 2 +- 8 files changed, 383 insertions(+), 41 deletions(-) create mode 100644 app/src/main/res/menu/menu_main.xml diff --git a/app/src/main/java/xdsopl/robot36/Decoder.java b/app/src/main/java/xdsopl/robot36/Decoder.java index c74f9f3..fbbae3f 100644 --- a/app/src/main/java/xdsopl/robot36/Decoder.java +++ b/app/src/main/java/xdsopl/robot36/Decoder.java @@ -193,11 +193,12 @@ public class Decoder { return true; } - public boolean process(float[] recordBuffer) { - boolean syncPulseDetected = demodulator.process(recordBuffer); + public boolean process(float[] recordBuffer, int channelSelect) { + boolean syncPulseDetected = demodulator.process(recordBuffer, channelSelect); int syncPulseIndex = curSample + demodulator.syncPulseOffset; - for (float v : recordBuffer) { - scanLineBuffer[curSample++] = v; + int channels = channelSelect > 0 ? 2 : 1; + for (int j = 0; j < recordBuffer.length / channels; ++j) { + scanLineBuffer[curSample++] = recordBuffer[j]; if (curSample >= scanLineBuffer.length) { int shift = scanLineReserveSamples; syncPulseIndex -= shift; diff --git a/app/src/main/java/xdsopl/robot36/Demodulator.java b/app/src/main/java/xdsopl/robot36/Demodulator.java index a3a5ef5..2bf882a 100644 --- a/app/src/main/java/xdsopl/robot36/Demodulator.java +++ b/app/src/main/java/xdsopl/robot36/Demodulator.java @@ -77,10 +77,27 @@ public class Demodulator { baseBand = new Complex(); } - public boolean process(float[] buffer) { + public boolean process(float[] buffer, int channelSelect) { boolean syncPulseDetected = false; - for (int i = 0; i < buffer.length; ++i) { - baseBand = baseBandLowPass.push(baseBand.set(buffer[i]).mul(baseBandOscillator.rotate())); + int channels = channelSelect > 0 ? 2 : 1; + for (int i = 0; i < buffer.length / channels; ++i) { + switch (channelSelect) { + case 1: + baseBand.set(buffer[2 * i]); + break; + case 2: + baseBand.set(buffer[2 * i + 1]); + break; + case 3: + baseBand.set(buffer[2 * i] + buffer[2 * i + 1]); + break; + case 4: + baseBand.set(buffer[2 * i], buffer[2 * i + 1]); + break; + default: + baseBand.set(buffer[i]); + } + baseBand = baseBandLowPass.push(baseBand.mul(baseBandOscillator.rotate())); float frequencyValue = frequencyModulation.demod(baseBand); float syncPulseValue = syncPulseFilter.avg(frequencyValue); float syncPulseDelayedValue = syncPulseValueDelay.push(syncPulseValue); diff --git a/app/src/main/java/xdsopl/robot36/MainActivity.java b/app/src/main/java/xdsopl/robot36/MainActivity.java index fc6ed8e..eaf2411 100644 --- a/app/src/main/java/xdsopl/robot36/MainActivity.java +++ b/app/src/main/java/xdsopl/robot36/MainActivity.java @@ -7,18 +7,22 @@ Copyright 2024 Ahmet Inan package xdsopl.robot36; import android.Manifest; +import android.content.Context; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.MediaRecorder; import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; import android.widget.ImageView; -import android.widget.TextView; import androidx.activity.EdgeToEdge; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.core.graphics.Insets; @@ -35,11 +39,18 @@ public class MainActivity extends AppCompatActivity { private ImageView scopeView; private float[] recordBuffer; private AudioRecord audioRecord; - private TextView status; private Decoder decoder; + private Menu menu; + private int recordRate; + private int recordChannel; + private int audioSource; private void setStatus(int id) { - status.setText(id); + setTitle(id); + } + + private void setStatus(String str) { + setTitle(str); } private final AudioRecord.OnRecordPositionUpdateListener recordListener = new AudioRecord.OnRecordPositionUpdateListener() { @@ -50,31 +61,46 @@ public class MainActivity extends AppCompatActivity { @Override public void onPeriodicNotification(AudioRecord audioRecord) { audioRecord.read(recordBuffer, 0, recordBuffer.length, AudioRecord.READ_BLOCKING); - if (decoder.process(recordBuffer)) { + if (decoder.process(recordBuffer, recordChannel)) { scopeBitmap.setPixels(scopeBuffer.pixels, scopeBuffer.width * decoder.curLine, scopeBuffer.width, 0, 0, scopeBuffer.width, scopeBuffer.height / 2); scopeView.invalidate(); - status.setText(decoder.lastMode.getName()); + setStatus(decoder.lastMode.getName()); } } }; private void initAudioRecord() { - int audioSource = MediaRecorder.AudioSource.UNPROCESSED; + boolean rateChanged = true; + if (audioRecord != null) { + rateChanged = audioRecord.getSampleRate() != recordRate; + boolean channelChanged = audioRecord.getChannelCount() != (recordChannel == 0 ? 1 : 2); + boolean sourceChanged = audioRecord.getAudioSource() != audioSource; + if (!rateChanged && !channelChanged && !sourceChanged) + return; + stopListening(); + audioRecord.release(); + audioRecord = null; + } int channelConfig = AudioFormat.CHANNEL_IN_MONO; - int audioFormat = AudioFormat.ENCODING_PCM_FLOAT; - int sampleRate = 8000; - int sampleSize = 4; int channelCount = 1; + if (recordChannel != 0) { + channelCount = 2; + channelConfig = AudioFormat.CHANNEL_IN_STEREO; + } + int sampleSize = 4; + int frameSize = sampleSize * channelCount; + int audioFormat = AudioFormat.ENCODING_PCM_FLOAT; int readsPerSecond = 50; - double bufferSeconds = 0.5; - int bufferSize = (int) (bufferSeconds * sampleRate * sampleSize); - recordBuffer = new float[(sampleRate * channelCount) / readsPerSecond]; + int bufferSize = Integer.highestOneBit(recordRate) * frameSize; + int frameCount = recordRate / readsPerSecond; + recordBuffer = new float[frameCount * channelCount]; try { - audioRecord = new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat, bufferSize); + audioRecord = new AudioRecord(audioSource, recordRate, channelConfig, audioFormat, bufferSize); if (audioRecord.getState() == AudioRecord.STATE_INITIALIZED) { audioRecord.setRecordPositionUpdateListener(recordListener); - audioRecord.setPositionNotificationPeriod(recordBuffer.length); - decoder = new Decoder(scopeBuffer, sampleRate); + audioRecord.setPositionNotificationPeriod(frameCount); + if (rateChanged) + decoder = new Decoder(scopeBuffer, recordRate); startListening(); } else { setStatus(R.string.audio_init_failed); @@ -103,6 +129,90 @@ public class MainActivity extends AppCompatActivity { audioRecord.stop(); } + private void setRecordRate(int newSampleRate) { + if (recordRate == newSampleRate) + return; + recordRate = newSampleRate; + updateRecordRateMenu(); + initAudioRecord(); + } + + private void setRecordChannel(int newChannelSelect) { + if (recordChannel == newChannelSelect) + return; + recordChannel = newChannelSelect; + updateRecordChannelMenu(); + initAudioRecord(); + } + + private void setAudioSource(int newAudioSource) { + if (audioSource == newAudioSource) + return; + audioSource = newAudioSource; + updateAudioSourceMenu(); + initAudioRecord(); + } + + private void updateRecordRateMenu() { + switch (recordRate) { + case 8000: + menu.findItem(R.id.action_set_record_rate_8000).setChecked(true); + break; + case 16000: + menu.findItem(R.id.action_set_record_rate_16000).setChecked(true); + break; + case 32000: + menu.findItem(R.id.action_set_record_rate_32000).setChecked(true); + break; + case 44100: + menu.findItem(R.id.action_set_record_rate_44100).setChecked(true); + break; + case 48000: + menu.findItem(R.id.action_set_record_rate_48000).setChecked(true); + break; + } + } + + private void updateRecordChannelMenu() { + switch (recordChannel) { + case 0: + menu.findItem(R.id.action_set_record_channel_default).setChecked(true); + break; + case 1: + menu.findItem(R.id.action_set_record_channel_first).setChecked(true); + break; + case 2: + menu.findItem(R.id.action_set_record_channel_second).setChecked(true); + break; + case 3: + menu.findItem(R.id.action_set_record_channel_summation).setChecked(true); + break; + case 4: + menu.findItem(R.id.action_set_record_channel_analytic).setChecked(true); + break; + } + } + + private void updateAudioSourceMenu() { + switch (audioSource) { + case MediaRecorder.AudioSource.DEFAULT: + menu.findItem(R.id.action_set_source_default).setChecked(true); + break; + case MediaRecorder.AudioSource.MIC: + menu.findItem(R.id.action_set_source_microphone).setChecked(true); + break; + case MediaRecorder.AudioSource.CAMCORDER: + menu.findItem(R.id.action_set_source_camcorder).setChecked(true); + break; + case MediaRecorder.AudioSource.VOICE_RECOGNITION: + menu.findItem(R.id.action_set_source_voice_recognition).setChecked(true); + break; + case MediaRecorder.AudioSource.UNPROCESSED: + menu.findItem(R.id.action_set_source_unprocessed).setChecked(true); + break; + } + } + private final int permissionID = 1; @Override @@ -116,8 +226,42 @@ public class MainActivity extends AppCompatActivity { } @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + protected void onSaveInstanceState(@NonNull Bundle state) { + state.putInt("nightMode", AppCompatDelegate.getDefaultNightMode()); + state.putInt("recordRate", recordRate); + state.putInt("recordChannel", recordChannel); + state.putInt("audioSource", audioSource); + super.onSaveInstanceState(state); + } + + private void storeSettings() { + SharedPreferences pref = getPreferences(Context.MODE_PRIVATE); + SharedPreferences.Editor edit = pref.edit(); + edit.putInt("nightMode", AppCompatDelegate.getDefaultNightMode()); + edit.putInt("recordRate", recordRate); + edit.putInt("recordChannel", recordChannel); + edit.putInt("audioSource", audioSource); + edit.apply(); + } + + @Override + protected void onCreate(Bundle state) { + final int defaultSampleRate = 8000; + final int defaultChannelSelect = 0; + final int defaultAudioSource = MediaRecorder.AudioSource.DEFAULT; + if (state == null) { + SharedPreferences pref = getPreferences(Context.MODE_PRIVATE); + AppCompatDelegate.setDefaultNightMode(pref.getInt("nightMode", AppCompatDelegate.getDefaultNightMode())); + recordRate = pref.getInt("recordRate", defaultSampleRate); + recordChannel = pref.getInt("recordChannel", defaultChannelSelect); + audioSource = pref.getInt("audioSource", defaultAudioSource); + } else { + AppCompatDelegate.setDefaultNightMode(state.getInt("nightMode", AppCompatDelegate.getDefaultNightMode())); + recordRate = state.getInt("recordRate", defaultSampleRate); + recordChannel = state.getInt("recordChannel", defaultChannelSelect); + audioSource = state.getInt("audioSource", defaultAudioSource); + } + super.onCreate(state); EdgeToEdge.enable(this); setContentView(R.layout.activity_main); ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { @@ -125,7 +269,6 @@ public class MainActivity extends AppCompatActivity { v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); return insets; }); - status = findViewById(R.id.status); scopeView = findViewById(R.id.scope); int scopeWidth = 640; int scopeHeight = 1280; @@ -143,6 +286,90 @@ public class MainActivity extends AppCompatActivity { ActivityCompat.requestPermissions(this, permissions.toArray(new String[0]), permissionID); } + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_main, menu); + this.menu = menu; + updateRecordRateMenu(); + updateRecordChannelMenu(); + updateAudioSourceMenu(); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + if (id == R.id.action_set_record_rate_8000) { + setRecordRate(8000); + return true; + } + if (id == R.id.action_set_record_rate_16000) { + setRecordRate(16000); + return true; + } + if (id == R.id.action_set_record_rate_32000) { + setRecordRate(32000); + return true; + } + if (id == R.id.action_set_record_rate_44100) { + setRecordRate(44100); + return true; + } + if (id == R.id.action_set_record_rate_48000) { + setRecordRate(48000); + return true; + } + if (id == R.id.action_set_record_channel_default) { + setRecordChannel(0); + return true; + } + if (id == R.id.action_set_record_channel_first) { + setRecordChannel(1); + return true; + } + if (id == R.id.action_set_record_channel_second) { + setRecordChannel(2); + return true; + } + if (id == R.id.action_set_record_channel_summation) { + setRecordChannel(3); + return true; + } + if (id == R.id.action_set_record_channel_analytic) { + setRecordChannel(4); + return true; + } + if (id == R.id.action_set_source_default) { + setAudioSource(MediaRecorder.AudioSource.DEFAULT); + return true; + } + if (id == R.id.action_set_source_microphone) { + setAudioSource(MediaRecorder.AudioSource.MIC); + return true; + } + if (id == R.id.action_set_source_camcorder) { + setAudioSource(MediaRecorder.AudioSource.CAMCORDER); + return true; + } + if (id == R.id.action_set_source_voice_recognition) { + setAudioSource(MediaRecorder.AudioSource.VOICE_RECOGNITION); + return true; + } + if (id == R.id.action_set_source_unprocessed) { + setAudioSource(MediaRecorder.AudioSource.UNPROCESSED); + return true; + } + if (id == R.id.action_enable_night_mode) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + return true; + } + if (id == R.id.action_disable_night_mode) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); + return true; + } + return super.onOptionsItemSelected(item); + } + @Override protected void onResume() { startListening(); @@ -152,6 +379,7 @@ public class MainActivity extends AppCompatActivity { @Override protected void onPause() { stopListening(); + storeSettings(); super.onPause(); } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 33621ff..df31c92 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -18,13 +18,5 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - \ No newline at end of file diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml new file mode 100644 index 0000000..fd5b4ad --- /dev/null +++ b/app/src/main/res/menu/menu_main.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 541b658..473cbd1 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,6 +1,6 @@ - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ccceb42..634d803 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,9 +1,31 @@ Robot36 - listening - audio init failed - audio setup failed - audio permission denied - audio recording error - visualization of audio signal + Listening + Audio Settings + Sample Rate + 8 kHz + 16 kHz + 32 kHz + 44.1 kHz + 48 kHz + Channel Select + Default + First + Second + Summation + Analytic + Audio Source + Default + Microphone + Camcorder + Voice Recognition + Unprocessed + Audio init failed + Audio setup failed + Audio permission denied + Audio recording error + Visualization of audio signal + Night Mode + Enable + Disable diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 33861b2..fc76dac 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,6 +1,6 @@ -