Source: csoundac.mjs

/*
 * CSOUNDAC MODULE FOR STRUDEL
 *
 * Author: Michael Gogins
 * 
 * [csound-ac](https://github.com/gogins/csound-ac), or CsoundAC, is a C++ 
 * algorithmic composition library designed for use with Csound.
 *
 * [csound-wasm](https://github.com/gogins/csound-wasm) is a WebAssembly 
 * library containing both Csound and CsoundAC, with a JavaScript API, 
 * designed for use in Web browsers and npm applications.
 *
 * This module brings chords and scales, and operations upon them, 
 * from the CsoundAC library for algorithmic composition into the 
 * Strudel (Tidal Cycles-based) JavaScript pattern language. This is 
 * done by deriving from the StatefulPatterns class new classes whose 
 * member functions become Patterns.
 *
 * Another use of StatefulPatterns is to define algorithmic note generators, 
 * often driven by a `pure` pattern that acts as a clock.
 *
 * Please note, however, that this module, although it defines a number of 
 * Patterns, is not built into Strudel and is designed to be dynamically 
 * imported in patches created by users in the Strudel REPL. Therefore, code 
 * in this module, as with all other modules directly imported in code 
 * run by the Strudel REPL, must not use template strings.
 */

/**
 * Global instance of Csound, shared with Strudel.
 */
let csound = globalThis.__csound__;
/**
 * Global instance of CsoundAC.
 */
let csoundac = globalThis.__csoundac__;
/**
 * Global reference to the cloud-5 parameters addon.
 */
let parameters = globalThis.__parameters__;

let audioContext = new AudioContext();

import {diagnostic, diagnostic_level, ALWAYS, DEBUG, INFORMATION, WARNING, ERROR, NEVER, StatefulPatterns} from '../statefulpatterns.mjs';
export {diagnostic, diagnostic_level, ALWAYS, DEBUG, INFORMATION, WARNING, ERROR, NEVER, StatefulPatterns};

/**
 * Similar to `arrange,` but permits a section to be silenced by setting its 
 * number of cycles to 0; useful for assembling Patterns into longer-form 
 * compositions.
 * 
 * @param  {...any} sections An array of arrays, in the format 
 * `[[cycles, Pattern],...]`. 
 * @returns {Pattern} A Pattern.
 */
export function track(...sections) {
    sections = sections.filter(function(element) {
        return element[0] >= 1;
    });
    const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
    sections = sections.map(([cycles, section]) => [cycles, section.fast(cycles)]);
    return timeCat(...sections).slow(total);
};

/**
 * Returns the frequency corresponding to any of various ways that pitch 
 * is represented in Strudel events.
 *
 * @param {Hap} hap A Hap that has some sort of pitch. 
 * @returns {number} Its frequency in cycles per second.
 */
const getFrequency = (hap) => {
    let {
        value,
        context
    } = hap;
    // if value is number => interpret as midi number as long as its not marked as frequency
    if (typeof value === 'object') {
        if (value.freq) {
            return value.freq;
        }
        return getFreq(value.note || value.n || value.value);
    }
    if (typeof value === 'number' && context.type !== 'frequency') {
        value = midiToFreq(hap.value);
    } else if (typeof value === 'string' && isNote(value)) {
        value = midiToFreq(noteToMidi(hap.value));
    } else if (typeof value !== 'number') {
        throw new Error('not a note or frequency: ' + value);
    }
    return value;
};

/**
 * A utility that assigns a pitch represented as a MIDI key number to the Hap, 
 * using the existing pitch property if it exists.
 *
 * @param {Hap} hap The Hap.
 * @param {number} midi_key A MIDI key number.
 * @returns {Hap} A new Hap.
 */
export function setPitch(hap, midi_key) {
    if (typeof hap.value === 'undefined') {
        hap.value = midi_key;
    } else if (typeof hap.value === 'object') {
        if (typeof hap.value.freq !== 'undefined') {
            hap.value.freq = midiToFreq(midi_key);
        } else if (typeof hap.value.note !== 'undefined') {
            hap.value.note = midi_key;
        } else if (typeof hap.value.n !== 'undefined') {
            hap.value.n = midi_key;
        }
    } else {
        // Number or string all get the MIDI key.
        hap.value = midi_key;
    } 
    return hap;
}

/**
 * A utility that returns the MIDI key number for a frequency in Hz, 
 * as a real number allowing fractions for microtones.
 *
 * @param {number} frequency The frequency in cycles per second.
 * @returns {number} A (possibly fractional) MIDI key number.
 */
export function frequencyToMidiReal(frequency) {
    const middle_c = 261.62558;
    let octave_ = Math.log(frequency / middle_c) / Math.log(2.) + 8.;
    let midi_key = octave_ * 12. - 36.;
    return midi_key;
}

/**
 * A utility that returns the MIDI key number for a frequency in Hz, 
 * as the nearest integer.
 * 
 * @param {number} frequency The frequency in cycles per second.
 * @returns {number} The MIDI key number as an integer.
 */
export function frequencyToMidiInteger(frequency) {
    let midi_key = frequencyToMidiReal(frequency);
    return Math.round(midi_key);
}

/**
 * A utility for making a _value_ copy of a Chord (or a Scale, which 
 * is derived from Chord). Object b is resized to the size of a, and a's 
 * pitches are copied to b. Currently, only pitches are copied.
 * 
 * @param {Chord} a The source Chord (or Scale).
 * @param {Chord} b The target Chord (or Scale).
 */
export function Clone(a, b) {
    b.resize(a.voices())
    for (let voice = 0; voice < a.voices(); ++voice) {
        let a_pitch = a.getPitch(voice);
        let b_pitch = b.getPitch(voice);
        b.setPitch(voice, a_pitch);
        if (diagnostic_level() >= DEBUG) registerPatterns(['[voice ', voice, 'a:', a_pitch, 'old b:', b_pitch, 'new b:', b.getPitch(voice), '\n'].join(' '));
    }
}

export function print_counter(pattern, counter, value) {
    if (value.constructor.name === 'Hap') {
        diagnostic('[' + pattern + '] sync: counter: ' + counter + ' value: ' + value.show() + '\n', ALWAYS);
    } else if (value.constructor.name === 'Chord') {
        diagnostic('[' + pattern + '] sync: counter: ' + counter + ' value: ' + value.toString() + '\n', ALWAYS);
    } else {
        diagnostic('[' + pattern + '] sync: counter: ' + counter + ' value: ' + value + '\n', ALWAYS);
    }
}

let instrument_count = 10;

export function set_instrument_count(new_count) {
    let old_count = instrument_count;
    instrument_count = new_count;
    return old_count;
}
/**
 * Returns the RGB color for an HSV color.
 * 
 * @param {number} h The hue.
 * @param {number} s The saturation.
 * @param {number} v The value.
 * @returns {Array} The RGB color.
 */
