mirror of
https://github.com/xdsopl/robot36.git
synced 2025-12-06 07:12:07 +01:00
added Demodulator class
This commit is contained in:
parent
fee4012c6a
commit
b8c98317b1
73
app/src/main/java/xdsopl/robot36/Demodulator.java
Normal file
73
app/src/main/java/xdsopl/robot36/Demodulator.java
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
SSTV Demodulator
|
||||||
|
|
||||||
|
Copyright 2024 Ahmet Inan <xdsopl@gmail.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xdsopl.robot36;
|
||||||
|
|
||||||
|
public class Demodulator {
|
||||||
|
private final SimpleMovingAverage powerAvg;
|
||||||
|
private final ComplexMovingAverage syncPulseFilter;
|
||||||
|
private final ComplexMovingAverage scanLineFilter;
|
||||||
|
private final ComplexMovingAverage baseBandLowPass;
|
||||||
|
private final FrequencyModulation scanLineDemod;
|
||||||
|
private final Phasor syncPulseOscillator;
|
||||||
|
private final Phasor scanLineOscillator;
|
||||||
|
private final Phasor baseBandOscillator;
|
||||||
|
private final Delay syncPulseDelay;
|
||||||
|
private final Delay scanLineDelay;
|
||||||
|
private Complex baseBand;
|
||||||
|
private Complex syncPulse;
|
||||||
|
private Complex scanLine;
|
||||||
|
|
||||||
|
Demodulator(int sampleRate) {
|
||||||
|
double powerWindowSeconds = 0.5;
|
||||||
|
int powerWindowSamples = (int) Math.round(powerWindowSeconds * sampleRate) | 1;
|
||||||
|
powerAvg = new SimpleMovingAverage(powerWindowSamples);
|
||||||
|
float blackFrequency = 1500;
|
||||||
|
float whiteFrequency = 2300;
|
||||||
|
float scanLineBandwidth = whiteFrequency - blackFrequency;
|
||||||
|
scanLineDemod = new FrequencyModulation(scanLineBandwidth, sampleRate);
|
||||||
|
float scanLineCutoff = scanLineBandwidth / 2;
|
||||||
|
int scanLineFilterSamples = (int) Math.round(0.443 * sampleRate / scanLineCutoff) | 1;
|
||||||
|
scanLineFilter = new ComplexMovingAverage(scanLineFilterSamples);
|
||||||
|
double syncPulseSeconds = 0.009;
|
||||||
|
int syncPulseFilterSamples = (int) Math.round(syncPulseSeconds * sampleRate) | 1;
|
||||||
|
syncPulseFilter = new ComplexMovingAverage(syncPulseFilterSamples);
|
||||||
|
float lowestFrequency = 1100;
|
||||||
|
float highestFrequency = 2300;
|
||||||
|
float cutoffFrequency = (highestFrequency - lowestFrequency) / 2;
|
||||||
|
int lowPassSamples = (int) Math.round(0.443 * sampleRate / cutoffFrequency) | 1;
|
||||||
|
baseBandLowPass = new ComplexMovingAverage(lowPassSamples);
|
||||||
|
float centerFrequency = (lowestFrequency + highestFrequency) / 2;
|
||||||
|
baseBandOscillator = new Phasor(-centerFrequency, sampleRate);
|
||||||
|
float syncPulseFrequency = 1200;
|
||||||
|
syncPulseOscillator = new Phasor(-(syncPulseFrequency - centerFrequency), sampleRate);
|
||||||
|
float grayFrequency = (blackFrequency + whiteFrequency) / 2;
|
||||||
|
scanLineOscillator = new Phasor(-(grayFrequency - centerFrequency), sampleRate);
|
||||||
|
int syncPulseDelaySamples = (powerWindowSamples - 1) / 2;
|
||||||
|
syncPulseDelay = new Delay(syncPulseDelaySamples);
|
||||||
|
int scanLineDelaySamples = (powerWindowSamples - 1) / 2 + (syncPulseFilterSamples - 1) / 2 - (scanLineFilterSamples - 1) / 2;
|
||||||
|
scanLineDelay = new Delay(scanLineDelaySamples);
|
||||||
|
baseBand = new Complex();
|
||||||
|
syncPulse = new Complex();
|
||||||
|
scanLine = new Complex();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void process(float[] buffer) {
|
||||||
|
for (int i = 0; i < buffer.length; ++i) {
|
||||||
|
baseBand = baseBandLowPass.avg(baseBand.set(buffer[i]).mul(baseBandOscillator.rotate()));
|
||||||
|
syncPulse = syncPulseFilter.avg(syncPulse.set(baseBand).mul(syncPulseOscillator.rotate()));
|
||||||
|
scanLine = scanLineFilter.avg(scanLine.set(baseBand).mul(scanLineOscillator.rotate()));
|
||||||
|
float syncPulseValue = syncPulseDelay.push(syncPulse.norm()) / powerAvg.avg(baseBand.norm());
|
||||||
|
float scanLineValue = scanLineDelay.push(scanLineDemod.demod(scanLine));
|
||||||
|
float syncPulseLevel = Math.min(Math.max(syncPulseValue, 0), 1);
|
||||||
|
float scanLineLevel = Math.min(Math.max(0.5f * (scanLineValue + 1), 0), 1);
|
||||||
|
if (syncPulseLevel > 0.1)
|
||||||
|
buffer[i] = -syncPulseLevel;
|
||||||
|
else
|
||||||
|
buffer[i] = scanLineLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,19 +37,8 @@ public class MainActivity extends AppCompatActivity {
|
||||||
private float[] recordBuffer;
|
private float[] recordBuffer;
|
||||||
private AudioRecord audioRecord;
|
private AudioRecord audioRecord;
|
||||||
private TextView status;
|
private TextView status;
|
||||||
private Delay syncPulseDelay;
|
private Demodulator demodulator;
|
||||||
private Delay scanLineDelay;
|
|
||||||
private SimpleMovingAverage powerAvg;
|
|
||||||
private ComplexMovingAverage syncPulseFilter;
|
|
||||||
private ComplexMovingAverage scanLineFilter;
|
|
||||||
private ComplexMovingAverage baseBandLowPass;
|
|
||||||
private FrequencyModulation scanLineDemod;
|
|
||||||
private Phasor syncPulseOscillator;
|
|
||||||
private Phasor scanLineOscillator;
|
|
||||||
private Phasor baseBandOscillator;
|
|
||||||
private Complex baseBand;
|
|
||||||
private Complex syncPulse;
|
|
||||||
private Complex scanLine;
|
|
||||||
private int tint;
|
private int tint;
|
||||||
private int curLine;
|
private int curLine;
|
||||||
private int curColumn;
|
private int curColumn;
|
||||||
|
|
@ -66,28 +55,22 @@ public class MainActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
public void onPeriodicNotification(AudioRecord audioRecord) {
|
public void onPeriodicNotification(AudioRecord audioRecord) {
|
||||||
audioRecord.read(recordBuffer, 0, recordBuffer.length, AudioRecord.READ_BLOCKING);
|
audioRecord.read(recordBuffer, 0, recordBuffer.length, AudioRecord.READ_BLOCKING);
|
||||||
processSamples();
|
demodulator.process(recordBuffer);
|
||||||
|
visualizeSignal();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private void processSamples() {
|
private void visualizeSignal() {
|
||||||
for (float v : recordBuffer) {
|
for (float v : recordBuffer) {
|
||||||
baseBand = baseBandLowPass.avg(baseBand.set(v).mul(baseBandOscillator.rotate()));
|
int pixelColor = 0x00010101;
|
||||||
syncPulse = syncPulseFilter.avg(syncPulse.set(baseBand).mul(syncPulseOscillator.rotate()));
|
float level = v;
|
||||||
scanLine = scanLineFilter.avg(scanLine.set(baseBand).mul(scanLineOscillator.rotate()));
|
if (v < 0) {
|
||||||
float syncPulseValue = syncPulseDelay.push(syncPulse.norm()) / powerAvg.avg(baseBand.norm());
|
level = -v;
|
||||||
float scanLineValue = scanLineDelay.push(scanLineDemod.demod(scanLine));
|
pixelColor = 0x00000100;
|
||||||
float syncPulseLevel = Math.min(Math.max(syncPulseValue, 0), 1);
|
}
|
||||||
float scanLineLevel = Math.min(Math.max(0.5f * (scanLineValue + 1), 0), 1);
|
int intensity = (int) Math.round(255 * Math.sqrt(level));
|
||||||
int syncPulseIntensity = (int) Math.round(255 * Math.sqrt(syncPulseLevel));
|
pixelColor *= intensity;
|
||||||
int scanLineIntensity = (int) Math.round(255 * Math.sqrt(scanLineLevel));
|
pixelColor |= 0xff000000;
|
||||||
int syncPulseColor = 0x00000100 * syncPulseIntensity;
|
|
||||||
int scanLineColor = 0x00010101 * scanLineIntensity;
|
|
||||||
int pixelColor = 0xff000000;
|
|
||||||
if (syncPulseLevel > 0.1)
|
|
||||||
pixelColor |= syncPulseColor;
|
|
||||||
else
|
|
||||||
pixelColor |= scanLineColor;
|
|
||||||
scopePixels[scopeWidth * curLine + curColumn] = pixelColor;
|
scopePixels[scopeWidth * curLine + curColumn] = pixelColor;
|
||||||
if (++curColumn >= scopeWidth) {
|
if (++curColumn >= scopeWidth) {
|
||||||
curColumn = 0;
|
curColumn = 0;
|
||||||
|
|
@ -100,40 +83,6 @@ public class MainActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initTools(int sampleRate) {
|
|
||||||
double powerWindowSeconds = 0.5;
|
|
||||||
int powerWindowSamples = (int) Math.round(powerWindowSeconds * sampleRate) | 1;
|
|
||||||
powerAvg = new SimpleMovingAverage(powerWindowSamples);
|
|
||||||
float blackFrequency = 1500;
|
|
||||||
float whiteFrequency = 2300;
|
|
||||||
float scanLineBandwidth = whiteFrequency - blackFrequency;
|
|
||||||
scanLineDemod = new FrequencyModulation(scanLineBandwidth, sampleRate);
|
|
||||||
float scanLineCutoff = scanLineBandwidth / 2;
|
|
||||||
int scanLineFilterSamples = (int) Math.round(0.443 * sampleRate / scanLineCutoff) | 1;
|
|
||||||
scanLineFilter = new ComplexMovingAverage(scanLineFilterSamples);
|
|
||||||
double syncPulseSeconds = 0.009;
|
|
||||||
int syncPulseFilterSamples = (int) Math.round(syncPulseSeconds * sampleRate) | 1;
|
|
||||||
syncPulseFilter = new ComplexMovingAverage(syncPulseFilterSamples);
|
|
||||||
float lowestFrequency = 1100;
|
|
||||||
float highestFrequency = 2300;
|
|
||||||
float cutoffFrequency = (highestFrequency - lowestFrequency) / 2;
|
|
||||||
int lowPassSamples = (int) Math.round(0.443 * sampleRate / cutoffFrequency) | 1;
|
|
||||||
baseBandLowPass = new ComplexMovingAverage(lowPassSamples);
|
|
||||||
float centerFrequency = (lowestFrequency + highestFrequency) / 2;
|
|
||||||
baseBandOscillator = new Phasor(-centerFrequency, sampleRate);
|
|
||||||
float syncPulseFrequency = 1200;
|
|
||||||
syncPulseOscillator = new Phasor(-(syncPulseFrequency - centerFrequency), sampleRate);
|
|
||||||
float grayFrequency = (blackFrequency + whiteFrequency) / 2;
|
|
||||||
scanLineOscillator = new Phasor(-(grayFrequency - centerFrequency), sampleRate);
|
|
||||||
int syncPulseDelaySamples = (powerWindowSamples - 1) / 2;
|
|
||||||
syncPulseDelay = new Delay(syncPulseDelaySamples);
|
|
||||||
int scanLineDelaySamples = (powerWindowSamples - 1) / 2 + (syncPulseFilterSamples - 1) / 2 - (scanLineFilterSamples - 1) / 2;
|
|
||||||
scanLineDelay = new Delay(scanLineDelaySamples);
|
|
||||||
baseBand = new Complex();
|
|
||||||
syncPulse = new Complex();
|
|
||||||
scanLine = new Complex();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initAudioRecord() {
|
private void initAudioRecord() {
|
||||||
int audioSource = MediaRecorder.AudioSource.UNPROCESSED;
|
int audioSource = MediaRecorder.AudioSource.UNPROCESSED;
|
||||||
int channelConfig = AudioFormat.CHANNEL_IN_MONO;
|
int channelConfig = AudioFormat.CHANNEL_IN_MONO;
|
||||||
|
|
@ -150,7 +99,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
if (audioRecord.getState() == AudioRecord.STATE_INITIALIZED) {
|
if (audioRecord.getState() == AudioRecord.STATE_INITIALIZED) {
|
||||||
audioRecord.setRecordPositionUpdateListener(recordListener);
|
audioRecord.setRecordPositionUpdateListener(recordListener);
|
||||||
audioRecord.setPositionNotificationPeriod(recordBuffer.length);
|
audioRecord.setPositionNotificationPeriod(recordBuffer.length);
|
||||||
initTools(sampleRate);
|
demodulator = new Demodulator(sampleRate);
|
||||||
startListening();
|
startListening();
|
||||||
} else {
|
} else {
|
||||||
setStatus(R.string.audio_init_failed);
|
setStatus(R.string.audio_init_failed);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue