Add all PD Modes.

This commit is contained in:
ckegel 2024-10-16 23:48:22 -04:00
parent ff0d4fcd35
commit a65f308216
8 changed files with 303 additions and 287 deletions

View File

@ -2,6 +2,8 @@
## 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/.
## Current State
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.
Currently Web SSTV supports encoding images using the Martin, Scottie, PD, and WRASSE SC2-180 formats. Support for transmitting in the Robot format and in black and white underway. Decoding has proven to be a greater challenge. I am currently in the process of writing a custom Web Audio Worklet that leverages the Goertzel Algorithm to detect VIS headers and sync pulses. Pull requests are welcome.
## Sources
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.
Both the [SSTV Handbook](https://www.sstv-handbook.com/) and [JL Barber's (N7CXI) Proposal for SSTV Mode Specifications ](http://www.barberdsp.com/downloads/Dayton%20Paper.pdf) were heavily referenced when implementing support for the Martin and Scottie formats.
## License
Web-SSTV is available freely under the MIT license. Should you decide to host your own instance of WebSSTV without substantial modification, please provide a link to this repository and a copy of the MIT license, including the original copyright statement.

View File

@ -3,7 +3,8 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.colors.min.css">
<link rel="stylesheet" href="./style.css">
<title>Web-SSTV</title>
</head>
@ -17,16 +18,27 @@
<li><a href="./learn.html">Learn</a></li>
</ul>
</nav>
<body>
<main class="container">
<h4>Stand by for SSTV decoding (coming soon...)</h4>
<center>
<h4><span class="pico-color-amber-200" id="decodeText">Waiting for Audio Feed...</span></h4>
<div id="previewArea" class="">
<canvas id="preview"></canvas>
</div>
<p>SSTV decoding is currently under development and not currently operational. Encountered an issue or unexpected behavior? Create an issue <a href="https://github.com/CKegel/Web-SSTV/issues">on GitHub</a>.</p>
</center>
</main>
<footer class="container">
<small>SSTV signal decoding and more encoding modes coming soon.</small>
<br>
<small><a href="https://github.com/CKegel/Web-SSTV/">Check out the project on Github</a></small>
<br>
<small>&copy 2023 Christian Kegel - Available under the MIT License</small>
<small>&copy <script>document.write(new Date().getFullYear())</script> Christian Kegel - Available under the MIT License</small>
</footer>
</body>
<script src="./goertzel.js"></script>
<script src="./decode.js"></script>
</html>

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
<link rel="stylesheet" href="./style.css">
<title>Web-SSTV</title>
</head>
@ -29,15 +29,22 @@
<option value="S1">Scottie 1</option>
<option value="S2">Scottie 2</option>
<option value="SDX">Scottie DX</option>
<option value="none" disabled>--- WRASSE ---</option>
<option value="WrasseSC2180">SC2-180</option>
<option value="none" disabled>--- PD ---</option>
<option value="PD50">PD-50</option>
<option value="PD90">PD-90</option>
<option value="PD120">PD-120</option>
<option value="PD160">PD-160</option>
<option value="PD180">PD-180</option>
<option value="PD240">PD-240</option>
<option value="PD290">PD-290</option>
</select>
<label id="imgLabel" for="imgPicker">Upload an image:</label>
<input type="file" name="imgPicker" id="imgPicker" disabled />
<center id="imgArea" class="container">
<canvas id="imgCanvas" width="320" height="256"></canvas>
<canvas id="imgCanvas" ></canvas>
<br>
<span id="warningText"></span>
</center>
@ -58,7 +65,7 @@
<br>
<small><a href="https://github.com/CKegel/Web-SSTV/">Check out the project on Github</a></small>
<br>
<small>&copy 2023 Christian Kegel - Available under the MIT License</small>
<small>&copy <script>document.write(new Date().getFullYear())</script> Christian Kegel - Available under the MIT License</small>
</footer>
</body>
<script src="./encode.js"></script>

502
encode.js
View File

@ -1,7 +1,7 @@
/*
MIT License
Copyright (c) 2023 Christian Kegel
Copyright (c) 2024 Christian Kegel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -13,6 +13,7 @@ furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
//---------- Encoding Constants ----------//
const PREFIX_PULSE_LENGTH = 0.1; //100 ms
@ -46,6 +47,12 @@ class Format {
this.#VISCode = VISCode;
}
getGreyscaleFreq(data, scanLine, vertPos) {
const index = scanLine * (this.#vertResolution * 4) + vertPos * 4;
let grey = data[index] * 0.299 + 0.587 * data[index + 1] + 0.114 * data[index + 2]
return grey * COLOR_FREQ_MULT + 1500
}
getRGBValueAsFreq(data, scanLine, vertPos) {
const index = scanLine * (this.#vertResolution * 4) + vertPos * 4;
let red = data[index] * COLOR_FREQ_MULT + 1500;
@ -160,9 +167,49 @@ class Format {
}
//---------- Format Encode Implementation ----------//
class MartinBase extends Format {
prepareImage(data) {
let preparedImage = [];
for(let scanLine = 0; scanLine < this.numScanLines; ++scanLine){
let red = [];
let green = [];
let blue = [];
for(let vertPos = 0; vertPos < this.vertResolution; ++vertPos){
let freqs = this.getRGBValueAsFreq(data, scanLine, vertPos);
red.push(freqs[0]);
green.push(freqs[1]);
blue.push(freqs[2]);
}
preparedImage.push([green, blue, red]);
}
class MartinMOne extends Format {
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){
oscillator.frequency.setValueAtTime(SYNC_PULSE_FREQ, time);
time += super.syncPulseLength;
oscillator.frequency.setValueAtTime(BLANKING_PULSE_FREQ, time);
time += super.blankingInterval;
for(let dataLine = 0; dataLine < 3; ++dataLine) {
oscillator.frequency.setValueCurveAtTime(super.preparedImage[scanLine][dataLine], time, super.scanLineLength);
time += super.scanLineLength;
oscillator.frequency.setValueAtTime(BLANKING_PULSE_FREQ, time);
time += super.blankingInterval;
}
}
oscillator.start(startTime);
oscillator.stop(time);
}
}
class MartinMOne extends MartinBase {
constructor() {
let numScanLines = 256;
let vertResolution = 320;
@ -173,50 +220,8 @@ class MartinMOne extends Format {
super(numScanLines, vertResolution, blankingInterval, scanLineLength, syncPulseLength, VISCode);
}
prepareImage(data) {
let preparedImage = [];
for(let scanLine = 0; scanLine < this.numScanLines; ++scanLine){
let red = [];
let green = [];
let blue = [];
for(let vertPos = 0; vertPos < this.vertResolution; ++vertPos){
let freqs = this.getRGBValueAsFreq(data, scanLine, vertPos);
red.push(freqs[0]);
green.push(freqs[1]);
blue.push(freqs[2]);
}
preparedImage.push([green, blue, red]);
}
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){
oscillator.frequency.setValueAtTime(SYNC_PULSE_FREQ, time);
time += super.syncPulseLength;
oscillator.frequency.setValueAtTime(BLANKING_PULSE_FREQ, time);
time += super.blankingInterval;
for(let dataLine = 0; dataLine < 3; ++dataLine) {
oscillator.frequency.setValueCurveAtTime(super.preparedImage[scanLine][dataLine], time, super.scanLineLength);
time += super.scanLineLength;
oscillator.frequency.setValueAtTime(BLANKING_PULSE_FREQ, time);
time += super.blankingInterval;
}
}
oscillator.start(startTime);
oscillator.stop(time);
}
}
class MartinMTwo extends Format {
class MartinMTwo extends MartinBase {
constructor() {
let numScanLines = 256;
let vertResolution = 320;
@ -227,7 +232,9 @@ class MartinMTwo extends Format {
super(numScanLines, vertResolution, blankingInterval, scanLineLength, syncPulseLength, VISCode);
}
}
class ScottieBase extends Format {
prepareImage(data) {
let preparedImage = [];
for(let scanLine = 0; scanLine < this.numScanLines; ++scanLine){
@ -252,16 +259,19 @@ class MartinMTwo extends Format {
time = super.encodePrefix(oscillator, time);
time = super.encodeHeader(oscillator, time);
oscillator.frequency.setValueAtTime(SYNC_PULSE_FREQ, time);
time += super.syncPulseLength;
for(let scanLine = 0; scanLine < super.numScanLines; ++scanLine){
oscillator.frequency.setValueAtTime(SYNC_PULSE_FREQ, time);
time += super.syncPulseLength;
oscillator.frequency.setValueAtTime(BLANKING_PULSE_FREQ, time);
time += super.blankingInterval;
for(let dataLine = 0; dataLine < 3; ++dataLine) {
oscillator.frequency.setValueCurveAtTime(super.preparedImage[scanLine][dataLine], time, super.scanLineLength);
time += super.scanLineLength;
if(dataLine == 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][dataLine], time, super.scanLineLength);
time += super.scanLineLength;
}
}
@ -269,8 +279,7 @@ class MartinMTwo extends Format {
oscillator.stop(time);
}
}
class ScottieOne extends Format {
class ScottieOne extends ScottieBase {
constructor() {
let numScanLines = 256;
let vertResolution = 320;
@ -281,53 +290,8 @@ class ScottieOne extends Format {
super(numScanLines, vertResolution, blankingInterval, scanLineLength, syncPulseLength, VISCode);
}
prepareImage(data) {
let preparedImage = [];
for(let scanLine = 0; scanLine < this.numScanLines; ++scanLine){
let red = [];
let green = [];
let blue = [];
for(let vertPos = 0; vertPos < this.vertResolution; ++vertPos){
let freqs = this.getRGBValueAsFreq(data, scanLine, vertPos);
red.push(freqs[0]);
green.push(freqs[1]);
blue.push(freqs[2]);
}
preparedImage.push([green, blue, red]);
}
super.prepareImage(preparedImage);
}
encodeSSTV(oscillator, startTime) {
let time = startTime;
time = super.encodePrefix(oscillator, time);
time = super.encodeHeader(oscillator, time);
oscillator.frequency.setValueAtTime(SYNC_PULSE_FREQ, time);
time += super.syncPulseLength;
for(let scanLine = 0; scanLine < super.numScanLines; ++scanLine){
for(let dataLine = 0; dataLine < 3; ++dataLine) {
if(dataLine == 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][dataLine], time, super.scanLineLength);
time += super.scanLineLength;
}
}
oscillator.start(startTime);
oscillator.stop(time);
}
}
class ScottieTwo extends Format {
constructor() {
let numScanLines = 256;
let vertResolution = 320;
@ -338,53 +302,8 @@ class ScottieTwo extends Format {
super(numScanLines, vertResolution, blankingInterval, scanLineLength, syncPulseLength, VISCode);
}
prepareImage(data) {
let preparedImage = [];
for(let scanLine = 0; scanLine < this.numScanLines; ++scanLine){
let red = [];
let green = [];
let blue = [];
for(let vertPos = 0; vertPos < this.vertResolution; ++vertPos){
let freqs = this.getRGBValueAsFreq(data, scanLine, vertPos);
red.push(freqs[0]);
green.push(freqs[1]);
blue.push(freqs[2]);
}
preparedImage.push([green, blue, red]);
}
super.prepareImage(preparedImage);
}
encodeSSTV(oscillator, startTime) {
let time = startTime;
time = super.encodePrefix(oscillator, time);
time = super.encodeHeader(oscillator, time);
oscillator.frequency.setValueAtTime(SYNC_PULSE_FREQ, time);
time += super.syncPulseLength;
for(let scanLine = 0; scanLine < super.numScanLines; ++scanLine){
for(let dataLine = 0; dataLine < 3; ++dataLine) {
if(dataLine == 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][dataLine], time, super.scanLineLength);
time += super.scanLineLength;
}
}
oscillator.start(startTime);
oscillator.stop(time);
}
}
class ScottieDX extends Format {
constructor() {
let numScanLines = 256;
let vertResolution = 320;
@ -395,7 +314,146 @@ class ScottieDX extends Format {
super(numScanLines, vertResolution, blankingInterval, scanLineLength, syncPulseLength, VISCode);
}
}
class PDBase extends Format {
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 PD50 extends PDBase {
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);
}
}
class PD90 extends PDBase {
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);
}
}
class PD120 extends PDBase {
constructor() {
let numScanLines = 496;
let vertResolution = 640;
let blankingInterval = 0.00208;
let scanLineLength = 0.121600;
let syncPulseLength = 0.02;
let VISCode = [true, false, true, true, true, true, true];
super(numScanLines, vertResolution, blankingInterval, scanLineLength, syncPulseLength, VISCode);
}
}
class PD160 extends PDBase {
constructor() {
let numScanLines = 400;
let vertResolution = 512;
let blankingInterval = 0.00208;
let scanLineLength = 0.195584;
let syncPulseLength = 0.02;
let VISCode = [true, true, false, false, true, false, false];
super(numScanLines, vertResolution, blankingInterval, scanLineLength, syncPulseLength, VISCode);
}
}
class PD180 extends PDBase {
constructor() {
let numScanLines = 496;
let vertResolution = 640;
let blankingInterval = 0.00208;
let scanLineLength = 0.18304;
let syncPulseLength = 0.02;
let VISCode = [true, true, false, false, false, false, false];
super(numScanLines, vertResolution, blankingInterval, scanLineLength, syncPulseLength, VISCode);
}
}
class PD240 extends PDBase {
constructor() {
let numScanLines = 496;
let vertResolution = 640;
let blankingInterval = 0.00208;
let scanLineLength = 0.24448;
let syncPulseLength = 0.02;
let VISCode = [true, true, false, false, false, false, true];
super(numScanLines, vertResolution, blankingInterval, scanLineLength, syncPulseLength, VISCode);
}
}
class PD290 extends PDBase {
constructor() {
let numScanLines = 616;
let vertResolution = 800;
let blankingInterval = 0.00208;
let scanLineLength = 0.2288;
let syncPulseLength = 0.02;
let VISCode = [true, false, true, true, true, true, false];
super(numScanLines, vertResolution, blankingInterval, scanLineLength, syncPulseLength, VISCode);
}
}
class WrasseSC2 extends Format {
prepareImage(data) {
let preparedImage = [];
for(let scanLine = 0; scanLine < this.numScanLines; ++scanLine){
@ -408,7 +466,7 @@ class ScottieDX extends Format {
green.push(freqs[1]);
blue.push(freqs[2]);
}
preparedImage.push([green, blue, red]);
preparedImage.push([red, green, blue]);
}
super.prepareImage(preparedImage);
@ -420,17 +478,12 @@ class ScottieDX extends Format {
time = super.encodePrefix(oscillator, time);
time = super.encodeHeader(oscillator, time);
oscillator.frequency.setValueAtTime(SYNC_PULSE_FREQ, time);
time += super.syncPulseLength;
for(let scanLine = 0; scanLine < super.numScanLines; ++scanLine){
oscillator.frequency.setValueAtTime(SYNC_PULSE_FREQ, time);
time += super.syncPulseLength;
oscillator.frequency.setValueAtTime(BLANKING_PULSE_FREQ, time);
time += super.blankingInterval;
for(let dataLine = 0; dataLine < 3; ++dataLine) {
if(dataLine == 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][dataLine], time, super.scanLineLength);
time += super.scanLineLength;
}
@ -440,133 +493,17 @@ class ScottieDX extends Format {
oscillator.stop(time);
}
}
class PD50 extends Format {
class WrasseSC2180 extends WrasseSC2 {
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];
let blankingInterval = 0.0005;
let scanLineLength = 0.235;
let syncPulseLength = 0.0055225;
let VISCode = [false, true, true, false, true, 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);
}
}
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 ----------//
@ -648,6 +585,23 @@ modeSelect.addEventListener("change", (e) => {
sstvFormat = new PD50();
else if(modeSelect.value == "PD90")
sstvFormat = new PD90();
else if(modeSelect.value == "PD120")
sstvFormat = new PD120();
else if(modeSelect.value == "PD160")
sstvFormat = new PD160();
else if(modeSelect.value == "PD180")
sstvFormat = new PD180();
else if(modeSelect.value == "PD240")
sstvFormat = new PD240();
else if(modeSelect.value == "PD290")
sstvFormat = new PD290();
else if(modeSelect.value == "RobotBW8")
sstvFormat = new RobotBW8();
else if(modeSelect.value == "WrasseSC2180")
sstvFormat = new WrasseSC2180();
if(imageLoaded)
drawPreview();
});
startButton.onclick = () => {

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
<link rel="stylesheet" href="./style.css">
<title>Web-SSTV</title>
</head>
@ -14,7 +14,7 @@
<ul>
<li><a href="./encode.html">Encode</a></li>
<li><a href="./decode.html">Decode</a></li>
<li><a href="#">Learn</a></li>
<li><a href="./learn.html">Learn</a></li>
</ul>
</nav>
<body>
@ -22,7 +22,8 @@
<h4>About:</h4>
<p>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.</p>
<h4>Changelog:</h4>
<p>October 2024 - Restructured the site in preperation for decoding. Added the ability to mark images with the users call sign.</p>
<p>October 16, 2024 - Added all PD Modes, patched several usability issues, and updated the styling.</p>
<p>October 13, 2024 - Restructured the site in preperation for decoding. Added the ability to mark images with the users call sign.</p>
<p>December 2023 - Initial site published via Github pages.</p>
</main>
<footer class="container">
@ -30,7 +31,7 @@
<br>
<small><a href="https://github.com/CKegel/Web-SSTV/">Check out the project on Github</a></small>
<br>
<small>&copy 2023 Christian Kegel - Available under the MIT License</small>
<small>&copy <script>document.write(new Date().getFullYear())</script> Christian Kegel - Available under the MIT License</small>
</footer>
</body>
</html>

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
<link rel="stylesheet" href="./style.css">
<title>Web-SSTV</title>
</head>

35
sstv-decoder.js Normal file
View File

@ -0,0 +1,35 @@
/*
MIT License
Copyright (c) 2024 Christian Kegel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
class SSTVDecoder extends AudioWorkletProcessor {
leaderToneDetected = false;
sstvFormat;
constructor(){
super();
}
process(inputs, outputs, parameters){
//Apply the Goertzel algorithm (more performant/save battery) to idenify the VIS Header, but use FFT (for now)
//Detect a mode and load appropriate parameters
//Apply Goertzel algorithm to recognize sync pulses, and call `onLineRecieved` Callback with freq data
//Note: We don't have to live decode the freq data, just collect it between sync pulses and measure the time to see if we missed a sync
return true;
}
}
registerProcessor('sstv-decoder', SSTVDecoder);

View File

@ -6,6 +6,11 @@
color: red;
}
#previewArea {
padding: 10px;
background: black;
}
.rounded {
border-radius: 10px;
}