added menu with audio and night mode settings

This commit is contained in:
Ahmet Inan 2024-04-25 17:47:06 +02:00
parent e1cebdab3c
commit 8325abf6a2
8 changed files with 383 additions and 41 deletions

View file

@ -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;

View file

@ -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);

View file

@ -7,18 +7,22 @@ Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
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();
}
}

View file

@ -18,13 +18,5 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context="xdsopl.robot36.MainActivity">
<item android:title="@string/audio_settings">
<menu>
<item android:title="@string/sample_rate">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/action_set_record_rate_8000"
android:title="@string/rate_8000" />
<item
android:id="@+id/action_set_record_rate_16000"
android:title="@string/rate_16000" />
<item
android:id="@+id/action_set_record_rate_32000"
android:title="@string/rate_32000" />
<item
android:id="@+id/action_set_record_rate_44100"
android:title="@string/rate_44100" />
<item
android:id="@+id/action_set_record_rate_48000"
android:title="@string/rate_48000" />
</group>
</menu>
</item>
<item android:title="@string/channel_select">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/action_set_record_channel_default"
android:title="@string/channel_default" />
<item
android:id="@+id/action_set_record_channel_first"
android:title="@string/channel_first" />
<item
android:id="@+id/action_set_record_channel_second"
android:title="@string/channel_second" />
<item
android:id="@+id/action_set_record_channel_summation"
android:title="@string/channel_summation" />
<item
android:id="@+id/action_set_record_channel_analytic"
android:title="@string/channel_analytic" />
</group>
</menu>
</item>
<item android:title="@string/audio_source">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/action_set_source_default"
android:title="@string/source_default" />
<item
android:id="@+id/action_set_source_microphone"
android:title="@string/source_microphone" />
<item
android:id="@+id/action_set_source_camcorder"
android:title="@string/source_camcorder" />
<item
android:id="@+id/action_set_source_voice_recognition"
android:title="@string/source_voice_recognition" />
<item
android:id="@+id/action_set_source_unprocessed"
android:title="@string/source_unprocessed" />
</group>
</menu>
</item>
</menu>
</item>
<item android:title="@string/night_mode">
<menu>
<item
android:id="@+id/action_enable_night_mode"
android:title="@string/enable" />
<item
android:id="@+id/action_disable_night_mode"
android:title="@string/disable" />
</menu>
</item>
</menu>

View file

@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.Robot36" parent="Theme.Material3.DayNight.NoActionBar">
<style name="Base.Theme.Robot36" parent="Theme.Material3.DayNight">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>

View file

@ -1,9 +1,31 @@
<resources>
<string name="app_name">Robot36</string>
<string name="listening">listening</string>
<string name="audio_init_failed">audio init failed</string>
<string name="audio_setup_failed">audio setup failed</string>
<string name="audio_permission_denied">audio permission denied</string>
<string name="audio_recording_error">audio recording error</string>
<string name="scope_description">visualization of audio signal</string>
<string name="listening">Listening</string>
<string name="audio_settings">Audio Settings</string>
<string name="sample_rate">Sample Rate</string>
<string name="rate_8000">8 kHz</string>
<string name="rate_16000">16 kHz</string>
<string name="rate_32000">32 kHz</string>
<string name="rate_44100">44.1 kHz</string>
<string name="rate_48000">48 kHz</string>
<string name="channel_select">Channel Select</string>
<string name="channel_default">Default</string>
<string name="channel_first">First</string>
<string name="channel_second">Second</string>
<string name="channel_summation">Summation</string>
<string name="channel_analytic">Analytic</string>
<string name="audio_source">Audio Source</string>
<string name="source_default">Default</string>
<string name="source_microphone">Microphone</string>
<string name="source_camcorder">Camcorder</string>
<string name="source_voice_recognition">Voice Recognition</string>
<string name="source_unprocessed">Unprocessed</string>
<string name="audio_init_failed">Audio init failed</string>
<string name="audio_setup_failed">Audio setup failed</string>
<string name="audio_permission_denied">Audio permission denied</string>
<string name="audio_recording_error">Audio recording error</string>
<string name="scope_description">Visualization of audio signal</string>
<string name="night_mode">Night Mode</string>
<string name="enable">Enable</string>
<string name="disable">Disable</string>
</resources>

View file

@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.Robot36" parent="Theme.Material3.DayNight.NoActionBar">
<style name="Base.Theme.Robot36" parent="Theme.Material3.DayNight">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>