export function hsvToRgb(h,s,v) {
  var rgb, i, data = [];
  if (s === 0) {
    rgb = [v,v,v];
  } else {
    h = h / 60;
    i = Math.floor(h);
    data = [v*(1-s), v*(1-s*(h-i)), v*(1-s*(1-(h-i)))];
    switch(i) {
      case 0:
        rgb = [v, data[2], data[0]];
        break;
      case 1:
        rgb = [data[1], v, data[0]];
        break;
      case 2:
        rgb = [data[0], v, data[2]];
        break;
      case 3:
        rgb = [data[0], data[1], v];
        break;
      case 4:
        rgb = [data[2], data[0], v];
        break;
      default:
        rgb = [v, data[0], data[1]];
        break;
    }
  }
  return '#' + rgb.map(function(x){ 
    return ('0a' + Math.round(x*255).toString(16)).slice(-2);
  }).join('');
};

let csoundn_counter = 0;

/**
 * @function csoundn
 * 
 * @description A Pattern that sends notes to Csound for rendering with MIDI 
 * semantics. Hap values are translated to Csound pfields as follows:
 * <pre>
 *  p1 -- Csound instrument either as a number (1-based, can be a fraction),
 *        or as a string name.
 *  p2 -- time in beats (usually seconds) from start of performance.
 *  p3 -- duration in beats (usually seconds).
 *  p4 -- MIDI key number from Strudel's Hap value (as a real number, not an 
 *        integer, in [0, 127].
 *  p5 -- MIDI velocity from Strudel's `gain` control (as a real number, not 
 *        an integer, in [0, 127].
 *  p6 -- Spatial depth dimension, from a `depth` control, defaulting to 0.
 *  p7 -- Spatial pan dimension, from Strudel's `pan` control, in [0, 1],
 *        defaulting to 0.5.
 *  p8 -- Spatial height dimension, from a `height` control, defaulting to 0.
 * </pre>
 * @param {number} instrument The Csound instrument number (p1); may be patternified.
 * @param {Pattern} pat The target of this Pattern.
 */
export const csoundn = register('csoundn', (instrument, pat) => {
    let p1;
    if (typeof instrument === 'string') {
        p1 = '\"' + instrument + '\"';
    } else {
        p1 = instrument;
    }
    return pat.onTrigger((tidal_time, hap) => {
        try {
            if (!csound) {
              diagnostic('[csoundn]: Csound is not yet loaded.\n', WARNING);
              return;
            }
            // Time in seconds counting from now.
            let p2 = tidal_time - getAudioContext().currentTime;
            if (p2 < 0) {
                p2 = 0;
            }
            const p3 = hap.duration.valueOf() + 0;
            const frequency = getFrequency(hap);
            // Translate frequency to MIDI key number _without_ rounding.
            const C4 = 261.62558;
            let octave = Math.log(frequency / C4) / Math.log(2.0) + 8.0;
            const p4 = octave * 12.0 - 36.0;
            // We prefer floating point precision, but over the MIDI range [0, 127].
            ///const p5 = 127 * (hap.context?.velocity ?? 0.9);
            let gain;
            if (typeof hap.value.gain === 'undefined') {
                gain = .9;
            } else {
                gain = hap.value.gain;
            }
            let p5 = 127 * gain;
            let p6;
            if (typeof hap.value.depth === 'undefined') {
                p6 = 0;
            } else {
                p6 = hap.value.depth;
            }
            let p7;
            if (typeof hap.value.pan === 'undefined') {
                p7 = 0;
            } else {
                p7 = hap.value.pan;
            }
            let p8;
            if (typeof hap.value.height === 'undefined') {
                p8 = 0;
            } else {
                p8 = hap.value.depth;
            }
            const i_statement = ['i', p1, p2, p3, p4, p5, p6, p7, p8, '\n'].join(' ');
            console.log('[csoundn] ' + i_statement);
            csound.readScore(i_statement);
            // Any controls in the Hap that start with 'gi' or 'gk' will be 
            // treated as Csound control channels, and their values will be 
            // sent to Csound. Normally, these channels have been defined in 
            // the Csound orchestra code.
            for (let control in hap.value) {
                if (control.startsWith('gi') || control.startsWith('gk')) {
                    csound.SetControlChannel(control, parseFloat(hap.value[control]));
                }
            }
            csoundn_counter = csoundn_counter + 1;
            if ((diagnostic_level() >= INFORMATION) === true) {
                print_counter('csoundn', csoundn_counter, hap);
            }
            // Color the event by both insno and gain.
            // insno is hue, and gain is value, in HSV.
            if (globalThis.haps_from_outputs) {
                if (typeof hap.value !== 'object') {
                    hap.value = {note: p4, gain: gain};
                } else {
                    hap.value.note = p4;
                    hap.value.gain = gain;
                }
                hap.value.color = hsvToRgb((p1 / instrument_count) * 360, 1, gain);
                globalThis.haps_from_outputs.push(hap);
            }
        } catch (except) {
            diagnostic('[csoundn] error: ' + except + '\n', ERROR);
        }
    });
});

let chordn_counter = 0;

/**
 * Creates and initializes a CsoundAC Chord object. This function should be 
 * called from module scope in Strudel code before creating any Patterns. The 
 * Chord class is based on Dmitri Tymoczko's model of chord space, and 
 * represents an equally tempered chord of the specified number of voices as 
 * a single point in chord space, where each dimension of the space 
 * corresponds to one voice of the Chord. Chords are equipped with numerous 
 * operations from pragmatic music theory, atonal music theory, and 
 * neo-Riemannian music theory.
 *
 * @param {string} name The common musical name of the Chord, e.g. "Cb9."
 * @returns {Chord} A new CsoundAC Chord object.
 */
export function Chord(name) {
    if (diagnostic_level() >= DEBUG) diagnostic('[csacChord] Creating Chord...\n');
    let chord_ = csoundac.chordForName(name);
    if (diagnostic_level() >= DEBUG) diagnostic('[csacChord]:' + chord_.toString() + '\n');
    return chord_;
}

/**
 * Creates and initializes a CsoundAC Scale object. This function can be 
 * called from module scope in Strudel code before creating any Patterns. The 
 * Scale class is derived from the CsoundAC Chord class, but has been 
 * equipped with additional methods based on Dimitri Tymoczko's model of 
 * functional harmony. This enables algorithmically generating Chords from 
 * scale degrees, transposing Chords by scale degrees, generating all 
 * possible modulations given a pivot chord, and implementing secondary 
 * dominants and tonicizations based on scale degree.
 * 
 * @param {Scale} name The common musical name of the Scale, e.g. "C major."
 * @returns {Scale} A new Scale object.
 */
