mirror of
https://github.com/CKegel/Web-SSTV.git
synced 2025-06-02 09:30:23 +00:00
Add the ability to download encoded images as a WAV file. Dervive Scotty modes from the Scottie base class.
This commit is contained in:
parent
a65f308216
commit
f325a9855e
@ -58,6 +58,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<button id="startButton" class="contrast" disabled>Encode</button>
|
||||
<button id="downloadButton" class="contrast" disabled>Download</button>
|
||||
|
||||
</main>
|
||||
<footer class="container">
|
||||
|
138
encode.js
138
encode.js
@ -13,7 +13,6 @@ 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
|
||||
@ -143,6 +142,10 @@ class Format {
|
||||
throw new Error("Must be defined by a subclass");
|
||||
}
|
||||
|
||||
getEncodedLength() {
|
||||
throw new Error("Must be defined by a subclass");
|
||||
}
|
||||
|
||||
get numScanLines() {
|
||||
return this.#numScanLines;
|
||||
}
|
||||
@ -205,8 +208,10 @@ class MartinBase extends Format {
|
||||
}
|
||||
}
|
||||
|
||||
oscillator.start(startTime);
|
||||
oscillator.stop(time);
|
||||
return time;
|
||||
}
|
||||
getEncodedLength() {
|
||||
return (super.numScanLines * (super.syncPulseLength + super.blankingInterval + 3 * (super.scanLineLength + super.blankingInterval)));
|
||||
}
|
||||
}
|
||||
class MartinMOne extends MartinBase {
|
||||
@ -275,8 +280,11 @@ class ScottieBase extends Format {
|
||||
}
|
||||
}
|
||||
|
||||
oscillator.start(startTime);
|
||||
oscillator.stop(time);
|
||||
return time;
|
||||
}
|
||||
|
||||
getEncodedLength() {
|
||||
return (super.syncPulseLength + (super.numScanLines * (super.syncPulseLength / 2 + super.blankingInterval + super.scanLineLength)));
|
||||
}
|
||||
}
|
||||
class ScottieOne extends ScottieBase {
|
||||
@ -291,7 +299,7 @@ class ScottieOne extends ScottieBase {
|
||||
super(numScanLines, vertResolution, blankingInterval, scanLineLength, syncPulseLength, VISCode);
|
||||
}
|
||||
}
|
||||
class ScottieTwo extends Format {
|
||||
class ScottieTwo extends ScottieBase {
|
||||
constructor() {
|
||||
let numScanLines = 256;
|
||||
let vertResolution = 320;
|
||||
@ -303,7 +311,7 @@ class ScottieTwo extends Format {
|
||||
super(numScanLines, vertResolution, blankingInterval, scanLineLength, syncPulseLength, VISCode);
|
||||
}
|
||||
}
|
||||
class ScottieDX extends Format {
|
||||
class ScottieDX extends ScottieBase {
|
||||
constructor() {
|
||||
let numScanLines = 256;
|
||||
let vertResolution = 320;
|
||||
@ -364,8 +372,11 @@ class PDBase extends Format {
|
||||
time += super.scanLineLength;
|
||||
}
|
||||
|
||||
oscillator.start(startTime);
|
||||
oscillator.stop(time);
|
||||
return time;
|
||||
}
|
||||
|
||||
getEncodedLength() {
|
||||
return (super.numScanLines * (super.syncPulseLength + super.blankingInterval + super.scanLineLength * 4));
|
||||
}
|
||||
}
|
||||
class PD50 extends PDBase {
|
||||
@ -489,8 +500,11 @@ class WrasseSC2 extends Format {
|
||||
}
|
||||
}
|
||||
|
||||
oscillator.start(startTime);
|
||||
oscillator.stop(time);
|
||||
return time;
|
||||
}
|
||||
|
||||
getEncodedLength() {
|
||||
return (super.numScanLines * (super.syncPulseLength + super.blankingInterval + super.scanLineLength * 3));
|
||||
}
|
||||
}
|
||||
class WrasseSC2180 extends WrasseSC2 {
|
||||
@ -511,12 +525,13 @@ class WrasseSC2180 extends WrasseSC2 {
|
||||
const audioCtx = new AudioContext();
|
||||
let imageLoaded = false;
|
||||
|
||||
let modeSelect = document.getElementById("modeSelect")
|
||||
let modeSelect = document.getElementById("modeSelect");
|
||||
let startButton = document.getElementById("startButton");
|
||||
let downloadButton = document.getElementById('downloadButton');
|
||||
let imgPicker = document.getElementById("imgPicker");
|
||||
let warningText = document.getElementById("warningText");
|
||||
let callSignEntry = document.getElementById("callSign");
|
||||
let callSignLocation = document.getElementById("callSignLocation")
|
||||
let callSignLocation = document.getElementById("callSignLocation");
|
||||
|
||||
let canvas = document.getElementById("imgCanvas");
|
||||
let canvasCtx = canvas.getContext("2d");
|
||||
@ -560,6 +575,7 @@ imgPicker.addEventListener("change", (e) => {
|
||||
if(modeSelect.value != "none"){
|
||||
warningText.textContent = "";
|
||||
startButton.disabled = false;
|
||||
downloadButton.disabled = false;
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(e.target.files[0]);
|
||||
@ -569,6 +585,7 @@ modeSelect.addEventListener("change", (e) => {
|
||||
if(modeSelect.value != "none"){
|
||||
warningText.textContent = "";
|
||||
startButton.disabled = !imageLoaded;
|
||||
downloadButton.disabled = !imageLoaded;
|
||||
imgPicker.disabled = false;
|
||||
}
|
||||
if(modeSelect.value == "M1")
|
||||
@ -631,5 +648,98 @@ startButton.onclick = () => {
|
||||
oscillator.connect(audioCtx.destination);
|
||||
|
||||
sstvFormat.prepareImage(canvasData.data);
|
||||
sstvFormat.encodeSSTV(oscillator, audioCtx.currentTime + 1);
|
||||
let startTime = audioCtx.currentTime + 1;
|
||||
let endTime = sstvFormat.encodeSSTV(oscillator, audioCtx.currentTime + 1);
|
||||
oscillator.start(startTime);
|
||||
oscillator.end(endTime);
|
||||
};
|
||||
|
||||
function createWAVHeader(audioLength) {
|
||||
const headerSize = 44;
|
||||
const header = new ArrayBuffer(headerSize);
|
||||
const view = new DataView(header);
|
||||
// RIFF chunk
|
||||
view.setUint32(0, 0x52494646, false); // "RIFF"
|
||||
view.setUint32(4, 36 + audioLength, true); // File size
|
||||
view.setUint32(8, 0x57415645, false); // "WAVE"
|
||||
// fmt chunk
|
||||
view.setUint32(12, 0x666d7420, false); // "fmt "
|
||||
view.setUint32(16, 16, true); // Chunk size
|
||||
view.setUint16(20, 1, true); // Audio format (PCM)
|
||||
view.setUint16(22, 1, true); // Number of channels (mono)
|
||||
view.setUint32(24, 48000, true); // Sample rate
|
||||
view.setUint32(28, 48000 * 1 * 16 / 8, true); // Byte rate
|
||||
view.setUint16(32, 1 * 16 / 8, true); // Block align
|
||||
view.setUint16(34, 16, true); // Bits per sample
|
||||
// data chunk
|
||||
view.setUint32(36, 0x64617461, false); // "data"
|
||||
view.setUint32(40, audioLength, true); // Data size
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
|
||||
|
||||
downloadButton.onclick = () => {
|
||||
|
||||
if (modeSelect.value == "none") {
|
||||
warningText.textContent = "You must select a mode";
|
||||
startButton.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!imageLoaded) {
|
||||
warningText.textContent = "You must upload an image";
|
||||
startButton.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
let canvasData = canvasCtx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
|
||||
warningText.textContent = "";
|
||||
|
||||
if (audioCtx.state === "suspended") {
|
||||
audioCtx.resume();
|
||||
}
|
||||
|
||||
sstvSignalDuration = sstvFormat.getEncodedLength() + 1;
|
||||
const offlineCtx = new OfflineAudioContext(1, 48000 * sstvSignalDuration, 48000);
|
||||
let oscillator = offlineCtx.createOscillator();
|
||||
oscillator.type = "sine";
|
||||
|
||||
oscillator.connect(offlineCtx.destination);
|
||||
|
||||
sstvFormat.prepareImage(canvasData.data);
|
||||
endTime = sstvFormat.encodeSSTV(oscillator, 1);
|
||||
console.log(endTime - 1);
|
||||
oscillator.start(1);
|
||||
|
||||
offlineCtx.startRendering().then((audioBuffer) => {
|
||||
const audioData = audioBuffer.getChannelData(0); // Get first audio channel
|
||||
|
||||
// Convert Float32Array to Int16Array
|
||||
const audioLength = audioData.length;
|
||||
const audioInt16 = new Int16Array(audioLength);
|
||||
for (let i = 0; i < audioLength; i++) {
|
||||
audioInt16[i] = Math.max(-1, Math.min(1, audioData[i])) * 32767;
|
||||
}
|
||||
|
||||
// Convert the audio data to a Blob for download
|
||||
const wavHeader = createWAVHeader(audioInt16.length * 2); // 2 bytes per sample
|
||||
const wavFile = new Uint8Array(wavHeader.byteLength + audioInt16.byteLength);
|
||||
wavFile.set(new Uint8Array(wavHeader), 0);
|
||||
wavFile.set(new Uint8Array(audioInt16.buffer), wavHeader.byteLength);
|
||||
|
||||
const blob = new Blob([wavFile], { type: 'audio/wav' });
|
||||
|
||||
// Download the Blob
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'sstv_signal.wav';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
};
|
@ -22,9 +22,10 @@
|
||||
<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>November 16, 2024 - Added the ability to download encoded images as WAVE files.</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>
|
||||
<p>December 14, 2023 - Initial site published via Github pages.</p>
|
||||
</main>
|
||||
<footer class="container">
|
||||
<small>SSTV signal decoding and more encoding modes coming soon.</small>
|
||||
|
Loading…
x
Reference in New Issue
Block a user