Source: CsoundAudioNode.js

/**
 * C S O U N D
 *
 * L I C E N S E
 *
 * This software is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This software 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
 *
 */
 
// import csound_audio_processor_module from 'CsoundAudioProcessor.js';

/**
 * Provides the Csound API as in csound_threaded.hpp, but implemented in 
 * WebAssembly. For a more complete description of what these functions 
 * do, see csound_threaded.hpp in the Csound GitHub repository, and of course 
 * the Csound API Reference. The semantics of all functions are virtually 
 * identical and the signatures are as equivalent as I could make them.
 * 
 * To conform with CsoundObj, method names in camel case are now primary, 
 * method names in initial caps defer to them.
 */
class CsoundAudioNode extends AudioWorkletNode {
    resolveCleanup(result) {
        return result;
    }
    resolveCompileCsdText(result) {
        this.message_callback("[" + window.performance.now() + " resolveCompileCsdText with: " + result + ", " + this + "]\n");
        return result;
    }
    resolveCompileOrc(result) {
        return result;
    }
    resolveGetControlChannel(result) {
        return result;
    }
    resolveGetFileData(result) {
        return result;
    }
    resolveReadScore(result) {
        return result;
    }
    resolveReset() {
        return;
    }
    resolveStop() {
        return;
    }
    async onMessage(event) {
            let data = event.data;
            switch(data[0]) {
                case "Message":
                    if (this.message_callback != null) {
                        this.message_callback(data[1]);
                    } else {
                        console.log(data[1]);
                    }
                    break;
                // Some Csound API calls should be serializable, i.e. 
                // synchronous. These cases resolve promises (above) from those calls.
                case "CleanupResult":
                    // this.message_callback("[" + window.performance.now() + " Received CleanupResult with: " + data[1] + ".]\n");
                    this.resolveCleanup(data[1]);
                    break;
                case "CompileOrcResult":
                    // this.message_callback("[" + window.performance.now() + " Received CompileOrcResult with: " + data[1] + ".]\n");
                    this.resolveCompileOrc(data[1]);
                    break;
                case "CompileCsdTextResult":
                    // this.message_callback("[" + window.performance.now() + " Received CompileCsdTextResult with: " + data[1] + ".]\n");
                    this.resolveCompileCsdText(data[1]);
                    break;
                case "GetControlChannelResult":
                     // this.message_callback("[" + window.performance.now() + " Received GetControlChannelResult with: " + data[1] + ".]\n");
                    this.resolveGetControlChannel(data[1]);
                    break;
                case "GetFileDataResult":
                    this.resolveGetFileData(data[1]);
                    break;
                case "GetScoreTimeResult":
                     // this.message_callback("[" + window.performance.now() + " Received GetScoreTimeResult with: " + data[1] + ".]\n");
                    this.resolveGetScoreTime(data[1]);
                    break;
                case "IsPlayingResult":
                    // this.message_callback("[" + window.performance.now() + " Received IsPlayingResult with: " + data[1] + ".]\n");
                    this.resolveIsPlaying(this.is_playing);
                    break;
                case "ReadScoreResult":
                    // this.message_callback("[" + window.performance.now() + " Received ReadScoreResult with: " + data[1] + ".]\n");
                    this.resolveReadScore(data[1]);
                    break;
                case "ResetResult":
                    // this.message_callback("[" + window.performance.now() + " Received ResetResult.]\n");
                    this.resolveReset();
                    break;
                case "StopResult":
                    // this.message_callback("[" + window.performance.now() + " Received StopResult.]\n");
                    this.resolveStop();
                    break;
            };
    };
    constructor(context, message_callback_, options) {
        options = options || {};
        options.numberOfInputs  = 1;
        options.numberOfOutputs = 1;
        options.outputChannelCount = [context.destination.channelCount];
        super(context, 'csound-audio-processor', options);
        this.message_callback = message_callback_;
        this.message_callback("CsoundAudioNode constructor...\n");
        this.reset_();
        this.CompileCsdTextPromise = null;
        this.CompileOrcPromise = null;
        this.IsPlayingPromise = null;
        this.StopPromise = null;
        this.CleanupPromise = null;
        this.GetFileDataPromise = null;
        this.GetScoreTimePromise = null;
        this.GetGetControlChannelPromise = null;
        this.ReadScorePromise = null;
        this.ResetPromise = null;
        this.port.onmessage = this.onMessage.bind(this);
        this.port.start();
    }
    reset_() {
        this.is_playing = false;
        this.is_realtime = false;
        this.userMediaAudioInputNode = null;
        this.input = null;
        this.output = null;
    }
    
