/* *
 *
 *  (c) 2009-2024 Øystein Moseng
 *
 *  Class representing an Instrument with mappable parameters for sonification.
 *
 *  License: www.highcharts.com/license
 *
 *  !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
 *
 * */
'use strict';
import SynthPatch from './SynthPatch.js';
import InstrumentPresets from './InstrumentPresets.js';
import U from '../../Core/Utilities.js';
const { defined, extend } = U;
/**
 * The SonificationInstrument class. This class represents an instrument with
 * mapping capabilities. The instrument wraps a SynthPatch object, and extends
 * it with functionality such as panning, tremolo, and global low/highpass
 * filters.
 *
 * @sample highcharts/sonification/instrument-raw
 *         Using SonificationInstrument directly, with no chart.
 *
 * @requires modules/sonification
 *
 * @class
 * @name Highcharts.SonificationInstrument
 *
 * @param {AudioContext} audioContext
 *        The AudioContext to use.
 * @param {AudioNode} outputNode
 *        The destination node to connect to.
 * @param {Highcharts.SonificationInstrumentOptionsObject} options
 *        Configuration for the instrument.
 */
class SonificationInstrument {
    constructor(audioContext, outputNode, options) {
        this.audioContext = audioContext;
        this.curParams = {};
        this.midiTrackName = options.midiTrackName;
        this.masterVolNode = new GainNode(audioContext);
        this.masterVolNode.connect(outputNode);
        this.volumeNode = new GainNode(audioContext);
        this.createNodesFromCapabilities(extend({
            pan: true
        }, options.capabilities || {}));
        this.connectCapabilityNodes(this.volumeNode, this.masterVolNode);
        this.synthPatch = new SynthPatch(audioContext, typeof options.synthPatch === 'string' ?
            InstrumentPresets[options.synthPatch] : options.synthPatch);
        this.midiInstrument = this.synthPatch.midiInstrument || 1;
        this.synthPatch.startSilently();
        this.synthPatch.connect(this.volumeNode);
    }
    /**
     * Set the overall volume.
     * @function Highcharts.SonificationInstrument#setMasterVolume
     * @param {number} volume The volume to set, from 0 to 1.
     */
    setMasterVolume(volume) {
        this.masterVolNode.gain.setTargetAtTime(volume, 0, SonificationInstrument.rampTime);
    }
    /**
     * Schedule an instrument event at a given time offset, whether it is
     * playing a note or changing the parameters of the instrument.
     * @function Highcharts.SonificationInstrument#scheduleEventAtTime
     * @param {number} time Time is given in seconds, where 0 is now.
     * @param {Highcharts.SonificationInstrumentScheduledEventOptionsObject} params
     * Parameters for the instrument event.
     */
    scheduleEventAtTime(time, params) {
        const mergedParams = extend(this.curParams, params), freq = defined(params.frequency) ?
            params.frequency : defined(params.note) ?
            SonificationInstrument.musicalNoteToFrequency(params.note) :
            220;
        if (defined(freq)) {
            this.synthPatch.playFreqAtTime(time, freq, mergedParams.noteDuration);
        }
        if (defined(mergedParams.tremoloDepth) ||
            defined(mergedParams.tremoloSpeed)) {
            this.setTremoloAtTime(time, mergedParams.tremoloDepth, mergedParams.tremoloSpeed);
        }
        if (defined(mergedParams.pan)) {
            this.setPanAtTime(time, mergedParams.pan);
        }
        if (defined(mergedParams.volume)) {
            this.setVolumeAtTime(time, mergedParams.volume);
        }
        if (defined(mergedParams.lowpassFreq) ||
            defined(mergedParams.lowpassResonance)) {
            this.setFilterAtTime('lowpass', time, mergedParams.lowpassFreq, mergedParams.lowpassResonance);
        }
        if (defined(mergedParams.highpassFreq) ||
            defined(mergedParams.highpassResonance)) {
            this.setFilterAtTime('highpass', time, mergedParams.highpassFreq, mergedParams.highpassResonance);
        }
    }
    /**
     * Schedule silencing the instrument at a given time offset.
     * @function Highcharts.SonificationInstrument#silenceAtTime
     * @param {number} time Time is given in seconds, where 0 is now.
     */
    silenceAtTime(time) {
        this.synthPatch.silenceAtTime(time);
    }
    /**
     * Cancel currently playing sounds and any scheduled actions.
     * @function Highcharts.SonificationInstrument#cancel
     */
    cancel() {
        this.synthPatch.mute();
        [
            this.tremoloDepth && this.tremoloDepth.gain,
            this.tremoloOsc && this.tremoloOsc.frequency,
            this.lowpassNode && this.lowpassNode.frequency,
            this.lowpassNode && this.lowpassNode.Q,
            this.highpassNode && this.highpassNode.frequency,
            this.highpassNode && this.highpassNode.Q,
            this.panNode && this.panNode.pan,
            this.volumeNode.gain
        ].forEach((p) => (p && p.cancelScheduledValues(0)));
    }
    /**
     * Stop instrument and destroy it, cleaning up used resources.
     * @function Highcharts.SonificationInstrument#destroy
     */
    destroy() {
        this.cancel();
        this.synthPatch.stop();
        if (this.tremoloOsc) {
            this.tremoloOsc.stop();
        }
        [
            this.tremoloDepth, this.tremoloOsc, this.lowpassNode,
            this.highpassNode, this.panNode, this.volumeNode,
            this.masterVolNode
        ].forEach(((n) => n && n.disconnect()));
    }
    /**
     * Schedule a pan value at a given time offset.
     * @private
     */
    setPanAtTime(time, pan) {
        if (this.panNode) {
            this.panNode.pan.setTargetAtTime(pan, time + this.audioContext.currentTime, SonificationInstrument.rampTime);
        }
    }
    /**
     * Schedule a filter configuration at a given time offset.
     * @private
     */
    setFilterAtTime(filter, time, frequency, resonance) {
        const node = this[filter + 'Node'], audioTime = this.audioContext.currentTime + time;
        if (node) {
            if (defined(resonance)) {
                node.Q.setTargetAtTime(resonance, audioTime, SonificationInstrument.rampTime);
            }
            if (defined(frequency)) {
                node.frequency.setTargetAtTime(frequency, audioTime, SonificationInstrument.rampTime);
            }
        }
    }
    /**
     * Schedule a volume value at a given time offset.
     * @private
     */
    setVolumeAtTime(time, volume) {
        if (this.volumeNode) {
            this.volumeNode.gain.setTargetAtTime(volume, time + this.audioContext.currentTime, SonificationInstrument.rampTime);
        }
    }
    /**
     * Schedule a tremolo configuration at a given time offset.
     * @private
     */
    setTremoloAtTime(time, depth, speed) {
        const audioTime = this.audioContext.currentTime + time;
        if (this.tremoloDepth && defined(depth)) {
            this.tremoloDepth.gain.setTargetAtTime(depth, audioTime, SonificationInstrument.rampTime);
        }
        if (this.tremoloOsc && defined(speed)) {
            this.tremoloOsc.frequency.setTargetAtTime(15 * speed, audioTime, SonificationInstrument.rampTime);
        }
    }
    /**
     * Create audio nodes according to instrument capabilities
     * @private
     */
    createNodesFromCapabilities(capabilities) {
        const ctx = this.audioContext;
        if (capabilities.pan) {
            this.panNode = new StereoPannerNode(ctx);
        }
        if (capabilities.tremolo) {
            this.tremoloOsc = new OscillatorNode(ctx, {
                type: 'sine',
                frequency: 3
            });
            this.tremoloDepth = new GainNode(ctx);
            this.tremoloOsc.connect(this.tremoloDepth);
            this.tremoloDepth.connect(this.masterVolNode.gain);
            this.tremoloOsc.start();
        }
        if (capabilities.filters) {
            this.lowpassNode = new BiquadFilterNode(ctx, {
                type: 'lowpass',
                frequency: 20000
            });
            this.highpassNode = new BiquadFilterNode(ctx, {
                type: 'highpass',
                frequency: 0
            });
        }
    }
    /**
     * Connect audio node chain from output down to input, depending on which
     * nodes exist.
     * @private
     */
    connectCapabilityNodes(input, output) {
        [
            this.panNode,
            this.lowpassNode,
            this.highpassNode,
            input
        ].reduce((prev, cur) => (cur ?
            (cur.connect(prev), cur) :
            prev), output);
    }
    /**
     * Get number of notes from C0 from a string like "F#4"
     * @static
     * @private
     */
    static noteStringToC0Distance(note) {
        // eslint-disable-next-line require-unicode-regexp
        const match = note.match(/^([a-g][#b]?)([0-8])$/i), semitone = match ? match[1] : 'a', wholetone = semitone[0].toLowerCase(), accidental = semitone[1], octave = match ? parseInt(match[2], 10) : 4, accidentalOffset = accidental === '#' ?
            1 : accidental === 'b' ? -1 : 0;
        return ({
            c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11
        }[wholetone] || 0) + accidentalOffset + octave * 12;
    }
    /**
     * Convert a note value to a frequency.
     * @static
     * @function Highcharts.SonificationInstrument#musicalNoteToFrequency
     * @param {string|number} note
     * Note to convert. Can be a string 'c0' to 'b8' or a number of semitones
     * from c0.
     * @return {number} The converted frequency
     */
    static musicalNoteToFrequency(note) {
        const notesFromC0 = typeof note === 'string' ?
            this.noteStringToC0Distance(note) : note;
        return 16.3516 * Math.pow(2, Math.min(notesFromC0, 107) / 12);
    }
}
SonificationInstrument.rampTime = SynthPatch.stopRampTime / 4;
/* *
 *
 *  Default Export
 *
 * */
export default SonificationInstrument;
/* *
 *
 *  API definitions
 *
 * */
/**
 * Capabilities configuration for a SonificationInstrument.
 * @requires modules/sonification
 * @interface Highcharts.SonificationInstrumentCapabilitiesOptionsObject
 */ /**
* Whether or not instrument should be able to pan. Defaults to `true`.
* @name Highcharts.SonificationInstrumentCapabilitiesOptionsObject#pan
* @type {boolean|undefined}
*/ /**
* Whether or not instrument should be able to use tremolo effects. Defaults
* to `false`.
* @name Highcharts.SonificationInstrumentCapabilitiesOptionsObject#tremolo
* @type {boolean|undefined}
*/ /**
* Whether or not instrument should be able to use filter effects. Defaults
* to `false`.
* @name Highcharts.SonificationInstrumentCapabilitiesOptionsObject#filters
* @type {boolean|undefined}
*/
/**
 * Configuration for a SonificationInstrument.
 * @requires modules/sonification
 * @interface Highcharts.SonificationInstrumentOptionsObject
 */ /**
* The synth configuration for the instrument. Can be either a string,
* referencing the instrument presets, or an actual SynthPatch configuration.
* @name Highcharts.SonificationInstrumentOptionsObject#synthPatch
* @type {Highcharts.SonificationSynthPreset|Highcharts.SynthPatchOptionsObject}
* @sample highcharts/demo/all-instruments
*      All instrument presets
* @sample highcharts/sonification/custom-instrument
*      Custom instrument preset
*/ /**
* Define additional capabilities for the instrument, such as panning, filters,
* and tremolo effects.
* @name Highcharts.SonificationInstrumentOptionsObject#capabilities
* @type {Highcharts.SonificationInstrumentCapabilitiesOptionsObject|undefined}
*/ /**
* A track name to use for this instrument in MIDI export.
* @name Highcharts.SonificationInstrumentOptionsObject#midiTrackName
* @type {string|undefined}
*/
/**
 * Options for a scheduled event for a SonificationInstrument
 * @requires modules/sonification
 * @interface Highcharts.SonificationInstrumentScheduledEventOptionsObject
 */ /**
* Number of semitones from c0, or a note string - such as "c4" or "F#6".
* @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#note
* @type {number|string|undefined}
*/ /**
* Note frequency in Hertz. Overrides note, if both are given.
* @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#frequency
* @type {number|undefined}
*/ /**
* Duration to play the note in milliseconds. If not given, the note keeps
* playing indefinitely
* @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#noteDuration
* @type {number|undefined}
*/ /**
* Depth/intensity of the tremolo effect - which is a periodic change in
* volume. From 0 to 1.
* @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#tremoloDepth
* @type {number|undefined}
*/ /**
* Speed of the tremolo effect, from 0 to 1.
* @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#tremoloSpeed
* @type {number|undefined}
*/ /**
* Stereo panning value, from -1 (left) to 1 (right).
* @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#pan
* @type {number|undefined}
*/ /**
* Volume of the instrument, from 0 to 1. Can be set independent of the
* master/overall volume.
* @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#volume
* @type {number|undefined}
*/ /**
* Frequency of the lowpass filter, in Hertz.
* @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#lowpassFreq
* @type {number|undefined}
*/ /**
* Resonance of the lowpass filter, in dB. Can be negative for a dip, or
* positive for a bump.
* @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#lowpassResonance
* @type {number|undefined}
*/ /**
* Frequency of the highpass filter, in Hertz.
* @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#highpassFreq
* @type {number|undefined}
*/ /**
* Resonance of the highpass filter, in dB. Can be negative for a dip, or
* positive for a bump.
* @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#highpassResonance
* @type {number|undefined}
*/
(''); // Keep above doclets in JS file
