/*
* 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 from cloud-5 to Strudel.
*/
let csound = parent.window.globalThis.csound;
/**
* Global instance of CsoundAC, shared from cloud-5 to Strudel.
*/
let csoundac = parent.window.globalThis.csound_ac;
/**
* Global reference to the cloud-5 parameters addon.
*/
let parameters = parent.window.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', DEBUG);
} else if (value.constructor.name === 'Chord') {
diagnostic('[' + pattern + '] sync: counter: ' + counter + ' value: ' + value.toString() + '\n', DEBUG);
} else {
diagnostic('[' + pattern + '] sync: counter: ' + counter + ' value: ' + value + '\n', DEBUG);
}
}
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 velmap
*
* @description A Pattern that increases or decreases the loudness of notes as
* a function of pitch ('velocity map'). The function can be concave, linear,
* or convex.
*
* Adjust MIDI velocity based on key number with tunable scale and curvature.
*
* @param {number} key - MIDI note number (0 to 127).
* @param {number} baseVelocity - Base velocity to adjust (0 to 127).
* @param {number} scale - Strength of adjustment. 0 = no change, 1 = full range. Default is 1.
* @param {number} curve - Curve exponent. 1 = linear, <1 = concave, >1 = convex. Default is 1.
* @returns {number} Adjusted velocity (1 to 127).
*/
function adjustedVelocityAdvanced(key, baseVelocity, scale = 1, curve = 1) {
// Clamp inputs.
key = Math.max(0, Math.min(127, key));
baseVelocity = Math.max(0, Math.min(127, baseVelocity));
// Define useful MIDI pitch range (typically piano keys).
const minKey = 21; // A0
const maxKey = 108; // C8
// Normalize key to range 0 (high) to 1 (low).
const t = 1 - (key - minKey) / (maxKey - minKey);
const shaped = Math.pow(t, curve); // Apply curvature
// Apply scale from 1.0 ± scale (e.g., if scale = 0.5 → range 0.5–1.5)
const factor = 1 + scale * (shaped - 0.5) * 2;
const velocity = Math.round(baseVelocity * factor);
return Math.max(1, Math.min(127, velocity));
}
export const velmap = register('velmap', (base_velocity, scale, curve, pat) => {
});
// ---------- helpers ----------
const isNum = (x) => typeof x === 'number' && Number.isFinite(x);
// If you can inject Strudel's AudioContext: set globalThis.getAudioNow = () => ctx.currentTime
let _dlEpoch = null, _tEpoch = null;
function secondsFromNow(deadline) {
const d = Number(deadline) || 0;
if (d >= 0 && d < 16) return Math.max(0, d); // likely already relative
const wallNow = (typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now()) / 1000;
if (_dlEpoch == null) { _dlEpoch = d; _tEpoch = wallNow; return 0; }
return Math.max(0, (d - _dlEpoch) - (wallNow - _tEpoch));
}
function computeP2(deadline) {
const d = Number(deadline) || 0;
const now = (typeof globalThis.getAudioNow === 'function')
? globalThis.getAudioNow()
: (globalThis.audioContext && isNum(globalThis.audioContext.currentTime)
? globalThis.audioContext.currentTime
: null);
return (now != null) ? Math.max(0, d > now + 1e-3 ? d - now : d) : secondsFromNow(d);
}
// Resolve MIDI note from common fields or via frequency; returns undefined for rests/meta.
function resolveMidi(hap) {
// Prefer explicit fractional MIDI pitch
if (isNum(hap?.pitch)) return hap.pitch;
if (isNum(hap?.midi)) return hap.midi;
if (isNum(hap?.note)) return hap.note;
// Then check structured value payloads
if (isNum(hap?.value?.pitch)) return hap.value.pitch;
if (isNum(hap?.value?.midi)) return hap.value.midi;
if (isNum(hap?.value?.note)) return hap.value.note;
// Frequency fallbacks
let f;
if (isNum(hap?.freq) && hap.freq > 0) {
f = hap.freq;
} else if (isNum(hap?.value?.freq) && hap.value.freq > 0) {
f = hap.value.freq;
} else if (typeof getFrequency === 'function') {
try {
const g = getFrequency(hap);
if (isNum(g) && g > 0) f = g;
} catch (_) { }
}
if (isNum(f) && f > 0) {
return 69 + 12 * Math.log2(f / 440); // Hz → fractional MIDI
}
return undefined;
}
function hapCycles(h) {
var s = h.part ? h.part : h.whole;
if (!s) return 0;
var b = (typeof s.begin === "number") ? s.begin : (s.begin && s.begin.value);
var e = (typeof s.end === "number") ? s.end : (s.end && s.end.value);
return (typeof b === "number" && typeof e === "number") ? (e - b) : 0;
}
function engineCycles(span) {
if (span && typeof span.valueOf === "function") return Number(span.valueOf());
if (span && typeof span.n === "number" && typeof span.d === "number" && span.d) return span.n / span.d;
if (typeof span === "number") return span;
return 0;
}
/**
* @function csoundn
*
* @description This is the actual Csound output function, and should work
* much like Strudel's `piano`. Sends Strudel Hap onsets with pitches to
* Csound for rendering with MIDI semantics. The values of the onset Haps 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 Strudel's 'now`.
* p3 -- duration in beats (usually seconds).
* p4 -- MIDI key number from Strudel's Hap value (as a real number, not an
* integer, i.e. in [0., 127.]; normally, Hap.pitch.
* 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.
*/
globalThis.csoundo = function (pattern) {
pattern.each((hap, deadline, duration) => {
try {
if (!csound) {
diagnostic('[csoundn]: Csound is not yet loaded.\n', WARNING);
return hap;
}
const p1 = (typeof instrument === 'string') ? `"${instrument}"` : instrument;
const p2 = computeP2(deadline);
// Prefer Strudel's own seconds via hap.duration.valueOf()
let p3;
const secFromValueOf =
(hap?.duration && typeof hap.duration.valueOf === 'function')
? Number(hap.duration.valueOf())
: NaN;
if (isNum(secFromValueOf) && secFromValueOf > 0) {
p3 = Math.max(1e-4, secFromValueOf);
} else {
// Fallbacks: cycles -> seconds using cps, then sustain (sec), then scheduler duration
const cps = (typeof getcps === 'function') ? Math.max(1e-9, getcps()) : 0.5; // REPL default
let cycles = undefined;
// Prefer the event's own span first
const bPart = hap?.part?.begin, ePart = hap?.part?.end;
const b = hap?.begin, e = hap?.end;
const bWhole = hap?.whole?.begin, eWhole = hap?.whole?.end;
if (isNum(bPart) && isNum(ePart)) cycles = ePart - bPart;
else if (isNum(b) && isNum(e)) cycles = e - b;
else if (isNum(bWhole) && isNum(eWhole)) cycles = eWhole - bWhole;
else if (isNum(hap?.duration)) cycles = hap.duration; // cycles
else if (isNum(hap?.delta)) cycles = hap.delta; // cycles
if (isNum(cycles) && cycles > 0) p3 = Math.max(1e-4, cycles / cps);
else if (isNum(hap?.value?.sustain)) p3 = Math.max(1e-4, hap.value.sustain);
else p3 = Math.max(1e-4, Number(duration) || 0.5);
}
// Optional: apply legato as a ratio if present (common Strudel idiom)
if (isNum(hap?.value?.legato) && p3 > 0) p3 = Math.max(1e-4, p3 * hap.value.legato);
if (!(p3 > 0)) {
return hap;
}
const midi = resolveMidi(hap);
// Don't send non-notes to csound -- I think.
if (!isNum(midi)) {
return hap;
};
const p4 = midi;
const gain = isNum(hap.gain) ? hap.gain : (isNum(hap.value?.gain) ? hap.value.gain : 0.9);
const p5 = Math.max(0, Math.min(127, 127 * gain));
const p6 = isNum(hap.value.depth) ? hap.value.depth : 0;
const p7 = isNum(hap.value.pan) ? hap.value.pan : 0;
const p8 = isNum(hap.value.height) ? hap.value.height : 0;
const score_line = `i ${p1} ${p2} ${p3} ${p4} ${p5} ${p6} ${p7} ${p8}\n`;
csound.readScore(score_line);
// gi/gk are Csound control channels.
for (const k in hap.value) {
if (k.startsWith('gi') || k.startsWith('gk')) {
csound.SetControlChannel(k, parseFloat(hap.value[k]));
}
}
} catch (e) {
diagnostic('[csoundn] error: ' + e + '\n', ERROR);
}
return hap;
});
return pattern;
};
/**
* This the Csound output as a Pattern with side effect playing the note with
* Csound, and never modifying the hap.
*/
export const csound_message_callback = register('csoundn', (instrument, pat) => {
return pat.onTrigger((hap, deadline, duration) => {
try {
if (!csound) {
diagnostic('[csoundn]: Csound is not yet loaded.\n', WARNING);
return hap;
}
const p1 = (typeof instrument === 'string') ? `"${instrument}"` : instrument;
const p2 = computeP2(deadline);
// Prefer Strudel's own seconds via hap.duration.valueOf()
let p3;
const secFromValueOf =
(hap?.duration && typeof hap.duration.valueOf === 'function')
? Number(hap.duration.valueOf())
: NaN;
if (isNum(secFromValueOf) && secFromValueOf > 0) {
p3 = Math.max(1e-4, secFromValueOf);
} else {
// Fallbacks: cycles -> seconds using cps, then sustain (sec), then scheduler duration
const cps = (typeof getcps === 'function') ? Math.max(1e-9, getcps()) : 0.5; // REPL default
let cycles = undefined;
// Prefer the event's own span first
const bPart = hap?.part?.begin, ePart = hap?.part?.end;
const b = hap?.begin, e = hap?.end;
const bWhole = hap?.whole?.begin, eWhole = hap?.whole?.end;
if (isNum(bPart) && isNum(ePart)) cycles = ePart - bPart;
else if (isNum(b) && isNum(e)) cycles = e - b;
else if (isNum(bWhole) && isNum(eWhole)) cycles = eWhole - bWhole;
else if (isNum(hap?.duration)) cycles = hap.duration; // cycles
else if (isNum(hap?.delta)) cycles = hap.delta; // cycles
if (isNum(cycles) && cycles > 0) p3 = Math.max(1e-4, cycles / cps);
else if (isNum(hap?.value?.sustain)) p3 = Math.max(1e-4, hap.value.sustain);
else p3 = Math.max(1e-4, Number(duration) || 0.5);
}
// Optional: apply legato as a ratio if present (common Strudel idiom)
if (isNum(hap?.value?.legato) && p3 > 0) {
p3 = Math.max(1e-4, p3 * hap.value.legato);
}
if (!(p3 > 0)) {
return hap;
}
const midi = resolveMidi(hap);
// Don't send non-notes to csound -- I think.
if (!isNum(midi)) {
return hap;
};
const p4 = midi;
const gain = isNum(hap.gain) ? hap.gain : (isNum(hap.value?.gain) ? hap.value.gain : 0.9);
const p5 = Math.max(0, Math.min(127, 127 * gain));
const p6 = isNum(hap.value.depth) ? hap.value.depth : 0;
const p7 = isNum(hap.value.pan) ? hap.value.pan : 0;
const p8 = isNum(hap.value.height) ? hap.value.height : 0;
const score_line = `i ${p1} ${p2} ${p3} ${p4} ${p5} ${p6} ${p7} ${p8}\n`;
csound.readScore(score_line);
// Controls starting with `gi` or `gk` are Csound control channels.
for (const k in hap.value) {
if (k.startsWith('gi') || k.startsWith('gk')) {
csound.SetControlChannel(k, parseFloat(hap.value[k]));
}
}
} catch (e) {
diagnostic('[csoundn] error: ' + e + '\n', ERROR);
}
return hap;
});
});
let chordn_counter = 0;
/**
* Takes one or more functions with side effects, typically factored
* out of a pattern's onTrigger body, and invokes them, dispatching
* their return haps to the consumers or sinks in the chain.
*/
export const tee = register("tee", (functions, pat) => {
return pat.onTrigger((hap, deadline, span) => {
if (Array.isArray(functions)) {
for (const function_ of functions) {
try {
function_(hap, deadline, span);
} catch (e) {
console.warn("[tee] error:", e);
}
}
}
return hap;
});
});
/**
* 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');
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;
}
}