Merge pull request #95 from Bubobubobubobubo/topas

Experimental Workshop Branch
This commit is contained in:
Raphaël Forment
2023-12-02 10:48:13 +01:00
committed by GitHub
12 changed files with 208 additions and 316 deletions

View File

@ -43,6 +43,7 @@
"unique-names-generator": "^4.7.1", "unique-names-generator": "^4.7.1",
"vite-plugin-markdown": "^2.1.0", "vite-plugin-markdown": "^2.1.0",
"zifferjs": "^0.0.44", "zifferjs": "^0.0.44",
"zyklus": "^0.1.4",
"zzfx": "^1.2.0" "zzfx": "^1.2.0"
} }
} }

View File

@ -1298,7 +1298,7 @@ export class UserAPI {
const results: boolean[] = nArray.map( const results: boolean[] = nArray.map(
(value) => (value) =>
(this.app.clock.pulses_since_origin - Math.floor(nudge * this.ppqn())) % (this.app.clock.pulses_since_origin - Math.floor(nudge * this.ppqn())) %
Math.floor(value * this.ppqn()) === Math.floor(value * this.ppqn()) ===
0, 0,
); );
return results.some((value) => value === true); return results.some((value) => value === true);
@ -1318,7 +1318,7 @@ export class UserAPI {
const results: boolean[] = nArray.map( const results: boolean[] = nArray.map(
(value) => (value) =>
(this.app.clock.pulses_since_origin - nudgeInPulses) % (this.app.clock.pulses_since_origin - nudgeInPulses) %
Math.floor(value * barLength) === Math.floor(value * barLength) ===
0, 0,
); );
return results.some((value) => value === true); return results.some((value) => value === true);
@ -1581,7 +1581,9 @@ export class UserAPI {
// Low Frequency Oscillators // Low Frequency Oscillators
// ============================================================= // =============================================================
line = (start: number, end: number, step: number = 1): number[] => { public range = (v: number, a: number, b: number): number => v * (b - a) + a;
public line = (start: number, end: number, step: number = 1): number[] => {
/** /**
* Returns an array of values between start and end, with a given step. * Returns an array of values between start and end, with a given step.
* *
@ -1603,7 +1605,11 @@ export class UserAPI {
return result; return result;
}; };
sine = (freq: number = 1, times: number = 1, offset: number = 0): number => { public sine = (
freq: number = 1,
times: number = 1,
offset: number = 0,
): number => {
/** /**
* Returns a sine wave between -1 and 1. * Returns a sine wave between -1 and 1.
* *
@ -1617,7 +1623,11 @@ export class UserAPI {
); );
}; };
usine = (freq: number = 1, times: number = 1, offset: number = 0): number => { public usine = (
freq: number = 1,
times: number = 1,
offset: number = 0,
): number => {
/** /**
* Returns a sine wave between 0 and 1. * Returns a sine wave between 0 and 1.
* *
@ -1780,18 +1790,6 @@ export class UserAPI {
return sum / values.length; return sum / values.length;
}; };
public range = (
inputY: number,
yMin: number,
yMax: number,
xMin: number,
xMax: number,
): number => {
const percent = (inputY - yMin) / (yMax - yMin);
const outputX = percent * (xMax - xMin) + xMin;
return outputX;
};
limit = (value: number, min: number, max: number): number => { limit = (value: number, min: number, max: number): number => {
/** /**
* Limits a value between a minimum and a maximum. * Limits a value between a minimum and a maximum.
@ -1918,7 +1916,7 @@ export class UserAPI {
// ============================================================= // =============================================================
register = (name: string, operation: EventOperation<AbstractEvent>): void => { register = (name: string, operation: EventOperation<AbstractEvent>): void => {
AbstractEvent.prototype[name] = function ( AbstractEvent.prototype[name] = function(
this: AbstractEvent, this: AbstractEvent,
...args: any[] ...args: any[]
) { ) {
@ -2101,19 +2099,6 @@ export class UserAPI {
// Transport functions // Transport functions
// ============================================================= // =============================================================
public nudge = (nudge?: number): number => {
/**
* Sets or returns the current clock nudge.
*
* @param nudge - [optional] the nudge to set
* @returns The current nudge
*/
if (nudge) {
this.app.clock.nudge = nudge;
}
return this.app.clock.nudge;
};
public tempo = (n?: number): number => { public tempo = (n?: number): number => {
/** /**
* Sets or returns the current bpm. * Sets or returns the current bpm.

View File

@ -1,7 +1,11 @@
// @ts-ignore
import { TransportNode } from "./TransportNode";
import TransportProcessor from "./TransportProcessor?worker&url";
import { Editor } from "./main"; import { Editor } from "./main";
import { tryEvaluate } from "./Evaluator";
// @ts-ignore
import { getAudioContext } from "superdough";
// @ts-ignore
import "zyklus";
const zeroPad = (num: number, places: number) =>
String(num).padStart(places, "0");
export interface TimePosition { export interface TimePosition {
/** /**
@ -18,35 +22,29 @@ export interface TimePosition {
export class Clock { export class Clock {
/** /**
* The Clock Class is responsible for keeping track of the current time.
* It is also responsible for starting and stopping the Clock TransportNode.
* *
* @param app - The main application instance * @param app - main application instance
* @param ctx - The current AudioContext used by app * @param clock - zyklus clock
* @param transportNode - The TransportNode helper * @param ctx - current AudioContext used by app
* @param bpm - The current beats per minute value * @param bpm - current beats per minute value
* @param time_signature - The time signature * @param time_signature - time signature
* @param time_position - The current time position * @param time_position - current time position
* @param ppqn - The pulses per quarter note * @param ppqn - pulses per quarter note
* @param tick - The current tick since origin * @param tick - current tick since origin
* @param running - Is the clock running? * @param running - Is the clock running?
* @param lastPauseTime - The last time the clock was paused
* @param lastPlayPressTime - The last time the clock was started
* @param totalPauseTime - The total time the clock has been paused / stopped
*/ */
private _bpm: number;
private _ppqn: number;
clock: any;
ctx: AudioContext; ctx: AudioContext;
logicalTime: number; logicalTime: number;
transportNode: TransportNode | null;
private _bpm: number;
time_signature: number[]; time_signature: number[];
time_position: TimePosition; time_position: TimePosition;
private _ppqn: number;
tick: number; tick: number;
running: boolean; running: boolean;
lastPauseTime: number; timeviewer: HTMLElement;
lastPlayPressTime: number; deadline: number;
totalPauseTime: number;
constructor( constructor(
public app: Editor, public app: Editor,
@ -58,31 +56,58 @@ export class Clock {
this.tick = 0; this.tick = 0;
this._bpm = 120; this._bpm = 120;
this._ppqn = 48; this._ppqn = 48;
this.transportNode = null;
this.ctx = ctx; this.ctx = ctx;
this.running = true; this.running = true;
this.lastPauseTime = 0; this.deadline = 0;
this.lastPlayPressTime = 0; this.timeviewer = document.getElementById("timeviewer")!;
this.totalPauseTime = 0; this.clock = getAudioContext().createClock(
ctx.audioWorklet this.clockCallback,
.addModule(TransportProcessor) this.pulse_duration,
.then((e) => { );
this.transportNode = new TransportNode(ctx, {}, this.app);
this.transportNode.connect(ctx.destination);
return e;
})
.catch((e) => {
console.log("Error loading TransportProcessor.js:", e);
});
} }
// @ts-ignore
clockCallback = (time: number, duration: number, tick: number) => {
/**
* Callback function for the zyklus clock. Updates the clock info and sends a
* MIDI clock message if the setting is enabled. Also evaluates the global buffer.
*
* @param time - precise AudioContext time when the tick should happen
* @param duration - seconds between each tick
* @param tick - count of the current tick
*/
let deadline = time - getAudioContext().currentTime;
this.deadline = deadline;
this.tick = tick;
if (this.app.clock.running) {
if (this.app.settings.send_clock) {
this.app.api.MidiConnection.sendMidiClock();
}
const futureTimeStamp = this.app.clock.convertTicksToTimeposition(
this.app.clock.tick,
);
this.app.clock.time_position = futureTimeStamp;
if (futureTimeStamp.pulse % this.app.clock.ppqn == 0) {
this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${futureTimeStamp.beat + 1
} / ${this.app.clock.bpm}`;
}
if (this.app.exampleIsPlaying) {
tryEvaluate(this.app, this.app.example_buffer);
} else {
tryEvaluate(this.app, this.app.global_buffer);
}
}
// Implement TransportNode clock callback and update clock info with it
};
convertTicksToTimeposition(ticks: number): TimePosition { convertTicksToTimeposition(ticks: number): TimePosition {
/** /**
* Converts ticks to a TimePosition object. * Converts ticks to a time position.
* @param ticks The number of ticks to convert. *
* @returns The TimePosition object representing the converted ticks. * @param ticks - ticks to convert
* @returns TimePosition
*/ */
const beatsPerBar = this.app.clock.time_signature[0]; const beatsPerBar = this.app.clock.time_signature[0];
const ppqnPosition = ticks % this.app.clock.ppqn; const ppqnPosition = ticks % this.app.clock.ppqn;
const beatNumber = Math.floor(ticks / this.app.clock.ppqn); const beatNumber = Math.floor(ticks / this.app.clock.ppqn);
@ -93,10 +118,9 @@ export class Clock {
get ticks_before_new_bar(): number { get ticks_before_new_bar(): number {
/** /**
* This function returns the number of ticks separating the current moment * Calculates the number of ticks before the next bar.
* from the beginning of the next bar. *
* * @returns number - ticks before the next bar
* @returns number of ticks until next bar
*/ */
const ticskMissingFromBeat = this.ppqn - this.time_position.pulse; const ticskMissingFromBeat = this.ppqn - this.time_position.pulse;
const beatsMissingFromBar = this.beats_per_bar - this.time_position.beat; const beatsMissingFromBar = this.beats_per_bar - this.time_position.beat;
@ -105,10 +129,9 @@ export class Clock {
get next_beat_in_ticks(): number { get next_beat_in_ticks(): number {
/** /**
* This function returns the number of ticks separating the current moment * Calculates the number of ticks before the next beat.
* from the beginning of the next beat. *
* * @returns number - ticks before the next beat
* @returns number of ticks until next beat
*/ */
return this.app.clock.pulses_since_origin + this.time_position.pulse; return this.app.clock.pulses_since_origin + this.time_position.pulse;
} }
@ -116,6 +139,8 @@ export class Clock {
get beats_per_bar(): number { get beats_per_bar(): number {
/** /**
* Returns the number of beats per bar. * Returns the number of beats per bar.
*
* @returns number - beats per bar
*/ */
return this.time_signature[0]; return this.time_signature[0];
} }
@ -123,8 +148,8 @@ export class Clock {
get beats_since_origin(): number { get beats_since_origin(): number {
/** /**
* Returns the number of beats since the origin. * Returns the number of beats since the origin.
* *
* @returns number of beats since origin * @returns number - beats since the origin
*/ */
return Math.floor(this.tick / this.ppqn); return Math.floor(this.tick / this.ppqn);
} }
@ -132,8 +157,8 @@ export class Clock {
get pulses_since_origin(): number { get pulses_since_origin(): number {
/** /**
* Returns the number of pulses since the origin. * Returns the number of pulses since the origin.
* *
* @returns number of pulses since origin * @returns number - pulses since the origin
*/ */
return this.tick; return this.tick;
} }
@ -141,119 +166,112 @@ export class Clock {
get pulse_duration(): number { get pulse_duration(): number {
/** /**
* Returns the duration of a pulse in seconds. * Returns the duration of a pulse in seconds.
* @returns number - duration of a pulse in seconds
*/ */
return 60 / this.bpm / this.ppqn; return 60 / this.bpm / this.ppqn;
} }
public pulse_duration_at_bpm(bpm: number = this.bpm): number { public pulse_duration_at_bpm(bpm: number = this.bpm): number {
/** /**
* Returns the duration of a pulse in seconds at a specific bpm. * Returns the duration of a pulse in seconds at a given bpm.
*
* @param bpm - bpm to calculate the pulse duration for
* @returns number - duration of a pulse in seconds
*/ */
return 60 / bpm / this.ppqn; return 60 / bpm / this.ppqn;
} }
get bpm(): number { get bpm(): number {
/**
* Returns the current bpm.
* @returns number - current bpm
*/
return this._bpm; return this._bpm;
} }
set nudge(nudge: number) { get tickDuration(): number {
this.transportNode?.setNudge(nudge); /**
* Returns the duration of a tick in seconds.
* @returns number - duration of a tick in seconds
*/
return 1 / this.ppqn;
} }
set bpm(bpm: number) { set bpm(bpm: number) {
/**
* Sets the bpm.
* @param bpm - bpm to set
*/
if (bpm > 0 && this._bpm !== bpm) { if (bpm > 0 && this._bpm !== bpm) {
this.transportNode?.setBPM(bpm);
this._bpm = bpm; this._bpm = bpm;
this.logicalTime = this.realTime; this.clock.setDuration(() => (this.tickDuration * 60) / this.bpm);
} }
} }
get ppqn(): number { get ppqn(): number {
/**
* Returns the current ppqn.
* @returns number - current ppqn
*/
return this._ppqn; return this._ppqn;
} }
get realTime(): number {
return this.app.audioContext.currentTime - this.totalPauseTime;
}
get deviation(): number {
return Math.abs(this.logicalTime - this.realTime);
}
set ppqn(ppqn: number) { set ppqn(ppqn: number) {
/**
* Sets the ppqn.
* @param ppqn - ppqn to set
* @returns number - current ppqn
*/
if (ppqn > 0 && this._ppqn !== ppqn) { if (ppqn > 0 && this._ppqn !== ppqn) {
this._ppqn = ppqn; this._ppqn = ppqn;
this.transportNode?.setPPQN(ppqn);
this.logicalTime = this.realTime;
} }
} }
public incrementTick(bpm: number) {
this.tick++;
this.logicalTime += this.pulse_duration_at_bpm(bpm);
}
public nextTickFrom(time: number, nudge: number): number { public nextTickFrom(time: number, nudge: number): number {
/**
* Compute the time remaining before the next clock tick.
* @param time - audio context currentTime
* @param nudge - nudge in the future (in seconds)
* @returns remainingTime
*/
const pulseDuration = this.pulse_duration; const pulseDuration = this.pulse_duration;
const nudgedTime = time + nudge; const nudgedTime = time + nudge;
const nextTickTime = Math.ceil(nudgedTime / pulseDuration) * pulseDuration; const nextTickTime = Math.ceil(nudgedTime / pulseDuration) * pulseDuration;
const remainingTime = nextTickTime - nudgedTime; const remainingTime = nextTickTime - nudgedTime;
return remainingTime; return remainingTime;
} }
public convertPulseToSecond(n: number): number { public convertPulseToSecond(n: number): number {
/**
* Converts a pulse to a second.
*/
return n * this.pulse_duration; return n * this.pulse_duration;
} }
public start(): void { public start(): void {
/** /**
* Starts the TransportNode (starts the clock). * Start the clock
* *
* @remark also sends a MIDI message if a port is declared * @remark also sends a MIDI message if a port is declared
*/ */
this.app.audioContext.resume(); this.app.audioContext.resume();
this.running = true; this.running = true;
this.app.api.MidiConnection.sendStartMessage(); this.app.api.MidiConnection.sendStartMessage();
this.lastPlayPressTime = this.app.audioContext.currentTime; this.clock.start();
this.totalPauseTime += this.lastPlayPressTime - this.lastPauseTime;
this.transportNode?.start();
} }
public pause(): void { public pause(): void {
/** /**
* Pauses the TransportNode (pauses the clock). * Pause the clock.
* *
* @remark also sends a MIDI message if a port is declared * @remark also sends a MIDI message if a port is declared
*/ */
this.running = false; this.running = false;
this.transportNode?.pause();
this.app.api.MidiConnection.sendStopMessage(); this.app.api.MidiConnection.sendStopMessage();
this.lastPauseTime = this.app.audioContext.currentTime; this.clock.pause();
this.logicalTime = this.realTime;
} }
public stop(): void { public stop(): void {
/** /**
* Stops the TransportNode (stops the clock). * Stops the clock.
* *
* @remark also sends a MIDI message if a port is declared * @remark also sends a MIDI message if a port is declared
*/ */
this.running = false; this.running = false;
this.tick = 0; this.tick = 0;
this.lastPauseTime = this.app.audioContext.currentTime;
this.logicalTime = this.realTime;
this.time_position = { bar: 0, beat: 0, pulse: 0 }; this.time_position = { bar: 0, beat: 0, pulse: 0 };
this.app.api.MidiConnection.sendStopMessage(); this.app.api.MidiConnection.sendStopMessage();
this.transportNode?.stop(); this.clock.stop();
} }
} }

View File

@ -144,9 +144,10 @@ export const installInterfaceLogic = (app: Editor) => {
}); });
app.interface.audio_nudge_range.addEventListener("input", () => { app.interface.audio_nudge_range.addEventListener("input", () => {
app.clock.nudge = parseInt( // TODO: rebuild this
(app.interface.audio_nudge_range as HTMLInputElement).value, // app.clock.nudge = parseInt(
); // (app.interface.audio_nudge_range as HTMLInputElement).value,
// );
}); });
app.interface.dough_nudge_range.addEventListener("input", () => { app.interface.dough_nudge_range.addEventListener("input", () => {

View File

@ -1,68 +0,0 @@
import { tryEvaluate } from "./Evaluator";
const zeroPad = (num, places) => String(num).padStart(places, "0");
export class TransportNode extends AudioWorkletNode {
constructor(context, options, application) {
super(context, "transport", options);
this.app = application;
this.port.addEventListener("message", this.handleMessage);
this.port.start();
this.timeviewer = document.getElementById("timeviewer");
}
/** @type {(this: MessagePort, ev: MessageEvent<any>) => any} */
handleMessage = (message) => {
if (message.data) {
if (message.data.type === "bang") {
if (this.app.clock.running) {
if (this.app.settings.send_clock) {
this.app.api.MidiConnection.sendMidiClock();
}
const futureTimeStamp = this.app.clock.convertTicksToTimeposition(
this.app.clock.tick,
);
this.app.clock.time_position = futureTimeStamp;
if (futureTimeStamp.pulse % this.app.clock.ppqn == 0) {
this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${
futureTimeStamp.beat + 1
} / ${this.app.clock.bpm}`;
}
if (this.app.exampleIsPlaying) {
tryEvaluate(this.app, this.app.example_buffer);
} else {
tryEvaluate(this.app, this.app.global_buffer);
}
this.app.clock.incrementTick(message.data.bpm);
}
}
}
};
start() {
this.port.postMessage({ type: "start" });
}
pause() {
this.port.postMessage({ type: "pause" });
}
resume() {
this.port.postMessage({ type: "resume" });
}
setBPM(bpm) {
this.port.postMessage({ type: "bpm", value: bpm });
}
setPPQN(ppqn) {
this.port.postMessage({ type: "ppqn", value: ppqn });
}
setNudge(nudge) {
this.port.postMessage({ type: "nudge", value: nudge });
}
stop() {
this.port.postMessage({ type: "stop" });
}
}

View File

@ -1,47 +0,0 @@
class TransportProcessor extends AudioWorkletProcessor {
constructor(options) {
super(options);
this.port.addEventListener("message", this.handleMessage);
this.port.start();
this.nudge = 0;
this.started = false;
this.bpm = 120;
this.ppqn = 48;
this.currentPulsePosition = 0;
}
handleMessage = (message) => {
if (message.data && message.data.type === "ping") {
this.port.postMessage(message.data);
} else if (message.data.type === "start") {
this.started = true;
} else if (message.data.type === "pause") {
this.started = false;
} else if (message.data.type === "stop") {
this.started = false;
} else if (message.data.type === "bpm") {
this.bpm = message.data.value;
this.currentPulsePosition = currentTime;
} else if (message.data.type === "ppqn") {
this.ppqn = message.data.value;
this.currentPulsePosition = currentTime;
} else if (message.data.type === "nudge") {
this.nudge = message.data.value;
}
};
process(inputs, outputs, parameters) {
if (this.started) {
const adjustedCurrentTime = currentTime + this.nudge / 100;
const beatNumber = adjustedCurrentTime / (60 / this.bpm);
const currentPulsePosition = Math.ceil(beatNumber * this.ppqn);
if (currentPulsePosition > this.currentPulsePosition) {
this.currentPulsePosition = currentPulsePosition;
this.port.postMessage({ type: "bang", bpm: this.bpm });
}
}
return true;
}
}
registerProcessor("transport", TransportProcessor);

View File

@ -1,5 +1,11 @@
import { type Editor } from "../main"; import { type Editor } from "../main";
import { freqToMidi, chord as parseChord, noteNameToMidi, resolvePitchBend, safeScale } from "zifferjs"; import {
freqToMidi,
chord as parseChord,
noteNameToMidi,
resolvePitchBend,
safeScale,
} from "zifferjs";
import { SkipEvent } from "./SkipEvent"; import { SkipEvent } from "./SkipEvent";
export type EventOperation<T> = (instance: T, ...args: any[]) => void; export type EventOperation<T> = (instance: T, ...args: any[]) => void;
@ -311,16 +317,16 @@ export abstract class AudibleEvent extends AbstractEvent {
return this; return this;
}; };
protected updateValue<T>( protected updateValue<T>(key: string, value: T | T[] | null): this {
key: string,
value: T | T[] | null
): this {
if (value == null) return this; if (value == null) return this;
this.values[key] = value; this.values[key] = value;
return this; return this;
} }
public note = (value: number | string | null, ...kwargs: number[]|string[]) => { public note = (
value: number | string | null,
...kwargs: number[] | string[]
) => {
if (typeof value === "string") { if (typeof value === "string") {
const parsedNote = noteNameToMidi(value); const parsedNote = noteNameToMidi(value);
return this.updateValue("note", [parsedNote, ...kwargs].flat(Infinity)); return this.updateValue("note", [parsedNote, ...kwargs].flat(Infinity));
@ -331,8 +337,8 @@ export abstract class AudibleEvent extends AbstractEvent {
} }
}; };
public chord = (value: number|string, ...kwargs: number[]) => { public chord = (value: number | string, ...kwargs: number[]) => {
if(typeof value === "string") { if (typeof value === "string") {
const chord = parseChord(value); const chord = parseChord(value);
return this.updateValue("note", chord); return this.updateValue("note", chord);
} else { } else {

View File

@ -5,10 +5,7 @@ import {
arrayOfObjectsToObjectWithArrays, arrayOfObjectsToObjectWithArrays,
objectWithArraysToArrayOfObjects, objectWithArraysToArrayOfObjects,
} from "../Utils/Generic"; } from "../Utils/Generic";
import { import { midiToFreq, noteFromPc } from "zifferjs";
midiToFreq,
noteFromPc,
} from "zifferjs";
import { import {
superdough, superdough,
@ -66,7 +63,7 @@ export class SoundEvent extends AudibleEvent {
phaserDepth: ["phaserDepth", "phasdepth"], phaserDepth: ["phaserDepth", "phasdepth"],
phaserSweep: ["phaserSweep", "phassweep"], phaserSweep: ["phaserSweep", "phassweep"],
phaserCenter: ["phaserCenter", "phascenter"], phaserCenter: ["phaserCenter", "phascenter"],
fmadsr: function( fmadsr: function (
self: SoundEvent, self: SoundEvent,
a: number, a: number,
d: number, d: number,
@ -79,7 +76,7 @@ export class SoundEvent extends AudibleEvent {
self.updateValue("fmrelease", r); self.updateValue("fmrelease", r);
return self; return self;
}, },
fmad: function(self: SoundEvent, a: number, d: number) { fmad: function (self: SoundEvent, a: number, d: number) {
self.updateValue("fmattack", a); self.updateValue("fmattack", a);
self.updateValue("fmdecay", d); self.updateValue("fmdecay", d);
return self; return self;
@ -90,7 +87,7 @@ export class SoundEvent extends AudibleEvent {
decay: ["decay", "dec"], decay: ["decay", "dec"],
sustain: ["sustain", "sus"], sustain: ["sustain", "sus"],
release: ["release", "rel"], release: ["release", "rel"],
adsr: function( adsr: function (
self: SoundEvent, self: SoundEvent,
a: number, a: number,
d: number, d: number,
@ -103,21 +100,21 @@ export class SoundEvent extends AudibleEvent {
self.updateValue("release", r); self.updateValue("release", r);
return self; return self;
}, },
ad: function(self: SoundEvent, a: number, d: number) { ad: function (self: SoundEvent, a: number, d: number) {
self.updateValue("attack", a); self.updateValue("attack", a);
self.updateValue("decay", d); self.updateValue("decay", d);
self.updateValue("sustain", 0.0); self.updateValue("sustain", 0.0);
self.updateValue("release", 0.0); self.updateValue("release", 0.0);
return self; return self;
}, },
scope: function(self: SoundEvent) { scope: function (self: SoundEvent) {
self.updateValue("analyze", true) self.updateValue("analyze", true);
return self return self;
}, },
debug: function(self: SoundEvent, callback?: Function) { debug: function (self: SoundEvent, callback?: Function) {
self.updateValue("debug", true) self.updateValue("debug", true);
if (callback) { if (callback) {
self.updateValue("debugFunction", callback) self.updateValue("debugFunction", callback);
} }
return self; return self;
}, },
@ -126,27 +123,27 @@ export class SoundEvent extends AudibleEvent {
lpdecay: ["lpdecay", "lpd"], lpdecay: ["lpdecay", "lpd"],
lpsustain: ["lpsustain", "lps"], lpsustain: ["lpsustain", "lps"],
lprelease: ["lprelease", "lpr"], lprelease: ["lprelease", "lpr"],
cutoff: function(self: SoundEvent, value: number, resonance?: number) { cutoff: function (self: SoundEvent, value: number, resonance?: number) {
self.updateValue("cutoff", value); self.updateValue("cutoff", value);
if (resonance) { if (resonance) {
self.updateValue("resonance", resonance); self.updateValue("resonance", resonance);
} }
return self; return self;
}, },
lpf: function(self: SoundEvent, value: number, resonance?: number) { lpf: function (self: SoundEvent, value: number, resonance?: number) {
self.updateValue("cutoff", value); self.updateValue("cutoff", value);
if (resonance) { if (resonance) {
self.updateValue("resonance", resonance); self.updateValue("resonance", resonance);
} }
return self; return self;
}, },
resonance: function(self: SoundEvent, value: number) { resonance: function (self: SoundEvent, value: number) {
if (value >= 0 && value <= 1) { if (value >= 0 && value <= 1) {
self.updateValue("resonance", 50 * value); self.updateValue("resonance", 50 * value);
} }
return self; return self;
}, },
lpadsr: function( lpadsr: function (
self: SoundEvent, self: SoundEvent,
depth: number, depth: number,
a: number, a: number,
@ -161,7 +158,7 @@ export class SoundEvent extends AudibleEvent {
self.updateValue("lprelease", r); self.updateValue("lprelease", r);
return self; return self;
}, },
lpad: function(self: SoundEvent, depth: number, a: number, d: number) { lpad: function (self: SoundEvent, depth: number, a: number, d: number) {
self.updateValue("lpenv", depth); self.updateValue("lpenv", depth);
self.updateValue("lpattack", a); self.updateValue("lpattack", a);
self.updateValue("lpdecay", d); self.updateValue("lpdecay", d);
@ -174,25 +171,25 @@ export class SoundEvent extends AudibleEvent {
hpdecay: ["hpdecay", "hpd"], hpdecay: ["hpdecay", "hpd"],
hpsustain: ["hpsustain", "hpsus"], hpsustain: ["hpsustain", "hpsus"],
hprelease: ["hprelease", "hpr"], hprelease: ["hprelease", "hpr"],
hcutoff: function(self: SoundEvent, value: number, resonance?: number) { hcutoff: function (self: SoundEvent, value: number, resonance?: number) {
self.updateValue("hcutoff", value); self.updateValue("hcutoff", value);
if (resonance) { if (resonance) {
self.updateValue("hresonance", resonance); self.updateValue("hresonance", resonance);
} }
return self; return self;
}, },
hpf: function(self: SoundEvent, value: number, resonance?: number) { hpf: function (self: SoundEvent, value: number, resonance?: number) {
self.updateValue("hcutoff", value); self.updateValue("hcutoff", value);
if (resonance) { if (resonance) {
self.updateValue("hresonance", resonance); self.updateValue("hresonance", resonance);
} }
return self; return self;
}, },
hpq: function(self: SoundEvent, value: number) { hpq: function (self: SoundEvent, value: number) {
self.updateValue("hresonance", value); self.updateValue("hresonance", value);
return self; return self;
}, },
hpadsr: function( hpadsr: function (
self: SoundEvent, self: SoundEvent,
depth: number, depth: number,
a: number, a: number,
@ -207,7 +204,7 @@ export class SoundEvent extends AudibleEvent {
self.updateValue("hprelease", r); self.updateValue("hprelease", r);
return self; return self;
}, },
hpad: function(self: SoundEvent, depth: number, a: number, d: number) { hpad: function (self: SoundEvent, depth: number, a: number, d: number) {
self.updateValue("hpenv", depth); self.updateValue("hpenv", depth);
self.updateValue("hpattack", a); self.updateValue("hpattack", a);
self.updateValue("hpdecay", d); self.updateValue("hpdecay", d);
@ -220,14 +217,14 @@ export class SoundEvent extends AudibleEvent {
bpdecay: ["bpdecay", "bpd"], bpdecay: ["bpdecay", "bpd"],
bpsustain: ["bpsustain", "bps"], bpsustain: ["bpsustain", "bps"],
bprelease: ["bprelease", "bpr"], bprelease: ["bprelease", "bpr"],
bandf: function(self: SoundEvent, value: number, resonance?: number) { bandf: function (self: SoundEvent, value: number, resonance?: number) {
self.updateValue("bandf", value); self.updateValue("bandf", value);
if (resonance) { if (resonance) {
self.updateValue("bandq", resonance); self.updateValue("bandq", resonance);
} }
return self; return self;
}, },
bpf: function(self: SoundEvent, value: number, resonance?: number) { bpf: function (self: SoundEvent, value: number, resonance?: number) {
self.updateValue("bandf", value); self.updateValue("bandf", value);
if (resonance) { if (resonance) {
self.updateValue("bandq", resonance); self.updateValue("bandq", resonance);
@ -235,7 +232,7 @@ export class SoundEvent extends AudibleEvent {
return self; return self;
}, },
bandq: ["bandq", "bpq"], bandq: ["bandq", "bpq"],
bpadsr: function( bpadsr: function (
self: SoundEvent, self: SoundEvent,
depth: number, depth: number,
a: number, a: number,
@ -250,7 +247,7 @@ export class SoundEvent extends AudibleEvent {
self.updateValue("bprelease", r); self.updateValue("bprelease", r);
return self; return self;
}, },
bpad: function(self: SoundEvent, depth: number, a: number, d: number) { bpad: function (self: SoundEvent, depth: number, a: number, d: number) {
self.updateValue("bpenv", depth); self.updateValue("bpenv", depth);
self.updateValue("bpattack", a); self.updateValue("bpattack", a);
self.updateValue("bpdecay", d); self.updateValue("bpdecay", d);
@ -260,7 +257,7 @@ export class SoundEvent extends AudibleEvent {
}, },
vib: ["vib"], vib: ["vib"],
vibmod: ["vibmod"], vibmod: ["vibmod"],
fm: function(self: SoundEvent, value: number | string) { fm: function (self: SoundEvent, value: number | string) {
if (typeof value === "number") { if (typeof value === "number") {
self.values["fmi"] = value; self.values["fmi"] = value;
} else { } else {
@ -276,11 +273,11 @@ export class SoundEvent extends AudibleEvent {
begin: ["begin"], begin: ["begin"],
end: ["end"], end: ["end"],
gain: ["gain"], gain: ["gain"],
dbgain: function(self: SoundEvent, value: number) { dbgain: function (self: SoundEvent, value: number) {
self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
return self; return self;
}, },
db: function(self: SoundEvent, value: number) { db: function (self: SoundEvent, value: number) {
self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
return self; return self;
}, },
@ -303,32 +300,32 @@ export class SoundEvent extends AudibleEvent {
roomlp: ["roomlp", "rlp"], roomlp: ["roomlp", "rlp"],
roomdim: ["roomdim", "rdim"], roomdim: ["roomdim", "rdim"],
sound: ["s", "sound"], sound: ["s", "sound"],
size: function(self: SoundEvent, value: number) { size: function (self: SoundEvent, value: number) {
self.updateValue("roomsize", value); self.updateValue("roomsize", value);
return self; return self;
}, },
sz: function(self: SoundEvent, value: number) { sz: function (self: SoundEvent, value: number) {
self.updateValue("roomsize", value); self.updateValue("roomsize", value);
return self; return self;
}, },
comp: ["compressor", "cmp"], comp: ["compressor", "cmp"],
ratio: function(self: SoundEvent, value: number) { ratio: function (self: SoundEvent, value: number) {
self.updateValue("compressorRatio", value); self.updateValue("compressorRatio", value);
return self; return self;
}, },
knee: function(self: SoundEvent, value: number) { knee: function (self: SoundEvent, value: number) {
self.updateValue("compressorKnee", value); self.updateValue("compressorKnee", value);
return self; return self;
}, },
compAttack: function(self: SoundEvent, value: number) { compAttack: function (self: SoundEvent, value: number) {
self.updateValue("compressorAttack", value); self.updateValue("compressorAttack", value);
return self; return self;
}, },
compRelease: function(self: SoundEvent, value: number) { compRelease: function (self: SoundEvent, value: number) {
self.updateValue("compressorRelease", value); self.updateValue("compressorRelease", value);
return self; return self;
}, },
stretch: function(self: SoundEvent, beat: number) { stretch: function (self: SoundEvent, beat: number) {
self.updateValue("unit", "c"); self.updateValue("unit", "c");
self.updateValue("speed", 1 / beat); self.updateValue("speed", 1 / beat);
self.updateValue("cut", beat); self.updateValue("cut", beat);
@ -467,16 +464,12 @@ export class SoundEvent extends AudibleEvent {
} }
if (this.values["debug"]) { if (this.values["debug"]) {
if (this.values["debugFunction"]) { if (this.values["debugFunction"]) {
this.values["debugFunction"](filteredEvent) this.values["debugFunction"](filteredEvent);
} else { } else {
console.log(filteredEvent) console.log(filteredEvent);
} }
} }
superdough( superdough(filteredEvent, this.app.clock.deadline, filteredEvent.dur);
filteredEvent,
this.nudge - this.app.clock.deviation,
filteredEvent.dur,
);
} }
}; };
} }

View File

@ -10,17 +10,18 @@ You can turn on the oscilloscope to generate interesting visuals or to inspect a
You need to manually feed the scope with the sounds you want to inspect: You need to manually feed the scope with the sounds you want to inspect:
${makeExample( ${makeExample(
"Feeding a sine to the oscilloscope", "Feeding a sine to the oscilloscope",
` `
beat(1)::sound('sine').freq(200).ad(0, .2).scope().out() beat(1)::sound('sine').freq(200).ad(0, .2).scope().out()
`, true `,
)} true,
)}
Here is a layout of the scope configuration options: Here is a layout of the scope configuration options:
${makeExample( ${makeExample(
"Oscilloscope configuration", "Oscilloscope configuration",
` `
scope({ scope({
enabled: true, // off by default enabled: true, // off by default
color: "#fdba74", // any valid CSS color or "random" color: "#fdba74", // any valid CSS color or "random"
@ -34,12 +35,12 @@ scope({
refresh: 1 // refresh rate (in pulses) refresh: 1 // refresh rate (in pulses)
}) })
`, `,
true, true,
)} )}
${makeExample( ${makeExample(
"Demo with multiple scope mode", "Demo with multiple scope mode",
` `
rhythm(.5, [4,5].dur(4*3, 4*1), 8)::sound('fhardkick').out() rhythm(.5, [4,5].dur(4*3, 4*1), 8)::sound('fhardkick').out()
beat(0.25)::sound('square').freq([ beat(0.25)::sound('square').freq([
[250, 250/2, 250/4].pick(), [250, 250/2, 250/4].pick(),
@ -55,8 +56,8 @@ scope({enabled: true, thickness: 8,
color: ['purple', 'green', 'random'].beat(), color: ['purple', 'green', 'random'].beat(),
size: 0.5, fftSize: 2048}) size: 0.5, fftSize: 2048})
`, `,
true, true,
)} )}
Note that these values can be patterned as well! You can transform the oscilloscope into its own light show if you want. The picture is not stable anyway so you won't have much use of it for precision work :) Note that these values can be patterned as well! You can transform the oscilloscope into its own light show if you want. The picture is not stable anyway so you won't have much use of it for precision work :)

View File

@ -25,7 +25,7 @@ declare global {
z15(): Player; z15(): Player;
z16(): Player; z16(): Player;
midi(): MidiEvent; midi(): MidiEvent;
sound(name: string): SoundEvent|SkipEvent; sound(name: string): SoundEvent | SkipEvent;
} }
} }
@ -102,11 +102,11 @@ export const makeNumberExtensions = (api: UserAPI) => {
return api.midi(this.valueOf(), ...kwargs); return api.midi(this.valueOf(), ...kwargs);
}; };
Number.prototype.sound = function (name: string): SoundEvent|SkipEvent { Number.prototype.sound = function (name: string): SoundEvent | SkipEvent {
if (Number.isInteger(this.valueOf())) { if (Number.isInteger(this.valueOf())) {
return (api.sound(name) as SoundEvent).note(this.valueOf()); return (api.sound(name) as SoundEvent).note(this.valueOf());
} else { } else {
return (api.sound(name) as SoundEvent).freq(this.valueOf()); return (api.sound(name) as SoundEvent).freq(this.valueOf());
} }
}; };
}; };

View File

@ -246,20 +246,17 @@ export class Editor {
"ui-known-universe-item-template", "ui-known-universe-item-template",
) as HTMLTemplateElement; ) as HTMLTemplateElement;
if (!itemTemplate) { if (!itemTemplate) {
console.warn("Missing template #ui-known-universe-item-template");
return; return;
} }
let existing_universes = document.getElementById("existing-universes"); let existing_universes = document.getElementById("existing-universes");
if (!existing_universes) { if (!existing_universes) {
console.warn("Missing element #existing-universes");
return; return;
} }
let list = document.createElement("ul"); let list = document.createElement("ul");
list.className = list.className =
"lg:h-80 lg:text-normal text-sm h-auto lg:w-80 w-auto lg:pb-2 lg:pt-2 overflow-y-scroll text-white lg:mb-4 border rounded-lg bg-neutral-800"; "lg:h-80 lg:text-normal text-sm h-auto lg:w-80 w-auto lg:pb-2 lg:pt-2 overflow-y-scroll text-white lg:mb-4 border rounded-lg bg-neutral-800";
list.append( list.append(
...Object.keys(this.universes).map((it) => { ...Object.keys(this.universes).map((it) => {
let item = itemTemplate.content.cloneNode(true) as DocumentFragment; let item = itemTemplate.content.cloneNode(true) as DocumentFragment;

View File

@ -3887,6 +3887,11 @@ zifferjs@^0.0.44:
resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.44.tgz#c6b425166488ec05e349867e3de2460b74204449" resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.44.tgz#c6b425166488ec05e349867e3de2460b74204449"
integrity sha512-Q+0affxeUZwl+oJpsa1nb4hqHV6V4VX+pkejCQq/e9+/0H6ooTpcDQ9IDopvrWBnhA8E11k0tbwUee5TJtE8UQ== integrity sha512-Q+0affxeUZwl+oJpsa1nb4hqHV6V4VX+pkejCQq/e9+/0H6ooTpcDQ9IDopvrWBnhA8E11k0tbwUee5TJtE8UQ==
zyklus@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/zyklus/-/zyklus-0.1.4.tgz#229b2966fd1126ef72c6004697269118762bdcd5"
integrity sha512-hbv2cyy4nOI7P8nL8b3ki1jswoLzkUzewPgCLDdDfABryDkV5iO8DAbU25OgO5ShRZHLjXJIylwv5PJQPl3Mpw==
zzfx@^1.2.0: zzfx@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/zzfx/-/zzfx-1.2.0.tgz#021e5df8e1605f507e2dde15608eba22798b424b" resolved "https://registry.yarnpkg.com/zzfx/-/zzfx-1.2.0.tgz#021e5df8e1605f507e2dde15608eba22798b424b"