From 6ebd89948ddc3e506b2e0ff21395673c56067812 Mon Sep 17 00:00:00 2001 From: ckegel <57967583+CKegel@users.noreply.github.com> Date: Fri, 15 Dec 2023 00:07:55 -0500 Subject: [PATCH] Add support for encoding PD-50 and PD-90 modes. --- README.md | 4 +- index.html | 11 ++-- script.js | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 158 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9c0cdbf..6e53e82 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,6 @@ ## 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. 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 -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. \ No newline at end of file +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. \ No newline at end of file diff --git a/index.html b/index.html index 516577e..f79d8e5 100644 --- a/index.html +++ b/index.html @@ -13,12 +13,17 @@

Web SSTV

Send SSTV signals from your browser:

@@ -32,9 +37,9 @@

Send SSTV signals from your browser:

diff --git a/script.js b/script.js index 256ae13..b839f2b 100644 --- a/script.js +++ b/script.js @@ -21,6 +21,7 @@ const HEADER_BREAK_LENGTH = 0.01; //10 ms const VIS_BIT_LENGTH = 0.03; //30 ms const SYNC_PULSE_FREQ = 1200; const BLANKING_PULSE_FREQ = 1500; +const COLOR_FREQ_MULT = 3.1372549; const VIS_BIT_FREQ = { ONE: 1100, ZERO: 1300, @@ -47,12 +48,24 @@ class Format { getRGBValueAsFreq(data, scanLine, vertPos) { const index = scanLine * (this.#vertResolution * 4) + vertPos * 4; - let red = data[index] * 3.137 + 1500; - let green = data[index + 1] * 3.137 + 1500; - let blue = data[index + 2] * 3.137 + 1500; + let red = data[index] * COLOR_FREQ_MULT + 1500; + let green = data[index + 1] * COLOR_FREQ_MULT + 1500; + let blue = data[index + 2] * COLOR_FREQ_MULT + 1500; 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) { let time = startTime; @@ -427,6 +440,134 @@ class ScottieDX extends Format { 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 ----------// @@ -479,6 +620,10 @@ startButton.onclick = () => { format = new ScottieTwo(); else if(modeSelect.value == "SDX") format = new ScottieDX(); + else if(modeSelect.value == "PD50") + format = new PD50(); + else if(modeSelect.value == "PD90") + format = new PD90(); else { warningText.textContent = "You must select a mode"; startButton.disabled = true;