359 lines
13 KiB
JavaScript
359 lines
13 KiB
JavaScript
function Filter(demodulator) {
|
|
this.demodulator = demodulator;
|
|
this.min_passband = 100;
|
|
}
|
|
|
|
Filter.prototype.getLimits = function() {
|
|
var max_bw;
|
|
if (this.demodulator.get_secondary_demod() === 'pocsag') {
|
|
max_bw = 12500;
|
|
} else if (this.demodulator.get_modulation() === 'wfm') {
|
|
max_bw = 80000;
|
|
} else {
|
|
max_bw = (audioEngine.getOutputRate() / 2) - 1;
|
|
}
|
|
return {
|
|
high: max_bw,
|
|
low: -max_bw
|
|
};
|
|
};
|
|
|
|
function Envelope(demodulator) {
|
|
this.demodulator = demodulator;
|
|
this.dragged_range = Demodulator.draggable_ranges.none;
|
|
}
|
|
|
|
Envelope.prototype.draw = function(visible_range){
|
|
this.visible_range = visible_range;
|
|
var line = center_freq + this.demodulator.offset_frequency;
|
|
|
|
// ____
|
|
// Draws a standard filter envelope like this: _/ \_
|
|
// Parameters are given in offset frequency (Hz).
|
|
// Envelope is drawn on the scale canvas.
|
|
// A "drag range" object is returned, containing information about the draggable areas of the envelope
|
|
// (beginning, ending and the line showing the offset frequency).
|
|
var env_bounding_line_w = 5; //
|
|
var env_att_w = 5; // _______ ___env_h2 in px ___|_____
|
|
var env_h1 = 17; // _/| \_ ___env_h1 in px _/ |_ \_
|
|
var env_h2 = 5; // |||env_att_line_w |_env_lineplus
|
|
var env_lineplus = 1; // ||env_bounding_line_w
|
|
var env_line_click_area = 6;
|
|
//range=get_visible_freq_range();
|
|
var from = center_freq + this.demodulator.offset_frequency + this.demodulator.low_cut;
|
|
var from_px = scale_px_from_freq(from, range);
|
|
var to = center_freq + this.demodulator.offset_frequency + this.demodulator.high_cut;
|
|
var to_px = scale_px_from_freq(to, range);
|
|
if (to_px < from_px) /* swap'em */ {
|
|
var temp_px = to_px;
|
|
to_px = from_px;
|
|
from_px = temp_px;
|
|
}
|
|
|
|
from_px -= (env_att_w + env_bounding_line_w);
|
|
to_px += (env_att_w + env_bounding_line_w);
|
|
// do drawing:
|
|
scale_ctx.lineWidth = 3;
|
|
var color = this.color || '#ffff00'; // yellow
|
|
scale_ctx.strokeStyle = color;
|
|
scale_ctx.fillStyle = color;
|
|
var drag_ranges = {envelope_on_screen: false, line_on_screen: false};
|
|
if (!(to_px < 0 || from_px > window.innerWidth)) // out of screen?
|
|
{
|
|
drag_ranges.beginning = {x1: from_px, x2: from_px + env_bounding_line_w + env_att_w};
|
|
drag_ranges.ending = {x1: to_px - env_bounding_line_w - env_att_w, x2: to_px};
|
|
drag_ranges.whole_envelope = {x1: from_px, x2: to_px};
|
|
drag_ranges.envelope_on_screen = true;
|
|
scale_ctx.beginPath();
|
|
scale_ctx.moveTo(from_px, env_h1);
|
|
scale_ctx.lineTo(from_px + env_bounding_line_w, env_h1);
|
|
scale_ctx.lineTo(from_px + env_bounding_line_w + env_att_w, env_h2);
|
|
scale_ctx.lineTo(to_px - env_bounding_line_w - env_att_w, env_h2);
|
|
scale_ctx.lineTo(to_px - env_bounding_line_w, env_h1);
|
|
scale_ctx.lineTo(to_px, env_h1);
|
|
scale_ctx.globalAlpha = 0.3;
|
|
scale_ctx.fill();
|
|
scale_ctx.globalAlpha = 1;
|
|
scale_ctx.stroke();
|
|
}
|
|
if (typeof line !== "undefined") // out of screen?
|
|
{
|
|
var line_px = scale_px_from_freq(line, range);
|
|
if (!(line_px < 0 || line_px > window.innerWidth)) {
|
|
drag_ranges.line = {x1: line_px - env_line_click_area / 2, x2: line_px + env_line_click_area / 2};
|
|
drag_ranges.line_on_screen = true;
|
|
scale_ctx.moveTo(line_px, env_h1 + env_lineplus);
|
|
scale_ctx.lineTo(line_px, env_h2 - env_lineplus);
|
|
scale_ctx.stroke();
|
|
}
|
|
}
|
|
this.drag_ranges = drag_ranges;
|
|
};
|
|
|
|
Envelope.prototype.drag_start = function(x, key_modifiers){
|
|
this.key_modifiers = key_modifiers;
|
|
this.dragged_range = this.where_clicked(x, this.drag_ranges, key_modifiers);
|
|
this.drag_origin = {
|
|
x: x,
|
|
low_cut: this.demodulator.low_cut,
|
|
high_cut: this.demodulator.high_cut,
|
|
offset_frequency: this.demodulator.offset_frequency
|
|
};
|
|
return this.dragged_range !== Demodulator.draggable_ranges.none;
|
|
};
|
|
|
|
Envelope.prototype.where_clicked = function(x, drag_ranges, key_modifiers) { // Check exactly what the user has clicked based on ranges returned by envelope_draw().
|
|
var in_range = function (x, range) {
|
|
return range.x1 <= x && range.x2 >= x;
|
|
};
|
|
var dr = Demodulator.draggable_ranges;
|
|
|
|
if (key_modifiers.shiftKey) {
|
|
//Check first: shift + center drag emulates BFO knob
|
|
if (drag_ranges.line_on_screen && in_range(x, drag_ranges.line)) return dr.bfo;
|
|
//Check second: shift + envelope drag emulates PBF knob
|
|
if (drag_ranges.envelope_on_screen && in_range(x, drag_ranges.whole_envelope)) return dr.pbs;
|
|
}
|
|
if (drag_ranges.envelope_on_screen) {
|
|
// For low and high cut:
|
|
if (in_range(x, drag_ranges.beginning)) return dr.beginning;
|
|
if (in_range(x, drag_ranges.ending)) return dr.ending;
|
|
// Last priority: having clicked anything else on the envelope, without holding the shift key
|
|
if (in_range(x, drag_ranges.whole_envelope)) return dr.anything_else;
|
|
}
|
|
return dr.none; //User doesn't drag the envelope for this demodulator
|
|
};
|
|
|
|
|
|
Envelope.prototype.drag_move = function(x) {
|
|
var dr = Demodulator.draggable_ranges;
|
|
var new_value;
|
|
if (this.dragged_range === dr.none) return false; // we return if user is not dragging (us) at all
|
|
var freq_change = Math.round(this.visible_range.hps * (x - this.drag_origin.x));
|
|
|
|
//dragging the line in the middle of the filter envelope while holding Shift does emulate
|
|
//the BFO knob on radio equipment: moving offset frequency, while passband remains unchanged
|
|
//Filter passband moves in the opposite direction than dragged, hence the minus below.
|
|
var minus = (this.dragged_range === dr.bfo) ? -1 : 1;
|
|
//dragging any other parts of the filter envelope while holding Shift does emulate the PBS knob
|
|
//(PassBand Shift) on radio equipment: PBS does move the whole passband without moving the offset
|
|
//frequency.
|
|
if (this.dragged_range === dr.beginning || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) {
|
|
//we don't let low_cut go beyond its limits
|
|
if ((new_value = this.drag_origin.low_cut + minus * freq_change) < this.demodulator.filter.getLimits().low) return true;
|
|
//nor the filter passband be too small
|
|
if (this.demodulator.high_cut - new_value < this.demodulator.filter.min_passband) return true;
|
|
//sanity check to prevent GNU Radio "firdes check failed: fa <= fb"
|
|
if (new_value >= this.demodulator.high_cut) return true;
|
|
this.demodulator.setLowCut(new_value);
|
|
}
|
|
if (this.dragged_range === dr.ending || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) {
|
|
//we don't let high_cut go beyond its limits
|
|
if ((new_value = this.drag_origin.high_cut + minus * freq_change) > this.demodulator.filter.getLimits().high) return true;
|
|
//nor the filter passband be too small
|
|
if (new_value - this.demodulator.low_cut < this.demodulator.filter.min_passband) return true;
|
|
//sanity check to prevent GNU Radio "firdes check failed: fa <= fb"
|
|
if (new_value <= this.demodulator.low_cut) return true;
|
|
this.demodulator.setHighCut(new_value);
|
|
}
|
|
if (this.dragged_range === dr.anything_else || this.dragged_range === dr.bfo) {
|
|
//when any other part of the envelope is dragged, the offset frequency is changed (whole passband also moves with it)
|
|
new_value = this.drag_origin.offset_frequency + freq_change;
|
|
if (new_value > bandwidth / 2 || new_value < -bandwidth / 2) return true; //we don't allow tuning above Nyquist frequency :-)
|
|
this.demodulator.set_offset_frequency(new_value);
|
|
}
|
|
//now do the actual modifications:
|
|
//mkenvelopes(this.visible_range);
|
|
//this.demodulator.set();
|
|
return true;
|
|
};
|
|
|
|
Envelope.prototype.drag_end = function(){
|
|
var to_return = this.dragged_range !== Demodulator.draggable_ranges.none; //this part is required for cliking anywhere on the scale to set offset
|
|
this.dragged_range = Demodulator.draggable_ranges.none;
|
|
return to_return;
|
|
};
|
|
|
|
|
|
//******* class Demodulator_default_analog *******
|
|
// This can be used as a base for basic audio demodulators.
|
|
// It already supports most basic modulations used for ham radio and commercial services: AM/FM/LSB/USB
|
|
|
|
function Demodulator(offset_frequency, modulation) {
|
|
this.offset_frequency = offset_frequency;
|
|
this.envelope = new Envelope(this);
|
|
this.color = Demodulator.get_next_color();
|
|
this.modulation = modulation;
|
|
this.filter = new Filter(this);
|
|
this.squelch_level = -150;
|
|
this.dmr_filter = 3;
|
|
this.started = false;
|
|
this.state = {};
|
|
this.secondary_demod = false;
|
|
var mode = Modes.findByModulation(modulation);
|
|
if (mode) {
|
|
this.low_cut = mode.bandpass.low_cut;
|
|
this.high_cut = mode.bandpass.high_cut;
|
|
}
|
|
this.listeners = {
|
|
"frequencychange": [],
|
|
"squelchchange": []
|
|
};
|
|
}
|
|
|
|
//ranges on filter envelope that can be dragged:
|
|
Demodulator.draggable_ranges = {
|
|
none: 0,
|
|
beginning: 1 /*from*/,
|
|
ending: 2 /*to*/,
|
|
anything_else: 3,
|
|
bfo: 4 /*line (while holding shift)*/,
|
|
pbs: 5
|
|
}; //to which parameter these correspond in envelope_draw()
|
|
|
|
Demodulator.color_index = 0;
|
|
Demodulator.colors = ["#ffff00", "#00ff00", "#00ffff", "#058cff", "#ff9600", "#a1ff39", "#ff4e39", "#ff5dbd"];
|
|
|
|
Demodulator.get_next_color = function() {
|
|
if (this.color_index >= this.colors.length) this.color_index = 0;
|
|
return (this.colors[this.color_index++]);
|
|
}
|
|
|
|
|
|
|
|
Demodulator.prototype.on = function(event, handler) {
|
|
this.listeners[event].push(handler);
|
|
};
|
|
|
|
Demodulator.prototype.emit = function(event, params) {
|
|
this.listeners[event].forEach(function(fn) {
|
|
fn(params);
|
|
});
|
|
};
|
|
|
|
Demodulator.prototype.set_offset_frequency = function(to_what) {
|
|
if (to_what > bandwidth / 2 || to_what < -bandwidth / 2) return;
|
|
to_what = Math.round(to_what);
|
|
if (this.offset_frequency === to_what) {
|
|
return;
|
|
}
|
|
this.offset_frequency = to_what;
|
|
this.set();
|
|
this.emit("frequencychange", to_what);
|
|
mkenvelopes(get_visible_freq_range());
|
|
};
|
|
|
|
Demodulator.prototype.get_offset_frequency = function() {
|
|
return this.offset_frequency;
|
|
};
|
|
|
|
Demodulator.prototype.get_modulation = function() {
|
|
return this.modulation;
|
|
};
|
|
|
|
Demodulator.prototype.start = function() {
|
|
this.started = true;
|
|
this.set();
|
|
ws.send(JSON.stringify({
|
|
"type": "dspcontrol",
|
|
"action": "start"
|
|
}));
|
|
};
|
|
|
|
// TODO check if this is actually used
|
|
Demodulator.prototype.stop = function() {
|
|
};
|
|
|
|
Demodulator.prototype.send = function(params) {
|
|
ws.send(JSON.stringify({"type": "dspcontrol", "params": params}));
|
|
}
|
|
|
|
Demodulator.prototype.set = function () { //this function sends demodulator parameters to the server
|
|
if (!this.started) return;
|
|
var params = {
|
|
"low_cut": this.low_cut,
|
|
"high_cut": this.high_cut,
|
|
"offset_freq": this.offset_frequency,
|
|
"mod": this.modulation,
|
|
"dmr_filter": this.dmr_filter,
|
|
"squelch_level": this.squelch_level,
|
|
"secondary_mod": this.secondary_demod,
|
|
"secondary_offset_freq": this.secondary_offset_freq
|
|
};
|
|
var to_send = {};
|
|
for (var key in params) {
|
|
if (!(key in this.state) || params[key] !== this.state[key]) {
|
|
to_send[key] = params[key];
|
|
}
|
|
}
|
|
if (Object.keys(to_send).length > 0) {
|
|
this.send(to_send);
|
|
for (var key in to_send) {
|
|
this.state[key] = to_send[key];
|
|
}
|
|
}
|
|
mkenvelopes(get_visible_freq_range());
|
|
};
|
|
|
|
Demodulator.prototype.setSquelch = function(squelch) {
|
|
if (this.squelch_level == squelch) {
|
|
return;
|
|
}
|
|
this.squelch_level = squelch;
|
|
this.set();
|
|
this.emit("squelchchange", squelch);
|
|
};
|
|
|
|
Demodulator.prototype.getSquelch = function() {
|
|
return this.squelch_level;
|
|
};
|
|
|
|
Demodulator.prototype.setDmrFilter = function(dmr_filter) {
|
|
this.dmr_filter = dmr_filter;
|
|
this.set();
|
|
};
|
|
|
|
Demodulator.prototype.setBandpass = function(bandpass) {
|
|
this.bandpass = bandpass;
|
|
this.low_cut = bandpass.low_cut;
|
|
this.high_cut = bandpass.high_cut;
|
|
this.set();
|
|
};
|
|
|
|
Demodulator.prototype.setLowCut = function(low_cut) {
|
|
this.low_cut = low_cut;
|
|
this.set();
|
|
};
|
|
|
|
Demodulator.prototype.setHighCut = function(high_cut) {
|
|
this.high_cut = high_cut;
|
|
this.set();
|
|
};
|
|
|
|
Demodulator.prototype.getBandpass = function() {
|
|
return {
|
|
low_cut: this.low_cut,
|
|
high_cut: this.high_cut
|
|
};
|
|
};
|
|
|
|
Demodulator.prototype.set_secondary_demod = function(secondary_demod) {
|
|
if (this.secondary_demod === secondary_demod) {
|
|
return;
|
|
}
|
|
this.secondary_demod = secondary_demod;
|
|
this.set();
|
|
};
|
|
|
|
Demodulator.prototype.get_secondary_demod = function() {
|
|
return this.secondary_demod;
|
|
};
|
|
|
|
Demodulator.prototype.set_secondary_offset_freq = function(secondary_offset) {
|
|
if (this.secondary_offset_freq === secondary_offset) {
|
|
return;
|
|
}
|
|
this.secondary_offset_freq = secondary_offset;
|
|
this.set();
|
|
};
|