/**
 * 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 AudioContext */
/* globals HUM */
/* globals webAudioPeakMeter */

"use strict";

// Patch up the AudioContext prefixes
window.AudioContext = window.AudioContext || window.webkitAudioContext;

/** 
 * The Synth class
 *    A tool for listen the tones computed by the DHC.<br>
 *    Provide a simple basic synth useful as reference sound.
 */
// @old icSYNTH
HUM.Synth = class {
    /**
    * @param {HUM.DHC} dhc - The DHC instance to which it belongs
    */
    constructor(dhc) {
        /**
        * The AudioContext instance from Web Audio API
        *
        * @member {AudioContext}
        */
        try {
            // @old icAudioContext
            this.audioContext = new AudioContext();
        }
        catch(error) {
            alert('The Web Audio API is apparently not supported in this browser.');
            return undefined;
        }            
        /**
        * The id of this Synth instance (same as the DHC id)
        *
        * @member {string}
        */
        this.id = dhc.id;
        /**
        * The DHC instance
        *
        * @member {HUM.DHC}
        */
        this.dhc = dhc;
        /**
         * The state of the Synth (Power ON/OFF); if `false`, it is turned off.
         *
         * @member {boolean}
         */
        this.status = false;
        /**
         * Namespace for FT and HT voices slots
         *
         * @member {Object}
         *
         * @property {HUM.Synth#SynthVoice}                 ft - FT slot for a SynthVoice object
         * @property {Object.<xtnum, HUM.Synth#SynthVoice>} ht - HT slot for a register of SynthVoice objects
         */
        // @old icVoices
        this.voices = {
            ft: null,
            ht: {}
        };
        /**
         * Namespace for Gain nodes
         *
         * @member {Object}
         *
         * @property {GainNode} master       - Final gain out node
         * @property {GainNode} mix          - FT+HT mixer gain
         * @property {GainNode} ft           - FT gain
         * @property {GainNode} ht           - HT gain
         * @property {number}   defaultValue - The default volume value for FT and HT
         */
        this.volume = {
            // Prepare the MASTER, MIX FT and HT gain out nodes
            master: this.audioContext.createGain(),
            mix: this.audioContext.createGain(),
            ft: this.audioContext.createGain(),
            ht: this.audioContext.createGain(),
            defaultValue: 0.8
        };
        /**
         * Namespace for FT and HT waveform
         *
         * @member {Object}
         *
         * @property {('sine'|'square'|'sawtooth'|'triangle')} ft - FTs waveform
         * @property {('sine'|'square'|'sawtooth'|'triangle')} ht - HTs waveform
         */
        this.waveform = {
            ft: "triangle",
            ht: "sine"
        };
        /**
         * Namespace for the ADSR envelope parameters
         *
         * @member {Object}
         *
         * @property {number} attack  - Attack time (seconds)
         * @property {number} decay   - Decay time (time-constant)
         * @property {number} sustain - Sustain gain value (amount from 0.0 to 1.0)
         * @property {number} release - Release time (seconds)
         */
        this.envelope = {
            attack: 0.3,
            decay: 0.15,
            sustain: 0.68,
            release: 0.3
        };
        /**
         * Portamento/Glide parameters for monophonic FT and FT/HT osc frequency updates.
         *
         * @member {Object}
         *
         * @property {number} amount     - Portamento time (time-constant)
         * @property {number} lastFreqFT - Last FT frequency expressed in hertz (Hz); init value should be `null`
         */
        this.portamento = {
            amount: 0.03,
            lastFreqFT: null
        };
        /**
         * Namespace for convolver Reverb and wet/dry mixer gains
         *
         * @member {Object}
         *
         * @property {ConvolverNode} convolver    - Slot for convolver reverb node
         * @property {GainNode}      wet          - Reverberated gain bus/carrier node
         * @property {GainNode}      dry          - Dry gain bus/carrier node
         * @property {GainNode}      amount       - Reverb wey/dry mixing amount (for cross-fade)
         * @property {number}        defaultValue - Default value for the wet/dry mixer gain
         */
        this.reverb = {
            // Prepare the REVERB and the wet/dry gain nodes
            convolver: this.tryCreateConvolver(),
            wet: this.audioContext.createGain(),
            dry: this.audioContext.createGain(),
            amount: null,
            defaultValue: 0.5
        };
        /**
        * The compressor node
        * 
        * @member {DynamicsCompressorNode}
        */
        this.compressor = this.audioContext.createDynamicsCompressor();
        /**
        * The vumeter from "webAudioPeakMeter" lib
        *     NB: the `{@link ScriptProcessorNode}` is deprecated but still working.
        * 
        * @member {ScriptProcessorNode}
        */
        this.vumeter = webAudioPeakMeter.createMeterNode(this.volume.master, this.audioContext);
        /**
         * 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: {
                checkboxSynth: this.dhc.harmonicarium.html.synthTab[dhc.id].children[0],
            },
            in: {
                portamento: document.getElementById('HTMLi_synth_portamento'+dhc.id),
                attack: document.getElementById('HTMLi_synth_attack'+dhc.id),
                decay: document.getElementById('HTMLi_synth_decay'+dhc.id),
                sustain: document.getElementById('HTMLi_synth_sustain'+dhc.id),
                release: document.getElementById('HTMLi_synth_release'+dhc.id),
                waveformFT: document.getElementById('HTMLi_synth_waveformFT'+dhc.id),
                waveformHT: document.getElementById('HTMLi_synth_waveformHT'+dhc.id),
                volumeFT: document.getElementById('HTMLi_synth_volumeFT'+dhc.id),
                volumeHT: document.getElementById('HTMLi_synth_volumeHT'+dhc.id),
                volume: document.getElementById('HTMLi_synth_volume'+dhc.id),
                reverb: document.getElementById('HTMLi_synth_reverb'+dhc.id),
                power: document.getElementById('HTMLi_synth_power'+dhc.id),
                irFile: document.getElementById('HTMLi_synth_irFile'+dhc.id),
            },
            out: {
                synth_meter: document.getElementById('HTMLo_synth_meter'+dhc.id),
            }
        };

        this._init();
        // =======================
    } // end class Constructor
    // ===========================

    /**
     * Initialize the new instance of Synth
     */
    _init() {
        // Conect the FT and HT gains to the MIX
        this.volume.ft.connect(this.volume.mix);
        this.volume.ht.connect(this.volume.mix);
        // Split the MIX to the REVERB and to DRY CARRIER
        this.volume.mix.connect(this.reverb.convolver);
        this.volume.mix.connect(this.reverb.dry);
        // Connect the REVERB to the WET CARRIER
        this.reverb.convolver.connect(this.reverb.wet);
        // Connect the WET/DRY CARRIERS to the COMPRESSOR
        this.reverb.wet.connect(this.compressor);
        this.reverb.dry.connect(this.compressor);
        // Connect the COMPRESSOR to the MASTER
        this.compressor.connect(this.volume.master);
        // Connect the MASTER to the final OUT
        this.volume.master.connect(this.audioContext.destination);
            // @todo - finish the visualizer
            // Create the VISUALIZER
            // icAnalyser = icAudioContext.createAnalyser();
            // icAnalyser.minDecibels = -90;
            // icAnalyser.maxDecibels = -10;
            // icAnalyser.smoothingTimeConstant = 0.85;
            // icVisualize();
            // @todo - finish the visualizer
            // this.volume.master.connect(icAnalyser);
            // icAnalyser.connect(icAudioContext.destination);

        // Init the Synth with default values

        // Load the Base64-coded default IR Reverb
        this.readIrFile(this.constructor.base64ToBlob(this.constructor.defaultReverb));
        // @todo - Implement XMLHttpRequest() to get IR reverbs from URLs on the net
        // @todo - https://codepen.io/andremichelle/pen/NPPEPY
        
        this.volume.ft.gain.setValueAtTime(this.volume.defaultValue, 0);
        this.volume.ht.gain.setValueAtTime(this.volume.defaultValue, 0);
        this.volume.master.gain.setValueAtTime(this.volume.defaultValue, 0);
        this.updateReverb(this.reverb.defaultValue);
        this.status = true;
        
        this._initUI();

        // Create the WEB AUDIO PEAK METERS (/assets/js/lib/web-audio-peak-meter.min.js)        
        webAudioPeakMeter.createMeter(this.uiElements.out.synth_meter, this.vumeter, {
            backgroundColor: 'rgb(38, 36, 54)',
            dbTickSize: 4,
            borderSize: 5,
            fontSize: 10,
            maskTransition: '0.1s'
        });
        
        // Start with the analysis suspended
        this.vumeter.onaudioprocess = undefined;

        // Disable the vu-meter analisys & animation if the accordion Synth tab is closed
        this.uiElements.fn.checkboxSynth.addEventListener('change', (e) => {
            if (e.target.checked === true) {
                this.vumeter.onaudioprocess = webAudioPeakMeter.updateMeter;
                webAudioPeakMeter.paintMeter.animate = true;
                webAudioPeakMeter.paintMeter();
            } else {
                this.vumeter.onaudioprocess = undefined;
                webAudioPeakMeter.paintMeter.animate = false;
            }
        });

        // Tell to the DHC that a new app is using it            
        this.dhc.registerApp(this, 'updatesFromDHC', 2);
    }

    /**
     * Manage and route an incoming message
     *
     * @param {HUM.DHCmsg} msg - The incoming message
     */
    updatesFromDHC(msg) {

        if (msg.cmd === 'panic') {
            this.allNotesOff();
        }

        if (msg.cmd === 'update') {
            if (msg.type === 'ft') {
                
                this.updateFTfrequency();

            } else if (msg.type === 'ht') {

                this.updateHTfrequency();

            } else if (msg.type === 'ctrlmap') {

            }

        } else if (msg.cmd === 'tone-on') {
            if (msg.type === 'ft') {

                this.voiceON("ft", msg.xtNum, msg.velocity);

            } else if (msg.type === 'ht') {

                if (msg.xtNum !== 0) {
                    this.voiceON("ht", msg.xtNum, msg.velocity);
                }

            }

        } else if (msg.cmd === 'tone-off') {
            if (msg.type === 'ft') {

                this.voiceOFF("ft", msg.xtNum, msg.panic);

            } else if (msg.type === 'ht') {
                if (msg.xtNum !== 0) {
                    this.voiceOFF("ht", msg.xtNum, msg.panic);
                }

            }

        }
    }

    /**
     * Create a new voice of the synth
     *
     * @param {tonetype} type     - If the voice will be a FTs or HTs
     * @param {xtnum}    toneID   - The ID of the new voice; the FT/HT tone number
     * @param {velocity} velocity - MIDI Velocity amount (from 0 to 127) of the MIDI-IN message from the controller
     */
    // @old icVoiceON
    voiceON(type, toneID, velocity) {
        let freq = this.dhc.tables[type][toneID].hz;
        // If the synth is turned-on
        if (this.status === true) {
            
            // **HT**
            if (type === "ht") {
                // @todo - implement the limit of polyphony
                // If there isn't a voice turned on with the same toneID
                // (prevent duplication in case of stuck note - not turned off)
                if (!this.voices.ht[toneID]) {   // && Object.keys(this.voices.ht).length < 2
                    // Create a new HT voice (POLYPHONIC)
                    this.voices.ht[toneID] = new this.SynthVoice(this, freq, velocity, type);
                } //  else {
                //     this.voiceOFF('ht', toneID);
                //     this.voices.ht[toneID] = new Synth.SynthVoice(this, freq, velocity, type);
                // }

            // **FT**
            } else if (type === "ft") {
                // Manage the monophonic FT voice
                // If the FT voice is active
                if (this.voices.ft) {
                    // Shutdown the voice
                    this.voices.ft.voiceMute();
                }
                // Create a new FT voice (MONOPHONIC)
                this.voices.ft = new this.SynthVoice(this, freq, velocity, type);
                // Update the frequency of all the HT oscillators because the FT is changed
                this.updateHTfrequency();
            }
        }
    }
    /**
     * Destroy a voice of the synth
     *
     * @param {tonetype} type   - If you need to destroy a FT or HT voice
     * @param {xtnum}    toneID - The ID of the voice to be destroy; the FT/HT tone number
     * @param {boolean}  panic  - If `true` tells that the message has been generated by a "hard" All-Notes-Off request.
     */
    // @old icVoiceOFF
    voiceOFF(type, toneID, panic=false) {
        if (this.status === true) {
           
            // **HT**
            if (type === "ht") {
                // Mute the voice only in there are no more HT tones with the same number
                // (in order to manage the pressure of multiple copy of the same HT on the controller eg. HT 8)
                let sameTones = this.dhc.playQueue.ht.findIndex(qt => qt.xtNum === toneID);
                if (sameTones < 0) {
                    // If there is an active voice with ctrlNoteNumber ID
                    if (this.voices.ht[toneID]) {
                        // Shut off the note playing and clear it
                        this.voices.ht[toneID].voiceMute();
                        this.voices.ht[toneID] = null;
                        delete this.voices.ht[toneID];
                    } else {
                        if (panic === false) {
                            console.log("STRANGE: there is NOT an HT active voice with ID:", toneID);
                        }
                    }
                }
            
            // **FT**
            } else {
                // Mute the voice only in there are no more HT tones with the same number
                // (in order to manage the pressure of multiple copy of the same FT on the controller)
                let sameTones = this.dhc.playQueue.ft.findIndex(qt => qt.xtNum === toneID);
                if (sameTones < 0) {
                    // Shut off the active voice and clear it
                    if (this.voices.ft) {
                        if (this.dhc.settings.ht.curr_ft === toneID) {
                            this.voices.ft.voiceMute();
                            this.voices.ft = null;
                        }
                    }
                }
            }
        }
    }
    /**
     * Turns off all the active voices
     */
    allNotesOff() {
        // Prevent HT stuck notes
        for (var i = 0; i < 128; i++) {
            if (this.voices.ht[i]) {
                this.voices.ht[i].voiceMute();
                this.voices.ht[i] = null;
                delete this.voices.ht[i];
            }
        }
        // Prevent FT stuck notes
        if (this.voices.ft) {
            this.voices.ft.voiceMute();
        }
    }
    /**
     * Update the frequency of the current playing FT oscillator
     * (on UI setting changes)
     */
    // @old icUpdateSynthFTfrequency
    updateFTfrequency() {
        if (this.voices.ft !== null) {
            var ftObj = this.dhc.tables.ft[this.dhc.settings.ht.curr_ft];
            this.voices.ft.initFrequency = ftObj.hz;
            this.voices.ft.setFrequency(true);
        }
    }
    /**
     * Update the frequencies of the current playing HT oscillators
     * (when the FT is changing or on other UI setting changes)
     */
    // @old icUpdateSynthHTfrequency
    updateHTfrequency() {
        for (const [toneID, voice] of Object.entries(this.voices.ht)) {
            // Get the data about the HT from the ht table
            var htObj = this.dhc.tables.ht[toneID];
            // Set a new osc frequency and apply the change
            voice.initFrequency = htObj.hz;
            voice.setFrequency(true);
        }
    }
    /**
     * Update the current FT or HT waveform
     *
     * @param {('sine'|'square'|'sawtooth'|'triangle')} type - Waveform type
     */
    // @old icUpdateWaveform
    updateWaveform(type) {
        // **HT**
        if (type === "ht") {
            for (var i=0; i<this.voices.ht.lenght; i++) {
                if (this.voices.ht[i] !== undefined) {
                    this.voices.ht[i].setWaveform( this.waveform[type] );
                }
            }
        // **FT**
        } else if (type === "ft") {
            if (this.voices.ft !== null) {
                this.voices.ft.setWaveform( this.waveform[type] );
            }
        }
    }
    /**
     * Update the Reverb amount mixing the wet and dry lines with an equal-power cross-fade
     *
     * @param {number} value - Reverb (wet) amount (normalized to 0.0 > 1.0)
     */
    // @old icUpdateReverb
    updateReverb(value) {
        // Wet/Dry equal-power cross-fade
        this.reverb.dry.gain.setValueAtTime(Math.cos(value * 0.5 * Math.PI), 0);
        this.reverb.wet.gain.setValueAtTime(Math.cos((1.0 - value) * 0.5 * Math.PI), 0);
    }
    /**
     * Apply the current Pitch Bend amount (from the controller) to every already active synth voices
     */
    // @old icSynthPitchBend
    updatePitchBend() {
        // If the synth is turned-on
        if (this.status === true) {
            for (var i=0; i<255; i++) {
                // For every HT active voice
                if (this.voices.ht[i]) {
                    // If the osc exist
                    if (this.voices.ht[i].osc){
                        // Detune the osc: "value" and "range" are in cents, "amount" is normalized to -1 > 0 > 0.99987792968750
                        this.voices.ht[i].osc.detune.value = this.dhc.settings.controller.pb.amount * this.dhc.settings.controller.pb.range;
                    }
                }
            }
            // If the FT voice is active
            if (this.voices.ft) {
                // If the osc exist
                if (this.voices.ft.osc){
                    // Detune the osc: "value" and "range" are in cents, "amount" is normalized to -1 > 0 > 0.99987792968750
                    this.voices.ft.osc.detune.value = this.dhc.settings.controller.pb.amount * this.dhc.settings.controller.pb.range;
                }
            }
        }
    }

    /*==============================================================================*
     * REVERB HANDLING
     *==============================================================================*/
    /**
     * Try to create a convolver node
     *
     * @return {ConvolverNode} - The new instance of a ConvolverNode (from Web Audio API)
     *
     * @throws Open an alert warning that the browser does not support convolution reverberation
     */
    tryCreateConvolver() {
        // If the convolver is not supported by the browser, create a normal gain node
        try {
            return this.audioContext.createConvolver();
        }
        catch(error) {
            alert('The reverb is not supported in this browser.');
            return this.audioContext.createGain();
        }
    }
    /**
     * Manage the event when the user is trying to load a IR Reverb file
     *
     * @param {Event} changeEvent - DOM change event on 'input' element (reverb file uploader)
     */
    // @old icHandleIrFile
    handleIrFile(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.readIrFile(changeEvent.target.files[0]);
        } else {
            alert('The File APIs are not fully supported in this browser.');
        }
    }
    /**
     * Initialize the reading process of the IR Reverb file
     *
     * @param {File} file - The file to be read
     */
    // @old icReadIrFile
    readIrFile(file) {
        let reader = new FileReader();
        // Handle loading errors
        reader.onerror = this.dhc.harmonicarium.components.backendUtils.fileErrorHandler;
        if (file) {
            // Read file into memory as ArrayBuffer     
            reader.readAsArrayBuffer(file);
            // Launch the data processing as soon as the file has been loaded
            reader.onload = (function(e){
                this.loadIrFile(e.target.result, file.name);
            }).bind(this);
        }
    }
    /**
     * Load the IR Reverb wav file on the convolver
     *
     * @param {ArrayBuffer} data     - The raw binary data of the file
     * @param {string}      fileName - The filename
     */
    // @old icProcessIrData
    loadIrFile(data, fileName) {
        this.audioContext.decodeAudioData(data, (function(buffer) {
            if (this.reverb.convolver) {
                this.reverb.convolver.buffer = buffer;
                this.dhc.harmonicarium.components.backendUtils.eventLog("IR Convolution Reverb file loaded.\n| filename: " + fileName + "\n| duration: " + Math.round(buffer.duration * 100)/100 + " sec\n| channels: " + buffer.numberOfChannels + "\n| sample rate: " + buffer.sampleRate + " Hz\n| ---------------------");
            } else {
                console.log("There is no Convolver!");
            }
        }).bind(this));
    }
    /**
     * Convert Base64 data held in a string into raw binary blob
     * 
     * @see {@link https://gist.github.com/fupslot/5015897|Based on this snippet}
     *
     * @param {Object} file      - A File-like object
     * @param {Object} file.name - Filename
     * @param {Object} file.data - Data content in Base64
     *
     * @return {Blob}             - The decoded file in a Blob object
     */
    static base64ToBlob(file) {
        // Note: doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
        var byteString = atob(file.data.split(',')[1]);
        // Separate out the mime component
        var mimeString = file.data.split(',')[0].split(':')[1].split(';')[0];
        // Write the bytes of the string to an ArrayBuffer
        var ab = new ArrayBuffer(byteString.length);
        var ia = new Uint8Array(ab);
        for (var i = 0; i < byteString.length; i++) {
            ia[i] = byteString.charCodeAt(i);
        }
        // Write the ArrayBuffer to a blob
        var bb = new Blob([ab], {type : mimeString});
        // Add the filename to the blob imitating a real file
        if (file.name) {
            bb.name = file.name;
        }
        // Return the decoded blob
        return bb;
    }

    /*==============================================================================*
     * UI CONTROLLERS
     *==============================================================================*/

    /**
     * @todo - finish the visualizer
     * Init the analyser element
     * var icAnalyser = null;
     */

    /**
     * Manage the event when the user clicks on the the status of the synth Power ON/OFF checkbox.<br>
     * Can be used as a sort of PANIC button for stuck synth Voices by the user.
     *
     * @param {Event} clickEvent - Click HTML event on 'input' element (synth  checkbox)
     */
    // @old icSynthState
    synthState(clickEvent) {
        if (clickEvent.target.checked === true) {
            this.status = true;
        } else {
            this.allNotesOff();
            this.status = false;
        }
    }

    /**
     * Initialize the Synth UI controllers  
     */
    // @old icSynthUIinit
    _initUI() {
        // Set default PORTAMENTO on UI slider
        this.uiElements.in.portamento.value = this.portamento.amount;
        this.uiElements.in.portamento.setAttribute("data-tooltip", this.portamento.amount);
        
        // Set default ATTACK and RELEASE on UI sliders
        this.uiElements.in.attack.value = this.envelope.attack;
        this.uiElements.in.attack.setAttribute("data-tooltip", this.envelope.attack + " s");
        this.uiElements.in.decay.value = this.envelope.decay;
        this.uiElements.in.decay.setAttribute("data-tooltip", this.envelope.decay + " tc");
        this.uiElements.in.sustain.value = this.envelope.sustain;
        this.uiElements.in.sustain.setAttribute("data-tooltip", this.envelope.sustain + " gain");
        this.uiElements.in.release.value = this.envelope.release;
        this.uiElements.in.release.setAttribute("data-tooltip", this.envelope.release + " s");
        
        // Set the default UI WAVEFORMS on UI dropdwon selector
        this.uiElements.in.waveformFT.value = this.waveform.ft;
        this.uiElements.in.waveformHT.value = this.waveform.ht;

        // Set the default GAIN amounts on UI sliders
        this.uiElements.in.volumeFT.value = this.volume.defaultValue;
        this.uiElements.in.volumeFT.setAttribute("data-tooltip", this.volume.defaultValue);
        this.uiElements.in.volumeHT.value = this.volume.defaultValue;
        this.uiElements.in.volumeHT.setAttribute("data-tooltip", this.volume.defaultValue);
        this.uiElements.in.volume.value = this.volume.defaultValue;
        this.uiElements.in.volume.setAttribute("data-tooltip", this.volume.defaultValue);
        
        // Set the default reverb dry/wet mix amount on UI
        this.uiElements.in.reverb.setAttribute("data-tooltip", this.reverb.defaultValue);

        // ** UI EVENT LISTENERS **
        // ---------------------

        // EventListener to keep updated the state of the ON/OFF Synth checkbox on UI
        // and prevent stuck notes.
        this.uiElements.in.power.addEventListener("click", (e) => this.synthState(e));
        // Change WAVEFORMS from UI
        this.uiElements.in.waveformFT.addEventListener("change", function(event) {
            this.waveform.ft = event.target.value;
            this.updateWaveform("ft");
        });
        this.uiElements.in.waveformHT.addEventListener("change", function(event) {
            this.waveform.ht = event.target.value;
            this.updateWaveform("ht");
        });
        // Upload a new IR reverb .wav file from UI
        this.uiElements.in.irFile.addEventListener('change', (e) => this.handleIrFile(e), false);

        // Change VOLUME from UI
        this.uiElements.in.volumeFT.addEventListener("input", function(event) {
            this.volume.ft.gain.setValueAtTime(event.target.value, 0);
            this.uiElements.in.volumeFT.setAttribute("data-tooltip", event.target.value);
        });
        this.uiElements.in.volumeHT.addEventListener("input", function(event) {
            this.volume.ht.gain.setValueAtTime(event.target.value, 0);
            this.uiElements.in.volumeHT.setAttribute("data-tooltip", event.target.value);
        });
        this.uiElements.in.volume.addEventListener("input", function(event) {
            this.volume.master.gain.setValueAtTime(event.target.value, 0);
            this.uiElements.in.volume.setAttribute("data-tooltip", event.target.value);
        });
        // Change PORTAMENTO from UI
        this.uiElements.in.portamento.addEventListener("input", function(event) {
            this.portamento.amount = event.target.value;
            this.uiElements.in.portamento.setAttribute("data-tooltip", event.target.value);
        });
        // Change ATTACK from UI
        this.uiElements.in.attack.addEventListener("input", function(event) {
            this.envelope.attack = Number(event.target.value);
            this.uiElements.in.attack.setAttribute("data-tooltip", event.target.value + " s");
        });
        // Change DECAY from UI
        this.uiElements.in.decay.addEventListener("input", function(event) {
            this.envelope.decay = Number(event.target.value);
            this.uiElements.in.decay.setAttribute("data-tooltip", event.target.value + " tc");
        });
        // Change SUSTAIN from UI
        this.uiElements.in.sustain.addEventListener("input", function(event) {
            this.envelope.sustain = Number(event.target.value);
            this.uiElements.in.sustain.setAttribute("data-tooltip", event.target.value + " gain");
        });
        // Change RELEASE from UI
        this.uiElements.in.release.addEventListener("input", function(event) {
            this.envelope.release = Number(event.target.value);
            this.uiElements.in.release.setAttribute("data-tooltip", event.target.value + " s");
        });
        // Change REVERB from UI
        this.uiElements.in.reverb.addEventListener("input", function(event) {
            // Update amount for crossfading
            this.updateReverb(event.target.value);
            this.uiElements.in.reverb.setAttribute("data-tooltip", event.target.value);
        });
        // Set the POWER ON checkbox according to the default state
        this.uiElements.in.power.checked = this.status;
    }

};

