This repository has been archived by the owner on Nov 20, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 31
/
morse-pro-cw-wave.js
150 lines (138 loc) · 6.47 KB
/
morse-pro-cw-wave.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/*!
This code is © Copyright Stephen C. Phillips, 2018.
Email: steve@scphillips.com
*/
/*
Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European Commission - subsequent versions of the EUPL (the "Licence");
You may not use this work except in compliance with the Licence.
You may obtain a copy of the Licence at: https://joinup.ec.europa.eu/community/eupl/
Unless required by applicable law or agreed to in writing, software distributed under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the Licence for the specific language governing permissions and limitations under the Licence.
*/
import MorseCW from './morse-pro-cw';
/**
* Class to create sine-wave samples of standard CW Morse.
*
* @example
* import MorseCWWave from 'morse-pro-cw-wave';
* var morseCWWave = new MorseCWWave();
* morseCWWave.translate("abc");
* var sample = morseCWWave.getSample();
*/
export default class MorseCWWave extends MorseCW {
/**
* @param {number} [frequency=550] - frequency of wave in Hz
* @param {number} [sampleRate=8000] - sample rate for the waveform in Hz
*/
constructor(useProsigns, wpm, fwpm, frequency = 550, sampleRate = 8000) {
super(useProsigns, wpm, fwpm);
/** @type {number} */
this.frequency = frequency; // frequency of wave in Hz
/** @type {number} */
this.sampleRate = sampleRate; // sample rate for the waveform in Hz
}
/**
* Get a sample waveform, not using Web Audio API (synchronous).
* @param {number} [endPadding=0] - how much silence in ms to add to the end of the waveform.
* @return {number[]} an array of floats in range [-1, 1] representing the wave-form.
*/
getSample(endPadding = 0) {
return MorseCWWave.getSampleGeneral(this.getTimings(), this.frequency, this.sampleRate, endPadding);
}
/**
* Get a sample waveform, not using Web Audio API (synchronous).
* @param {number[]} timings - millisecond timings, +ve numbers representing sound, -ve for no sound (+ve/-ve can be in any order)
* @param {number} frequency - frequency of sound in Hz.
* @param {number} sampleRate - sample rate in Hz.
* @param {number} [endPadding=0] - how much silence in ms to add to the end of the waveform.
* @return {number[]} an array of floats in range [-1, 1] representing the wave-form.
*/
static getSampleGeneral(timings, frequency, sampleRate, endPadding = 0) {
var sample = [];
if (timings.length === 0) {
return [];
}
// add minimum of 5ms silence to the end to ensure the filtered signal can finish cleanly
timings.push(-Math.max(5, endPadding));
/*
Compute lowpass biquad filter coefficients using method from Chromium
*/
// set lowpass frequency cutoff to 1.5 x wave frequency
var lowpassFreq = (frequency * 1.5) / sampleRate;
var q = Math.SQRT1_2;
var sin = Math.sin(2 * Math.PI * lowpassFreq);
var cos = Math.cos(2 * Math.PI * lowpassFreq);
var alpha = sin / (2 * Math.pow(10, q / 20));
var a0 = 1 + alpha;
var b0 = ((1 - cos) * 0.5) / a0;
var b1 = (1 - cos) / a0;
var b2 = ((1 - cos) * 0.5) / a0;
var a1 = (-2 * cos) / a0;
var a2 = (1 - alpha) / a0;
/*
Compute filtered signal
*/
var step = Math.PI * 2 * frequency / sampleRate;
var on = timings[0] > 0 ? 1 : 0;
var x0, x1 = 0, x2 = 0;
var y0, y1 = 0, y2 = 0;
var gain = 0.813; // empirically, the lowpass filter outputs waveform of magnitude 1.23, so need to scale it down to avoid clipping
for (var t = 0; t < timings.length; t += 1) {
var duration = sampleRate * Math.abs(timings[t]) / 1000;
for (var i = 0; i < duration; i += 1) {
x0 = on * Math.sin(i * step); // the input signal
y0 = b0 * x0 + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
sample.push(y0 * gain);
x2 = x1;
x1 = x0;
y2 = y1;
y1 = y0;
}
on = 1 - on;
}
return sample;
}
/**
* Get a sample waveform using Web Audio API (asynchronous).
* @param {number} [endPadding=0] - how much silence in ms to add to the end of the waveform.
* @return {Promise(number[])} a Promise resolving to an array of floats in range [-1, 1] representing the wave-form.
*/
getWAASample(endPadding = 0) {
// add minimum of 5ms silence to the end to ensure the filtered signal can finish cleanly
endPadding = Math.max(5, endPadding);
var timings = this.getTimings();
timings.push(-endPadding);
var offlineAudioContextClass = window.OfflineAudioContext || window.webkitOfflineAudioContext;
if (offlineAudioContextClass === undefined) {
throw new Error("No OfflineAudioContext class defined");
}
// buffer length is the Morse duration + 5ms to let the lowpass filter end cleanly
var offlineCtx = new offlineAudioContextClass(1, this.sampleRate * (this.getDuration() + endPadding) / 1000, this.sampleRate);
var gainNode = offlineCtx.createGain();
// empirically, the lowpass filter outputs waveform of magnitude 1.23, so need to scale it down to avoid clipping
gainNode.gain.setValueAtTime(0.813, 0);
var lowPassNode = offlineCtx.createBiquadFilter();
lowPassNode.type = "lowpass";
lowPassNode.frequency.setValueAtTime(this.frequency * 1.1, 0); // TODO: remove this magic number and make the filter configurable?
gainNode.connect(lowPassNode);
lowPassNode.connect(offlineCtx.destination);
var t = 0;
var oscillator;
var duration;
for (var i = 0; i < timings.length; i++) {
duration = Math.abs(timings[i]) / 1000;
if (timings[i] > 0) { // -ve timings are silence
oscillator = offlineCtx.createOscillator();
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(this.frequency, t);
oscillator.start(t);
oscillator.stop(t + duration);
oscillator.connect(gainNode);
}
t += duration;
}
return offlineCtx.startRendering().then(function(renderedBuffer) {
return renderedBuffer.getChannelData(0);
});
}
}