Source: statefulpatterns.mjs

/*
 * STATEFULPATTERNS MODULE FOR STRUDEL
 *
 * Author: Michael Gogins
 * 
 * This module implements "stateful Patterns" by defining a base class, 
 * `StatefulPatterns`, that knows how to register class methods as Patterns.
 */
let csound = globalThis.__csound__;
let csoundac = globalThis.__csoundac__;
let audioContext = new AudioContext();

/**
 * Sets the level of diagnostic messages in this module.
 */
let diagnostic_level_ = 5;

/**
 * Gets and/or sets the level of diagnostic messages.
 * 
 * @param {number} new_level The new diagnostic level.
 * @returns {number} The previous diagnostic level.
 */
export function diagnostic_level(new_level) {
    let old_level = diagnostic_level_;
    if (typeof new_level !== 'undefined') {
        diagnostic_level_ = new_level;
    }
    return old_level;
};

export const ALWAYS = 5;
export const DEBUG = 4;
export const INFORMATION = 3;
export const WARNING = 2;
export const ERROR = 1;
export const NEVER = 0;

/**
 * Prints a diagnostic message to both the Strudel logger and the Csound 
 * log. Messages are printed only for the specifed diagnostic level or less.
 *
 * @param {string} message Text of the message.
 * @param {number} level Diagnostic level, defaulting to WARNING.
 */
export function diagnostic(message, level = WARNING) {
    if (level >= diagnostic_level_) {
        const text = 'L' + level + ' ' + audioContext.currentTime.toFixed(4) + ' [csac]' + message;
        logger(text, 'debug');
        if (csound) csound.message(text);
    }
};

/**
 * This is a base class that can be used to _automatically_ define Patterns 
 * that hold state between queries. Derived classes, which must be defined at 
 * module scope, must in their constructor call `this.registerPatterns`, which 
 * will automatically register (most of) of their methods as Strudel Patterns, 
 * each of which takes an instance of the class as a first parameter, and the 
 * Pattern as the last parameter. Class methods must have the following syntax 
 * and semantics:
 * <pre>
 * Class.Pat(is_onset, [0 or more arguments to be patternified], hap) {...}
 * </pre>
 * Strudel will pass `true` for `is_onset` on the onset of the Pattern's cycle, 
 * and `false` for `is_onset` for every query in that cycle. Therefore, the 
 * class method must update its state if `is_onset` is true, and return the 
 * hap, without changing its value; and if 'is_onset' is false, the method must 
 * update and return the hap, and its usually new value.
 *
 * In this way, derived classes act like stateful values that have Pattern 
 * methods as class methods.
 */
export class StatefulPatterns {
    constructor() {
    }
    registerPatterns() {
        for (let name of Object.getOwnPropertyNames(Object.getPrototypeOf(this))) {
            let method = this[name];
            if ((method instanceof Function) &&
                (method.name !== this.constructor.name) && 
                (method.name !== 'registerMethods')) {
                let instance = this;
                // Problem: the Pattern function must explicitly declare its 
                // parameters. We can't push that information from the class 
                // method Function object into the Pattern function 
                // declaration, but we do know the arity of the class method, 
                // which is always at least 1 because of the need to pass the 
                // Hap.
                let arity = method.length;
                // For now, we will set up separate registrations for the 
                // first few arities. The actual arity of the Pattern function 
                // is always at least 3 because of the need to pass the class 
                // instance, the is_onset flag, and the Hap along with any 
                // patternifiable rguments.
                arity = arity + 1;
                if (arity === 3) {
                    let registration = register(name, (stateful, pat) => {
                        return pat.onTrigger((t, hap) => {
                            method.call(stateful, true, hap);
                            if (diagnostic_level_ >= DEBUG) diagnostic('[registerStateful][' + method.name + '] onset:' + JSON.stringify({x, stateful}, null, 4) + '\n');
                        }, false).withHap((hap) => {
                            stateful.current_time = getAudioContext().currentTime;
                            if (diagnostic_level_ >= DEBUG) diagnostic('[registerStateful][' + method.name + '] value:' + JSON.stringify({x, stateful}, null, 4) + '\n');
                            hap = method.call(stateful, false, hap);
                            return hap;
                        });
                    });
                    // There are no dynamic exports in JavaScript, so we just stuff 
                    // these into the window scope as global functions.
                    window[name] = registration;
                } else if (arity === 4) {
                    let registration = register(name, (stateful, p2, pat) => {
                        return pat.onTrigger((t, hap) => {
                            method.call(stateful, true, p2, hap);
                            if (diagnostic_level_ >= DEBUG) diagnostic('[registerStateful][' + method.name + '] onset:' + JSON.stringify({x, stateful}, null, 4) + '\n');
                        }, false).withHap((hap) => {
                            stateful.current_time = getAudioContext().currentTime;
                            if (diagnostic_level_ >= DEBUG) diagnostic('[registerStateful][' + method.name + '] value:' + JSON.stringify({x, stateful}, null, 4) + '\n');
                            hap = method.call(stateful, false, p2, hap);
                            return hap;
                        });
                    });
                    window[name] = registration;
                } else if (arity === 5) {
                    let registration = register(name, (stateful, p2, p3, pat) => {
                        return pat.onTrigger((t, hap) => {
                            method.call(stateful, true, p2, p3, hap);
                            if (diagnostic_level_ >= DEBUG) diagnostic('[registerStateful][' + method.name + '] onset:' + JSON.stringify({x, stateful}, null, 4) + '\n');
                        }, false).withHap((hap) => {
                            stateful.current_time = getAudioContext().currentTime;
                            if (diagnostic_level_ >= DEBUG) diagnostic('[registerStateful][' + method.name + '] value:' + JSON.stringify({x, stateful}, null, 4) + '\n');
                            hap = method.call(stateful, false, p2, p3, hap);
                            return hap;
                        });
                    });
                    window[name] = registration;
               } else if (arity === 6) {
                    let registration = register(name, (stateful, p2, p3, p4, pat) => {
                        return pat.onTrigger((t, hap) => {
                            method.call(stateful, true, p2, p3, p4, hap);
                            if (diagnostic_level_ >= DEBUG) diagnostic('[registerStateful][' + method.name + '] onset:' + JSON.stringify({x, stateful}, null, 4) + '\n');
                        }, false).withHap((hap) => {
                            stateful.current_time = getAudioContext().currentTime;
                            if (diagnostic_level_ >= DEBUG) diagnostic('[registerStateful][' + method.name + '] value:' + JSON.stringify({x, stateful}, null, 4) + '\n');
                            hap = method.call(stateful, false, p2, p3, p4, hap);
                            return hap;
                        });
                    });
                    window[name] = registration;
                }
            }
        }
    }
}