Add support for encoding PD-50 and PD-90 modes.

This commit is contained in:
ckegel 2023-12-15 00:07:55 -05:00
parent 1c96d71a69
commit 6ebd89948d
3 changed files with 158 additions and 8 deletions

View File

@ -2,6 +2,6 @@
## Summary ## Summary
Web SSTV aims to both encode and decode SSTV using plain JavaScript and Web Audio API. Web SSTV can be run entirely offline (without styling), and on any platform from Chromebooks to phones, so long as they support JavaScript and Web Audio. By making SSTV readily available on many platforms, we aim to create educational opportunities and introduce more people to STEM and amateur radio. Web SSTV is currently hosted at https://ckegel.github.io/Web-SSTV/. Web SSTV aims to both encode and decode SSTV using plain JavaScript and Web Audio API. Web SSTV can be run entirely offline (without styling), and on any platform from Chromebooks to phones, so long as they support JavaScript and Web Audio. By making SSTV readily available on many platforms, we aim to create educational opportunities and introduce more people to STEM and amateur radio. Web SSTV is currently hosted at https://ckegel.github.io/Web-SSTV/.
## Current State ## Current State
Currently Web SSTV only supports encoding images using the Martin or Scottie formats. Support for more formats and recieving SSTV signals is actively being developed. We welcome any pull requests. Currently Web SSTV only supports encoding images using the Martin or Scottie formats, along with a few PD formats. Support for more formats and recieving SSTV signals is actively being developed. We welcome any pull requests.
## Sources ## Sources
Both the [SSTV Handbook](https://www.sstv-handbook.com/) and [JL Barber (N7CXI) paper](http://www.barberdsp.com/downloads/Dayton%20Paper.pdf) were heavily referenced when implementing support for the Martin and Scottie formats. Both the [SSTV Handbook](https://www.sstv-handbook.com/) and [JL Barber's (N7CXI) paper](http://www.barberdsp.com/downloads/Dayton%20Paper.pdf) were heavily referenced when implementing support for the Martin and Scottie formats.

View File

@ -13,12 +13,17 @@
<h3>Send SSTV signals from your browser:</h3> <h3>Send SSTV signals from your browser:</h3>
<label id="modeLabel" for="modeSelect">Select a mode:</label> <label id="modeLabel" for="modeSelect">Select a mode:</label>
<select id="modeSelect"> <select id="modeSelect">
<option value="none" selected disabled hidden>Select a mode</option> <option value="none" selected disabled hidden>No mode selected</option>
<option value="none" disabled>--- Martin ---</option>
<option value="M1">Martin M1</option> <option value="M1">Martin M1</option>
<option value="M2">Martin M2</option> <option value="M2">Martin M2</option>
<option value="none" disabled>--- Scottie ---</option>
<option value="S1">Scottie 1</option> <option value="S1">Scottie 1</option>
<option value="S2">Scottie 2</option> <option value="S2">Scottie 2</option>
<option value="SDX">Scottie DX</option> <option value="SDX">Scottie DX</option>
<option value="none" disabled>--- PD ---</option>
<option value="PD50">PD-50</option>
<option value="PD90">PD-90</option>
</select> </select>
<label id="imgLabel" for="imgPicker">Upload an image:</label> <label id="imgLabel" for="imgPicker">Upload an image:</label>
@ -32,9 +37,9 @@
</main> </main>
<footer class="container"> <footer class="container">
<small>Currently supports encoding (sending) in Martin and Scottie formats. SSTV signal decoding and more encoding modes coming soon.</small> <small>SSTV signal decoding and more encoding modes coming soon.</small>
<br> <br>
<small><a href="https://ckegel.github.io/Web-SSTV/">Check out the project on Github</a></small> <small><a href="https://github.com/CKegel/Web-SSTV/">Check out the project on Github</a></small>
<br> <br>
<small>&copy 2023 Christian Kegel - Available under the MIT License</small> <small>&copy 2023 Christian Kegel - Available under the MIT License</small>
</footer> </footer>

151
script.js
View File

@ -21,6 +21,7 @@ const HEADER_BREAK_LENGTH = 0.01; //10 ms
const VIS_BIT_LENGTH = 0.03; //30 ms const VIS_BIT_LENGTH = 0.03; //30 ms
const SYNC_PULSE_FREQ = 1200; const SYNC_PULSE_FREQ = 1200;
const BLANKING_PULSE_FREQ = 1500; const BLANKING_PULSE_FREQ = 1500;
const COLOR_FREQ_MULT = 3.1372549;
const VIS_BIT_FREQ = { const VIS_BIT_FREQ = {
ONE: 1100, ONE: 1100,
ZERO: 1300, ZERO: 1300,
@ -47,12 +48,24 @@ class Format {
getRGBValueAsFreq(data, scanLine, vertPos) { getRGBValueAsFreq(data, scanLine, vertPos) {
const index = scanLine * (this.#vertResolution * 4) + vertPos * 4; const index = scanLine * (this.#vertResolution * 4) + vertPos * 4;
let red = data[index] * 3.137 + 1500; let red = data[index] * COLOR_FREQ_MULT + 1500;
let green = data[index + 1] * 3.137 + 1500; let green = data[index + 1] * COLOR_FREQ_MULT + 1500;
let blue = data[index + 2] * 3.137 + 1500; let blue = data[index + 2] * COLOR_FREQ_MULT + 1500;
return [red, green, blue]; return [red, green, blue];
} }
getYRYBYValueAsFreq(data, scanLine, vertPos) {
const index = scanLine * (this.#vertResolution * 4) + vertPos * 4;
let red = data[index];
let green = data[index + 1];
let blue = data[index + 2];
let Y = 6.0 + (.003906 * ((65.738 * red) + (129.057 * green) + (25.064 * blue)));
let RY = 128.0 + (.003906 * ((112.439 * red) + (-94.154 * green) + (-18.285 * blue)));
let BY = 128.0 + (.003906 * ((-37.945 * red) + (-74.494 * green) + (112.439 * blue)));
return [1500 + Y * COLOR_FREQ_MULT , 1500 + RY * COLOR_FREQ_MULT, 1500 + BY * COLOR_FREQ_MULT];
}
encodePrefix(oscillator, startTime) { encodePrefix(oscillator, startTime) {
let time = startTime; let time = startTime;
@ -427,6 +440,134 @@ class ScottieDX extends Format {
oscillator.stop(time); oscillator.stop(time);
} }
} }
class PD50 extends Format {
constructor() {
let numScanLines = 256;
let vertResolution = 320;
let blankingInterval = 0.00208;
let scanLineLength = 0.091520;
let syncPulseLength = 0.02;
let VISCode = [true, false, true, true, true, false, true];
super(numScanLines, vertResolution, blankingInterval, scanLineLength, syncPulseLength, VISCode);
}
prepareImage(data) {
let preparedImage = [];
for(let scanLine = 0; scanLine < this.numScanLines; ++scanLine){
let Y = [];
let RY = [];
let BY = [];
for(let vertPos = 0; vertPos < this.vertResolution; ++vertPos){
let freqs = this.getYRYBYValueAsFreq(data, scanLine, vertPos);
Y.push(freqs[0]);
RY.push(freqs[1]);
BY.push(freqs[2]);
}
preparedImage.push([Y, RY, BY]);
}
for(let scanLine = 0; scanLine < this.numScanLines; scanLine += 2){
for(let vertPos = 0; vertPos < this.vertResolution; ++vertPos){
let RY = preparedImage[scanLine][1][vertPos] + preparedImage[scanLine + 1][1][vertPos]
preparedImage[scanLine][1][vertPos] = RY / 2;
let BY = preparedImage[scanLine][2][vertPos] + preparedImage[scanLine + 1][2][vertPos]
preparedImage[scanLine][2][vertPos] = BY / 2;
}
}
super.prepareImage(preparedImage);
}
encodeSSTV(oscillator, startTime) {
let time = startTime;
time = super.encodePrefix(oscillator, time);
time = super.encodeHeader(oscillator, time);
for(let scanLine = 0; scanLine < super.numScanLines; scanLine += 2){
oscillator.frequency.setValueAtTime(SYNC_PULSE_FREQ, time);
time += super.syncPulseLength;
oscillator.frequency.setValueAtTime(BLANKING_PULSE_FREQ, time);
time += super.blankingInterval;
oscillator.frequency.setValueCurveAtTime(super.preparedImage[scanLine][0], time, super.scanLineLength);
time += super.scanLineLength;
oscillator.frequency.setValueCurveAtTime(super.preparedImage[scanLine][1], time, super.scanLineLength);
time += super.scanLineLength;
oscillator.frequency.setValueCurveAtTime(super.preparedImage[scanLine][2], time, super.scanLineLength);
time += super.scanLineLength;
oscillator.frequency.setValueCurveAtTime(super.preparedImage[scanLine + 1][0], time, super.scanLineLength);
time += super.scanLineLength;
}
oscillator.start(startTime);
oscillator.stop(time);
}
}
class PD90 extends Format {
constructor() {
let numScanLines = 256;
let vertResolution = 320;
let blankingInterval = 0.00208;
let scanLineLength = 0.170240;
let syncPulseLength = 0.02;
let VISCode = [true, true, false, false, false, true, true];
super(numScanLines, vertResolution, blankingInterval, scanLineLength, syncPulseLength, VISCode);
}
prepareImage(data) {
let preparedImage = [];
for(let scanLine = 0; scanLine < this.numScanLines; ++scanLine){
let Y = [];
let RY = [];
let BY = [];
for(let vertPos = 0; vertPos < this.vertResolution; ++vertPos){
let freqs = this.getYRYBYValueAsFreq(data, scanLine, vertPos);
Y.push(freqs[0]);
RY.push(freqs[1]);
BY.push(freqs[2]);
}
preparedImage.push([Y, RY, BY]);
}
for(let scanLine = 0; scanLine < this.numScanLines; scanLine += 2){
for(let vertPos = 0; vertPos < this.vertResolution; ++vertPos){
let RY = preparedImage[scanLine][1][vertPos] + preparedImage[scanLine + 1][1][vertPos]
preparedImage[scanLine][1][vertPos] = RY / 2;
let BY = preparedImage[scanLine][2][vertPos] + preparedImage[scanLine + 1][2][vertPos]
preparedImage[scanLine][2][vertPos] = BY / 2;
}
}
super.prepareImage(preparedImage);
}
encodeSSTV(oscillator, startTime) {
let time = startTime;
time = super.encodePrefix(oscillator, time);
time = super.encodeHeader(oscillator, time);
for(let scanLine = 0; scanLine < super.numScanLines; scanLine += 2){
oscillator.frequency.setValueAtTime(SYNC_PULSE_FREQ, time);
time += super.syncPulseLength;
oscillator.frequency.setValueAtTime(BLANKING_PULSE_FREQ, time);
time += super.blankingInterval;
oscillator.frequency.setValueCurveAtTime(super.preparedImage[scanLine][0], time, super.scanLineLength);
time += super.scanLineLength;
oscillator.frequency.setValueCurveAtTime(super.preparedImage[scanLine][1], time, super.scanLineLength);
time += super.scanLineLength;
oscillator.frequency.setValueCurveAtTime(super.preparedImage[scanLine][2], time, super.scanLineLength);
time += super.scanLineLength;
oscillator.frequency.setValueCurveAtTime(super.preparedImage[scanLine + 1][0], time, super.scanLineLength);
time += super.scanLineLength;
}
oscillator.start(startTime);
oscillator.stop(time);
}
}
//---------- Frontend Controls ----------// //---------- Frontend Controls ----------//
@ -479,6 +620,10 @@ startButton.onclick = () => {
format = new ScottieTwo(); format = new ScottieTwo();
else if(modeSelect.value == "SDX") else if(modeSelect.value == "SDX")
format = new ScottieDX(); format = new ScottieDX();
else if(modeSelect.value == "PD50")
format = new PD50();
else if(modeSelect.value == "PD90")
format = new PD90();
else { else {
warningText.textContent = "You must select a mode"; warningText.textContent = "You must select a mode";
startButton.disabled = true; startButton.disabled = true;