export function Scale(name) {
    name = name.replace('_', ' ');
    if (diagnostic_level() >= DEBUG) diagnostic('[Scale] Creating Scale...\n');
    let scale_ = csoundac.scaleForName(name);
    if (diagnostic_level() >= DEBUG) diagnostic('[Scale] ' + scale_.name() + '\n');
    return scale_;
}

/**
 * Creates and initializes a CsoundAC PITV object. This function should be 
 * called from module scope in Strudel code before creating any Patterns. The 
 * PITV object is a 4 dimensional cyclic group whose dimensions are TI set 
 * class (P), chord inversion (I), pitch-class transposition (T), and index 
 * of octavewise revoicing within the specified range (V). The elements of 
 * the group are chords in 12 tone equal temperament with the specified 
 * number of voices. There is a one-to-one mapping between PITV indices and 
 * chords, such that each voiced chord corresponds to a PITV index, and each 
 * PITV index corresponds to a voiced chord. This enables algorithmically 
 * generating harmonies and voicings by independently varying P, I, T, and V.
 *
 * @param {number} voices The number of voices in the chord space.
 * @param {number} bass The lowest pitch (as a MIDI key number) in the chord 
 * space.
 * @param {number} range The range (in MIDI key numbers) of the chord space.
 * @returns {PITV} A new PITV object.
 */
export function Pitv(voices, bass, range) {
    if (diagnostic_level() >= DEBUG) diagnostic('[Pitv] Creating PITV group...\n');
    let pitv = new csoundac.PITV();
    pitv.bass = bass;
    pitv.initialize(voices, range, 1., false);
    pitv.P = 0;
    pitv.I = 0;
    pitv.T = 0;
    pitv.V = 0;
    pitv.list(true, false, false);
    return pitv;
}

/**
 * Creates a class to hold state, and defines Patterns for creating and using 
 * that state to work with CsoundAC Chords. An instance of this class must be 
 * created at module scope and passed to the relevant Patterns.
 *
 * Some hacks are used to co-ordinate state with triggers:
 *  - Assume that chord changes happen only once at any given time.
 *  - In the trigger, apply the input to the Pattern if and only if the input 
 *    is different from the old input.
 */