    // NOTE: All class member function names, i.e. the actual Csound API, 
    // are declared and defined both in initial caps (as in C++), and in camel 
    // case (for compatibility with CsoundObj). If CsoundObj has a member 
    // function that is needed here and that has a different name, it should 
    // get an alias or an implementation here.

    async cleanup() {
        // this.message_callback("[" + window.performance.now() + " Cleanup.]\n");
        let promise = new Promise((resolve, reject) => {
            // Not exactly intuitive!
            this.resolveCleanup = resolve;
            this.port.postMessage(["Cleanup"]);
        });
        let result = await promise;
        // this.message_callback("[" + window.performance.now() + " Cleanup resolved with: " + result + ".]\n");
        return result;
    };
    async Cleanup() {
        await this.cleanup();
    };
    async compileCsd(filename) {
        this.port.postMessage(["CompileCsd", filename]);
    };
    async CompileCsd(filename) {
        await this.compileCsd(filename);
    };
    async compileCsdText(csd) {
        // this.message_callback("[" + window.performance.now() + " CompileCsdText.]\n");
        let promise = new Promise((resolve, reject) => {
            // Not exactly intuitive!
            this.resolveCompileCsdText = resolve;
            this.port.postMessage(["CompileCsdText", csd]);
        });
        let result = await promise;
        // this.message_callback("[" + window.performance.now() + " CompileCsdText resolved with: " + result + ".]\n");
        return result;
    };
    async CompileCsdText(csd) {
        return this.compileCsdText(csd);
    };
    async compileOrc(orc) {
        // this.message_callback("[" + window.performance.now() + " CompileOrc.]\n");
        let promise = new Promise((resolve, reject) => {
            // Not exactly intuitive!
            this.resolveCompileOrc = resolve;
            this.port.postMessage(["CompileOrc", orc]);
        });
        let result = await promise;
        // this.message_callback("[" + window.performance.now() + " CompileOrc resolved with: " + result + ".]\n");
        return result;
    };
    async CompileOrc(orc) {
        return await this.compileOrc(orc);
    };
    destroy() {
        this.port.postMessage(["Destroy"]);
    };
    Destroy() {
        this.destroy();
    };
    evalCode(code) {
        this.port.postMessage(["EvalCode", code]);
    };
    EvalCode(code) {
        this.evalCode(code);
    };
    get0dBFS() {
        this.port.postMessage(["Get0dBFS"]);
    };
    Get0dBFS() {
        this.get0dBFS();
    }
    getAPIVersion() {
        this.port.postMessage(["GetAPIVersion"]);
    };
    GetAPIVersion() {
        this.getAPIVersion();
    };
    async getControlChannel(name) {
        // this.message_callback("[" + window.performance.now() + " GetControlChannel.]\n");
        let promise = new Promise((resolve, reject) => {
            // Not exactly intuitive!
            this.resolveGetControlChannel = resolve;
            this.port.postMessage(["GetControlChannel", name]);
        });
        let result = await promise;
        // this.message_callback("[" + window.performance.now() + " GetControlChannel resolved with: " + result + ".]\n");
        return result;    
    };
    async GetControlChannel(name) {
        return this.getControlChannel(name);
    };
    async getFileData(filename) {
        // this.message_callback("[" + window.performance.now() + " GetControlChannel.]\n");
        let promise = new Promise((resolve, reject) => {
            // Not exactly intuitive!
            this.resolveGetFileData = resolve;
            this.port.postMessage(["GetFileData", filename]);
        });
        let result = await promise;
        // this.message_callback("[" + window.performance.now() + " GetControlChannel resolved with: " + result + ".]\n");
        return result;    
    };
    async GetFileData(name) {
        return this.getFileData(name);
    };
    getCurrentTimeSamples() {
        this.port.postMessage(["GetCurrentTimeSamples"]);
    };
    GetCurrentTimeSamples() {
        this.getCurrentTimeSamples();
    };
    getEnv(name) {
        this.port.postMessage(["GetEnv", name]);
    };
    GetEnv(name) {
        this.getEnv();
    };
    getInputName() {
        this.port.postMessage(["GetInputName"]);
    };
    GetInputName() {
        this.getInputName();
    };
    getKsmps() {
        this.port.postMessage(["GetKsmps"]);
    };
    GetKsmps() {
        this.getKsmps();
    };
    getNchnls() {
        this.port.postMessage(["GetNchnls"]);
    };
    GetNchnls() {
        this.getNchnls();
    };
    getNchnlsInput() {
        this.port.postMessage(["GetNchnlsInput"]);
    };
    GetNchnlsInput() {
        this.getNchnlsInput();
    };
    getOutputName() {
        this.port.postMessage(["GetOutputName"]);
    };
    GetOutputName() {
        this.getOutputName();
    };
    getScoreOffsetSeconds() {
        this.port.postMessage(["GetScoreOffsetSeconds"]);
    };
    GetScoreOffsetSeconds() {
        this.GetScoreOffsetSeconds();
    };
    async getScoreTime() {
        // this.message_callback("[" + window.performance.now() + " GetScoreTime.]\n");
        let promise = new Promise((resolve, reject) => {
            // Not exactly intuitive!
            this.resolveGetScoreTime = resolve;
            this.port.postMessage(["GetScoreTime"]);
        });
        let result = await promise;
        // this.message_callback("[" + window.performance.now() + " GetScoreTime resolved with: " + result + ".]\n");
        return result;
    };
    async GetScoreTime() {
        return await this.getScoreTime();
    };
    getSr() {
        this.port.postMessage(["GetSr"]);
    };
    GetSr() {
        this.getSr();
    };
    getStringChannel(name) {
        this.port.postMessage(["GetStringChannel", name]);
    };
    GetStringChannel(name) {
        this.getStringChannel();
    };
    getVersion() {
        this.port.postMessage(["GetVersion"]);
    };
    GetVersion() {
        this.getVersion();
    };
    inputMessage(text) {
        this.port.postMessage(["InputMessage", text]);
    };
    InputMessage(text) {
        this.inputMessage(text);
    };
    async isPlaying() {
        // this.message_callback("[" + window.performance.now() + " IsPlaying.]\n");
        let promise = new Promise((resolve, reject) => {
            // Not exactly intuitive!
            this.resolveIsPlaying = resolve;
            this.port.postMessage(["IsPlaying"]);
        });
        let result = await promise;
        // this.message_callback("[" + window.performance.now() + " IsPlaying resolved with: " + result + ".]\n");
        return result;
    };
    async IsPlaying() {
        return await this.isPlaying();
    };
    isScorePending() {
        this.port.postMessage(["IsScorePending"]);
    };
    IsScorePending() {
        this.isScorePending();
    };
    killInstance(p1, insname, mode, release) {
        this.port.postMessage(["KillInstance", p1, insname, mode, release]);
    };
    KillInstance(p1, insname, mode, release) {
        this.killInstance(p1, insname, mode, release);
    };
    message(text) {
        this.port.postMessage(["Message", text]);
    };
    Message(text) {
        this.message(text);
    };
    perform() {
        // this.message_callback("[" + window.performance.now() + " Perform.]\n");
        this.port.postMessage(["Perform"]);
    };
    Perform() {
        this.perform();
    };
    /**
     * Because AudioWorklet messages are asynchronous, a sequence 
     * of method calls cannot be guaranteed to execute in proper order. 
     * Hence, this helper.
     */
    performCsd(options, csd) {
        this.port.postMessage(["PerformCsd", options, csd]);
    }
    PerformCsd(options, csd) {
        this.performCsd(options, csd);
    };
    /**
     * Because AudioWorklet messages are asynchronous, a sequence 
     * of method calls cannot be guaranteed to execute in proper order. 
     * Hence, this helper.
     */
    performOrc(options, orc, sco) {
        this.port.postMessage(["PerformOrc", options, orc, sco]);
    };
    PerformOrc(options, orc, sco) {
        this.performOrc(options, orc, sco);
    };
    async readScore(score) {
        // this.message_callback("[" + window.performance.now() + " ReadScore.]\n");
        let promise = new Promise((resolve, reject) => {
            // Not exactly intuitive!
            this.resolveReadScore = resolve;
            this.port.postMessage(["ReadScore", score]);
        });
        let result = await promise;
        // this.message_callback("[" + window.performance.now() + " ReadScore resolved with: " + result + ".]\n");
        return result;
    };
    async ReadScore(score) {
        return await this.readScore(score);
    }
    async reset() {
        // this.message_callback("[" + window.performance.now() + " Reset.]\n");
        let promise = new Promise((resolve, reject) => {
            // Not exactly intuitive!
            this.resolveReset = resolve;
            this.port.postMessage(["Reset"]);
        });
        await promise;
        // this.message_callback("[" + window.performance.now() + " Reset resolved.]\n");
    };
    Reset() {
        this.reset();
    };
    rewindScore() {
        this.port.postMessage(["RewindScore"]);
    };
    RewindScore() {
        this.rewindScore();
    };
    setControlChannel(name, value) {
        this.port.postMessage(["SetControlChannel", name, value]);
    };
    SetControlChannel(name, value) {
        this.setControlChannel(name, value);
    };
    setGlobalEnv(name, value) {
        this.port.postMessage(["SetGlobalEnv", name, value]);
    };
    SetGlobalEnv(name, value) {
        this.setGlobalEnv(name, value);
    };
    setInput(name) {
        this.input = name;
        this.port.postMessage(["SetInput", name]);
    };
    SetInput(name) {
        this.setInput(name);
    };
    setMessageCallback(message_callback_) {
        this.message_callback = message_callback_;
    };
    SetMessageCallback(message_callback_) {
        this.setMessageCallback(message_callback_);
    };
    setOption(option) {
        if (option.startsWith("-odac")) {
            this.output = option.substr(2);
        }
        if (option.startsWith("-iadc")) {
            this.input = option.substr(2);
        }
        this.port.postMessage(["SetOption", option]);
    };
    SetOption(option) {
        this.setOption(option);
    };
    setOutput(name, type, format) {
        this.output = name;
        this.port.postMessage(["SetOutput", name, type, format]);
    };
    SetOutput(name, type, format) {
        this.setOutput(name, type, format);
    };
    SetScoreOffsetSeconds(seconds) {
        this.port.postMessage(["SetScoreOffsetSeconds", seconds]);
    };
    setScorePending(is_pending) {
        this.port.postMessage(["SetScorePending", is_pending]);
    };
    SetScorePending(is_pending) {
        this.setScorePending(is_pending);
    };
    setStringChannel(name, value) {
        this.port.postMessage(["SetStringChannel", name, value]);
    };
    SetStringChannel(name, value) {
        this.setStringChannel(name, value);
    };
    /**
     * Starts the Csound performance with or without any connection to the 
     * Web Audio signal flow graph. Such connections may be created in the 
     * usual manner for constructing Web Audio graphs, either before, or 
     * after, calling StartNode.
     */
    startNode() {
        // this.message_callback("[" + window.performance.now() + " StartNode.]\n");
        try {
            this.port.postMessage(["Start"]);
            this.is_playing = true;
            this.message_callback("is_playing...\n");
        } catch (e) {
            this.message_callback(e);
        }
    }
    startNode() {
        this.StartNode();
    }
    /**
     * First connects to the default WebAudio output and the WebAudio 
     * context's media source input, if it exists, and the MIDI input, 
     * if it exists, then starts the Csound performance. Wiring into the Web 
     * Audio graph is up here in the upper half, wiring within Csound is down 
     * in the lower half.
     */
    async start() {
        // this.message_callback("[" + window.performance.now() + " Start.]\n");
        try {
            let device_list = await navigator.mediaDevices.enumerateDevices();
            var message_callback_ = this.message_callback;
            var index = 0;
            var input_connected = false;
            var print_device = function(device) {
                message_callback_("mediaDevices: " + index + " " + device.kind + ": " + device.label + "\n");
                index++;
            };     
            device_list.forEach(print_device);
            this.message_callback("WebAudio frames per second:         " +  this.context.sampleRate + "\n");
            this.message_callback("WebAudio maximum output channels:   " +  this.context.destination.maxChannelCount + "\n");
            this.connect(this.context.destination);
            if (navigator.requestMIDIAccess) {
              let midi_access = await navigator.requestMIDIAccess({sysex:false});
              const inputs = midi_access.inputs.values();
              let thus = this;
              for (let entry of midi_access.inputs) {
                  const midi_input = entry[1];
                  message_callback_("MIDI port: type: " + midi_input.type + "  manufacturer: " + midi_input.manufacturer + " name: " + midi_input.name +
                      " version: " + midi_input.version + "\n");
                  // Using the MessagePort for this is probably not good enough.
                  midi_input.onmidimessage = function(event) {
                      console.log(event, event.data[0], event.data[1], event.data[2]);
                      thus.port.postMessage(["MidiEvent", event.data[0], event.data[1], event.data[2]]);
                  };
              }
              for (let entry of midi_access.outputs) {
                  var port_ = entry[1];
                  message_callback_( "MIDI port: type: " + port_.type + " manufacturer: " + port_.manufacturer + " name: " + port_.name +
                    " version: " + port_.version + "\n");
              }
            }
            // Try to obtain the Web browser audio input, if available.
            // Not to be confused with any other audio input interfaces on the 
            // computer, which are inputs in the device list above!
            try {
                this.message_callback("Trying to open browser audio input...\n")
                let stream = await navigator.mediaDevices.getUserMedia({audio: true});
                this.userMediaAudioInputNode = this.context.createMediaStreamSource(stream);
                this.message_callback("WebAudio UserMedia outputs:         " +  this.userMediaAudioInputNode.numberOfOutputs + "\n");
                this.userMediaAudioInputNode.connect(this);
                this.message_callback("Audio input initialized.\n");
            } catch (e) {
                this.message_callback(e + "\n");
            }
            this.port.postMessage(["Start"]);
            this.is_playing = true;
            this.message_callback("is_playing...\n");
        } catch (e) {
            this.message_callback(e);
        }
    }
    async Start() {
        await this.start();
    };
    async stop() {
        this.message_callback("[" + window.performance.now() + " Stop.]\n");
        let promise = new Promise((resolve, reject) => {
            // Not exactly intuitive!
            this.resolveStop = resolve;
            this.port.postMessage(["Stop"]);
            if (this.userMediaAudioInputNode !== null) {
                ///this.userMediaAudioInputNode.stop();
                this.userMediaAudioInputNode.disconnect(this);
            }
            this.disconnect();
            this.reset_();
        });
        await promise;
        this.message_callback("[" + window.performance.now() + " Stop resolved.]\n");
    };
    async Stop() {
        await this.stop();
    };
    tableGet(number, index) {
        this.port.postMessage(["TableGet", number, index]);
    };
    TableGet(number, index) {
        this.tableGet(number, index);
    };
    tableLength(number) {
        this.port.postMessage(["TableLength", number]);
    };
    TableLength(number) {
        this.tableLength(number);
    };
    tableSet(number, index, value) {
        this.port.postMessage(["TableSet", index, value]);
    };   
    TableSet(number, index, value) {
        this.tableSet(number, index, value);
    };
}