integrated new default spectrogram

This commit is contained in:
Ahmet Inan 2025-02-25 19:45:17 +01:00
parent 771ab0fe0b
commit 913168adc3
13 changed files with 128 additions and 59 deletions

View file

@ -64,9 +64,9 @@ public class MainActivity extends AppCompatActivity {
private Bitmap scopeBitmap;
private PixelBuffer scopeBuffer;
private ImageView scopeView;
private Bitmap freqPlotBitmap;
private PixelBuffer freqPlotBuffer;
private ImageView freqPlotView;
private Bitmap waterfallPlotBitmap;
private PixelBuffer waterfallPlotBuffer;
private ImageView waterfallPlotView;
private Bitmap peakMeterBitmap;
private PixelBuffer peakMeterBuffer;
private ImageView peakMeterView;
@ -88,6 +88,7 @@ public class MainActivity extends AppCompatActivity {
private int thinColor;
private int tintColor;
private boolean autoSave;
private boolean showSpectrogram;
private void setStatus(int id) {
setTitle(id);
@ -139,9 +140,11 @@ public class MainActivity extends AppCompatActivity {
recordBuffer[i] = .000030517578125f * shortBuffer[i];
}
processPeakMeter();
processSpectrogram();
if (showSpectrogram)
processSpectrogram();
boolean newLines = decoder.process(recordBuffer, recordChannel);
//processFreqPlot();
if (!showSpectrogram)
processFreqPlot();
if (newLines) {
processScope();
processImage();
@ -214,50 +217,50 @@ public class MainActivity extends AppCompatActivity {
}
if (stft.push(input)) {
process = true;
int stride = freqPlotBuffer.width;
int line = stride * freqPlotBuffer.line;
int stride = waterfallPlotBuffer.width;
int line = stride * waterfallPlotBuffer.line;
double lowest = Math.log(1e-9);
double highest = Math.log(1);
double range = highest - lowest;
for (int i = 0; i < stride; ++i)
freqPlotBuffer.pixels[line + i] = rainbow((Math.log(stft.power[i + 14]) - lowest) / range);
System.arraycopy(freqPlotBuffer.pixels, line, freqPlotBuffer.pixels, line + stride * (freqPlotBuffer.height / 2), stride);
freqPlotBuffer.line = (freqPlotBuffer.line + 1) % (freqPlotBuffer.height / 2);
waterfallPlotBuffer.pixels[line + i] = rainbow((Math.log(stft.power[i + 14]) - lowest) / range);
System.arraycopy(waterfallPlotBuffer.pixels, line, waterfallPlotBuffer.pixels, line + stride * (waterfallPlotBuffer.height / 2), stride);
waterfallPlotBuffer.line = (waterfallPlotBuffer.line + 1) % (waterfallPlotBuffer.height / 2);
}
}
if (process) {
int width = freqPlotBitmap.getWidth();
int height = freqPlotBitmap.getHeight();
int stride = freqPlotBuffer.width;
int offset = stride * (freqPlotBuffer.line + freqPlotBuffer.height / 2 - height);
freqPlotBitmap.setPixels(freqPlotBuffer.pixels, offset, stride, 0, 0, width, height);
freqPlotView.invalidate();
int width = waterfallPlotBitmap.getWidth();
int height = waterfallPlotBitmap.getHeight();
int stride = waterfallPlotBuffer.width;
int offset = stride * (waterfallPlotBuffer.line + waterfallPlotBuffer.height / 2 - height);
waterfallPlotBitmap.setPixels(waterfallPlotBuffer.pixels, offset, stride, 0, 0, width, height);
waterfallPlotView.invalidate();
}
}
private void processFreqPlot() {
int width = freqPlotBitmap.getWidth();
int height = freqPlotBitmap.getHeight();
int stride = freqPlotBuffer.width;
int line = stride * freqPlotBuffer.line;
int width = waterfallPlotBitmap.getWidth();
int height = waterfallPlotBitmap.getHeight();
int stride = waterfallPlotBuffer.width;
int line = stride * waterfallPlotBuffer.line;
int channels = recordChannel > 0 ? 2 : 1;
int samples = recordBuffer.length / channels;
int spread = 2;
Arrays.fill(freqPlotBuffer.pixels, line, line + stride, 0);
Arrays.fill(waterfallPlotBuffer.pixels, line, line + stride, 0);
for (int i = 0; i < samples; ++i) {
int x = Math.round((recordBuffer[i] + 2.5f) * 0.25f * stride);
if (x >= spread && x < stride - spread)
for (int j = -spread; j <= spread; ++j)
freqPlotBuffer.pixels[line + x + j] += 1 + spread * spread - j * j;
waterfallPlotBuffer.pixels[line + x + j] += 1 + spread * spread - j * j;
}
int factor = 960 / samples;
for (int i = 0; i < stride; ++i)
freqPlotBuffer.pixels[line + i] = 0x00FFFFFF & fgColor | Math.min(factor * freqPlotBuffer.pixels[line + i], 255) << 24;
System.arraycopy(freqPlotBuffer.pixels, line, freqPlotBuffer.pixels, line + stride * (freqPlotBuffer.height / 2), stride);
freqPlotBuffer.line = (freqPlotBuffer.line + 1) % (freqPlotBuffer.height / 2);
int offset = stride * (freqPlotBuffer.line + freqPlotBuffer.height / 2 - height);
freqPlotBitmap.setPixels(freqPlotBuffer.pixels, offset, stride, 0, 0, width, height);
freqPlotView.invalidate();
waterfallPlotBuffer.pixels[line + i] = 0x00FFFFFF & fgColor | Math.min(factor * waterfallPlotBuffer.pixels[line + i], 255) << 24;
System.arraycopy(waterfallPlotBuffer.pixels, line, waterfallPlotBuffer.pixels, line + stride * (waterfallPlotBuffer.height / 2), stride);
waterfallPlotBuffer.line = (waterfallPlotBuffer.line + 1) % (waterfallPlotBuffer.height / 2);
int offset = stride * (waterfallPlotBuffer.line + waterfallPlotBuffer.height / 2 - height);
waterfallPlotBitmap.setPixels(waterfallPlotBuffer.pixels, offset, stride, 0, 0, width, height);
waterfallPlotView.invalidate();
}
private void processScope() {
@ -379,6 +382,20 @@ public class MainActivity extends AppCompatActivity {
initAudioRecord();
}
private void setShowSpectrogram(boolean newShowSpectrogram) {
if (showSpectrogram == newShowSpectrogram)
return;
showSpectrogram = newShowSpectrogram;
updateWaterfallPlotMenu();
}
private void updateWaterfallPlotMenu() {
if (showSpectrogram)
menu.findItem(R.id.action_show_spectrogram).setChecked(true);
else
menu.findItem(R.id.action_show_frequency_plot).setChecked(true);
}
private void setAutoSave(boolean newAutoSave) {
if (autoSave == newAutoSave)
return;
@ -477,6 +494,7 @@ public class MainActivity extends AppCompatActivity {
state.putInt("audioSource", audioSource);
state.putInt("audioFormat", audioFormat);
state.putBoolean("autoSave", autoSave);
state.putBoolean("showSpectrogram", showSpectrogram);
state.putString("language", language);
super.onSaveInstanceState(state);
}
@ -490,6 +508,7 @@ public class MainActivity extends AppCompatActivity {
edit.putInt("audioSource", audioSource);
edit.putInt("audioFormat", audioFormat);
edit.putBoolean("autoSave", autoSave);
edit.putBoolean("showSpectrogram", showSpectrogram);
edit.putString("language", language);
edit.apply();
}
@ -501,6 +520,7 @@ public class MainActivity extends AppCompatActivity {
final int defaultAudioSource = MediaRecorder.AudioSource.MIC;
final int defaultAudioFormat = AudioFormat.ENCODING_PCM_16BIT;
final boolean defaultAutoSave = true;
final boolean defaultShowSpectrogram = true;
final String defaultLanguage = "system";
if (state == null) {
SharedPreferences pref = getPreferences(Context.MODE_PRIVATE);
@ -510,6 +530,7 @@ public class MainActivity extends AppCompatActivity {
audioSource = pref.getInt("audioSource", defaultAudioSource);
audioFormat = pref.getInt("audioFormat", defaultAudioFormat);
autoSave = pref.getBoolean("autoSave", defaultAutoSave);
showSpectrogram = pref.getBoolean("showSpectrogram", defaultShowSpectrogram);
language = pref.getString("language", defaultLanguage);
} else {
AppCompatDelegate.setDefaultNightMode(state.getInt("nightMode", AppCompatDelegate.getDefaultNightMode()));
@ -518,6 +539,7 @@ public class MainActivity extends AppCompatActivity {
audioSource = state.getInt("audioSource", defaultAudioSource);
audioFormat = state.getInt("audioFormat", defaultAudioFormat);
autoSave = state.getBoolean("autoSave", defaultAutoSave);
showSpectrogram = state.getBoolean("showSpectrogram", defaultShowSpectrogram);
language = state.getString("language", defaultLanguage);
}
super.onCreate(state);
@ -530,12 +552,12 @@ public class MainActivity extends AppCompatActivity {
thinColor = getColor(R.color.thin);
tintColor = getColor(R.color.tint);
scopeBuffer = new PixelBuffer(640, 2 * 1280);
freqPlotBuffer = new PixelBuffer(256, 2 * 256);
waterfallPlotBuffer = new PixelBuffer(256, 2 * 256);
peakMeterBuffer = new PixelBuffer(1, 16);
imageBuffer = new PixelBuffer(800, 616);
input = new Complex();
createScope(config);
createFreqPlot(config);
createWaterfallPlot(config);
createPeakMeter();
List<String> permissions = new ArrayList<>();
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
@ -566,6 +588,7 @@ public class MainActivity extends AppCompatActivity {
updateRecordChannelMenu();
updateAudioSourceMenu();
updateAudioFormatMenu();
updateWaterfallPlotMenu();
updateAutoSaveMenu();
return true;
}
@ -717,6 +740,14 @@ public class MainActivity extends AppCompatActivity {
setAudioFormat(AudioFormat.ENCODING_PCM_16BIT);
return true;
}
if (id == R.id.action_show_spectrogram) {
setShowSpectrogram(true);
return true;
}
if (id == R.id.action_show_frequency_plot) {
setShowSpectrogram(false);
return true;
}
if (id == R.id.action_enable_auto_save) {
setAutoSave(true);
return true;
@ -793,11 +824,11 @@ public class MainActivity extends AppCompatActivity {
private void createScope(Configuration config) {
int screenWidthDp = config.screenWidthDp;
int screenHeightDp = config.screenHeightDp;
int freqPlotHeightDp = 64;
int waterfallPlotHeightDp = 64;
if (config.orientation == Configuration.ORIENTATION_LANDSCAPE)
screenWidthDp /= 2;
else
screenHeightDp -= freqPlotHeightDp;
screenHeightDp -= waterfallPlotHeightDp;
int actionBarHeightDp = 64;
screenHeightDp -= actionBarHeightDp;
int width = scopeBuffer.width;
@ -811,18 +842,18 @@ public class MainActivity extends AppCompatActivity {
scopeView.setImageBitmap(scopeBitmap);
}
private void createFreqPlot(Configuration config) {
int width = freqPlotBuffer.width;
int height = freqPlotBuffer.height / 2;
private void createWaterfallPlot(Configuration config) {
int width = waterfallPlotBuffer.width;
int height = waterfallPlotBuffer.height / 2;
if (config.orientation != Configuration.ORIENTATION_LANDSCAPE)
height /= 4;
freqPlotBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
int stride = freqPlotBuffer.width;
int offset = stride * (freqPlotBuffer.line + freqPlotBuffer.height / 2 - height);
freqPlotBitmap.setPixels(freqPlotBuffer.pixels, offset, stride, 0, 0, width, height);
freqPlotView = findViewById(R.id.freq_plot);
freqPlotView.setScaleType(ImageView.ScaleType.FIT_XY);
freqPlotView.setImageBitmap(freqPlotBitmap);
waterfallPlotBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
int stride = waterfallPlotBuffer.width;
int offset = stride * (waterfallPlotBuffer.line + waterfallPlotBuffer.height / 2 - height);
waterfallPlotBitmap.setPixels(waterfallPlotBuffer.pixels, offset, stride, 0, 0, width, height);
waterfallPlotView = findViewById(R.id.waterfall_plot);
waterfallPlotView.setScaleType(ImageView.ScaleType.FIT_XY);
waterfallPlotView.setImageBitmap(waterfallPlotBitmap);
}
private void createPeakMeter() {
@ -839,7 +870,7 @@ public class MainActivity extends AppCompatActivity {
setContentView(config.orientation == Configuration.ORIENTATION_LANDSCAPE ? R.layout.activity_main_land : R.layout.activity_main);
handleInsets();
createScope(config);
createFreqPlot(config);
createWaterfallPlot(config);
createPeakMeter();
}

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M21.98,14H22H21.98zM5.35,13c1.19,0 1.42,1 3.33,1c1.95,0 2.09,-1 3.33,-1c1.19,0 1.42,1 3.33,1c1.95,0 2.09,-1 3.33,-1c1.19,0 1.4,0.98 3.31,1v-2c-1.19,0 -1.42,-1 -3.33,-1c-1.95,0 -2.09,1 -3.33,1c-1.19,0 -1.42,-1 -3.33,-1c-1.95,0 -2.09,1 -3.33,1c-1.19,0 -1.42,-1 -3.33,-1C3.38,11 3.24,12 2,12v2C3.9,14 4.17,13 5.35,13zM18.67,15c-1.95,0 -2.09,1 -3.33,1c-1.19,0 -1.42,-1 -3.33,-1c-1.95,0 -2.1,1 -3.34,1c-1.24,0 -1.38,-1 -3.33,-1c-1.95,0 -2.1,1 -3.34,1v2c1.95,0 2.11,-1 3.34,-1c1.24,0 1.38,1 3.33,1c1.95,0 2.1,-1 3.34,-1c1.19,0 1.42,1 3.33,1c1.94,0 2.09,-1 3.33,-1c1.19,0 1.42,1 3.33,1v-2C20.76,16 20.62,15 18.67,15zM5.35,9c1.19,0 1.42,1 3.33,1c1.95,0 2.09,-1 3.33,-1c1.19,0 1.42,1 3.33,1c1.95,0 2.09,-1 3.33,-1c1.19,0 1.4,0.98 3.31,1V8c-1.19,0 -1.42,-1 -3.33,-1c-1.95,0 -2.09,1 -3.33,1c-1.19,0 -1.42,-1 -3.33,-1C10.04,7 9.9,8 8.66,8C7.47,8 7.24,7 5.33,7C3.38,7 3.24,8 2,8v2C3.9,10 4.17,9 5.35,9z"/>
</vector>

View file

@ -13,16 +13,16 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/scope_description"
app:layout_constraintBottom_toTopOf="@+id/freq_plot"
app:layout_constraintBottom_toTopOf="@+id/waterfall_plot"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/freq_plot"
android:id="@+id/waterfall_plot"
android:layout_width="0dp"
android:layout_height="64dp"
android:contentDescription="@string/freq_plot_description"
android:contentDescription="@string/waterfall_plot"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/peak_meter"
app:layout_constraintStart_toStartOf="parent"
@ -35,6 +35,6 @@
android:contentDescription="@string/peak_meter_description"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/freq_plot"
app:layout_constraintStart_toEndOf="@+id/waterfall_plot"
app:layout_constraintTop_toBottomOf="@+id/scope" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -14,15 +14,15 @@
android:layout_height="0dp"
android:contentDescription="@string/scope_description"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/freq_plot"
app:layout_constraintEnd_toStartOf="@+id/waterfall_plot"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/freq_plot"
android:id="@+id/waterfall_plot"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/freq_plot_description"
android:contentDescription="@string/waterfall_plot"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/peak_meter"
app:layout_constraintStart_toEndOf="@+id/scope"
@ -35,6 +35,6 @@
android:contentDescription="@string/peak_meter_description"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/freq_plot"
app:layout_constraintStart_toEndOf="@+id/waterfall_plot"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -208,6 +208,23 @@
app:iconTint="@color/tint" />
</menu>
</item>
<item
android:icon="@drawable/baseline_water_24"
android:title="@string/waterfall_plot"
app:iconTint="@color/tint">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/action_show_spectrogram"
android:title="@string/spectrogram"
app:iconTint="@color/tint" />
<item
android:id="@+id/action_show_frequency_plot"
android:title="@string/frequency_plot"
app:iconTint="@color/tint" />
</group>
</menu>
</item>
<item
android:icon="@drawable/baseline_auto_mode_24"
android:title="@string/auto_save"

View file

@ -34,8 +34,10 @@
<string name="creating_picture_file_failed">Erstellen der Bilddatei fehlgeschlagen</string>
<string name="storing_picture_failed">Speichern des Bildes fehlgeschlagen</string>
<string name="scope_description">Dekodiertes SSTV-Bild</string>
<string name="freq_plot_description">Frequenzdiagramm</string>
<string name="peak_meter_description">Spitzenpegel des Audiosignals</string>
<string name="waterfall_plot">Wasserfalldiagramm</string>
<string name="frequency_plot">Frequenzdiagramm</string>
<string name="spectrogram">Spektrogramm</string>
<string name="auto_save">Automatisches Speichern</string>
<string name="night_mode">Nachtmodus</string>
<string name="enable">Aktivieren</string>

View file

@ -34,8 +34,10 @@
<string name="creating_picture_file_failed">Fallo en crear imagen</string>
<string name="storing_picture_failed">Fallo en guardar imagen</string>
<string name="scope_description">Imagen SSTV decodificada</string>
<string name="freq_plot_description">Gráfico de frecuencia</string>
<string name="peak_meter_description">Nivel de señal de audio máximo</string>
<string name="waterfall_plot">Gráfico de cascada</string>
<string name="frequency_plot">Gráfico de frecuencia</string>
<string name="spectrogram">Espectrograma</string>
<string name="auto_save">Guardado automático</string>
<string name="night_mode">Modo nocturno</string>
<string name="enable">Habilitar</string>

View file

@ -34,8 +34,10 @@
<string name="creating_picture_file_failed">Tworzenie pliku obrazu nie powiodło się</string>
<string name="storing_picture_failed">Zapisywanie obrazu nie powiodło się</string>
<string name="scope_description">Zdekodowano obraz SSTV</string>
<string name="freq_plot_description">Wykres częstotliwości</string>
<string name="peak_meter_description">Szczytowy poziom sygnału audio</string>
<string name="waterfall_plot">Wykres wodospadowy</string>
<string name="frequency_plot">Wykres częstotliwości</string>
<string name="spectrogram">Spektrogram</string>
<string name="auto_save">Automatyczne zapisywanie</string>
<string name="night_mode">Tryb nocny</string>
<string name="enable">Włącz</string>

View file

@ -34,8 +34,10 @@
<string name="creating_picture_file_failed">Falha ao criar imagem</string>
<string name="storing_picture_failed">Falha ao salvar imagem</string>
<string name="scope_description">Imagem SSTV decodificada</string>
<string name="freq_plot_description">Gráfico de frequência</string>
<string name="peak_meter_description">Nível de sinal de áudio máximo</string>
<string name="waterfall_plot">Gráfico de cascata</string>
<string name="frequency_plot">Gráfico de frequência</string>
<string name="spectrogram">Espectrograma</string>
<string name="auto_save">Salvamento automático</string>
<string name="night_mode">Modo noturno</string>
<string name="enable">Habilitar</string>

View file

@ -34,8 +34,10 @@
<string name="creating_picture_file_failed">Ошибка создания файла изображения</string>
<string name="storing_picture_failed">Ошибка сохранения изображения</string>
<string name="scope_description">Декодированное изображение SSTV</string>
<string name="freq_plot_description">График частот</string>
<string name="peak_meter_description">Пиковый уровень аудиосигнала</string>
<string name="waterfall_plot">Водопадный график</string>
<string name="frequency_plot">График частот</string>
<string name="spectrogram">Спектрограмма</string>
<string name="auto_save">Автосохранение</string>
<string name="night_mode">Ночной режим</string>
<string name="enable">Включить</string>

View file

@ -34,8 +34,10 @@
<string name="creating_picture_file_failed">Помилка створення файлу зображення</string>
<string name="storing_picture_failed">Помилка збереження зображення</string>
<string name="scope_description">Декодоване зображення SSTV</string>
<string name="freq_plot_description">Графік частот</string>
<string name="peak_meter_description">Піковий рівень аудіосигналу</string>
<string name="waterfall_plot">Графік водоспаду</string>
<string name="frequency_plot">Графік частот</string>
<string name="spectrogram">Спектрограма</string>
<string name="auto_save">Автозбереження</string>
<string name="night_mode">Нічний режим</string>
<string name="enable">Увімкнути</string>

View file

@ -34,8 +34,10 @@
<string name="creating_picture_file_failed">创建图片文件出错</string>
<string name="storing_picture_failed">写入图像数据失败</string>
<string name="scope_description">解码的SSTV图像</string>
<string name="freq_plot_description">频率图</string>
<string name="peak_meter_description">音频峰值信号水平</string>
<string name="waterfall_plot">瀑布图</string>
<string name="frequency_plot">频率图</string>
<string name="spectrogram">频谱图</string>
<string name="auto_save">自动保存</string>
<string name="night_mode">夜间模式</string>
<string name="enable">开启</string>

View file

@ -69,8 +69,10 @@
<string name="creating_picture_file_failed">Creating picture file failed</string>
<string name="storing_picture_failed">Storing picture failed</string>
<string name="scope_description">Decoded SSTV picture</string>
<string name="freq_plot_description">Frequency plot</string>
<string name="peak_meter_description">Peak audio signal level</string>
<string name="waterfall_plot">Waterfall plot</string>
<string name="frequency_plot">Frequency plot</string>
<string name="spectrogram">Spectrogram</string>
<string name="auto_save">Auto Save</string>
<string name="night_mode">Night Mode</string>
<string name="enable">Enable</string>