450 lines
12 KiB
TypeScript
450 lines
12 KiB
TypeScript
import { Chord, Pitch, Rest as ZRest, Ziffers } from "zifferjs";
|
|
import { Editor } from "../main";
|
|
import { AbstractEvent } from "./AbstractEvents";
|
|
import { SkipEvent } from "./SkipEvent";
|
|
import { SoundEvent, SoundParams } from "./SoundEvent";
|
|
import { MidiEvent, MidiParams } from "./MidiEvent";
|
|
import { RestEvent } from "./RestEvent";
|
|
import { arrayOfObjectsToObjectWithArrays, isGenerator } from "../Utils/Generic";
|
|
import { TonnetzSpaces } from "zifferjs/src/tonnetz";
|
|
|
|
export type InputOptions = { [key: string]: string | number };
|
|
|
|
export class Player extends AbstractEvent {
|
|
input: string | number;
|
|
ziffers: Ziffers;
|
|
initCallTime: number = 0;
|
|
startCallTime: number = 0;
|
|
lastCallTime: number = 0;
|
|
waitTime = 0;
|
|
cueName: string|undefined = undefined;
|
|
played: boolean = false;
|
|
current!: Pitch | Chord | ZRest;
|
|
retro: boolean = false;
|
|
index: number = -1;
|
|
zid: string = "";
|
|
options: InputOptions = {};
|
|
skipIndex = 0;
|
|
|
|
constructor(
|
|
input: string | number | Generator<number>,
|
|
options: InputOptions,
|
|
public app: Editor,
|
|
zid: string = "",
|
|
) {
|
|
super(app);
|
|
this.options = options;
|
|
if (typeof input === "string") {
|
|
this.input = input;
|
|
this.ziffers = new Ziffers(input, options);
|
|
} else if (typeof input === "number") {
|
|
this.input = input;
|
|
this.ziffers = Ziffers.fromNumber(input, options);
|
|
} else if (isGenerator(input)) {
|
|
this.ziffers = Ziffers.fromGenerator(input, options);
|
|
this.input = this.ziffers.input;
|
|
} else {
|
|
throw new Error("Invalid input");
|
|
}
|
|
this.zid = zid;
|
|
}
|
|
|
|
isValid() {
|
|
return this.ziffers.values.length > 0;
|
|
}
|
|
|
|
reset() {
|
|
this.initCallTime = 0;
|
|
this.startCallTime = 0;
|
|
this.lastCallTime = 0;
|
|
this.waitTime = 0;
|
|
this.index = 0;
|
|
this.skipIndex = 0;
|
|
this.played = false;
|
|
this.skipIndex = 0;
|
|
this.ziffers.reset();
|
|
}
|
|
|
|
get ticks(): number {
|
|
const dur = this.ziffers.duration;
|
|
return dur * 4 * this.app.clock.ppqn;
|
|
}
|
|
|
|
nextEndTime(): number {
|
|
return this.startCallTime + this.ticks;
|
|
}
|
|
|
|
updateLastCallTime(): void {
|
|
if (this.notStarted() || this.played) {
|
|
this.lastCallTime = this.app.clock.pulses_since_origin;
|
|
this.played = false;
|
|
}
|
|
}
|
|
|
|
notStarted(): boolean {
|
|
return this.ziffers.notStarted();
|
|
}
|
|
|
|
next = (): Pitch | Chord | ZRest => {
|
|
this.current = this.ziffers.next() as Pitch | Chord | ZRest;
|
|
this.played = true;
|
|
return this.current;
|
|
};
|
|
|
|
pulseToSecond = (pulse: number): number => {
|
|
return this.app.clock.convertPulseToSecond(pulse);
|
|
};
|
|
|
|
firstRun = (): boolean => {
|
|
return this.notStarted();
|
|
};
|
|
|
|
atTheBeginning = (): boolean => {
|
|
return this.skipIndex === 0 && this.ziffers.index <= 0;
|
|
};
|
|
|
|
origin = (): number => {
|
|
return this.app.clock.pulses_since_origin + 1;
|
|
};
|
|
|
|
pulse = (): number => {
|
|
return this.app.clock.time_position.pulse;
|
|
};
|
|
|
|
beat = (): number => {
|
|
return this.app.clock.time_position.beat;
|
|
};
|
|
|
|
nextBeat = (): number => {
|
|
return this.app.clock.next_beat_in_ticks;
|
|
};
|
|
|
|
nextBeatInTicks = (): number => {
|
|
return this.app.clock.next_beat_in_ticks;
|
|
};
|
|
|
|
// Check if it's time to play the event
|
|
areWeThereYet = (): boolean => {
|
|
// If clock has stopped
|
|
if (this.app.clock.pulses_since_origin < this.lastCallTime) {
|
|
this.app.api.resetAllFromCache();
|
|
}
|
|
|
|
const patternIsStarting =
|
|
this.notStarted() &&
|
|
this.waitTime >= 0 &&
|
|
this.origin() >= this.waitTime &&
|
|
(this.pulse() === 0 || this.origin() >= this.nextBeatInTicks());
|
|
|
|
const timeToPlayNext =
|
|
this.current &&
|
|
this.waitTime >= 0 &&
|
|
this.pulseToSecond(this.origin()) >=
|
|
this.pulseToSecond(this.lastCallTime) +
|
|
this.pulseToSecond(this.current.duration * 4 * this.app.clock.ppqn) &&
|
|
this.origin() >= this.waitTime;
|
|
|
|
// If pattern is starting or it's time to play next event
|
|
const areWeThereYet = patternIsStarting || timeToPlayNext;
|
|
|
|
// Increment index of how many times call is skipped
|
|
this.skipIndex = areWeThereYet ? 0 : this.skipIndex + 1;
|
|
|
|
// Increment index of how many times sound/midi have been called
|
|
this.index = areWeThereYet ? this.index + 1 : this.index;
|
|
|
|
if (areWeThereYet && this.notStarted()) {
|
|
this.initCallTime = this.app.clock.pulses_since_origin;
|
|
}
|
|
|
|
if (this.atTheBeginning()) {
|
|
this.startCallTime = this.app.clock.pulses_since_origin;
|
|
}
|
|
|
|
return areWeThereYet;
|
|
};
|
|
|
|
checkCue() {
|
|
if(this.ziffers.atLast()) {
|
|
if(this.cueName && this.app.api.cueTimes[this.cueName]) {
|
|
delete this.app.api.cueTimes[this.cueName];
|
|
this.cueName = undefined;
|
|
this.waitTime = -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
sound(name?: string | string[] | SoundParams | SoundParams[]) {
|
|
if (this.areWeThereYet()) {
|
|
this.checkCue();
|
|
const event = this.next() as Pitch | Chord | ZRest;
|
|
const noteLengthInSeconds = this.app.clock.convertPulseToSecond(
|
|
event.duration * 4 * this.app.clock.ppqn,
|
|
);
|
|
if (event instanceof Pitch) {
|
|
let obj = event.getExisting(
|
|
"freq",
|
|
"note",
|
|
"pitch",
|
|
"originalPitch",
|
|
"key",
|
|
"scale",
|
|
"octave",
|
|
"pitchOctave",
|
|
"addedOctave",
|
|
"parsedScale",
|
|
) as SoundParams;
|
|
|
|
if (event.sound) name = event.sound as string;
|
|
if(name) obj = {...obj, ...this.processSound(name)};
|
|
else obj.s = "sine";
|
|
|
|
if (event.soundIndex) obj.n = event.soundIndex as number;
|
|
obj.dur = noteLengthInSeconds;
|
|
return new SoundEvent(obj, this.app);
|
|
} else if (event instanceof Chord) {
|
|
const pitches = event.pitches.map((p) => {
|
|
return p.getExisting(
|
|
"freq",
|
|
"note",
|
|
"pitch",
|
|
"originalPitch",
|
|
"key",
|
|
"scale",
|
|
"octave",
|
|
"pitchOctave",
|
|
"addedOctave",
|
|
"parsedScale",
|
|
);
|
|
}) as SoundParams[];
|
|
|
|
let add = { dur: noteLengthInSeconds} as SoundParams;
|
|
if(name) add = {...add, ...this.processSound(name)};
|
|
else add.s = "sine";
|
|
|
|
let sound = arrayOfObjectsToObjectWithArrays(
|
|
pitches,
|
|
add,
|
|
) as SoundParams;
|
|
return new SoundEvent(sound, this.app);
|
|
} else if (event instanceof ZRest) {
|
|
return RestEvent.createRestProxy(event.duration, this.app);
|
|
}
|
|
} else {
|
|
return SkipEvent.createSkipProxy();
|
|
}
|
|
}
|
|
|
|
midi(value: number | undefined = undefined) {
|
|
if (this.areWeThereYet()) {
|
|
this.checkCue();
|
|
const event = this.next() as Pitch | Chord | ZRest;
|
|
const obj = event.getExisting(
|
|
"note",
|
|
"pitch",
|
|
"originalPitch",
|
|
"bend",
|
|
"key",
|
|
"scale",
|
|
"octave",
|
|
"pitchOctave",
|
|
"addedOctave",
|
|
"parsedScale",
|
|
) as MidiParams;
|
|
if (event instanceof Pitch) {
|
|
if (event.soundIndex) obj.channel = event.soundIndex as number;
|
|
const note = new MidiEvent(obj, this.app);
|
|
return value ? note.note(value) : note;
|
|
} else if (event instanceof ZRest) {
|
|
return RestEvent.createRestProxy(event.duration, this.app);
|
|
} else if (event instanceof Chord) {
|
|
const pitches = event.midiChord() as MidiParams[];
|
|
const obj = arrayOfObjectsToObjectWithArrays(pitches) as MidiParams;
|
|
return new MidiEvent(obj, this.app);
|
|
}
|
|
} else {
|
|
return SkipEvent.createSkipProxy();
|
|
}
|
|
}
|
|
|
|
scale(name: string|number[]) {
|
|
if (this.atTheBeginning()) this.ziffers.scale(name);
|
|
return this;
|
|
}
|
|
|
|
semitones(values: number|number[], ...rest: number[]) {
|
|
values = typeof values === "number" ? [values, ...rest] : values;
|
|
if (this.atTheBeginning()) this.ziffers.semitones(values);
|
|
return this;
|
|
}
|
|
|
|
cents(values: number|number[], ...rest: number[]) {
|
|
values = typeof values === "number" ? [values, ...rest] : values;
|
|
if (this.atTheBeginning()) this.ziffers.cents(values);
|
|
return this;
|
|
}
|
|
|
|
ratios(values: number|number[], ...rest: number[]) {
|
|
values = typeof values === "number" ? [values, ...rest] : values;
|
|
if (this.atTheBeginning()) this.ziffers.ratios(values);
|
|
return this;
|
|
}
|
|
|
|
edo(value: number, scale: string|number[] = new Array(value).fill(1)) {
|
|
if (this.atTheBeginning()) this.ziffers.edo(value, scale);
|
|
return this;
|
|
}
|
|
|
|
key(name: string) {
|
|
if (this.atTheBeginning()) this.ziffers.key(name);
|
|
return this;
|
|
}
|
|
|
|
octave(value: number) {
|
|
if (this.atTheBeginning()) this.ziffers.octave(value);
|
|
return this;
|
|
}
|
|
|
|
tonnetz(transform: string, tonnetz: TonnetzSpaces = [3, 4, 5]) {
|
|
// @ts-ignore
|
|
if (this.atTheBeginning()) this.ziffers.tonnetz(transform, tonnetz);
|
|
return this;
|
|
}
|
|
|
|
triadTonnetz(transform: string, tonnetz: TonnetzSpaces = [3, 4, 5]) {
|
|
if (this.atTheBeginning()) this.ziffers.triadTonnetz(transform, tonnetz);
|
|
return this;
|
|
}
|
|
|
|
tetraTonnetz(transform: string, tonnetz: TonnetzSpaces = [3, 4, 5]) {
|
|
if (this.atTheBeginning()) this.ziffers.tetraTonnetz(transform, tonnetz);
|
|
return this;
|
|
}
|
|
|
|
octaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 4) {
|
|
if (this.atTheBeginning()) this.ziffers.octaCycle(tonnetz, repeats);
|
|
return this;
|
|
}
|
|
|
|
hexaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3) {
|
|
if (this.atTheBeginning()) this.ziffers.hexaCycle(tonnetz, repeats);
|
|
return this;
|
|
}
|
|
|
|
enneaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3) {
|
|
if (this.atTheBeginning()) this.ziffers.enneaCycle(tonnetz, repeats);
|
|
return this;
|
|
}
|
|
|
|
tonnetzChord(chord: string) {
|
|
if (this.atTheBeginning()) this.ziffers.tonnetzChords(chord);
|
|
return this;
|
|
}
|
|
|
|
voiceleading() {
|
|
if (this.atTheBeginning()) this.ziffers.lead();
|
|
return this;
|
|
}
|
|
|
|
lead = () => this.voiceleading();
|
|
|
|
arpeggio(indexes: string|number[], ...rest: number[]) {
|
|
if(typeof indexes === "number") indexes = [indexes, ...rest];
|
|
if (this.atTheBeginning()) this.ziffers.arpeggio(indexes);
|
|
return this;
|
|
}
|
|
|
|
invert = (n: number) => {
|
|
if (this.atTheBeginning()) {
|
|
this.ziffers.invert(n);
|
|
}
|
|
return this;
|
|
};
|
|
|
|
retrograde() {
|
|
if (this.atTheBeginning()) this.ziffers.retrograde();
|
|
return this;
|
|
}
|
|
|
|
listen(value: string) {
|
|
if(typeof value === "string") {
|
|
const cueTime = this.app.api.cueTimes[value];
|
|
this.cueName = value;
|
|
if(cueTime && this.app.clock.pulses_since_origin <= cueTime) {
|
|
this.waitTime = cueTime;
|
|
} else {
|
|
this.waitTime = -1;
|
|
}
|
|
return this;
|
|
}
|
|
}
|
|
|
|
wait(value: number | string | Function) {
|
|
|
|
if(typeof value === "string") {
|
|
const cueTime = this.app.api.cueTimes[value];
|
|
this.cueName = value;
|
|
if(cueTime && this.app.clock.pulses_since_origin <= cueTime) {
|
|
this.waitTime = cueTime;
|
|
} else if(this.atTheBeginning()){
|
|
this.waitTime = -1;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
if (this.atTheBeginning()) {
|
|
if (typeof value === "function") {
|
|
const refPat = this.app.api.patternCache.get(value.name) as Player;
|
|
if (refPat) this.waitTime = refPat.nextEndTime();
|
|
return this;
|
|
} else if(typeof value === "number") {
|
|
this.waitTime =
|
|
this.origin() + Math.ceil(value * 4 * this.app.clock.ppqn);
|
|
return this;
|
|
}
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
sync(value: string | Function, manualSync: boolean = true) {
|
|
|
|
if(typeof value === "string") {
|
|
if(manualSync) {
|
|
const cueTime = this.app.api.cueTimes[value];
|
|
if(cueTime) {
|
|
this.waitTime = cueTime;
|
|
} else {
|
|
this.waitTime = -1;
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
|
|
if (this.atTheBeginning() && this.notStarted()) {
|
|
const origin = this.app.clock.pulses_since_origin;
|
|
if (origin > 0) {
|
|
const syncPattern = this.app.api.patternCache.get(value.name) as Player;
|
|
if (syncPattern) {
|
|
const syncPatternDuration = syncPattern.ziffers.duration;
|
|
const syncPatternStart = syncPattern.startCallTime;
|
|
const syncInPulses = syncPatternDuration * 4 * this.app.clock.ppqn;
|
|
this.waitTime = syncPatternStart + syncInPulses;
|
|
}
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
|
|
log(key: string, ...args: string[]) {
|
|
this.app.api.log(this.ziffers.evaluated.map((p) => {
|
|
return Object.values(p.getExisting(...[key,...args]));
|
|
}).join(" "));
|
|
return this;
|
|
}
|
|
|
|
out = (): void => {
|
|
// TODO?
|
|
};
|
|
}
|