diff --git a/src/index.html b/src/index.html
index aba0bb3..10c1a4d 100644
--- a/src/index.html
+++ b/src/index.html
@@ -152,18 +152,34 @@
@@ -580,7 +595,7 @@
Getting Started
After completing an exchange, click "TU" to send a wrap-up, thank
you message. Each mode has a slightly different way of completing.
After clicking TU, new stations sometimes hop in! But, in case they
- don't, you can always go back and click CQ.
+ don’t, you can always go back and click CQ.
diff --git a/src/js/app.js b/src/js/app.js
index 4ce74da..6c0cf4f 100644
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -32,6 +32,7 @@ import {
import {getYourStation, getCallingStation} from "./stationGenerator.js";
import {updateStaticIntensity} from "./audio.js";
import {modeLogicConfig, modeUIConfig} from "./modes.js";
+import { morseInput } from './morse-input/morse-input.js'; // Import morse input functionality
/**
* Application state variables.
@@ -82,12 +83,18 @@ document.addEventListener('DOMContentLoaded', () => {
const modeRadios = document.querySelectorAll('input[name="mode"]');
const yourCallsign = document.getElementById("yourCallsign");
const yourName = document.getElementById("yourName");
+ const yourState = document.getElementById("yourState"); // Added yourState
const yourSpeed = document.getElementById("yourSpeed");
const yourSidetone = document.getElementById("yourSidetone");
const yourVolume = document.getElementById("yourVolume");
+ const keyerMode = document.getElementById("keyerMode");
// Event Listeners
- cqButton.addEventListener('click', cq);
+ cqButton.addEventListener('click', () => {
+ // Initialize morse input when user starts using MorseWalker
+ morseInput.initialize();
+ cq();
+ });
sendButton.addEventListener('click', send);
tuButton.addEventListener('click', tu);
resetButton.addEventListener('click', reset);
@@ -114,6 +121,15 @@ document.addEventListener('DOMContentLoaded', () => {
// Toggle the Farnsworth speed input when the checkbox changes
enableFarnsworthCheckbox.addEventListener('change', () => {
farnsworthSpeedInput.disabled = !enableFarnsworthCheckbox.checked;
+ if (enableFarnsworthCheckbox.checked) {
+ morseInput.updateSettings({ farnsworth: parseInt(farnsworthSpeedInput.value) });
+ }
+ });
+
+ farnsworthSpeedInput.addEventListener('input', () => {
+ if (enableFarnsworthCheckbox.checked) {
+ morseInput.updateSettings({ farnsworth: parseInt(farnsworthSpeedInput.value) });
+ }
});
// Add hotkey for CQ (Ctrl + Shift + C)
@@ -161,7 +177,8 @@ document.addEventListener('keydown', (event) => {
yourState: "yourState", // Added yourState
yourSpeed: "yourSpeed",
yourSidetone: "yourSidetone",
- yourVolume: "yourVolume"
+ yourVolume: "yourVolume",
+ keyerMode: "keyerMode"
};
/**
@@ -177,6 +194,7 @@ document.addEventListener('keydown', (event) => {
yourSpeed.value = localStorage.getItem(keys.yourSpeed) || yourSpeed.value;
yourSidetone.value = localStorage.getItem(keys.yourSidetone) || yourSidetone.value;
yourVolume.value = localStorage.getItem(keys.yourVolume) || yourVolume.value;
+ keyerMode.value = localStorage.getItem(keys.keyerMode) || keyerMode.value;
// Save user settings to localStorage on input change
yourCallsign.addEventListener("input", () => {
@@ -190,13 +208,20 @@ document.addEventListener('keydown', (event) => {
});
yourSpeed.addEventListener("input", () => {
localStorage.setItem(keys.yourSpeed, yourSpeed.value);
+ // Update morse input speed when changed
+ morseInput.updateSettings({ wpm: parseInt(yourSpeed.value) });
});
yourSidetone.addEventListener("input", () => {
localStorage.setItem(keys.yourSidetone, yourSidetone.value);
+ // Update morse input tone when changed
+ morseInput.updateSettings({ tone: parseInt(yourSidetone.value) });
});
yourVolume.addEventListener("input", () => {
localStorage.setItem(keys.yourVolume, yourVolume.value);
});
+ keyerMode.addEventListener("change", () => {
+ localStorage.setItem(keys.keyerMode, keyerMode.value);
+ });
// Handle QRN intensity changes
const qrnRadioButtons = document.querySelectorAll('input[name="qrn"]');
diff --git a/src/js/inputs.js b/src/js/inputs.js
index 97c6a1a..ccab2a0 100644
--- a/src/js/inputs.js
+++ b/src/js/inputs.js
@@ -33,6 +33,7 @@ function getDOMInputs() {
yourCallsign: document.getElementById('yourCallsign').value.trim().toUpperCase(),
yourName: document.getElementById('yourName').value.trim(),
yourState: document.getElementById('yourState').value.trim().toUpperCase(), // Convert to uppercase for consistency
+ keyerMode: parseInt(document.getElementById('keyerMode').value, 10),
yourSpeed: parseInt(document.getElementById('yourSpeed').value, 10),
yourSidetone: parseInt(document.getElementById('yourSidetone').value, 10),
// convert volume to a float between 0 and 1
diff --git a/src/js/morse-input/decoder.js b/src/js/morse-input/decoder.js
new file mode 100644
index 0000000..ae55cbe
--- /dev/null
+++ b/src/js/morse-input/decoder.js
@@ -0,0 +1,148 @@
+const morseToAlphabet = new Map([
+ ["12", "A"],
+ ["2111", "B"],
+ ["2121", "C"],
+ ["211", "D"],
+ ["1", "E"],
+ ["1121", "F"],
+ ["221", "G"],
+ ["1111", "H"],
+ ["11", "I"],
+ ["1222", "J"],
+ ["212", "K"],
+ ["1211", "L"],
+ ["22", "M"],
+ ["21", "N"],
+ ["222", "O"],
+ ["1221", "P"],
+ ["2212", "Q"],
+ ["121", "R"],
+ ["111", "S"],
+ ["2", "T"],
+ ["112", "U"],
+ ["1112", "V"],
+ ["122", "W"],
+ ["2112", "X"],
+ ["2122", "Y"],
+ ["2211", "Z"],
+ ["12222", "1"],
+ ["11222", "2"],
+ ["11122", "3"],
+ ["11112", "4"],
+ ["11111", "5"],
+ ["21111", "6"],
+ ["22111", "7"],
+ ["22211", "8"],
+ ["22221", "9"],
+ ["22222", "0"],
+ ["121212", "."],
+ ["221122", ","],
+ ["21121", "/"],
+ ["112211", "?"],
+ ["212122", "!"],
+ ["211112", "-"],
+ ["21221", "("],
+ ["212212", ")"],
+ ["222111", ":"],
+]);
+
+export class Decoder {
+ constructor(onLetterDecoded) {
+ this.onLetterDecoded = onLetterDecoded; // Store the callback function
+ this.lastLetter = '';
+ this.decodeArray = '';
+ this.unit = 80; // adjustment: short dit reduces, long dah lengthens
+ this.keyStartTime = null;
+ this.keyEndTime = null;
+ this.spaceTimer = null;
+ this.farnsworth = 3;
+ this.wordTimer = null; // Timer for word boundaries
+ this.wordTimeout = this.unit * 7; // A typical word gap is 7 units
+ }
+
+ keyOn() {
+ clearTimeout(this.spaceTimer);
+ clearTimeout(this.wordTimer); // Clear the wordTimer as well since we are receiving input
+ this.keyStartTime = Date.now();
+ //var pauseDuration = (this.keyEndTime) ? this.keyStartTime - this.keyEndTime : 0;
+ //if (pauseDuration > this.unit + (this.unit/10)) { // end sequence and decode letter
+ //this.registerLetter();
+ //}
+ }
+
+ keyOff() {
+ this.keyEndTime = Date.now();
+ var keyDuration = (this.keyStartTime) ? this.keyEndTime - this.keyStartTime : 0;
+ if (keyDuration < this.unit) {
+ // reduce unit based on short dit
+ this.unit = (keyDuration + this.unit) / 2;
+ this.registerDit();
+ } else if (keyDuration > this.unit * 3) {
+ // lengthen unit based on long dah
+ this.unit = ((keyDuration / 3) + this.unit) / 2;
+ this.registerDah();
+ } else {
+ var ditAndDahThreshold = (this.unit * 2);
+ if (keyDuration >= ditAndDahThreshold) {
+ this.registerDah();
+ } else {
+ this.registerDit();
+ }
+ }
+ let spaceTime = this.unit * this.farnsworth;
+ this.spaceTimer = setTimeout(() => { // end sequence and decode letter
+ this.updateLastLetter(this.morseToLetter(this.decodeArray));
+ this.decodeArray = '';
+ this.startWordTimer(); // Start the word timer after finishing a letter
+ }, spaceTime, "keyOff");
+ }
+
+ registerDit() {
+ this.decodeArray += '1';
+ }
+
+ registerDah() {
+ this.decodeArray += '2';
+ }
+
+ updateLastLetter(letter) {
+ //updateCurrentLetter(letter);
+ this.lastLetter = letter;
+ //console.log(this.lastLetter);
+
+ // Notify the callback function that a new letter is decoded
+ if (this.onLetterDecoded) {
+ this.onLetterDecoded(letter);
+ }
+ }
+
+ morseToLetter(sequence) {
+ var letter = morseToAlphabet.get(sequence);
+ if (letter) {
+ return letter;
+ } else {
+ return '*';
+ }
+ }
+
+ startWordTimer() {
+ // Comment out word spacing for MorseWalker integration
+ // Set up the word timer to add a space after a word boundary
+ /*
+ this.wordTimer = setTimeout(() => {
+ // Update with a space to indicate a word boundary
+ if (this.onLetterDecoded) {
+ this.onLetterDecoded(' ');
+ }
+ }, this.wordTimeout);
+ */
+ }
+
+ calculateWpm() {
+ return 60000 / (this.unit * 50);
+ }
+
+ setFarnsworth(farnsworth) {
+ this.farnsworth = farnsworth;
+ }
+}
\ No newline at end of file
diff --git a/src/js/morse-input/keyer.js b/src/js/morse-input/keyer.js
new file mode 100644
index 0000000..47ddc87
--- /dev/null
+++ b/src/js/morse-input/keyer.js
@@ -0,0 +1,256 @@
+import { restartAudioNeeded, restartAudio } from './sounder.js';
+
+export class Keyer {
+ constructor(sndr, decoder) {
+ this.sndr = sndr; // Sounder instance
+ this.decoder = decoder; // Decoder instance
+ // Using [ and ] for dits and dahs respectively for compatibility with the vband usb interface
+ this.ditKey1 = 'ControlLeft';
+ this.dahKey1 = 'ControlRight';
+ this.ditKey2 = 'BracketLeft';
+ this.dahKey2 = 'BracketRight';
+ this.wpm = 20;
+ this.unit = 60; // length of dit in milliseconds; 60 is 20wpm
+ this.mode = 2; // 1: straight key, 2: iambicA, 3: iambicB, 4: ultimatic
+ this.tone = 550;
+ this.queue = [];
+ this.ditKeyState = 0;
+ this.dahKeyState = 0;
+ this.lastKey = null;
+ this.ditStreak = 0;
+ this.dahStreak = 0;
+ this.streak = 0;
+ this.ditStart = null;
+ this.ditStop = null;
+ this.dahStart = null;
+ this.dahStop = null;
+ this.sending = false;
+ this.lastSendTimestamp = null;
+ this.oscillatorTimer = setInterval(() => {
+ this.oscillate();
+ }, 0);
+ }
+
+ setWpm(wpm){
+ this.wpm = wpm;
+ this.unit = 60000 / (wpm * 50) // based on the PARIS method 60 seconds / 50 elements per word * WPM
+ this.decoder.unit = this.unit;
+ }
+
+ setMode(mode){
+ this.mode = mode;
+ }
+
+ setTone(tone){
+ this.tone = tone;
+ }
+
+ sendSignal() {
+ this.sending = true;
+ //console.log('startSignal');
+ if (restartAudioNeeded()) {
+ restartAudio();
+ }
+ this.sndr.setTone(this.tone);
+ this.sndr.on();
+ this.decoder.keyOn();
+ }
+
+ stopSignal() {
+ //console.log('stopSignal');
+ this.sndr.off();
+ this.decoder.keyOff();
+ this.lastSendTimestamp = Date.now();
+ setTimeout(() => {
+ this.sending = false;
+ }, this.unit);
+ }
+
+ press(event, down, mode=this.mode) {
+ if (mode > 1 && event.code != this.ditKey1 && event.code != this.dahKey1 && event.code != this.ditKey2 && event.code != this.dahKey2) return;
+ if (mode == 1) {
+ if (down) {
+ if (restartAudioNeeded()) {
+ restartAudio();
+ }
+ this.sndr.setTone(this.tone);
+ this.sndr.on();
+ this.decoder.keyOn();
+
+ } else {
+ this.sndr.off();
+ this.decoder.keyOff();
+ }
+ } else if (mode > 1) {
+ //console.log(key);
+ if (event.code == this.ditKey1 || event.code == this.ditKey2) {
+ if (down) { // dit key down
+ this.ditKeyState = 1;
+ this.ditStart = Date.now()
+ } else { // dit key up
+ this.ditKeyState = 0;
+ this.ditStop = Date.now()
+ }
+ }
+ if (event.code == this.dahKey1 || event.code == this.dahKey2) {
+ if (down) { // dah key down
+ this.dahKeyState = 1;
+ this.dahStart = Date.now()
+ } else { // dah key up
+ this.dahKeyState = 0;
+ this.dahStop = Date.now()
+ }
+ }
+ }
+ }
+
+ processQueue() {
+ //console.log('processQueue');
+ if (!this.sending && this.queue.length) {
+ this.lastKey = this.queue.shift();
+ var signalLength = (this.lastKey == 1) ? this.unit : this.unit * 3;
+ this.sendSignal();
+ setTimeout(() => {
+ this.stopSignal();
+ }, signalLength);
+ }
+ }
+
+ oscillatev1() {
+ if (!this.ditKeyState && !this.dahKeyState) {
+ this.queue = [];
+ }
+ if (this.ditKeyState) {
+ if (this.queue.length == 0) {
+ if (!this.dahKeyState && !this.sending || this.lastKey == 2) {
+ this.queue.push(1);
+ }
+ }
+ }
+ if (this.dahKeyState) {
+ if (this.queue.length == 0) {
+ if (!this.ditKeyState && !this.sending || this.lastKey == 1) {
+ this.queue.push(2);
+ }
+ }
+ }
+ if (!this.sending && Date.now() - this.lastSendTimestamp > this.unit) {
+ this.processQueue();
+ }
+ }
+
+ oscillatev2() {
+ if (this.mode == 2 && !this.ditKeyState && !this.dahKeyState) { // Iambic B doesn't clear the queue
+ if (this.streak > 1) {
+ //console.log(this.streak + " queue: " + this.queue[0]);
+ this.queue = [];
+ }
+ this.streak = 0;
+ }
+ if (this.ditKeyState) {
+ if (this.queue.length < 1) {
+ if (!this.sending || this.lastKey == 2) {
+ this.queue.push(1);
+ if (this.lastKey == 2 && this.dahKeyState) {
+ this.streak++;
+ } else {
+ this.streak = 0;
+ }
+ }
+ }
+ }
+ if (this.dahKeyState) {
+ if (this.queue.length < 1) {
+ if (!this.sending || this.lastKey == 1) {
+ this.queue.push(2);
+ if (this.lastKey == 1 && this.ditKeyState) {
+ this.streak++;
+ } else {
+ this.streak = 0;
+ }
+ }
+ }
+ }
+ if (!this.sending && Date.now() - this.lastSendTimestamp >= this.unit) {
+ this.processQueue();
+ }
+ }
+
+ oscillatev3() {
+ if (this.mode == 2 && !this.ditKeyState && !this.dahKeyState && this.queue.length) { // Iambic B doesn't clear the queue
+ if ((this.ditStreak && this.queue[0] == 1) || (this.dahStreak && this.queue[0] == 2)) {
+ console.log("ditStreak: "+this.ditStreak+" dahStreak: "+this.dahStreak+" queue: "+this.queue[0]);
+ this.queue = [];
+ }
+ //console.log("NO CLEAR ditStreak: "+this.ditStreak+" dahStreak: "+this.dahStreak+" queue: "+this.queue[0]);
+ this.ditStreak = 0;
+ this.dahStreak = 0;
+ }
+ if (this.ditKeyState) {
+ if (!this.queue.length) {
+ if (!this.sending || this.lastKey == 2) {
+ this.queue.push(1);
+ this.ditStreak++;
+ }
+ } else if (!this.dahKeyState && this.queue[0] == 2) { // dah was canceled. Replace in queue
+ this.queue[0] = 1;
+ this.dahStreak = 0;
+ this.ditStreak++;
+ }
+ }
+ if (this.dahKeyState) {
+ if (!this.queue.length) {
+ if (!this.sending || this.lastKey == 1) {
+ this.queue.push(2);
+ this.dahStreak++;
+ }
+ } else if (!this.ditKeyState && this.queue[0] == 1) { // dit was canceled. Replace in queue
+ this.queue[0] = 2;
+ this.ditStreak = 0;
+ this.dahStreak++;
+ }
+ }
+ if (!this.sending && Date.now() - this.lastSendTimestamp >= this.unit) {
+ this.processQueue();
+ }
+ }
+
+ oscillate() {
+ if (this.mode == 2 && !this.ditKeyState && !this.dahKeyState && this.queue.length) {
+ if (this.queue[0] == 1) { // Dit is in the queue
+ if (this.ditStart < this.dahStart || this.ditStop - this.ditStart > this.unit * 4) {
+ this.queue.pop();
+ }
+ } else { // Dah is in the queue
+ if (this.dahStart < this.ditStart || this.dahStop - this.dahStart > this.unit * 2) {
+ this.queue.pop();
+ }
+ }
+ }
+ if (this.ditKeyState) {
+ if (this.queue.length == 0) {
+ if ((!this.dahKeyState && !this.sending) || this.lastKey == 2) {
+ this.queue.push(1);
+ }
+ } else { // dah key was lifted and is still in queue
+ if (this.mode == 2 && !this.dahKeyState && this.dahStart < this.ditStart && this.queue[0] == 2) {
+ this.queue.pop();
+ }
+ }
+ }
+ if (this.dahKeyState) {
+ if (this.queue.length == 0) {
+ if ((!this.ditKeyState && !this.sending) || this.lastKey == 1) {
+ this.queue.push(2);
+ }
+ } else { // dit key was lifted and is still in queue
+ if (this.mode == 2 && !this.ditKeyState && this.ditStart < this.dahStart && this.queue[0] == 1) {
+ this.queue.pop();
+ }
+ }
+ }
+ if (!this.sending && Date.now() - this.lastSendTimestamp > this.unit) {
+ this.processQueue();
+ }
+ }
+}
diff --git a/src/js/morse-input/morse-input.js b/src/js/morse-input/morse-input.js
new file mode 100644
index 0000000..9a1ef54
--- /dev/null
+++ b/src/js/morse-input/morse-input.js
@@ -0,0 +1,113 @@
+import { Sounder } from './sounder.js';
+import { Decoder } from './decoder.js';
+import { Keyer } from './keyer.js';
+import { getInputs } from '../inputs.js';
+
+/**
+ * MorseInput class handles real-time morse code input from keyboard or USB devices.
+ * It integrates with MorseWalker's input fields and settings.
+ */
+class MorseInput {
+ constructor() {
+ this.sounder = null;
+ this.decoder = null;
+ this.keyer = null;
+ this.initialized = false;
+
+ // Set up keyboard input - but don't initialize audio yet
+ window.addEventListener('keydown', (e) => this.handleKeyEvent(e, true));
+ window.addEventListener('keyup', (e) => this.handleKeyEvent(e, false));
+ }
+
+ /**
+ * Initialize the morse input system after user interaction
+ */
+ initialize() {
+ if (this.initialized) return;
+
+ this.sounder = new Sounder();
+ this.decoder = new Decoder(this.handleDecodedLetter.bind(this));
+ this.keyer = new Keyer(this.sounder, this.decoder);
+
+ // Load settings from MorseWalker
+ this.syncWithMorseWalker();
+
+ this.initialized = true;
+ }
+
+ /**
+ * Handle key events, initializing if needed
+ */
+ handleKeyEvent(e, down) {
+ if (!this.initialized) {
+ this.initialize();
+ }
+ this.keyer?.press(e, down);
+ }
+
+ /**
+ * Synchronizes settings with MorseWalker's current configuration
+ */
+ syncWithMorseWalker() {
+ const inputs = getInputs();
+ if (!inputs) return;
+
+ // Update keyer settings
+ this.keyer?.setWpm(inputs.yourSpeed);
+ this.keyer?.setMode(inputs.keyerMode);
+ this.keyer?.setTone(inputs.yourSidetone);
+
+ // Sync Farnsworth if enabled
+ if (inputs.enableFarnsworth) {
+ this.decoder?.setFarnsworth(inputs.farnsworthSpeed);
+ }
+ }
+
+ /**
+ * Handles decoded letters from morse input
+ * @param {string} letter - The decoded letter
+ */
+ handleDecodedLetter(letter) {
+ const activeField = document.activeElement;
+ if (!activeField || !['INPUT', 'TEXTAREA'].includes(activeField.tagName)) {
+ return;
+ }
+
+ // Insert the letter at the cursor position
+ const start = activeField.selectionStart;
+ const end = activeField.selectionEnd;
+ const value = activeField.value;
+
+ activeField.value = value.substring(0, start) + letter + value.substring(end);
+
+ // Move cursor after inserted letter
+ activeField.selectionStart = activeField.selectionEnd = start + letter.length;
+
+ // Trigger input event to ensure MorseWalker processes the change
+ activeField.dispatchEvent(new Event('input', { bubbles: true }));
+ }
+
+ /**
+ * Updates settings when MorseWalker configuration changes
+ * @param {Object} settings - New settings object
+ */
+ updateSettings(settings) {
+ if (!this.initialized) return;
+
+ if (settings.wpm !== undefined) {
+ this.keyer.setWpm(settings.wpm);
+ }
+ if (settings.tone !== undefined) {
+ this.keyer.setTone(settings.tone);
+ }
+ if (settings.farnsworth !== undefined) {
+ this.decoder.setFarnsworth(settings.farnsworth);
+ }
+ if (settings.mode !== undefined) {
+ this.keyer.setMode(settings.mode);
+ }
+ }
+}
+
+// Create and export a singleton instance
+export const morseInput = new MorseInput();
diff --git a/src/js/morse-input/sounder.js b/src/js/morse-input/sounder.js
new file mode 100644
index 0000000..8072b06
--- /dev/null
+++ b/src/js/morse-input/sounder.js
@@ -0,0 +1,112 @@
+// Morse Code Audio Generation Component
+import { audioContext } from '../audio.js';
+
+let sounderGlobalVolume = 0.5;
+
+export function setSounderGlobalVolume(volume) {
+ sounderGlobalVolume = volume;
+}
+
+export class Sounder {
+ constructor() {
+ this.oscillator = null;
+ this.gainNode = null;
+ this.isPlaying = false;
+ this.frequency = 550; // Default frequency
+ this.volume = sounderGlobalVolume; // Default volume
+
+ // Attack and release parameters for smooth transitions
+ this.attackTime = 0.005; // 5ms attack
+ this.releaseTime = 0.005; // 5ms release
+ }
+
+ /**
+ * Initialize audio components
+ */
+ initialize() {
+ if (this.oscillator) return;
+
+ this.oscillator = audioContext.createOscillator();
+ this.gainNode = audioContext.createGain();
+
+ this.oscillator.type = 'sine';
+ this.oscillator.frequency.value = this.frequency;
+ this.gainNode.gain.value = 0; // Start silent
+
+ this.oscillator.connect(this.gainNode);
+ this.gainNode.connect(audioContext.destination);
+ this.oscillator.start();
+ }
+
+ /**
+ * Clean up audio resources
+ */
+ cleanup() {
+ if (this.oscillator) {
+ this.oscillator.stop();
+ this.oscillator.disconnect();
+ this.oscillator = null;
+ }
+ if (this.gainNode) {
+ this.gainNode.disconnect();
+ this.gainNode = null;
+ }
+ this.isPlaying = false;
+ }
+
+ /**
+ * Set the frequency of the oscillator
+ * @param {number} freq - Frequency in Hz
+ */
+ setTone(freq) {
+ this.frequency = freq;
+ if (this.oscillator) {
+ this.oscillator.frequency.value = freq;
+ }
+ }
+
+ /**
+ * Set the volume of the audio
+ * @param {number} vol - Volume between 0 and 1
+ */
+ setVolume(vol) {
+ this.volume = Math.max(0, Math.min(1, vol));
+ }
+
+ /**
+ * Start playing the tone with smooth attack
+ */
+ on() {
+ if (!this.gainNode) this.initialize();
+
+ const now = audioContext.currentTime;
+ this.gainNode.gain.cancelScheduledValues(now);
+ this.gainNode.gain.setValueAtTime(0, now);
+ this.gainNode.gain.linearRampToValueAtTime(this.volume, now + this.attackTime);
+
+ this.isPlaying = true;
+ }
+
+ /**
+ * Stop playing the tone with smooth release
+ */
+ off() {
+ if (!this.gainNode) return;
+
+ const now = audioContext.currentTime;
+ this.gainNode.gain.cancelScheduledValues(now);
+ this.gainNode.gain.setValueAtTime(this.gainNode.gain.value, now);
+ this.gainNode.gain.linearRampToValueAtTime(0, now + this.releaseTime);
+
+ this.isPlaying = false;
+ }
+}
+
+// These are needed by keyer.js but we don't need them anymore since we're using MorseWalker's audio context
+export function restartAudioNeeded() {
+ return false;
+}
+
+export function restartAudio() {
+ // No-op since we're using MorseWalker's audio context
+}
diff --git a/webpack.common.js b/webpack.common.js
index 1580aba..21481fb 100644
--- a/webpack.common.js
+++ b/webpack.common.js
@@ -36,7 +36,14 @@ module.exports = {
new CopyPlugin({
patterns: [
{ from: 'src/audio', to: 'audio' }, // Ensure all files in 'audio' are copied to 'dist/audio'
- ],
+ { from: 'src/img', to: 'img' },
+ { from: 'src/favicon.ico', to: 'favicon.ico' },
+ { from: 'src/robots.txt', to: 'robots.txt' },
+ { from: 'src/site.webmanifest', to: 'site.webmanifest' },
+ { from: 'src/js/morse-input/sounder.js', to: 'js/morse-input/sounder.js' },
+ { from: 'src/js/morse-input/decoder.js', to: 'js/morse-input/decoder.js' },
+ { from: 'src/js/morse-input/keyer.js', to: 'js/morse-input/keyer.js' }
+ ]
}),
],
};