/*==============================================================================*
 * SYNTH VOICE
 *==============================================================================*/
/**
 * Class for a single voice of the Synth
 */
// @old ICvoice
HUM.Synth.prototype.SynthVoice = class {
    /**
     * @param {Synth}    synth    - The Synth instance to which it belongs
     * @param {hertz}    freq     - Frequency expressed in hertz (Hz)
     * @param {velocity} velocity - MIDI Velocity amount (from 0 to 127)
     * @param {tonetype} type     - If the voice will be a FTs or HTs
     */
    constructor(synth, freq, velocity, type) {
        /**
         * The Synth instance
         *
         * @member {Synth}
         */
        this.synth = synth;
        /**
         * The tone type of the SynthVoice
         * Type of the voice; FT or HT
         *
         * @member {Synth}
         */
        this.type = type;
        /**
         * Initial frequency expressed in hertz (Hz)
         *
         * @member {hz}
         */
        this.initFrequency = freq;
        /**
         * The oscillator
         *
         * @member {OscillatorNode}
         */
        this.osc = this.synth.audioContext.createOscillator();
        /**
         * The gain/volume to implement the Envelope Generator
         *
         * @member {OscillatorNode}
         */
        this.envelope = this.synth.audioContext.createGain();
        /**
         * The gain/volume to implement the Velocity
         * A gain to manage the final voice volume if needed (currently not used)
         *
         * @member {OscillatorNode}
         */
        this.volume = this.synth.audioContext.createGain();

        // - - - - - - - - -
        // INIT AUDIO NODES
        // - - - - - - - - -

        // Init the starting frequency to emulate the right portamento
        // for the monophonic FT and all frequency update for all voices (FT & HT)
        if (type === "ft" && this.synth.portamento.lastFreqFT) {
            // If it's an FT and it's not the first played tone:
            // Init the oscillator's start frequency to the last voiced FT
            this.osc.frequency.setValueAtTime(this.synth.portamento.lastFreqFT, 0);
        } else if (type === "ft" && !this.synth.portamento.lastFreqFT) {
            // If it's an FT and it's the first played tone:
            // Init the oscillator's start frequency to the FM (FT0)
            this.osc.frequency.setValueAtTime(this.synth.dhc.settings.fm.hz, 0);
        } // If it's an HT, the frequency is set in "this.setFrequency"
        // Set the oscillator's waveform
        this.osc.type = this.synth.waveform[type];

        // Sensitivity scale for velocity
        this.volume.gain.setValueAtTime((velocity / 127), 0);
        // The final voice volume is connected to the main FT or HT volume
        this.volume.connect( this.synth.volume[type] );
        // The envelope is connected to the volume
        this.envelope.connect( this.volume );
        // The oscillator is connected to the envelope
        this.osc.connect( this.envelope );

        // Set envelope parameters ADS (to avoid oscillator's start/stop clicks)
        // Initialize the envelope with 0 value
        // [Deprecation] .value setter smoothing is deprecated and will be removed in M64, around Jan 2018
        // [Deprecation] .setValueAtTime only does clicks (why?) I leave .value untill clicks are fixed
        this.envelope.gain.value = 0.0; // Repeated ?!
        this.envelope.gain.setValueAtTime(0, this.synth.audioContext.currentTime); // Repeated ?!

        // Call the method to tune the oscillator
        this.setFrequency(false);
        // Start the oscillator
        this.osc.start(0);

        // **ATTACK**
        // Set the time when the Attack must be completed
        var envAttackEnd = this.synth.audioContext.currentTime + this.synth.envelope.attack;
        // Go to the max gain in x seconds with a LINEAR ramp
        this.envelope.gain.linearRampToValueAtTime(1.0, envAttackEnd); // NEW
        // this.envelope.gain.setTargetAtTime(1.0, icAudioContext.currentTime, this.synth.envelope.attack / 10); // OLD
        // **DECAY** + **SUSTAIN**
        // When the Attack is concluded,
        // Decay the gain to the Sustain level > then > maintain the Sustain gain level until a .voiceMute() event
        this.envelope.gain.setTargetAtTime(this.synth.envelope.sustain, envAttackEnd, this.synth.envelope.decay + 0.001 );

        // =======================
    } // end class Constructor
    // ===========================
    
    /**
     * Set/update the voice waveform
     *
     * @param {('sine'|'square'|'sawtooth'|'triangle')} waveform - Waveform type
     */
    setWaveform(waveform) {
        this.osc.type = waveform;
    }
    /**
     * Set/update the voice frequency
     *
     * @param {boolean} update - `false`: The voice must be created.<br>
     *                           `true `: The voice must be updated.
     */
    setFrequency(update) {
        // NEW VOICE
        if (update === false) {
            if (this.type === "ft") {
                this.osc.frequency.setTargetAtTime(this.initFrequency, 0, this.synth.portamento.amount);
                this.synth.portamento.lastFreqFT = this.initFrequency;
            } else if (this.type === "ht") {
                this.osc.frequency.setValueAtTime(this.initFrequency, 0);
            }
        // UPDATE VOICE
        } else if (update === true) {
            // @todo - Apply the normal envelope ADS to the updated voice (like the "new" "ft") or implement a
            this.osc.frequency.setTargetAtTime( this.initFrequency, 0, this.synth.portamento.amount);
        }
        // APPLY CURRENT DETUNING (if present): "value" and "range" are in cents, "amount" is normalized to -1 > 0 > 0.99987792968750
        this.osc.detune.setValueAtTime((this.synth.dhc.settings.controller.pb.amount * this.synth.dhc.settings.controller.pb.range), 0);
    }
    /**
     * Turn off the sound of the Voice (release)
     */
    // @old noteOff
    voiceMute() {
        // Shutdown the envelope before stopping the oscillator (release)
        // To avoid sound artifact in case the Attack or Release are still running...
        // ...cancel the previous scheduled values (if there are)
        this.envelope.gain.cancelScheduledValues(0);
        // Read the actual gain value and make sure that it stay fixed (this clicks under Firefox)
        const val = this.envelope.gain.value > 0 ? this.envelope.gain.value : 0.0001;
        this.envelope.gain.setValueAtTime(val, 0);
        // **RELEASE**
        // Set the time when the Release must be completed
        let envReleaseEnd = this.synth.audioContext.currentTime + this.synth.envelope.release;
        // Go near to 0 gain with an EXPONENTIAL ramp
        this.envelope.gain.exponentialRampToValueAtTime(0.0001, envReleaseEnd); // NEW
        // this.envelope.gain.setTargetAtTime(0, icAudioContext.currentTime, this.synth.envelope.release / 10); // OLD
        // Stop the oscillator 0.2 second after the Release has been completed
        this.osc.stop(envReleaseEnd + 0.2);
    }
};