export class ChordPatterns extends StatefulPatterns {
    constructor(chord, modality) {
        super();
        this.registerPatterns();
        if (typeof chord === 'string') {
            this.ac_chord = csoundac.chordForName(chord);
            if (diagnostic_level() >= DEBUG) diagnostic('[ChordPatterns] created new chord.\n');
        } else {
            this.ac_chord = chord;            if (diagnostic_level() >= DEBUG) diagnostic('[ChordPatterns] using existing chord.\n');
        }
        if (typeof modality == 'undefined') {
            this.ac_modality = this.ac_chord;
        } else {
            this.ac_modality = modality;
        }
        this.prior_chord = this.ac_chord;
        this.value = 0;
        this.acC_counter = 0;
        this.acC_chord_string = null;
        this.acCT_counter = 0;
        this.acCT_semitones = null
        this.acCI_counter = 0;
        this.acCI_center = null;
        this.acCK_counter = 0;
        this.acCK_state = null;
        this.acCQ_counter = 0;
        this.acCQ_semitones = null;
        this.acCOP_counter = 0;
        this.acCRP_counter = 0;
        this.acCO_counter = 0;
        this.acCV_counter = 0;
        this.acCVV_counter = 0;
        this.acCVVL_counter = 0;
    }
    /**
     * Applies a Chord or chord name to this.
     * 
     * @param {boolean} is_onset Whether this Hap is the onset of its cycle.
     * @param {string} chord_id Identifies the chord.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acC(is_onset, chord_id, hap) {
        if (is_onset === true) {
            if (typeof chord_id === 'string') {
                this.ac_chord = csoundac.chordForName(chord_id);
                if (diagnostic_level() >= DEBUG) diagnostic('[acC onset] created new Chord.\n');
            } else {
                this.ac_scale = scale;
                if (diagnostic_level() >= DEBUG) diagnostic('[acC onset] using existing Chord.\n');
            }
            if (this.acS_chord_string != this.ac_chord.toString()) {
                this.acS_chord_string = this.ac_chord.toString();
                this.ac_chord = this.ac_scale.chord(1, this.voices, 3);
                if (diagnostic_level() >= WARNING) {
                    diagnostic(['[acS onset] new Chord:', this.ac_chord.toString(), this.ac_chord.name(), '\n'].join(' '));
                 }
                this.acC_counter = this.acC_counter + 1;
                if (diagnostic_level() >= INFORMATION) {
                    print_counter('acC', this.acC_counter, hap);
                }
            }
        }
        return hap;
    }
    /**
     * Applies a transposition to the Chord of this.
     * 
     * @param {boolean} is_onset Indicates whether or not this is Hap onset of its cycle.
     * @param {number} semitones Number of semitones to transpose; may be negative.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acCT(is_onset, semitones, hap) {
        if (is_onset === true) {
            if (this.acCT_semitones != semitones) {
                this.acCT_semitones = semitones;
                if (diagnostic_level() >= DEBUG) diagnostic(['[acCT onset] current chord:    ', this.ac_chord.toString(), this.ac_chord.eOP().name(), hap.show(), '\n'].join(' '));
                this.ac_chord = this.ac_chord.T(semitones);
                if (diagnostic_level() >= WARNING) diagnostic(['[acCT onset] transformed chord:', this.ac_chord.toString(), this.ac_chord.eOP().name(), hap.show(), '\n'].join(' '));
                this.acCT_counter = this.acCT_counter + 1;
                if (diagnostic_level() >= INFORMATION) {
                    print_counter('acCT', this.acCT_counter, hap);
                }
            }
        }
        return hap;
    }
    /**
     * Applies an inversion to the Chord of this. The transformation can be 
     * patternified with a Pattern of flips (changes in the value of the flip 
     * input).
     * 
     * @param {boolean} is_onset Indicates whether or not this is Hap onset of its cycle.
     * @param {number} center The center of reflection.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} The new Hap.
     */
    acCI(is_onset, center, flip, hap) {
        if (is_onset === true) {
            if (this.acCI_flip != flip) {
                this.acCI_flip = flip;
                if (diagnostic_level() >= DEBUG) diagnostic(['[acCI] onset: current chord:    ', this.ac_chord.toString(), this.ac_chord.eOP().name(), hap.show(), '\n'].join(' '));
                this.ac_chord = this.ac_chord.I(center);
                if (diagnostic_level() >= WARNING) diagnostic(['[acCI] onset: transformed chord:', this.ac_chord.toString(), this.ac_chord.eOP().name(), hap.show(), '\n'].join(' '));
                this.acCI_counter = this.acCI_counter + 1;
                if (diagnostic_level() >= INFORMATION) {
                    print_counter('acCI', this.acCI_counter, hap);
                }
            }
        }
        return hap;
    }
    /**
     * Applies the interchange by inversion operation of the Generalized 
     * Contextual Group of Fiore and Satyendra to the Chord of this. The 
     * transformation can be patternified with a Pattern of flips (changes in 
     * the value of the flip input).
     * 
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {number} flip If this value changes, the transformation is applied.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acCK(is_onset, flip, hap) {
        if (is_onset === true) {
            if (this.flip != flip) {
                this.flip = flip;
                if (diagnostic_level() >= DEBUG) diagnostic(['[acCK onset] current chord:    ', this.ac_chord.toString(), this.ac_chord.eOP().name(), hap.show(), '\n'].join(' '));
                this.ac_chord = this.ac_chord.K();
                if (diagnostic_level() >= WARNING) diagnostic(['[acCK onset] transformed chord:', this.ac_chord.toString(), this.ac_chord.eOP().name(), hap.show(), '\n'].join(' '));
                this.acCK_counter = this.acCK_counter + 1;
                if (diagnostic_level() >= INFORMATION) {
                    print_counter('acCK', this.acCK_counter, hap);
                }
            }
        }
        return hap;
    }
    /**
     * Applies the contexual transposition operation of the Generalized 
     * Contextual Group of Fiore and Satyendra to the Chord of this. The 
     * modality is set in the constructor of this class.
     * 
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {number} semitones The number of semitones by which this Chord 
     * is to be tranposed; may be negative.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
     acCQ(is_onset, semitones, hap) {
        if (is_onset === true) {
            if (diagnostic_level() >= DEBUG) diagnostic(['[acCQ onset] current chord:    ', this.ac_chord.toString(), this.ac_chord.eOP().name(), hap.show(), '\n'].join(' '));
            this.ac_chord = this.ac_chord.Q(semitones, this.ac_modality, 1);
            if (diagnostic_level() >= WARNING) diagnostic(['[acCQ onset] transformed chord:', this.ac_chord.toString(), this.ac_chord.eOP().name(), hap.show(), '\n'].join(' '));
            this.acCQ_counter = this.acCQ_counter + 1;
            if (diagnostic_level() >= INFORMATION) {
                print_counter('acCQ', this.acCQ_counter, hap);
            }
        }
        return hap;
    }
    /**
     * Transforms the Chord of this to its 'OP' form; 'chord' is an extremely 
     * flexible and therefore ambiguous term, but the 'OP' form is what most 
     * musicians usually mean by 'chord': A chord where the octaves of the 
     * pitches do not matter and the order of the voices does not matter. This 
     * transformation can be useful for returning chords that have been 
     * transformed such that their voices are out of range back to a more 
     * normal form.
     *
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acCOP(is_onset, hap) {
        if (is_onset === true) {
            if (diagnostic_level() >= DEBUG) diagnostic(['[acCOP onset] current chord:    ', this.ac_chord.toString(), this.ac_chord.eOP().name(), hap.show(), '\n'].join(' '));
            this.ac_chord = this.ac_chord.eOP();
            if (diagnostic_level() >= WARNING) diagnostic(['[acCOP onset] transformed chord:', this.ac_chord.toString(), this.ac_chord.eOP().name(), hap.show(), '\n'].join(' '));
            this.acCOP_counter = this.acCOP_counter + 1;
            if (diagnostic_level() >= INFORMATION) {
                print_counter('acCOP', this.acCOP_counter, hap);
            }
        }
        return hap;
    }
    /**
     * Transforms the Chord of this to its 'RP' form; 'chord' is an extremely 
     * flexible and therefore ambiguous term, but the 'RP' form is a chord 
     * where the octaves are folded within the indicated range, and like 'OP'
     * the order of the voices does not matter. This 
     * transformation can be useful for returning chords that have been 
     * transformed such that their voices are out of range back to a user-
     * defined range.
     *
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {number} range The range of this chord space.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acCRP(is_onset, range, hap) {
        if (is_onset === true) {
            if (diagnostic_level() >= DEBUG) diagnostic(['[acCRP onset] current chord:    ', this.ac_chord.toString(), this.ac_chord.eOP().name(), hap.show(), '\n'].join(' '));
            this.ac_chord = this.ac_chord.eRP(range);
            if (diagnostic_level() >= WARNING) diagnostic(['[acCRP onset] transformed chord:', this.ac_chord.toString(), this.ac_chord.eOP().name(), hap.show(), '\n'].join(' '));
            this.acCRP_counter = this.acCRP_counter + 1;
            if (diagnostic_level() >= INFORMATION) {
                print_counter('acCRP', this.acCRP_counter, hap);
            }
        }
        return hap;
    }    
    /**
     * Applies the Chord of this to the _pitch-class_ of the Hap, i.e., moves 
     * the _pitch-class_ of the Hap to the nearest _pitch-class_ of the Chord.
     * 
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acCV(is_onset, hap) {
        if (is_onset === true) {
            let frequency;
            try {
                frequency = getFrequency(hap);
            } catch (error) {
                diagnostic('[acCV value]: not a note!\n');
                return;
            }
            let current_midi_key = frequencyToMidiInteger(frequency);
            let epcs = this.ac_chord.epcs();
            if (diagnostic_level() >= DEBUG) diagnostic(['[acCV value] current chord:  ', this.ac_chord.toString(), this.ac_chord.eOP().name(), '\n'].join(' '));
            if (diagnostic_level() >= DEBUG) diagnostic(['[acCV value] current hap:    ', hap.show(), '\n'].join(' '));
            let note = csoundac.conformToPitchClassSet(current_midi_key, epcs);
            hap = setPitch(hap, note);
            ChordPatterns.acCV_counter = ChordPatterns.acCV_counter + 1;
            if (diagnostic_level() >= WARNING) diagnostic(['[acCV value] new hap:        ', hap.show(), '\n'].join(' '));   
            if (diagnostic_level() >= INFORMATION) {
                print_counter('acCV onset', ChordPatterns.acCV_counter, hap);
            }
        } else {
            let frequency;
            try {
                frequency = getFrequency(hap);
            } catch (error) {
                diagnostic('[acCV value]: not a note!\n');
                return;
            }
            let current_midi_key = frequencyToMidiInteger(frequency);
            let epcs = this.ac_chord.epcs();
            if (diagnostic_level() >= DEBUG) diagnostic(['[acCV value] current chord:  ', this.ac_chord.toString(), this.ac_chord.eOP().name(), '\n'].join(' '));
            if (diagnostic_level() >= DEBUG) diagnostic(['[acCV value] current hap:    ', hap.show(), '\n'].join(' '));
            let note = csoundac.conformToPitchClassSet(current_midi_key, epcs);
            hap = setPitch(hap, note);
            //~ if (diagnostic_level() >= DEBUG) diagnostic(['[acCV value] new hap:        ', hap.show(), '\n'].join(' '));
            //~ if (diagnostic_level() >= INFORMATION) {
                //~ print_counter('acCV value', ChordPatterns.acCV_counter, hap);
            //~ }
        }
        return hap;
    }

    /**  
     * acCO:      Transforms the Chord of this by the indicated number of 
     *            octavewise revoicings: negative means subtract an octave 
     *            from the highest voice, positive means add an octave to the 
     *            lowest voice. This corresponds to the musician's notion of 
     *            "inversion."
     * 
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {number} revoicings The number of octavewise revoicings to apply.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acCO(is_onset, revoicings, hap) {
        if (is_onset) {
            if (diagnostic_level() >= DEBUG) diagnostic(['[acCO] onset: current chord:    ', this.ac_chord.toString(), this.ac_chord.eOP().name(), hap.show(), '\n'].join(' '));
            this.ac_chord = this.ac_chord.v(revoicings);
            if (diagnostic_level() >= WARNING) diagnostic(['[acCO] onset: transformed chord:', this.ac_chord.toString(), this.ac_chord.eOP().name(), hap.show(), '\n'].join(' '));
            this.acCO_counter = this.acCO_counter + 1;
            if (diagnostic_level() >= INFORMATION) {
                print_counter('acCO', this.acCO_counter, hap);
            }
            this.prior_chord = this.ac_chord;  
        }
        return hap;       
    }
    
    /**
     * acCVV:      Generate a note that represents a particular voice of the 
     *             Chord.
     * 
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {number} bass The MIDI key number of the lowest pitch.
     * @param {number} voice The number of the voice of the Chord to use.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acCVV(is_onset, bass, voice, hap) {
        let new_midi_key = bass + this.ac_chord.getPitch(voice);
        hap = setPitch(hap, new_midi_key);
        if (diagnostic_level() >= DEBUG) diagnostic(['[acCVV value]:', 'new_midi_key:', new_midi_key, 'new note:', hap.show(), '\n'].join(' '));
        this.prior_chord = this.ac_chord;  
        return hap;
    }
    /**
     * acCVVL:     Generate a note that represents a particular voice of the 
     *             Chord, as the closest voice-leading from the prior Chord.
     *
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {number} bass The MIDI key of the lowest pitch to use.
     * @param {number} range The range in MIDI keys. Pitches are wrapped back up or down
     * if the revoicing takes them out of this range.
     * @param {number} voice The number of the voice in the Chord to use.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acCVVL(is_onset, bass, range, voice, hap) {
        if (this.prior_chord != this.ac_chord) {
            let new_chord = csoundac.voiceleadingClosestRange(this.prior_chord, this.ac_chord, range, true);
            const message = ['[acCVVL]:', '\n  prior_chord: ', this.prior_chord.toString(), '\n  ac_chord:    ', this.ac_chord.toString(), '\n  new ac_chord:',new_chord.toString() + '\n'].join(' ');
            if (diagnostic_level() >= DEBUG) diagnostic(message);
            console.log(message);
            this.ac_chord = new_chord;
        }
        let new_midi_key = bass + this.ac_chord.getPitch(voice);
        hap = setPitch(hap, new_midi_key);
        if (diagnostic_level() >= DEBUG) diagnostic(['[acCVVL value]:', 'new_midi_key:', new_midi_key, 'new note:', hap.show(), '\n'].join(' '));
        this.prior_chord = this.ac_chord;  
        return hap;
    }
}

/**
 * Creates a class to hold state, and defines Patterns for creating and using 
 * that state to work with CsoundAC Scales. An instance of this class must be 
 * created at module scope and passed to the relevant Patterns. The 
 * constructor sets the number of voices in Chords associated with the Scale,
 * by default 4.
 *
 * State is co-ordinated with the triggers of the Patterns by only updating 
 * the state when the input of the Pattern changes.
*/
export class ScalePatterns extends StatefulPatterns {
    constructor(scale, voices = 3) {
        super();
        this.registerPatterns();
        this.voices = voices;
        if (typeof scale === 'string') {
            // Have to use underscores instead of spaces in the Strudel REPL.
            scale = scale.replace('_', ' ');
            this.ac_scale = csoundac.scaleForName(scale);
            if (diagnostic_level() >= WARNING) diagnostic('[acS onset] created new scale.\n');
        } else {
            this.ac_scale = scale;
            if (diagnostic_level() >= DEBUG) diagnostic('[acS onset] using existing scale.\n');
        }
        this.ac_chord = this.ac_scale.chord(1, this.voices, 3);
        this.prior_chord = this.ac_chord;
        this.acS_counter = 0;
        this.acS_scale_string = null;
        this.acSS_counter = 0;
        this.acSS_scale_step = null;
        this.acST_counter = 0;
        this.acST_scale_steps = null;
        this.acSM_counter = 0;
        this.acSM_index = null;
        this.acSO_counter = 0;
        this.acSV_counter = 0;
        this.acSCV_counter = 0;
 
    }
    /**
     * acS:        Insert a CsoundAC Scale into the Pattern's state.
     *
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {Scale} scale The Scale object to be used.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acS(is_onset, scale, hap) {
        if (is_onset === true) {
            if (typeof scale === 'string') {
                // Have to use underscores instead of spaces in the Strudel REPL.
                scale = scale.replace('_', ' ');
                this.ac_scale = csoundac.scaleForName(scale);
                if (diagnostic_level() >= DEBUG) diagnostic('[acS onset] created new scale.\n');
            } else {
                this.ac_scale = scale;
                if (diagnostic_level() >= DEBUG) diagnostic('[acS onset] using existing scale.\n');
            }
            if (this.acS_scale_string != this.ac_scale.toString()) {
                this.acS_scale_string = this.ac_scale.toString();
                this.ac_chord = this.ac_scale.chord(1, this.voices, 3);
                if (diagnostic_level() >= WARNING) {
                    diagnostic(['[acS onset] new scale:', this.ac_scale.toString(), this.ac_scale.name(), '\n'].join(' '));
                    diagnostic(['[acS onset] new chord:', this.ac_chord.toString(), this.ac_chord.name(), '\n'].join(' '));
                }
                this.acS_counter = this.acS_counter + 1;
                if (diagnostic_level() >= INFORMATION) {
                    print_counter('acS', this.acS_counter, hap);
                }
            }
        }
        return hap;
    }
    /** 
     * acSS:       Insert the Chord at the specified scale step of the Scale in 
     *             the Pattern's state, into the state.
     * 
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {number} scale_step The specific scale step of the Chord in the Scale.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acSS(is_onset, scale_step, hap) {
        if (is_onset === true) {
            if (this.acSS_scale_step != scale_step) {
                this.acSS_scale_step = scale_step;
                if (diagnostic_level() >= DEBUG) diagnostic(['[acSS onset] current chord:    ', this.ac_chord.toString(), this.ac_chord.eOP().name(), '\n'].join(' '));
                this.ac_chord = this.ac_scale.chord(scale_step, this.voices, 3);
                if (diagnostic_level() >= WARNING) diagnostic(['[acSS onset] transformed chord:', this.ac_chord.toString(), this.ac_chord.eOP().name(), '\n'].join(' '));
                this.acSS_counter = this.acSS_counter + 1;
                if (diagnostic_level() >= INFORMATION) {
                    print_counter('acSS', this.acSS_counter, hap);
                }
            }
        }  
        return hap;
    }
    /**
     * acST:       Transpose the Chord in the Pattern's state by the specified 
     *             number of scale steps in the Scale in the state.
     *
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {number} scale_steps The number of steps in this Scale by which to 
     * transpose the Chord in this.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acST(is_onset, scale_steps, hap) {
        if (is_onset === true) {
            if (this.acST_scale_steps != scale_steps) {
                this.acST_scale_steps = scale_steps;
                if (diagnostic_level() >= WARNING) diagnostic(['[acST onset] current chord:    ', this.ac_chord.toString(), this.ac_chord.eOP().name(), '\n'].join(' '));
                this.ac_chord = this.ac_scale.transpose_degrees(this.ac_chord, scale_steps, 3);    
                if (diagnostic_level() >= WARNING) diagnostic(['[acST onset] transformed chord:', this.ac_chord.toString(), this.ac_chord.eOP().name(), '\n'].join(' '));
                this.acST_counter = this.acST_counter + 1;
                if (diagnostic_level() >= INFORMATION) {
                    print_counter('acST', this.acST_counter, hap);
                }
            }
        }
        return hap;
    }
    /**
     * acSM:       Modulate from the Scale in the Pattern's state, using the 
     *             Chord in the state as a pivot, choosing one of the possible 
     *             modulations by index.
     *
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {number} index The index of the specific modulation to be used.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acSM(is_onset, index, hap) {
        if (is_onset === true) {
             if (this.acSM_index != index) {
                this.acSM_index = index;
                let pivot_chord_eop = this.ac_chord.eOP();
                let possible_modulations = this.ac_scale.modulations(pivot_chord_eop);
                let new_scale = this.ac_scale;
                let modulation_count = possible_modulations.size();
                let wrapped_index = -1;
                if (modulation_count > 0) {
                    wrapped_index = index % modulation_count;
                    new_scale = possible_modulations.get(wrapped_index);
                    if (diagnostic_level() >= WARNING) {
                        diagnostic('[acSM onset] modulating in: ' + this.ac_scale.toString() + ' ' + this.ac_scale.name() + '\n');
                        diagnostic('[acSM onset] from pivot:    ' + pivot_chord_eop.toString(), + ' ' + pivot_chord_eop.name() + '\n');
                        diagnostic('[acSM onset] modulations:   ' + modulation_count + ' => ' + wrapped_index + '\n');
                        diagnostic('[acSM onset] modulated to:  ' + new_scale.toString() + ' ' + new_scale.name() + '\n');
                        diagnostic('[acSM onset] hap:           ' + hap.show() + '\n');
                    }
                    this.ac_scale = new_scale;
                }
                this.acSM_counter = this.acSM_counter + 1;
                if (diagnostic_level() >= INFORMATION) {
                    print_counter('acSM', this.acSM_counter, hap);
                }
            }
        }
        return hap;
    }
    /**
     * acSV:       Move notes in the Pattern to fit the Scale in the Pattern's 
     *             state.
     * 
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {Hap} The current Hap.
     * @returns {Hap} A new Hap.
     */
    acSV(is_onset, hap) {
        if (is_onset === true) {
            let frequency;
            try {
                frequency = getFrequency(hap);
            } catch (error) {
                diagnostic('[acSV value] not a note!\n');
                return;
            }
            let current_midi_key = frequencyToMidiInteger(frequency);
            let epcs = this.ac_scale.epcs();
            if (diagnostic_level() >= DEBUG) diagnostic(['[acSV value] current scale:  ', this.ac_scale.toString(), this.ac_scale.eOP().name(), '\n'].join(' '));
            if (diagnostic_level() >= DEBUG) diagnostic(['[acSV value] current hap:    ', hap.show(), '\n'].join(' '));
            let note = csoundac.conformToPitchClassSet(current_midi_key, epcs);
            hap = setPitch(hap, note);
            if (diagnostic_level() >= WARNING) diagnostic(['[acSV value] new hap:        ', hap.show(), '\n'].join(' '));
            this.acSV_counter = this.acSV_counter + 1;
            if (diagnostic_level() >= INFORMATION) {
                print_counter('acSV', this.acSV_counter, hap);
            }
        } else {
            let frequency;
            try {
                frequency = getFrequency(hap);
            } catch (error) {
                diagnostic('[acSV value] not a note!\n');
                return;
            }
            let current_midi_key = frequencyToMidiInteger(frequency);
            let epcs = this.ac_scale.epcs();
            if (diagnostic_level() >= DEBUG) diagnostic(['[acSV value] current scale:  ', this.ac_scale.toString(), this.ac_scale.eOP().name(), '\n'].join(' '));
            if (diagnostic_level() >= DEBUG) diagnostic(['[acSV value] current hap:    ', hap.show(), '\n'].join(' '));
            let note = csoundac.conformToPitchClassSet(current_midi_key, epcs);
            hap = setPitch(hap, note);
            if (diagnostic_level() >= DEBUG) diagnostic(['[acSV value] new hap:        ', hap.show(), '\n'].join(' '));
        }
        return hap;
    }
    /**
     * acSCV:      Move notes in the Pattern to fit the Chord in the Pattern's 
     *             state.
     *
     * @param {number} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acSCV(is_onset, hap) {
        if (is_onset === true) {
            let frequency;
            try {
                frequency = getFrequency(hap);
            } catch (error) {
                diagnostic('[acSCV value] not a note!\n');
                return;
            }
            let current_midi_key = frequencyToMidiInteger(frequency);
            let epcs = this.ac_chord.epcs();
            if (diagnostic_level() >= DEBUG) diagnostic(['[acSCV onset] current scale:  ', this.ac_scale.toString(), this.ac_scale.eOP().name(), '\n'].join(' '));
            if (diagnostic_level() >= DEBUG) diagnostic(['[acSCV onset] current chord:  ', this.ac_chord.toString(), this.ac_chord.eOP().name(), '\n'].join(' '));
            if (diagnostic_level() >= DEBUG) diagnostic(['[acSCV onset] current hap:    ', hap.show(), '\n'].join(' '));
            let note = csoundac.conformToPitchClassSet(current_midi_key, epcs);
            hap = setPitch(hap, note);
            if (diagnostic_level() >= WARNING) diagnostic(['[acSCV onset] new hap:        ', hap.show(), '\n'].join(' '));
            this.acSCV_counter = this.acSCV_counter + 1;
            if (diagnostic_level() >= INFORMATION) {
                print_counter('acSCV', this.acSCV_counter, hap);
            }
        } else {
            let frequency;
            try {
                frequency = getFrequency(hap);
            } catch (error) {
                diagnostic('[acSCV value] not a note!\n');
                return;
            }
            let current_midi_key = frequencyToMidiInteger(frequency);
            let epcs = this.ac_chord.epcs();
            if (diagnostic_level() >= DEBUG) diagnostic(['[acSCV value] current scale:  ', this.ac_scale.toString(), this.ac_scale.eOP().name(), '\n'].join(' '));
            if (diagnostic_level() >= DEBUG) diagnostic(['[acSCV value] current chord:  ', this.ac_chord.toString(), this.ac_chord.eOP().name(), '\n'].join(' '));
            if (diagnostic_level() >= DEBUG) diagnostic(['[acSCV value] current hap:    ', hap.show(), '\n'].join(' '));
            let note = csoundac.conformToPitchClassSet(current_midi_key, epcs);
            hap = setPitch(hap, note);
            if (diagnostic_level() >= DEBUG) diagnostic(['[acSCV value] new hap:        ', hap.show(), '\n'].join(' '));
            this.acSCV_counter = this.acSCV_counter + 1;
         }
        return hap;
    }
    /**  
     * acSO:      Transforms the Chord of this by the indicated number of 
     *            octavewise revoicings: negative means subtract an octave 
     *            from the highest voice, positive means add an octave to the 
     *            lowest voice. This corresponds to the musician's notion of 
     *            "inversion."
     * 
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {number} revoicings The number of octavewise revoicings to apply 
     * to the Chord in this.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acSO(is_onset, revoicings, hap) {
        if (is_onset) {
            if (diagnostic_level() >= DEBUG) diagnostic(['[acSO] onset: current chord:    ', this.ac_chord.toString(), this.ac_chord.eOP().name(), hap.show(), '\n'].join(' '));
            this.ac_chord = this.ac_chord.v(revoicings);
            if (diagnostic_level() >= WARNING) diagnostic(['[acSO] onset: transformed chord:', this.ac_chord.toString(), this.ac_chord.eOP().name(), hap.show(), '\n'].join(' '));
            this.acSO_counter = this.acSO_counter + 1;
            if (diagnostic_level() >= INFORMATION) {
                print_counter('acSO', this.acSO_counter, hap);
            }
            this.prior_chord = this.ac_chord;  
        }
        return hap;       
    }
    
    /**
     * acSVV:      Generate a note that represents a particular voice of the 
     *             Chord of this.
     *
     * @param {boolean} is_onset Indicates whether the Hap is the onset of this cycle.
     * @param {number} bass The lowest possible voice; lower pitches are 
     * reflected back up.
     * @param {number} voice The voice of the Chord to be used.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acCVV(is_onset, bass, voice, hap) {
        let new_midi_key = bass + this.ac_chord.getPitch(voice);
        hap = setPitch(hap, new_midi_key);
        if (diagnostic_level() >= DEBUG) diagnostic(['[acCVV value]:', 'new_midi_key:', new_midi_key, 'new note:', hap.show(), '\n'].join(' '));
        this.prior_chord = this.ac_chord;  
        return hap;
    }
    /**
     * acSVVL:     Generate a note that represents a particular voice of the 
     *             current Chord, as the closest voice-leading from the prior 
     *             Chord.
     *
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {number} bass The lowest pitch in this chord space.
     * @param {number} range The range of this chord space.
     * @param {number} voice The number of the Chord voice to be used.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acSVVL(is_onset, bass, range, voice, hap) {
        if (this.prior_chord != this.ac_chord) {
            this.ac_chord = csoundac.voiceleadingClosestRange(this.prior_chord, this.ac_chord, range, true);
        }
        let new_midi_key = bass + this.ac_chord.getPitch(voice);
        hap = setPitch(hap, new_midi_key);
        if (diagnostic_level() >= DEBUG) diagnostic(['[acSVVL value]:', 'new_midi_key:', new_midi_key, 'new note:', hap.show(), '\n'].join(' '));
        this.prior_chord = this.ac_chord;  
        return hap;
    }}

/**
 * Creates a class to hold state and defines Patterns for creating and using 
 * that state to work with CsoundAC PITV groups. An instance of this class 
 * must be created at module scope and passed to the relevant Patterns.
 */
export class PitvPatterns extends StatefulPatterns {
    constructor(pitv) {
        super();
        this.registerPatterns();
        this.prior_chord = null;
        this.pitv = pitv;
        this.acPP_counter = 0;
        this.acPP_P = null;
        this.acPI_counter = 0;
        this.acPI_I = null;
        this.acPT_counter = 0;
        this.acPT_T = null;
        this.acPV_counter = 0;
        this.acPV_V = null;
        this.acPO_counter = 0;
        this.acPO_value = null;
        this.acPC_counter = 0;
        this.acPVS_counter = 0;
        this.acPVV_counter = 0;
    }
    /**
     * acP:        Insert a CsoundAC PITV group into the Pattern's state.
     * 
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {PITV} pitv The PITV object to be inserted.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acP(is_onset, pitv, hap) {
        if (is_onset == true) {
            if (diagnostic_level() >= DEBUG) diagnostic(['[acP onset] current PITV:  ', this.this.pitv.list(true, true, false), '\n'].join(' '));
            this.pitv = pitv;
            this.acP_counter = this.acP_counter + 1;
            if (diagnostic_level() >= INFORMATION) {
                print_counter('acP', this.acP_counter, hap);
            }
        } 
        return hap;
    }
    /**
     * acPP:       Set the prime form index of the PITV element in the Pattern's 
     *             state.
     * 
     * @param {boolean} is_onset 
     * @param {number} P The PITV index of the prime form.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acPP(is_onset, P, hap) {
        if (is_onset === true) {
            if (this.acPP_P != P) {
                this.acPP_P = P;
                this.pitv.P = P;
                this.acPP_counter = this.acPP_counter + 1;
                if (diagnostic_level() >= INFORMATION) {
                    print_counter('acPP', this.acPP_counter, hap);
                }
            }
        }
        return hap;
    }

    static acPI_counter = 1;

    /**
     * acPI:       Set the inversion index of the PITV element in the Pattern's 
     *             state.
     * 
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {number} I The PITV inversion.
     * @param {Hap} hap  The current Hap.
     * @returns {Hap} A new Hap.
     */
    acPI(is_onset, I, hap) {
        if (is_onset === true) {
            if (this.acPI_I != I) {
                this.acPI_I = I;
                this.pitv.I = I;
                this.acPI_counter = this.acPI_counter + 1;
                if (diagnostic_level() >= INFORMATION) {
                    print_counter('acPI', this.acPI_counter, hap);
                }
            }
        }
        return hap;
    }
    /**
     * acPT:       Set the transposition of the PITV element in the 
     *             Pattern's state.
     * 
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {number} T The PITV transposition.
     * @param {Hap} hap  The current Hap.
     * @returns {Hap} A new Hap.
     */
    acPT(is_onset, T, hap) {
        if (is_onset === true) {
            if (this.acPT_T != T) {
                this.acPT_T = T;
                this.pitv.T = T;
                this.acPT_counter = this.acPT_counter + 1;
                if (diagnostic_level() >= INFORMATION) {
                    print_counter('acPT', this.acPT_counter, hap);
                }
            }
        }
        return hap;
    }
    /**
     * acPO:       Set the octavewise voicing index of the PITV element in the 
     *             Pattern's state.
     * 
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {number} V The index of the octavewise revoicing in this PITV.
     * @param {Hap} hap  The current Hap.
     * @returns {Hap} A new Hap.
     */
    acPO(is_onset, V, hap) {
        if (is_onset == true) {
            if (this.acPO_O != V) {
                this.acPO_O = V;
                this.pitv.V = V;
                this.acPO_counter = this.acPO_counter + 1;
                if (diagnostic_level() >= INFORMATION) {
                    print_counter('acPO', this.acPO_counter, hap);
                }
            }
        }
        return hap;
    }
    /**
     * acPC:       Insert the Chord corresponding to the current PITV element 
     *             into the Pattern's state.
     * 
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acPC(is_onset, hap) {
        if (is_onset === true) {
            this.ac_chord = this.pitv.toChord(this.pitv.P, this.pitv.I, this.pitv.T, this.pitv.V, true).get(0);
            if (diagnostic_level() >= WARNING) diagnostic(['[acPC onset]:', this.ac_chord.toString(), this.ac_chord.eOP().name(), '\n'].join(' '));
            this.acPC_counter = this.acPC_counter + 1;
            if (diagnostic_level() >= INFORMATION) {
                print_counter('acPC', this.acPC_counter, hap);
            }
        }
        return hap;
    }
    /**
     * acPV:       Move notes in the Pattern to fit the pitch-class set of the 
     *             current element of the PITV group in the Pattern's state.
     * 
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acPV(is_onset, hap) {
        let frequency;
        try {
            frequency = getFrequency(hap);
        } catch (error) {
            diagnostic('[acPV] not a note!\n', WARNING);
            return;
        }
        let current_midi_key = frequencyToMidiInteger(frequency);
        let result = this.pitv.toChord(this.pitv.P, this.pitv.I, this.pitv.T, this.pitv.V, true);
        let eop = result.get(1);
        let epcs = eop.epcs();
        let new_midi_key = csoundac.conformToPitchClassSet(current_midi_key, epcs);
        hap = setPitch(hap, new_midi_key);
        if (diagnostic_level() >= DEBUG) diagnostic(['[acPV value]:', eop.toString(), eop.name(), 'old note:', current_midi_key, 'new note:', hap.show(), '\n'].join(' '));
        this.prior_chord = result.get(0);
        return hap;
    }
    /**
     * acPVV:      Generate a note that represents a particular voice of the 
     *             Chord represented by the current elemet of the PITV in this.
     * 
     * @param {boolean} is_onset 
     * @param {number} voice 
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
    acPVV(is_onset, voice, hap) {
        let voiced_chord = this.pitv.toChord(this.pitv.P, this.pitv.I, this.pitv.T, this.pitv.V, true).get(0);
        let new_midi_key = voiced_chord.getPitch(voice) + this.pitv.bass;
        hap = setPitch(hap, new_midi_key);
        if (diagnostic_level() >= DEBUG) diagnostic(['[acPVV value]:', 'new_midi_key:', new_midi_key, 'new note:', hap.show(), '\n'].join(' '));
        this.prior_chord = voiced_chord;
        return hap;
    }    
    /**
     * acPVVL:     Generate a note that represents a particular voice of the 
     *             Chord, as the closest voice-leading from the prior element of this.
     * 
     * @param {boolean} is_onset Indicates whether this Hap is the onset of its cycle.
     * @param {number} voice The number of the voice in thre chord that is to ber used.
     * @param {Hap} hap The current Hap.
     * @returns {Hap} A new Hap.
     */
   acPVVL(is_onset, voice, hap) {
       this.ac_chord = this.pitv.toChord(this.pitv.P, this.pitv.I, this.pitv.T, this.pitv.V, true).get(0);
       if (this.prior_chord != this.ac_chord) {
            this.ac_chord = csoundac.voiceleadingClosestRange(this.prior_chord, this.ac_chord, range, true);
       }
       let new_midi_key = this.ac_chord.getPitch(voice) + this.pitv.bass;
       hap = setPitch(hap, new_midi_key);
       if (diagnostic_level() >= DEBUG) diagnostic(['[acPVVL value]:', 'new_midi_key:', new_midi_key, 'new note:', hap.show(), '\n'].join(' '));
       this.prior_chord = this.ac_chord;
       return hap;
   }
}

/**
 * Assigns the value of the Pattern of this to a cloud-5 control parameter 
 * addon. Enables controlling external JavaScript code in the browser using  
 * Strudel Patterns. The following example controls the hue of a GLSL shader, 
 * and the hue in turn is sampled to determine the orchestration of the music 
 * generated from the shader:
 * 
 * const csac = await import('../csoundac.mjs');
 * let hue = new csac.Cloud5('GraphicsHue');
 * pure(0)
 *   .control(hue, "<.1 .8>".slow(8))
 */
export class Cloud5 extends StatefulPatterns {
    constructor(name_) {
        super();
        this.registerPatterns();
        this.name = name_;
    }
    control(is_onset, value_, hap) {
        // Assign on every query, or only at onsets?
        globalThis.__parameters__[this.name] = value_;
        return hap;
    }
}