diff --git a/app/src/main/java/xdsopl/robot36/Demodulator.java b/app/src/main/java/xdsopl/robot36/Demodulator.java new file mode 100644 index 0000000..54799cf --- /dev/null +++ b/app/src/main/java/xdsopl/robot36/Demodulator.java @@ -0,0 +1,73 @@ +/* +SSTV Demodulator + +Copyright 2024 Ahmet Inan +*/ + +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; + } + } +} diff --git a/app/src/main/java/xdsopl/robot36/MainActivity.java b/app/src/main/java/xdsopl/robot36/MainActivity.java index bf33db7..ff193be 100644 --- a/app/src/main/java/xdsopl/robot36/MainActivity.java +++ b/app/src/main/java/xdsopl/robot36/MainActivity.java @@ -37,19 +37,8 @@ public class MainActivity extends AppCompatActivity { private float[] recordBuffer; private AudioRecord audioRecord; private TextView status; - private Delay syncPulseDelay; - 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 Demodulator demodulator; + private int tint; private int curLine; private int curColumn; @@ -66,28 +55,22 @@ public class MainActivity extends AppCompatActivity { @Override public void onPeriodicNotification(AudioRecord audioRecord) { audioRecord.read(recordBuffer, 0, recordBuffer.length, AudioRecord.READ_BLOCKING); - processSamples(); + demodulator.process(recordBuffer); + visualizeSignal(); } }; - private void processSamples() { + private void visualizeSignal() { for (float v : recordBuffer) { - baseBand = baseBandLowPass.avg(baseBand.set(v).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); - int syncPulseIntensity = (int) Math.round(255 * Math.sqrt(syncPulseLevel)); - int scanLineIntensity = (int) Math.round(255 * Math.sqrt(scanLineLevel)); - int syncPulseColor = 0x00000100 * syncPulseIntensity; - int scanLineColor = 0x00010101 * scanLineIntensity; - int pixelColor = 0xff000000; - if (syncPulseLevel > 0.1) - pixelColor |= syncPulseColor; - else - pixelColor |= scanLineColor; + int pixelColor = 0x00010101; + float level = v; + if (v < 0) { + level = -v; + pixelColor = 0x00000100; + } + int intensity = (int) Math.round(255 * Math.sqrt(level)); + pixelColor *= intensity; + pixelColor |= 0xff000000; scopePixels[scopeWidth * curLine + curColumn] = pixelColor; if (++curColumn >= scopeWidth) { 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() { int audioSource = MediaRecorder.AudioSource.UNPROCESSED; int channelConfig = AudioFormat.CHANNEL_IN_MONO; @@ -150,7 +99,7 @@ public class MainActivity extends AppCompatActivity { if (audioRecord.getState() == AudioRecord.STATE_INITIALIZED) { audioRecord.setRecordPositionUpdateListener(recordListener); audioRecord.setPositionNotificationPeriod(recordBuffer.length); - initTools(sampleRate); + demodulator = new Demodulator(sampleRate); startListening(); } else { setStatus(R.string.audio_init_failed);