/**
* This file is part of HARMONICARIUM, a web app which allows users to play
* the Harmonic Series dynamically by changing its fundamental tone in real-time.
* It is available in its latest version from:
* https://github.com/IndustrieCreative/Harmonicarium
*
* @license
* Copyright (C) 2017-2020 by Walter Mantovani (http://armonici.it).
* Written by Walter Mantovani.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* globals HUM */
"use strict";
/**
* The Dynamic Harmonics Calculator class<br>
* This is the computational kernel for the frequency/midicent tables.
* Manage an route the communications from and to the other App components.
*/
HUM.DHC = class {
/**
* @param {string} id - The DHC id, in format harmonicariumID-dhcID (eg. '1-0')
* @param {HUM} harmonicarium - The HUM instance to which the this DHC must refer
*/
constructor(id, harmonicarium) {
/**
* The HUM instance
*
* @member {HUM}
*/
this.harmonicarium = harmonicarium;
/**
* The id of this DHC instance
*
* @member {string}
*/
this.id = id;
/**
* DHC Settings
*
* @member {HUM.DHC#DHCsettings}
*
*/
this.settings = new this.DHCsettings();
/**
* DHC Tables
*
* @member {Object}
*
* @property {CtrlKeymap} ctrl - The current Controller Keymap
* @property {Object.<xtnum, HUM.DHC#Xtone>} ft - The current Fundamental Tones table
* @property {Object.<xtnum, HUM.DHC#Xtone>} ht - The current Harmonic/Subharmonic Tones table
* @property {Object} reverse - Reverse tables namespace
* @property {Object.<midinnum, xtnum>} reverse.ft - Reverse Fundamental Tones table
* @property {Object.<midinnum, xtnum>} reverse.ht - Reverse Harmonic/Subharmonic Tones table
*/
this.tables = {
ctrl: {},
ft: {},
ht: {},
reverse: {
ft: {},
ht: {},
},
};
/**
* UI HTML elements
*
* @member {Object}
*
* @property {Object.<string, HTMLElement>} fn - Functional UI elements
* @property {Object.<string, HTMLElement>} in - Input UI elements
* @property {Object.<string, HTMLElement>} out - Output UI elements
*/
this.uiElements = {
fn: {
controllerKeymapClose: document.getElementById("HTMLf_controllerKeymapClose"+id),
controllerKeymapTableShow: document.getElementById("HTMLf_controllerKeymapTableShow"+id),
controllerKeymapModal: document.getElementById('HTMLf_controllerKeymapModal'+id),
ftHS: document.getElementById("HTMLf_ftHS"+id),
ftNEDX: document.getElementById("HTMLf_ftNEDX"+id),
ftSys_HSnat: document.getElementById("HTMLf_ftSys_HSnat"+id),
ftSys_HStrans: document.getElementById("HTMLf_ftSys_HStrans"+id),
ftSys_NEDX: document.getElementById("HTMLf_ftSys_NEDX"+id),
keymapTableShow: document.getElementById("HTMLf_keymapTableShow"+id),
motPanelClose: document.getElementById("HTMLf_motPanelClose"+id),
// checkboxMidi: this.harmonicarium.html.midiTab[id].children[0],
// checkboxDHC: this.harmonicarium.html.dhcTab[id].children[0],
// checkboxFM: this.harmonicarium.html.fmTab[id].children[0],
// checkboxFT: this.harmonicarium.html.ftTab[id].children[0],
// checkboxHT: this.harmonicarium.html.htTab[id].children[0],
},
in: {
controllerKeymapFile: document.getElementById('HTMLi_controllerKeymapFile'+id),
controllerKeymapPresets: document.getElementById('HTMLi_controllerKeymapPresets'+id),
dhc_hzAccuracy: document.getElementById("HTMLi_dhc_hzAccuracy"+id),
dhc_mcAccuracy: document.getElementById("HTMLi_dhc_mcAccuracy"+id),
dhc_middleC: document.getElementById("HTMLi_dhc_middleC"+id),
dhc_piperSteps: document.getElementById("HTMLi_dhc_piperSteps"+id),
dhc_pitchbendRange: document.getElementById("HTMLi_dhc_pitchbendRange"+id),
fm_hz: document.getElementById("HTMLi_fm_hz"+id),
fm_mc: document.getElementById("HTMLi_fm_mc"+id),
ftHStranspose_h_minus: document.getElementById("HTMLi_ftHStranspose_h_minus"+id),
ftHStranspose_h_plus: document.getElementById("HTMLi_ftHStranspose_h_plus"+id),
ftHStranspose_s_minus: document.getElementById("HTMLi_ftHStranspose_s_minus"+id),
ftHStranspose_s_plus: document.getElementById("HTMLi_ftHStranspose_s_plus"+id),
ftNEDX_division: document.getElementById("HTMLi_ftNEDX_division"+id),
ftNEDX_ok: document.getElementById("HTMLi_ftNEDX_ok"+id),
ftNEDX_unit: document.getElementById("HTMLi_ftNEDX_unit"+id),
htTranspose_h_minus: document.getElementById("HTMLi_htTranspose_h_minus"+id),
htTranspose_h_plus: document.getElementById("HTMLi_htTranspose_h_plus"+id),
htTranspose_h_ratio: document.getElementById("HTMLi_htTranspose_h_ratio"+id),
htTranspose_s_minus: document.getElementById("HTMLi_htTranspose_s_minus"+id),
htTranspose_s_plus: document.getElementById("HTMLi_htTranspose_s_plus"+id),
htTranspose_s_ratio: document.getElementById("HTMLi_htTranspose_s_ratio"+id),
},
out: {
controllerKeymapTable: document.getElementById("HTMLo_controllerKeymapTable"+id),
fm_hz: document.getElementById("HTMLo_fm_hz"+id),
fm_mc: document.getElementById("HTMLo_fm_mc"+id),
ftHStranspose_h_ratio: document.getElementById("HTMLo_ftHStranspose_h_ratio"+id),
ftHStranspose_s_ratio: document.getElementById("HTMLo_ftHStranspose_s_ratio"+id),
toneMonitorFT_frequency: document.getElementById("HTMLo_toneMonitorFT_frequency"+id),
toneMonitorFT_midicents: document.getElementById("HTMLo_toneMonitorFT_midicents"+id),
toneMonitorFT_notename: document.getElementById("HTMLo_toneMonitorFT_notename"+id),
toneMonitorFT_tone: document.getElementById("HTMLo_toneMonitorFT_tone"+id),
toneMonitorHT_frequency: document.getElementById("HTMLo_toneMonitorHT_frequency"+id),
toneMonitorHT_midicents: document.getElementById("HTMLo_toneMonitorHT_midicents"+id),
toneMonitorHT_notename: document.getElementById("HTMLo_toneMonitorHT_notename"+id),
toneMonitorHT_tone: document.getElementById("HTMLo_toneMonitorHT_tone"+id),
}
};
/**
* Registered Apps<br>
* The <em>key</em> of each record is an app Object and the <em>value</em> is the metod that must be invoked
* to send messages towards the app.
*
* @member {Map.<Object, string>}
*/
this.registeredApps = new Map();
// @todo: Pass by constructor parameters
// and if false, no signals will be sent to the component
/**
* The Hstack instance
*
* @member {HUM.Hstack}
*/
this.hstack = new HUM.Hstack(this);
/**
* The Synth instance
*
* @member {HUM.Synth}
*/
this.synth = {}; // see this.init()
/**
* The MidiHub instance
*
* @member {HUM.midi.MidiHub}
*/
this.midi = {}; // see this.init()
/**
* The Hstack instance
*
* @member {HUM.Hancock}
*/
this.hancock = {}; // see this.init()
/**
* Piper's default settings
*
* @member {Object}
*
* @property {number} maxLenght - How many steps has the Pipe
* @property {Array.<HUM.DHCmsg>} queue - Last HT MIDI Note-ON messages received
* @property {Array.<HUM.DHCmsg>} pipe - MIDI Note-ON messages stored into the Pipe
* @property {number} currStep - Last step played by the Piper
* @property {HUM.DHCmsg} currTone - Last fake MIDI Note-ON message send
*/
this.pipe = {
maxLenght: 5,
queue: [],
pipe: [],
currStep: 5,
currTone: null
};
/**
* Queues for FT/HT playing and muting management
*
* @member {Object}
*
* @property {Array.<HUM.DHCmsg>} ft - Queue for FT key-press tracking
* @property {Array.<HUM.DHCmsg>} ht - Queue for HT key-press tracking
*/
this.playQueue = {
ft: [],
ht: [],
};
/**
* The container for all the Controller keymap presets
*
* @member {HUM.CtrlKeymapPreset}
*/
this.ctrlKeymapPreset = new HUM.CtrlKeymapPreset(this);
/**
* The Backend Utils instance
*
* @member {BackendUtils}
*/
this.backendUtils = this.harmonicarium.components.backendUtils;
this.backendUtils.uiElements.fn.settings[this.id] = document.getElementById("HTMLf_dhc_container"+this.id);
this._init();
// =======================
} // end class Constructor
// ===========================
/**
* Initialize the new instance of DHC
*/
_init() {
this._initUI();
this.settings.fm.hz = this.getFM(this.settings.fm.init);
this.synth = new HUM.Synth(this);
this.hancock = new HUM.Hancock(this);
this.switchFTsys(this.settings.ft.selected, false, true); // initTables() + updateKeymapPreset()
this.midi = new HUM.midi.MidiHub(this);
}
/**
* Register a new App (module)
*
* @param {Object} app - The instance of the app to be registered
* @param {string} method - The name of the method to use to send messages
* @param {number} priority - The priority with which the registered app will receive messages
*/
registerApp(app, method, priority) {
let rA = this.registeredApps;
rA.set(app, {method: method, priority: priority});
// Re-arrage the registered apps by priority
this.registeredApps = new Map([...rA.entries()].sort((a, b) => a[1].priority-b[1].priority));
}
/**
* Send a DHCmsg message to all the registered apps
*
* @param {HUM.DHCmsg} dhcMsg - The message to send
*/
sendMessageToApps(dhcMsg) {
for (const [app, handler] of this.registeredApps) {
app[handler.method](dhcMsg);
}
}
/*==============================================================================*
* DHC TABLES CREATION
*==============================================================================*/
/**
* Recompile FT & HT tables in the right order
*/
// @old icTablesCreate
initTables() {
// Create the FT tables
this.createFTtable();
// Create the HT tables on the current FT
this.createHTtable(this.tables.ft[this.settings.ht.curr_ft].hz);
// Update the UI Monitors
this.initUImonitors();
}
/**
* Recompile the Fundamental Tones (FT) table
*/
// @old icFTtableCreate
createFTtable() {
// Temp object
let fundamentalsTable = {},
fundamentalsReverseTable = {};
// Select current FT Tuning Systems
switch (this.settings.ft.selected) {
// n-EDx EQUAL TEMPERAMENT
case "nEDx":
for (let i = -this.settings.ft.steps; i <= this.settings.ft.steps; i++) {
let freq = this.constructor.compute_nEDx(i, this.settings.ft.nEDx.unit, this.settings.ft.nEDx.division, this.settings.fm.hz);
let midicents = this.constructor.freqToMc(freq);
let ok_rev = false;
fundamentalsTable[i] = new this.Xtone(freq, midicents);
// Insert in the reverse table only if present on the keymap
for (let key of Object.keys(this.tables.ctrl)) {
if (this.tables.ctrl[key].ft === i) {
ok_rev = true;
}
}
if (ok_rev === true) {
fundamentalsReverseTable[midicents] = i;
}
}
break;
// HARMONICS / SUBHARMONICS FT
case "h_s":
if (this.settings.ft.h_s.selected === "natural") {
// Compute the sub/harmonics naturally (NOT transposed to the Same Octave)
for (let i = 1; i <= this.settings.ft.steps; i++) {
let sFreq = this.settings.fm.hz / i * this.settings.ft.h_s.natural.s_tr;
let hFreq = this.settings.fm.hz * i * this.settings.ft.h_s.natural.h_tr;
let sMidicents = this.constructor.freqToMc(sFreq);
let hMidicents = this.constructor.freqToMc(hFreq);
let ok_rev_h = false;
let ok_rev_s = false;
fundamentalsTable[-i] = new this.Xtone(sFreq, sMidicents);
fundamentalsTable[i] = new this.Xtone(hFreq, hMidicents);
// Insert in the reverse table only if present on the keymap
for (let key of Object.keys(this.tables.ctrl)) {
if (this.tables.ctrl[key].ft === i) {
ok_rev_h = true;
}
if (this.tables.ctrl[key].ft === -i) {
ok_rev_s = true;
}
}
if (ok_rev_h === true) {
fundamentalsReverseTable[hMidicents] = i;
}
if (ok_rev_s === true) {
fundamentalsReverseTable[sMidicents] = -i;
}
}
// FT0 is always the FM
fundamentalsTable[0] = new this.Xtone(this.settings.fm.hz, this.settings.fm.mc);
fundamentalsReverseTable[Number(this.settings.fm.mc)] = 0;
}
if (this.settings.ft.h_s.selected === "sameOctave") {
// Compute the sub/harmonics all transposed to the Same Octave
for (let i = 1; i <= this.settings.ft.steps; i++) {
let h_so_tr = null;
let s_so_tr = null;
if (i <= 2) {
h_so_tr = 1;
s_so_tr = 1;
} else if (i < 4) {
h_so_tr = i / 2;
s_so_tr = 1 / i * 2;
} else if (i < 8) {
h_so_tr = i / 4;
s_so_tr = 1 / i * 4;
} else if (i < 16) {
h_so_tr = i / 8;
s_so_tr = 1 / i * 8;
} else if (i < 32) {
h_so_tr = i / 16;
s_so_tr = 1 / i * 16;
} else if (i <= 64) {
h_so_tr = i / 32;
s_so_tr = 1 / i * 32;
}
let hFreq = this.settings.fm.hz * h_so_tr * this.settings.ft.h_s.sameOctave.h_tr;
let sFreq = this.settings.fm.hz * s_so_tr * this.settings.ft.h_s.sameOctave.s_tr;
let sMidicents = this.constructor.freqToMc(sFreq);
let hMidicents = this.constructor.freqToMc(hFreq);
let ok_rev_h = false;
let ok_rev_s = false;
fundamentalsTable[-i] = new this.Xtone(sFreq, sMidicents);
fundamentalsTable[i] = new this.Xtone(hFreq, hMidicents);
// Insert in the reverse table only if present on the keymap
for (let key of Object.keys(this.tables.ctrl)) {
if (this.tables.ctrl[key].ft === i) {
ok_rev_h = true;
}
if (this.tables.ctrl[key].ft === -i) {
ok_rev_s = true;
}
}
if (ok_rev_h === true) {
fundamentalsReverseTable[hMidicents] = i;
}
if (ok_rev_s === true){
fundamentalsReverseTable[sMidicents] = -i;
}
}
// FT0 is always the FM
fundamentalsTable[0] = new this.Xtone(this.settings.fm.hz, this.settings.fm.mc);
fundamentalsReverseTable[Number(this.settings.fm.mc)] = 0;
}
break;
// @todo - TUNING FILES FT
case "file":
break;
}
this.tables.ft = fundamentalsTable;
this.tables.reverse.ft = fundamentalsReverseTable;
this.sendMessageToApps(HUM.DHCmsg.ftUpd('dhc'));
}
/**
* Recompile the Harmonic/Subarmonic Tones (HT) table
*
* @param {hertz} fundamental - The tone on which to build the table, expressed in hertz (Hz)
*/
// @old icHTtableCreate
createHTtable(fundamental) {
// @todo - Implement custom H/S table length (16>32>64>128) to increase performances if needed
let harmonicsTable = {},
harmonicsReverseTable = {};
for (let i = -128; i < 0; i++) {
let freq = fundamental / -i * this.settings.ht.transpose.s;
let midicents = this.constructor.freqToMc(freq);
harmonicsTable[i] = new this.Xtone(freq, midicents);
harmonicsReverseTable[midicents] = i;
}
for (let i = 1; i < 129; i++) {
let freq = fundamental * i * this.settings.ht.transpose.h;
let midicents = this.constructor.freqToMc(freq);
harmonicsTable[i] = new this.Xtone(freq, midicents);
harmonicsReverseTable[midicents] = i;
}
this.tables.ht = harmonicsTable;
this.tables.reverse.ht = harmonicsReverseTable;
this.sendMessageToApps(HUM.DHCmsg.htUpd('dhc'));
}
/*==============================================================================*
* FM UI tools
*==============================================================================*/
/**
* Get the Fundamental Mother (FM) from the UI input
*
* @param {('mc'|'hz')} method - Method to use to get the FM
*
* @return {hertz} - Frequency expressed in hertz (Hz)
*/
// @old icGetFM
getFM(method) {
let freq = null,
midicents = null;
switch (method) {
case "mc":
let fmMC = this.uiElements.in.fm_mc.value;
midicents = fmMC;
freq = this.constructor.mcToFreq(fmMC);
// Store the midi.cents value
this.settings.fm.mc = fmMC;
break;
case "hz":
if (this.uiElements.in.fm_hz.value <= 0) {
this.uiElements.in.fm_hz.value = 1;
}
let fmHZ = this.uiElements.in.fm_hz.value;
midicents = this.constructor.freqToMc(fmHZ);
// Store the midi.cents value
this.settings.fm.mc = midicents;
freq = fmHZ;
break;
}
// Change the 'init' for eventual icDHCinit
this.settings.fm.init = method;
// Return the frequency on the FM (Hz)
return freq;
}
/**
* Set the Fundamental Mother (FM) and re-init the tone tables
*
* @param {hertz} hz - Frequency expressed in hertz (Hz)
*/
// @old icSetFM
setFM(hz) {
// Store the FM to the global variable
this.settings.fm.hz = hz;
// Recreate all tables
this.initTables();
}
/**
* Print the Fundamental Mother (FM) data to the UI output
*
* @param {hertz} hz - Frequency expressed in hertz (Hz)
* @param {midicent} mc - MIDI note number expressed in midicent
*/
// @old icPrintFundamentalMother
printFundamentalMother(hz, mc){
let xtObj = new this.Xtone(hz, mc);
// Apply the controller pitchbend (if present) to the xtObj
let bent_xtObj = this.bendXtone(xtObj);
// Print the FM infos on the UI
let notename = this.mcToName(bent_xtObj.mc),
name = notename[0],
sign = notename[1],
cent = notename[2];
this.uiElements.out.fm_mc.innerText = bent_xtObj.mc.toFixed(this.settings.global.cent_accuracy + 2) + " = " + name + " " + sign + cent + "\u00A2";
this.uiElements.out.fm_hz.innerText = bent_xtObj.hz.toFixed(this.settings.global.hz_accuracy);
}
/*==============================================================================*
* FT UI tools
*==============================================================================*/
/**
* Switch the FT TUNING SYSTEM (called when UI is updated)
*
* @param {('nedx'|'hs')} sys - FTs tuning method; 'nedx' (equal temperament) or 'hs' (harm/subharm)
* @param {('natural'|'sameOctave')=} sys_hs - FTs Harm/Subharm tuning method; 'natural' (no transposition) or 'sameOctave' (to the same octave)
* @param {boolean} init - If the method has been called by the ._init() method
*/
// @old icSwitchFTsys
switchFTsys(sys, sys_hs=false, init=false) {
if (sys === "nEDx") {
if (init) {
this.uiElements.fn.ftSys_NEDX.checked = true;
}
this.uiElements.fn.ftNEDX.style.display = "initial";
this.uiElements.fn.ftHS.style.display = "none";
this.settings.ft.selected = "nEDx";
} else if (sys === "h_s") {
this.uiElements.fn.ftNEDX.style.display = "none";
this.uiElements.fn.ftHS.style.display = "initial";
this.settings.ft.selected = "h_s";
if (sys_hs === "natural") {
if (init) {
this.uiElements.fn.ftSys_HSnat.checked = true;
}
this.settings.ft.h_s.selected = "natural";
this.uiElements.out.ftHStranspose_h_ratio.innerText = this.settings.ft.h_s.natural.h_tr;
this.uiElements.out.ftHStranspose_s_ratio.innerText = this.settings.ft.h_s.natural.s_tr;
} else if (sys_hs === "sameOctave") {
if (init) {
this.uiElements.fn.ftSys_HStrans.checked = true;
}
this.settings.ft.h_s.selected = "sameOctave";
this.uiElements.out.ftHStranspose_h_ratio.innerText = this.settings.ft.h_s.sameOctave.h_tr;
this.uiElements.out.ftHStranspose_s_ratio.innerText = this.settings.ft.h_s.sameOctave.s_tr;
}
}
this.updateKeymapPreset();
}
/**
* Set the nEDx (called when UI is updated)
*/
// @old icSetNEDX
setNEDX() {
if (this.uiElements.in.ftNEDX_unit.value < 1) {
this.uiElements.in.ftNEDX_unit.value = 1;
}
if (this.uiElements.in.ftNEDX_division.value < 1) {
this.uiElements.in.ftNEDX_division.value = 1;
}
this.settings.ft.nEDx.unit = Number(this.uiElements.in.ftNEDX_unit.value);
this.settings.ft.nEDx.division = Number(this.uiElements.in.ftNEDX_division.value);
// Recreate all tables
this.initTables();
}
/**
* Transpose FT under harmonics/subharmonics Tuning System (called when UI is updated)
*
* @param {tratio} ratio - The ratio for the transposition
* @param {tonetype} type - The tone type
*/
// @old icFThsTranspose
transposeFThs(ratio, type) {
if (this.settings.ft.h_s.selected === "natural") {
this.settings.ft.h_s.natural[type+"_tr"] *= ratio;
this.uiElements.out[`ftHStranspose_${type}_ratio`].innerText = this.settings.ft.h_s.natural[type+"_tr"];
} else if (this.settings.ft.h_s.selected === "sameOctave") {
this.settings.ft.h_s.sameOctave[type+"_tr"] *= ratio;
this.uiElements.out[`ftHStranspose_${type}_ratio`].innerText = this.settings.ft.h_s.sameOctave[type+"_tr"];
}
this.initTables();
}
/*==============================================================================*
* HT UI tools
*==============================================================================*/
/**
* Transpose HT (sub)harmonics (called when UI is updated)
*
* @param {tratio} ratio - The ratio with which to compute the transposition
* @param {('h'|'s')} type - Type of transposition; 'h' for harmonics or 's' for subharmonics
* @param {boolean} octave - If it's an octave transposition or not.<br>
* If it's true, the 'ratio' should be 2 (for octave up) or 0.5 (for octave down).
*/
// @old icHTtranspose
transposeHT(ratio, type, octave) {
// If it's an octave transpose
if (octave === true) {
// Multiply the current transpose ratio by the input ratio
this.settings.ht.transpose[type] *= ratio;
// Update the new computed transpose ratio to the UI
this.uiElements.in[`htTranspose_${type}_ratio`].value = this.settings.ht.transpose[type];
} else if (octave === false) {
// Check if the ratio is > 0
if (ratio > 0) {
// Set the new transpose ratio
this.settings.ht.transpose[type] = ratio;
// If it's not, restore the last valid ratio
} else {
this.uiElements.in[`htTranspose_${type}_ratio`].value = this.settings.ht.transpose[type];
// Stop executing the rest of the function
return;
}
}
// Recreate the HT table on the last FT
this.createHTtable(this.tables.ft[this.settings.ht.curr_ft].hz);
}
/*==============================================================================*
* KEYMAP HANDLING METHODS
*==============================================================================*/
/**
* Update the preset list according to the selected FTs Tuning System
*/
// @old icUpdateKeymapPreset
updateKeymapPreset() {
let htmlElem = this.uiElements.in.controllerKeymapPresets;
let lastValue = this.ctrlKeymapPreset.current[this.settings.ft.selected];
let changeEvent = {target: {value: lastValue}};
let keymaps = Object.keys(this.ctrlKeymapPreset[this.settings.ft.selected]);
let optionFile = document.createElement("option");
htmlElem.innerHTML = "";
for (let key of keymaps) {
let option = document.createElement("option");
option.value = key;
option.text = this.ctrlKeymapPreset[this.settings.ft.selected][key].notes;
htmlElem.add(option);
}
optionFile.text = "Load from file...";
optionFile.value = 99;
htmlElem.add(optionFile);
htmlElem.value = lastValue;
this.loadKeymapPreset(changeEvent);
}
/**
* Load a Controller keymap from 'ctrlKeymapPreset' according to the selection on UI
*
* @param {Event} changeEvent - Change HTML event on 'select' element (ctrl keymap dropdown)
*/
// @old icLoadKeymapPreset
loadKeymapPreset(changeEvent) {
let indexValue = changeEvent.target.value;
if (indexValue != 99) {
let keymap = this.ctrlKeymapPreset[this.settings.ft.selected][indexValue].map;
// Store the current Keymap <option> value in a global slot
this.ctrlKeymapPreset.current[this.settings.ft.selected] = indexValue;
// Write the Controller Keymap into the global object
this.tables.ctrl = keymap;
this.initPipeQueue('h');
// re-Create FT & HT tables (necessary for reverse-tables on tsnap midi receivig mode)
this.initTables();
this.sendMessageToApps(HUM.DHCmsg.ctrlmapUpd('dhc'));
this.uiElements.in.controllerKeymapFile.style.visibility = "hidden";
} else {
this.uiElements.in.controllerKeymapFile.style.visibility = "initial";
}
}
/**
* On loading the Controller Keymap file
*
* @param {Event} changeEvent - HTML change event on 'input' element (ctrl keymap file uploader)
*/
// @old icHandleKeymapFile
handleKeymapFile(changeEvent) {
// Check for the various File API support.
if (window.File && window.FileReader && window.FileList && window.Blob) {
// Great success! All the File APIs are supported.
// Access to the file and send it to read function
this.readKeymapFile(changeEvent.target.files[0]);
} else {
alert('The File APIs are not fully supported in this browser.');
}
}
/**
* Initialize the reading process of the Controller Keymap file
*
* @param {File} file - The file to be read
*/
// @old icReadKeymapFile
readKeymapFile(file) {
let reader = new FileReader();
// Handle loading errors
reader.onerror = this.backendUtils.fileErrorHandler;
if (file) {
// Read file into memory as UTF-8 text
reader.readAsText(file);
// Launch the data processing as soon as the file has been loaded
reader.onload = function(event){
this.processKeymapData(event.target.result, file.name);
};
}
}
/**
* Build the Controller Keymap table on the incoming raw data from .hcmap file
*
* @param {string} data - The text content of the Controller keymap file
* @param {string} name - The filename
*/
// @old icProcessKeymapData
processKeymapData(data, name) {
// Get the key for the new slot
let optionValue = this.uiElements.in.controllerKeymapPresets.length;
// Split by lines
let allTextLines = data.split(/\r\n|\n/);
let lines = {};
// For every line
for (let i = 0; i < allTextLines.length; i++) {
// Split the line by spaces or tabs
let elements = allTextLines[i].split(/\s\s*/);
lines[parseInt(elements[0])] = { ft: parseInt(elements[1]), ht: parseInt(elements[2]) };
}
// Write the Controller Keymap into a new slot in this.ctrlKeymapPreset
this.ctrlKeymapPreset[this.settings.ft.selected][optionValue] = {};
this.ctrlKeymapPreset[this.settings.ft.selected][optionValue].map = lines;
this.ctrlKeymapPreset[this.settings.ft.selected][optionValue].name = name;
this.ctrlKeymapPreset[this.settings.ft.selected][optionValue].notes = "FILE: " + name;
this.ctrlKeymapPreset.current[this.settings.ft.selected] = optionValue;
// Update the dropdown (and the UI monitors)
this.updateKeymapPreset();
}
/**
* Create an HTML table from the controller keymap and write it to the UI under a modal element
*/
// @old icCtrlKeymap2HTML
keymap2Html() {
let txt = "";
let map = this.tables.ctrl;
txt += '<table class="dataTable"><tr><th>MIDI #</th><th>FT</th><th>HT</th></tr>';
for (let key of Object.keys(map)) {
let ft, ht = "";
if (map[key].ft === 129) {
ft = "N/A";
} else {
ft = map[key].ft;
}
if (map[key].ht === 129) {
ht = "N/A";
} else {
ht = map[key].ht;
}
txt += "<tr><td>" + key + "</td><td>" + ft + "</td><td>" + ht + "</td></tr>";
}
txt += "<tr><th>MIDI #</th><th>FT</th><th>HT</th></tr></table>";
this.uiElements.out.controllerKeymapTable.innerHTML = txt;
// Get the modal element
let modal = this.uiElements.fn.controllerKeymapModal;
// Get the <span> element that closes the modal element
let close = this.uiElements.fn.controllerKeymapClose;
// When the user clicks the button, open the modal element
modal.style.display = "block";
// When the user clicks on <span> (x), close the modal element
close.onclick = function() {
modal.style.display = "none";
};
// window.addEventListener("click", function(event) {
// if (event.target == modal) {
// modal.style.display = "none";
// }
// });
// When the user clicks anywhere outside of the modal element, close it
window.onclick = function(event) {
if (event.target == modal) {
modal.style.display = "none";
}
};
}
/*==============================================================================*
* DHC PLAYING METHODS
*==============================================================================*/
/**
* Play a Fundamental Tone
*
* @param {HUM.DHCmsg} dhcMsg - The message containing the FT to be played
*/
playFT(dhcMsg) {
// Recalculate the ht table passing the frequency (Hz)
this.createHTtable(this.tables.ft[dhcMsg.xtNum].hz);
// - - - - - - - - - - - - - -
// ANTI NOTES STUCKING
if (dhcMsg.ctrlNum === false) {
// Search by xtNum
let remPos = false;
// If there is ONE FT is already pressed, stop it before playing it again
this.playQueue.ft.forEach( (queueTone, position) => {
// If the key is already pressed
if (queueTone.xtNum === dhcMsg.xtNum) {
this.sendMessageToApps(HUM.DHCmsg.ftOFF(queueTone.source, queueTone.xtNum, queueTone.velocity, queueTone.ctrlNum));
// this.playQueue.ft.splice(position, 1);
remPos = position;
}
});
if (remPos !== false) {
this.playQueue.ft.splice(remPos, 1);
}
} else {
// Search by ctrlNum
let remPos = false;
// If there is ONE FT is already pressed, stop it before playing it again
this.playQueue.ft.forEach( (queueTone, position) => {
// If the key is already pressed
if (queueTone.ctrlNum === dhcMsg.ctrlNum) {
this.sendMessageToApps(HUM.DHCmsg.ftOFF(queueTone.source, queueTone.xtNum, queueTone.velocity, queueTone.ctrlNum));
// this.playQueue.ft.splice(position, 1);
remPos = position;
}
});
if (remPos !== false) {
this.playQueue.ft.splice(remPos, 1);
}
}
// - - - - - - - - - - - - - -
this.playQueue.ft.push(dhcMsg);
// Store the current FT into the global slot for future HT table re-computations and UI monitor updates
this.settings.ht.curr_ft = dhcMsg.xtNum;
this.sendMessageToApps(dhcMsg);
// Update the UI
this.dhcMonitor("ft", dhcMsg.xtNum);
}
/**
* Stop playing a Fundamental Tone
*
* @param {HUM.DHCmsg} dhcMsg - The message containing the FT to be muted
*/
muteFT(dhcMsg) {
// Search the FT number in the playQueue array
let position = this.playQueue.ft.findIndex(qt => qt.xtNum === dhcMsg.xtNum);
// If the FTn exist
if (position > -1) {
// Remove the FTn from the playQueue array
this.playQueue.ft.splice(position, 1);
// If the FTn does not exist
} else {
if (dhcMsg.panic === false) {
console.log("STRANGE: there is NOT a FT pressed key #:", dhcMsg.xtNum);
}
}
this.sendMessageToApps(dhcMsg);
// If there are other notes, read and play the next note on the playQueue array
if (this.playQueue.ft.length > 0) {
// Read the next FT
let nextIndex = this.playQueue.ft.length - 1;
let nextTone = this.playQueue.ft[nextIndex];
// If the next tone is NOT the active one
if (nextTone.xtNum !== this.settings.ht.curr_ft) {
// Recalculate the ht table passing the frequency (Hz)
this.createHTtable(this.tables.ft[nextTone.xtNum].hz);
// Store the current FT into the global slot for future HT table re-computations and UI monitor updates
this.settings.ht.curr_ft = nextTone.xtNum;
this.sendMessageToApps(nextTone);
// Update the UI
this.dhcMonitor("ft", nextTone.xtNum);
}
}
}
/**
* Play a Harmonic/Subharmonic Tone
*
* @param {HUM.DHCmsg} dhcMsg - The message containing the HT to be played
*/
playHT(dhcMsg) {
// - - - - - - - - - - - - - -
// ANTI NOTES STUCKING
if (dhcMsg.ctrlNum === false) {
// Search by xtNum
let remPos = false;
// If there is ONE HT is already pressed, stop it before playing it again
this.playQueue.ht.forEach( (queueTone, position) => {
// If the key is already pressed
if (queueTone.xtNum === dhcMsg.xtNum) {
this.sendMessageToApps(HUM.DHCmsg.htOFF(queueTone.source, queueTone.xtNum, queueTone.velocity, queueTone.ctrlNum));
// this.playQueue.ht.splice(position, 1);
remPos = position;
}
});
if (remPos !== false) {
this.playQueue.ht.splice(remPos, 1);
}
} else {
// Search by ctrlNum
let remPos = false;
// If there is ONE HT is already pressed, stop it before playing it again
this.playQueue.ht.forEach( (queueTone, position) => {
// If the key is already pressed
if (queueTone.ctrlNum === dhcMsg.ctrlNum) {
this.sendMessageToApps(HUM.DHCmsg.htOFF(queueTone.source, queueTone.xtNum, queueTone.velocity, queueTone.ctrlNum));
// this.playQueue.ft.splice(position, 1);
remPos = position;
}
});
if (remPos !== false) {
this.playQueue.ht.splice(remPos, 1);
}
}
// - - - - - - - - - - - - - -
// If it's a normal HT
if (dhcMsg.xtNum !== 0) {
// Store the current HT into the global slot for future UI monitor updates
this.settings.ht.curr_ht = dhcMsg.xtNum;
this.playQueue.ht.push(dhcMsg);
// If the Note ON is not a Piper's fake midievent (FT0)
if (dhcMsg.piper === false) {
// Add the HT to the Pipe
this.piper(dhcMsg);
}
// Update the UI
this.dhcMonitor("ht", dhcMsg.xtNum);
// If HT0 is pressed, it's the Piper feature!
} else if (dhcMsg.xtNum === 0) {
// Note ON the next piped HT
this.piping(1);
}
this.sendMessageToApps(dhcMsg);
}
/**
* Stop playing a Harmonic/Subharmonic Tone
*
* @param {HUM.DHCmsg} dhcMsg - The message containing the HT to be muted
*/
muteHT(dhcMsg) {
// If it's a normal HT
if (dhcMsg.xtNum !== 0) {
// Search the HT number in the queue array
let position = this.playQueue.ht.findIndex(qt => qt.xtNum === dhcMsg.xtNum);
// If the HTn exist
if (position > -1) {
// Remove the HTn from the queue array
this.playQueue.ht.splice(position, 1);
// Update the curr_ht
if (this.playQueue.ht.length > 0) {
this.settings.ht.curr_ht = this.playQueue.ht[this.playQueue.ht.length-1].xtNum;
}
// If the FTn does not exist
} else {
if (dhcMsg.panic === false) {
console.log("STRANGE: there is NOT a HT pressed key #:", dhcMsg.xtNum);
}
}
// If HT0 is pressed, it's the Piper feature
} else if (dhcMsg.xtNum === 0 && dhcMsg.panic === false) {
// Note OFF the active piped HT
this.piping(0);
}
this.sendMessageToApps(dhcMsg);
}
/**
* Force to stop playing all Fundamental and Harmonic/Subharmonic Tones
*/
panic() {
this.playQueue.ft = [];
this.playQueue.ht = [];
this.sendMessageToApps(HUM.DHCmsg.allNotesOff('dhc'));
}
/*==============================================================================*
* PIPER HT0 FEATURE
* The Piper store the last N pressed HTs and repeat them when HT0 is pressed
* simulating a special fake MIDI message
*==============================================================================*/
/**
* Store a play-HT message into the Piper's queue
*
* @param {HUM.DHCmsg} dhcMsg - The message containing the HT to be piped
*/
piper(dhcMsg) { // statusByte, ctrlNum, velocity, type) {
// Prepare the fake MIDI message
// let pack = [statusByte, ctrlNum, velocity];
// If the pipe is not full
if (this.pipe.queue.length < this.pipe.maxLenght) {
// Insert the message at the beginning of the queue
this.pipe.queue.push(dhcMsg);
// Else, if the pipe is full
} else {
// Remove the oldest message in the pipe
this.pipe.queue.shift();
// Insert in the pipe a new message
this.pipe.queue.push(dhcMsg);
}
}
/**
* Play or mute the next HT available in the piper's queue.<br>
* Usually when HT0 is pressed (or released).
*
* @param {(0|1)} state - Note ON/OFF; 1 is ON (play), 0 is OFF (mute)
*/
piping(state) {
// Get the index (current step)
let i = this.pipe.currStep;
// If there are notes in the queue
if (this.pipe.queue.length > 0) {
// Inject the queue into the pipe at the current step position
this.pipe.pipe.splice.apply(this.pipe.pipe, [i, this.pipe.queue.length].concat(this.pipe.queue));
// If the final pipe is longer than the maxLenght
if (this.pipe.pipe.length > this.pipe.maxLenght) {
// Cut the pipe according to the maxLenght
this.pipe.pipe.splice(0, (this.pipe.pipe.length - this.pipe.maxLenght));
}
// Increase current step and index in order to start playing
// after the notes that have just been inserted
this.pipe.currStep += this.pipe.queue.length;
i += this.pipe.queue.length;
// Empty the queue
this.pipe.queue = [];
}
// If there are notes in the pipe
if (this.pipe.pipe.length > 0) {
// If step count is not at the end of the pipe
if (i < this.pipe.maxLenght) {
// Note ON
if (state === 1) {
// If there is some message at the current step in the pipe
if (this.pipe.pipe[i]) {
// Create the special-marked fake MIDI message
// in order to not to be confused with a normal MIDI message
// let hancock = this.pipe.pipe[i][3];
// if (this.settings.controller.receive_mode !== 'keymap') {
// hancock = 'hancock';
// }
// let midievent = {
// data: [this.pipe.pipe[i][0], this.pipe.pipe[i][1], this.pipe.pipe[i][2], hancock, "piper"]
// };
this.pipe.pipe[i].piper = true;
this.playHT(this.pipe.pipe[i]);
// Send the fake MIDI message
// this.midi.in.midiMessageReceived(midievent);
// Store the last sent MIDI message in 'currTone'
this.pipe.currTone = this.pipe.pipe[i];
} else {
this.pipe.currTone = null;
}
// Note OFF
} else if (state === 0) {
// If there is a stored MIDI message in 'currTone'
if (this.pipe.currTone) {
let currToneOFF = HUM.DHCmsg.copyOFF(this.pipe.currTone);
// this.pipe.currTone.cmd = "tone-off";
// Set the velocity to zero (Note OFF)
// this.pipe.currTone.data[2] = 0;
// Send the fake MIDI message
// this.midi.in.midiMessageReceived(this.pipe.currTone);
this.muteHT(currToneOFF);
}
// Go to the next step in the pipe
this.pipe.currStep++;
}
// If step count is at the end (or out) of the pipe
} else {
// Reset the step counter
this.pipe.currStep = 0;
// Retry to execute the icPiping (itself) again
this.piping(state);
}
}
}
/**
* Experimental function for dynamic default/preloaded piper melody.<br>
* Fill the Piper's queue with a sequence of HTs.
*
* @todo - The preloaded Pipe must use only available keys
*
* @param {('h'|'s'|'hs')} type - The HTs scale type of the current Controller keymap
*/
initPipeQueue(type) {
// let ctrlMap = this.tables.ctrl;
let melodies = {
h: [9, 10, 8, 4, 6],
s: [-6, -5, -4, -3, -4, -5],
hs: [6, -6, 6, -6, 6, -6]
};
let msgsQueue = [];
for (let tone of melodies[type]) {
msgsQueue.push(HUM.DHCmsg.htON('dhc', tone, 120, false, true));
}
this.pipe.maxLenght = this.uiElements.in.dhc_piperSteps.value = msgsQueue.length;
this.pipe.queue = msgsQueue;
}
/*==============================================================================*
* UI TONE MONITORS
*==============================================================================*/
/**
* Apply the current controller pitchbend amount (if present) to a Xtone object and return a pitch-bent copy of it.
*
* @param {HUM.DHC#Xtone} xtObj - FT or HT object of the tone to bend
*
* @return {HUM.DHC#Xtone} - The pitch-bent object
*/
// @old icArrayPitchbender
bendXtone(xtObj) {
// Compute the Controller Pitchbend amount in cents
let pitchbend = this.settings.controller.pb.amount * this.settings.controller.pb.range,
// Apply the controller pitchbend if present
hz = Math.pow(2, pitchbend / 1200) * xtObj.hz,
mc = (xtObj.mc + pitchbend / 100);
return new this.Xtone(hz, mc);
}
/**
* Update all parts of the UI with the last computed or set values.
* Send the 'init' message to all the redistered Apps.
*/
// @old icMONITORSinit
initUImonitors() {
// Compile the FM monitors
this.printFundamentalMother(this.settings.fm.hz, this.settings.fm.mc);
// Compile the FT monitors
this.dhcMonitor("ft", this.settings.ht.curr_ft);
// If a HT has been already pressed
if (this.settings.ht.curr_ht) {
// Compile the HT monitor
this.dhcMonitor("ht", this.settings.ht.curr_ht);
}
this.sendMessageToApps(HUM.DHCmsg.init('dhc'));
}
/**
* Monitor UI
*
* @param {tonetype} type - If the tone is a FT or HT
* @param {xtnum} xtNum - FT or HT relative tone number
*/
// @old icDHCmonitor
dhcMonitor(type, xtNum) {
let xtObj = this.tables[type][xtNum];
// Apply the controller pitchbend (if present) to the array
xtObj = this.bendXtone(xtObj);
let notename = this.mcToName(xtObj.mc),
name = notename[0],
sign = notename[1],
cent = notename[2],
hzAccuracy = this.settings.global.hz_accuracy,
mcAccuracy = this.settings.global.cent_accuracy;
if (type === "ft") {
// Update the log on MONITOR FT info on the UI
this.uiElements.out.toneMonitorFT_tone.innerText = xtNum;
this.uiElements.out.toneMonitorFT_midicents.innerText = xtObj.mc.toFixed(mcAccuracy + 2);
this.uiElements.out.toneMonitorFT_notename.innerText = name + " " + sign + cent + "\u00A2";
this.uiElements.out.toneMonitorFT_frequency.innerText = xtObj.hz.toFixed(hzAccuracy);
} else if (type === "ht") {
// Update the log on MONITOR HT info on the UI
this.uiElements.out.toneMonitorHT_tone.innerText = xtNum;
this.uiElements.out.toneMonitorHT_midicents.innerText = xtObj.mc.toFixed(mcAccuracy + 2);
this.uiElements.out.toneMonitorHT_notename.innerText = name + " " + sign + cent + "\u00A2";
this.uiElements.out.toneMonitorHT_frequency.innerText = xtObj.hz.toFixed(hzAccuracy);
}
}
/*==============================================================================*
* DHC UI INIT
*==============================================================================*/
/**
* Initialize the DHC UI controllers
*/
// @old icUIinit
_initUI() {
/* UI DEFAULT SETTINGS
* ====================*/
// Default UI FM
if (this.settings.fm.init === "mc") {
// If the FM 'init' value is 'mc'
// Set UI midicents FM from this.settings.fm.mc
this.uiElements.in.fm_mc.value = this.settings.fm[this.settings.fm.init];
} else if (this.settings.fm.init === "hz") {
// If the FM 'init' value is 'hz'
// Set UI Hz FM from this.settings.fm.hz
this.uiElements.in.fm_hz.value = this.settings.fm[this.settings.fm.init];
} else {
console.log("The 'DHC.settings.fm.init' attribute has an unexpected value: " + this.settings.fm.init);
}
// Default FT nED-x on UI textboxes
this.uiElements.in.ftNEDX_unit.value = this.settings.ft.nEDx.unit;
this.uiElements.in.ftNEDX_division.value = this.settings.ft.nEDx.division;
// Default HT TRANSPOSE on UI textboxes
this.uiElements.in.htTranspose_h_ratio.value = this.settings.ht.transpose.h;
this.uiElements.in.htTranspose_s_ratio.value = this.settings.ht.transpose.s;
// Default DHC SETTINGS on UI textboxes
this.uiElements.in.dhc_hzAccuracy.value = this.settings.global.hz_accuracy;
this.uiElements.in.dhc_mcAccuracy.value = this.settings.global.cent_accuracy;
this.uiElements.in.dhc_middleC.value = this.settings.global.middle_c + 5;
this.uiElements.in.dhc_pitchbendRange.value = this.settings.controller.pb.range;
this.uiElements.in.dhc_piperSteps.value = this.pipe.maxLenght;
/* UI EVENT LISTENERS
* ====================*/
//------------------------
// UI GENERAL DHC settings
//------------------------
// Set the UI HZ DECIMAL PRECISION from UI HTML inputs
this.uiElements.in.dhc_hzAccuracy.addEventListener("change", (event) => {
// Store the Hz accuracy in the global slot
this.settings.global.hz_accuracy = Number(event.target.value);
// Reinitialize the DHC to apply also to the Monitors on the FM MIDI/Hz UI Input
this.initUImonitors();
// icDHCinit();
});
// Set the UI MIDI.CENTS DECIMAL PRECISION from UI HTML inputs
this.uiElements.in.dhc_mcAccuracy.addEventListener("change", (event) => {
// Store the mc accuracy in the global slot
this.settings.global.cent_accuracy = Number(event.target.value);
// Reinitialize the DHC to apply also to the Monitors on the FM MIDI/Hz UI Input
this.initUImonitors();
// icDHCinit();
});
// Set the MIDDLE C OCTAVE from UI HTML inputs
this.uiElements.in.dhc_middleC.addEventListener("change", (event) => {
// Beginning octave = Middle C octave - 5 (-1 => C4)
this.settings.global.middle_c = event.target.value - 5;
// Reinitialize the DHC to apply also to the Monitors on the FM MIDI/Hz UI Input
this.initUImonitors();
// icDHCinit();
});
// Set the CONTROLLER PITCHBEND RANGE from UI HTML inputs
this.uiElements.in.dhc_pitchbendRange.addEventListener("change", (event) => {
this.settings.controller.pb.range = event.target.value;
});
// Set the PIPER HT0 FEATURE from UI HTML inputs
this.uiElements.in.dhc_piperSteps.addEventListener("change", (event) => {
this.pipe.maxLenght = event.target.value;
});
//-------------------
// UI FM DHC settings
//-------------------
// Set the Fundamental Mother (FM) from UI HTML inputs
this.uiElements.in.fm_mc.addEventListener("change", () => {
this.setFM(this.getFM("mc"));
this.uiElements.in.fm_hz.value = "";
});
this.uiElements.in.fm_hz.addEventListener("change", () => {
this.setFM(this.getFM("hz"));
this.uiElements.in.fm_mc.value = "";
});
//-------------------
// UI FT DHC settings
//-------------------
// Set the RADIO BUTTONS for FT TUNING SYSTEM
// Radio NEDX system
this.uiElements.fn.ftSys_NEDX.addEventListener("click", (event) => {
if (event.target.checked) {
this.switchFTsys("nEDx");
}
});
// Radio HS Natural system
this.uiElements.fn.ftSys_HSnat.addEventListener("click", (event) => {
if (event.target.checked) {
this.switchFTsys("h_s", "natural");
}
});
// Radio HS Transposed Same Octave system
this.uiElements.fn.ftSys_HStrans.addEventListener("click", (event) => {
if (event.target.checked) {
this.switchFTsys("h_s", "sameOctave");
}
});
// Set default FT Tuning System after the radio buttons are set-up
// this.uiElements.fn.ftSys_NEDX.click();
// Get and set the FT NEDX UI HTML inputs
this.uiElements.in.ftNEDX_unit.addEventListener("change", this.setNEDX.bind(this));
this.uiElements.in.ftNEDX_division.addEventListener("change", this.setNEDX.bind(this));
this.uiElements.in.ftNEDX_ok.addEventListener("click", this.setNEDX.bind(this));
// Get and set the FT HARM/SUBHARM UI HTML inputs
// Harmonic Tones transposition
// + Octave
this.uiElements.in.ftHStranspose_h_plus.addEventListener("click", () => {
this.transposeFThs(2, "h");
});
// - Octave
this.uiElements.in.ftHStranspose_h_minus.addEventListener("click", () => {
this.transposeFThs(0.5, "h");
});
// Subharmonic Tones transposition
// + Octave
this.uiElements.in.ftHStranspose_s_plus.addEventListener("click", () => {
this.transposeFThs(2, "s");
});
// - Octave
this.uiElements.in.ftHStranspose_s_minus.addEventListener("click", () => {
this.transposeFThs(0.5, "s");
});
//-------------------
// UI HT DHC settings
//-------------------
// Get and set the HT UI HTML inputs
// Harmonic Tones transposition
// + Octave
this.uiElements.in.htTranspose_h_plus.addEventListener("click", () => {
this.transposeHT(2, "h", true);
});
// – Octave
this.uiElements.in.htTranspose_h_minus.addEventListener("click", () => {
this.transposeHT(0.5, "h", true);
});
// Free ratio
this.uiElements.in.htTranspose_h_ratio.addEventListener("change", (e) => {
this.transposeHT(e.target.value, "h", false);
});
// Subharmonic Tones transposition
// + Octave
this.uiElements.in.htTranspose_s_plus.addEventListener("click", () => {
this.transposeHT(2, "s", true);
});
// – Octave
this.uiElements.in.htTranspose_s_minus.addEventListener("click", () => {
this.transposeHT(0.5, "s", true);
});
// Free ratio
this.uiElements.in.htTranspose_s_ratio.addEventListener("change", (e) => {
this.transposeHT(e.target.value, "s", false);
});
//-------------------
// UI of the Controller KEYMAP readers
//-------------------
// @old icKeymapsUIinit
// Add an EventListener to the (on)change event of the Controller Keymap File <input> tag
this.uiElements.in.controllerKeymapFile.addEventListener('change', this.handleKeymapFile.bind(this), false);
// Add an EventListener to the (on)change event of the Controller Keymap File <select> tag
this.uiElements.in.controllerKeymapPresets.addEventListener('change', this.loadKeymapPreset.bind(this));
// Get the button that opens the modal controller keymap table
this.uiElements.fn.controllerKeymapTableShow.addEventListener("click", this.keymap2Html.bind(this));
}
/*==============================================================================*
* NOTE NAMING CONVERSION TOOLS
*==============================================================================*/
/**
* Parse a note name string to get the MIDI note number
*
* @example
* 'hancock' C0 == 0 midicent == 0 midinnum
* 'scientific' C0 == 12 midicent == 12 midinnum
* 'ui' C0 == // depends on Middle C setting ({@link DHCsettings.global.middle_c})
*
* @param {('hancock'|'ui'|'scientific')} mode - The method in which the 'note' should be interpreted.
* @param {string} note - The note name in format <em>[A-G]#?-?\d+</em>. E.g. C0, A#4, G-3, D#-1
*
* @return {midinnum} - MIDI note number
*/
// @old icHancockToMidi
nameToMidiNumber(mode, note) {
let ref = {
"C": 0,
"C#": 1,
"D": 2,
"D#": 3,
"E": 4,
"F": 5,
"F#": 6,
"G": 7,
"G#": 8,
"A": 9,
"A#": 10,
"B": 11,
};
// Get the number of the octave
let num = note.match(/-?\d+/g);
// Get the note name
let char = note.match(/[A-Z]+#*/);
if (num && char) {
let octave = Number(num[0]);
let note = char[0];
if (mode === 'hancock') {
// Return the MIDI note number
return ref[note] + (12 * octave);
} else if (mode === 'ui') {
return ref[note] + (12 * (octave + (this.settings.global.middle_c * -1)));
} else if (mode === 'scientific') {
return ref[note] + (12 * (octave + 1));
}
} else {
return undefined;
}
}
/**
* Convert a MIDI note number to an array containing 'hancock', 'ui' and 'scientific' note name,
* plus the information if the key on the piano should be black or white.
*
* @param {midinnum} midikey - MIDI note number (integer)
*
* @return {Array.<string, string, boolean, string>} - An array with Hancock, UI and Scientific note name, and if the key is white or black
*/
// @old icMidiToHancock
midiNumberToNames(midikey) {
let ref = {
0: ['C', false],
1: ['C#', true],
2: ['D', false],
3: ['D#', true],
4: ['E', false],
5: ['F', false],
6: ['F#', true],
7: ['G', false],
8: ['G#', true],
9: ['A', false],
10:['A#', true],
11:['B', false],
};
// If positive - Higher than midikey 0 / 8.1758 Hz (C-1 in scientific pitch notation)
if (midikey >= 0) {
// Compute the quotient and remainder of the MIDI note number
let quotient = Math.trunc(midikey/12);
let remainder = midikey % 12;
// String concatenation: Hancock note name
let hancockNotename = ref[remainder][0] + quotient;
// String concatenation: UI note name
let uiNotename = ref[remainder][0] + (quotient + this.settings.global.middle_c);
let sciNotename = ref[remainder][0] + (quotient - 1);
let isBlack = ref[remainder][1];
// Return an array with both, Hancock and UI note name
return [hancockNotename, uiNotename, isBlack, sciNotename];
// If negative - Lower than midikey 0 / 8.1758 Hz (C-1 in scientific pitch notation)
} else {
// Compute the quotient and remainder of the MIDI note number
let quotient = Math.trunc(midikey/12)-1;
let remainder = midikey % 12;
remainder = (remainder === 0) ? remainder : (12 + remainder);
// String concatenation: Hancock note name
let hancockNotename = ref[remainder][0] + quotient;
// String concatenation: UI note name
let uiNotename = ref[remainder][0] + (quotient + this.settings.global.middle_c);
let sciNotename = ref[remainder][0] + (quotient - 1);
let isBlack = ref[remainder][1];
// Return an array with both, Hancock and UI note name
return [hancockNotename, uiNotename, isBlack, sciNotename];
}
}
/**
* UI Util to get the UI note name +/- cents from given midicent
*
* @param {midicent} mc - Pitch in midicent (float)
*
* @return {Array.<string, number, string, boolean>} - Array containing [note name, +/- sign, cents, black key y/n]
*/
mcToName(mc) {
let noteNumber = Math.trunc(mc),
noteCents = mc - noteNumber,
centSign;
// If positive - Higher than 0mc / 8.1758 Hz (C-1 in scientific pitch notation)
if (mc >= 0) {
centSign = "\u002B"; // +
if (noteCents > 0.5) {
noteNumber = noteNumber + 1;
noteCents = 1 - noteCents;
centSign = "\u2212"; // −
}
// If negative - Lower than 0mc / 8.1758 Hz (C-1 in scientific pitch notation)
} else {
centSign = "\u2212"; // −
noteCents = Math.abs(noteCents);
if (noteCents > 0.5) {
noteNumber = noteNumber - 1;
noteCents = 1 - noteCents;
centSign = "\u002B"; // +
}
}
// Get the note names
let noteNames = this.midiNumberToNames(noteNumber);
// Convert the cents in decimal notation to unit
noteCents = (noteCents.toFixed(this.settings.global.cent_accuracy + 2) * 100).toFixed(this.settings.global.cent_accuracy);
let noteCentsUI = Number(noteCents, 10);
// Remove the centSign if cents are zero
centSign = (noteCentsUI === 0) ? "" : centSign;
// note name, +/- sign, cents, black key y/n
return [noteNames[1], centSign, noteCentsUI, noteNames[2]];
}
/**
* Util to get the full string of UI note name +/- cents
*
* @param {midicent} mc - Pitch in midicent (float)
*
* @return {string} - The compliled string; e.g. D#3 -45¢;
*/
mcToNameString(mc) {
let result = this.mcToName(mc);
return result[0] + " " + result[1] + result[2] + "\u00A2";
}
/*==============================================================================*
* GENERAL DHC COMPUTING TOOLS
*==============================================================================*/
/**
* From MIDI note number to frequency (Hz)
*
* @param {midicent} mc - MIDI note number expressed in midi.cents
*
* @return {hertz} - Frequency expressed in hertz (Hz)
*/
// @old icMtoF
static mcToFreq(mc) {
// Use the icCompute_nEDx() function to get frequency
return this.compute_nEDx(mc - 69, 2, 12, 440);
}
/**
* From frequency (Hz) to MIDI note number
*
* @param {hertz} freq - Frequency expressed in hertz (Hz)
*
* @return {midicent} - MIDI note number expressed in midicent
*/
// @old icFtoM
static freqToMc(freq) {
let midicent = 69 + 12 * Math.log2(freq / 440);
// Return full accuracy midicent
return midicent;
}
/**
* Calculate the n-EDx ("free" equal temperament) of a relative tone
*
* @param {number} relativeTone - Relative number of the "step" in the scale (should be integer)
* @param {number} unit - Ratio unit (must be greater than zero)
* @param {number} division - Equal divisions of the ratio unit (must be greater than zero)
* @param {hertz} masterTuning - Reference frequency expressed in hertz (Hz)
*
* @return {hertz} - Frequency expressed in hertz (Hz)
*/
// @old icCompute_nEDx
static compute_nEDx(relativeTone, unit, division, masterTuning) {
let frequency = Math.pow(unit, relativeTone / division) * masterTuning;
// Return full accuracy frequency
return frequency;
}
/**
* Remove duplicated values on the array passed via the argument
*
* @param {Array.<number>} arrArg - Array of numbers
*
* @return {Array.<number>} - Uniquified array
*/
static uniqArray(arrArg) {
return arrArg.filter((elem, pos, arr) => arr.indexOf(elem) === pos );
}
// end class Prototype
}; // end Class
/**
* A FT or HT relative tone (used in ft_table ht_table)
*/
HUM.DHC.prototype.Xtone = class {
/**
* @property {hertz} hz - Frequency expressed in hertz (Hz)
* @property {midicent} mc - MIDI note number expressed in midi.cents
*/
constructor(hz, mc) {
this.hz = Number(hz);
this.mc = Number(mc);
Object.freeze(this);
}
};
/**
* The main Dynamic Harmonics Calculator class
*/
HUM.DHC.prototype.DHCsettings = class {
/**
* @param {string} preset - A JSON string containing...
*/
constructor(preset=false) {
/**
* Global settings
*
* @member {Object}
*
* @property {number} hz_accuracy - Number of decimal places (on UI) for numbers expressed in hertz (Hz)
* @property {number} cent_accuracy - Number of decimal places (on UI) for numbers expressed in cents
* @property {('sharp'|'flat'|'relative')} enharmonic_nn - Enharmonic note naming; 'sharp' for [#], 'flat' for [b] or "relative" for [#/b]
* @property {number} middle_c - Middle C octave (-1 = from octave -1 >> Middle C = 60) - Starting octave
*/
this.global = {
hz_accuracy: 2,
cent_accuracy: 0,
// @todo - Enharmonic note naming
enharmonic_nn: "sharp",
middle_c: -1,
};
/**
* Controller settings
*
* @todo - Move to midi-in (one per input channel?)
*
* @member {Object}
*
* @property {Object} pb - Controller's Pitch Bend settings
* @property {cent} pb.range - Range value in cents (use hundreds when use MIDI-OUT and possibly the same as the instrument)
* @property {number} pb.amount - Current input controller pitchbend amount.
* Value from -8192 to +8191, normalized to the ratio from -1 to 0,99987792968750
* Init amount is always 0.
* @property {('keymap'|'tsnap-channel'|'tsnap-divider')} receive_mode - FT/HT controller note receiving mode
* @property {Object} tsnap - Tone snapping receiving mode settings
* @property {midicent} tsnap.tolerance - Snap tolerance in midicent; it's the maximum difference within which you can consider
* two frequencies as the same note
* @property {midinnum} tsnap.divider - The note, on a MIDI piano keyboard, that divides the FTs from HTs;
* the notes smaller or equal to the divider will be considered FT, the greater ones will be considered HT
* @property {Object} tsnap.channel - Namesapace for tone snapping Channel Mode settings
* @property {midichan} tsnap.channel.ft - The MIDI channel assigned to FTs; all notes received on this channel will be considered as FT
* @property {midichan} tsnap.channel.ht - The MIDI channel assigned to HTs; all notes received on this channel will be considered as HT
*/
this.controller = {
pb: {
range: 100,
amount: 0.0
},
receive_mode: "keymap",
tsnap: {
tolerance: 0.5,
divider: 56,
channel: {
ft: 1,
ht: 0,
divider: 0
}
},
};
/**
* Fundamental Mother (FM) settings
*
* @member {Object}
*
* @property {hertz} hz - Frequency expressed in hertz (Hz)
* @property {midicent} mc - Frequency expressed in midi.cents (mc)
* @property {('mc'|'hz')} init - What unit to use to initialize, 'hz' or 'mc'
*/
this.fm = {
hz: false, // 130.8127826502993,
mc: 48,
init: "mc"
};
/**
* Fundamental Tones (FTs) scale tuning method settings
*
* @member {Object}
*
* @property {Object} nEDx - "Equal Temperament" tuning method parameters
* @property {number} nEDx.unit - The ratio to divide
* @property {number} nEDx.division - The equal divisions
* @property {Object} h_s - "Harmonics and subharmonics" tuning method parameters
* @property {Object} h_s.natural - Natural (no transposition)
* @property {tratio} h_s.natural.h_tr - Transpose ratio in decimal for Harmonics
* @property {tratio} h_s.natural.s_tr - Transpose ratio in decimal for Subharmonics
* @property {Object} h_s.sameOctave - Same octave transposition
* @property {tratio} h_s.sameOctave.h_tr - Transpose ratio in decimal for Harmonics
* @property {tratio} h_s.sameOctave.s_tr - Transpose ratio in decimal for Subharmonics
* @property {('natural'|'sameOctave')} h_s.selected - Default/current sub-method selected
* @property {Object} file - Tuning files method (currently not used)
* @property {('nEDx'|'h_s'|'file')} selected - Default/current method selected
* @property {number} steps - +/- Steps for {DHC#tables.ft}<br>
* 'steps' to final range: MIDI Note number range considerations<br>
* 32 = -32 > +32 : (old default: I think +-64 is too wide, but let's try)<br>
* 64 = -64 > +64 : FM = midi#63 or midi#64 to use the full MIDI note range<br>
* ( midi#63 out of range on -1 )<br>
* ( midi#64 out of range on 128 )
*/
this.ft = {
nEDx: {
unit: 2,
division: 12
},
h_s: {
natural: {
h_tr: 1,
s_tr: 16
},
sameOctave: {
h_tr: 1,
s_tr: 2
},
selected: "sameOctave"
},
/** @todo - Tuning file formats */
file: {
scl: {},
tun: {},
mtx: {},
lmso: {},
selected: "scl"
},
selected: "nEDx",
steps: 64
};
/**
* Harmonic/subharmonic Tones (HTs) scale tuning settings
*
* @member {Object}
*
* @property {Object} transpose - Harmonics and subharmonics transpose
* @property {tratio} transpose.h - Transpose ratio in decimal for Harmonics
* @property {tratio} transpose.s - Transpose ratio in decimal for Subharmonics
* @property {xtnum} curr_ft - The current FT (that generated the last HT table); init value must be 0
* @property {xtnum} curr_ft - The last pressed HT; init value should be null
*/
this.ht = {
transpose: {
h: 1,
s: 1 // 16
},
curr_ft: 0,
curr_ht: null
};
}
};
/**
* DHC Message class
*
* @property {string} #source - Name of the App component that generated the message
* @property {('init'|'panic'|'update'|'tone-on'|'tone-off')} #cmd - Command code of the message
* @property {tonetype} #type - The tone typeto which the message is directed; FT or HT
* @property {xtnum} #xtNum - The FT or HT number
* @property {velocity} #velocity - The intensity of the sound to be generated in MIDI velocity format
* @property {midinnum} #ctrlNum - The MIDI Note Number corresponding to the FT or HT on the keymap (if present)
* @property {boolean} #piper - If the message is generated by the Piper feature
* @property {boolean} #panic - Only in case of `cmd` 'note-off', it tells that the message has been generated by a "hard" All-Notes-Off request
* @property {boolean} #tsnap - If the message has been converted by the Tone-Snap receiving mode
*
*/
HUM.DHCmsg = class {
/**
* @param {string} source - Name of the App component that generated the message
* @param {('init'|'panic'|'update'|'tone-on'|'tone-off')} cmd - Command code of the message
* @param {tonetype=} type - The tone typeto which the message is directed; FT or HT
* @param {xtnum=} xtNum - The FT or HT number
* @param {velocity=} velocity - The intensity of the sound to be generated in MIDI velocity format.<br>
* If the `cmd` is 'tone-on' , the values must be from 1 to 127.<br>
* If the `cmd` is 'tone-off' , the values must be from 0 to 127.<br>
* @param {midinnum=} ctrlNum - The MIDI Note Number corresponding to the FT or HT on the keymap (if present).<br>
* If it is not provided, the Apps that need this information should ignore the message.
* @param {boolean=} piper - If the message is generated by the Piper feature
* @param {boolean=} panic - Only in case of `cmd` 'note-off', it tells that the message has been generated by a "hard" All-Notes-Off request.
* @param {boolean=} tsnap - If the message has been converted by the Tone-Snap receiving mode.<br>
* If `true`, the `ctrlNum` may not be the same as the MIDI Note Number pressed on the controller.
*/
constructor(source, cmd, type=false, xtNum=false, velocity=false, ctrlNum=false, piper=false, panic=false, tsnap=false) {
this.source = source;
this.cmd = cmd;
this.type = type;
this.xtNum = xtNum;
this.velocity = velocity;
this.ctrlNum = ctrlNum;
this.piper = piper;
this.panic = panic;
this.tsnap = tsnap;
}
/**
* Create a new 'init' DHCmsg
*
* @param {string} source - Name of the App component that generated the message
*
* @return {Array.<number>} - A new instance of DHCmsg
*/
static init(source) {
return new HUM.DHCmsg(source, 'init');
}
/**
* Create a new 'panic' DHCmsg
*
* @param {string} source - Name of the App component that generated the message
*
* @return {Array.<number>} - A new instance of DHCmsg
*/
static allNotesOff(source) {
return new HUM.DHCmsg(source, 'panic');
}
/**
* Create a new 'update FT' DHCmsg
*
* @param {string} source - Name of the App component that generated the message
*
* @return {Array.<number>} - A new instance of DHCmsg
*/
static ftUpd(source) {
return new HUM.DHCmsg(source, 'update', 'ft');
}
/**
* Create a new 'update HT' DHCmsg
*
* @param {string} source - Name of the App component that generated the message
*
* @return {Array.<number>} - A new instance of DHCmsg
*/
static htUpd(source) {
return new HUM.DHCmsg(source, 'update', 'ht');
}
/**
* Create a new 'update Controller Keymap' DHCmsg
*
* @param {string} source - Name of the App component that generated the message
*
* @return {Array.<number>} - A new instance of DHCmsg
*/
static ctrlmapUpd(source) {
return new HUM.DHCmsg(source, 'update', 'ctrlmap');
}
/**
* Create a new 'FT Note-ON' DHCmsg
*
* @param {string} source - Name of the App component that generated the message
* @param {xtnum} xtNum - The FT number
* @param {velocity} velocity - The intensity of the sound to be generated in MIDI velocity format, from 1 to 127.<br>
* @param {midinnum=} ctrlNum - The MIDI Note Number corresponding to the FT on the keymap (if present)
* @param {boolean=} tsnap - If the message has been converted by the Tone-Snap receiving mode
*
* @return {Array.<number>} - A new instance of DHCmsg
*/
static ftON(source, xtNum, velocity, ctrlNum=false, tsnap=false) {
return new HUM.DHCmsg(source, 'tone-on', 'ft',
Number(xtNum),
(velocity ? Number(velocity) : false),
(ctrlNum ? Number(ctrlNum) : false),
false,
false,
tsnap);
}
/**
* Create a new 'HT Note-ON' DHCmsg
*
* @param {string} source - Name of the App component that generated the message
* @param {xtnum} xtNum - The HT number
* @param {velocity} velocity - The intensity of the sound to be generated in MIDI velocity format, from 1 to 127.<br>
* @param {midinnum=} ctrlNum - The MIDI Note Number corresponding to the HT on the keymap (if present)
* @param {boolean=} piper - If the message is generated by the Piper feature
* @param {boolean=} tsnap - If the message has been converted by the Tone-Snap receiving mode
*
* @return {Array.<number>} - A new instance of DHCmsg
*/
static htON(source, xtNum, velocity, ctrlNum=false, piper=false, tsnap=false) {
return new HUM.DHCmsg(source, 'tone-on', 'ht',
Number(xtNum),
(velocity ? Number(velocity) : false),
(ctrlNum ? Number(ctrlNum) : false),
piper,
false,
tsnap);
}
/**
* Create a new 'FT Note-OFF' DHCmsg
*
* @param {string} source - Name of the App component that generated the message
* @param {xtnum} xtNum - The FT number
* @param {velocity=} velocity - The intensity of the sound to be generated in MIDI velocity format, from 1 to 127.<br>
* @param {midinnum=} ctrlNum - The MIDI Note Number corresponding to the FT on the keymap (if present)
* @param {boolean=} panic - If `true`, it tells that the message has been generated by a "hard" All-Notes-Off request.
*
* @return {Array.<number>} - A new instance of DHCmsg
*/
static ftOFF(source, xtNum, velocity=false, ctrlNum=false, panic=false) {
return new HUM.DHCmsg(source, 'tone-off', 'ft',
Number(xtNum),
(velocity ? Number(velocity) : false),
(ctrlNum ? Number(ctrlNum) : false),
false,
panic);
}
/**
* Create a new 'HT Note-OFF' DHCmsg
*
* @param {string} source - Name of the App component that generated the message
* @param {xtnum} xtNum - The HT number
* @param {velocity=} velocity - The intensity of the sound to be generated in MIDI velocity format, from 1 to 127.<br>
* @param {midinnum=} ctrlNum - The MIDI Note Number corresponding to the HT on the keymap (if present)
* @param {boolean=} panic - If `true`, it tells that the message has been generated by a "hard" All-Notes-Off request.
*
* @return {Array.<number>} - A new instance of DHCmsg
*/
static htOFF(source, xtNum, velocity=false, ctrlNum=false, panic=false) {
return new HUM.DHCmsg(source, 'tone-off', 'ht',
Number(xtNum),
(velocity ? Number(velocity) : false),
(ctrlNum ? Number(ctrlNum) : false),
false,
panic);
}
/**
* Create a copy of a 'Note-OFF' DHCmsg
*
* @param {HUM.DHCmsg} dhcMsg - The original DHCmsg to be copied
*
* @return {Array.<number>} - A copy of the original DHCmsg
*/
static copyOFF(dhcMsg) {
return new HUM.DHCmsg(dhcMsg.source, 'tone-off', dhcMsg.type,
dhcMsg.xtNum,
dhcMsg.velocity,
dhcMsg.ctrlNum,
dhcMsg.piper,
dhcMsg.panic);
}
};