Merge pull request #1 from Bubobubobubobubo/two-clocks

Two clocks
This commit is contained in:
Raphaël Forment
2023-08-02 19:39:54 +02:00
committed by GitHub
6 changed files with 104 additions and 42 deletions

View File

@ -43,7 +43,7 @@ To evaluate code, press `Ctrl+Enter` (no visible animation). This is true for ev
- [x] Add a way to set the clock's time signature.
- [ ] Add a way to set the clock's swing.
- [ ] MIDI Clock In/Out support.
- [ ] Performance optimisations and metrics.
- [x] Performance optimisations and metrics.
- [ ] Add a way to save the current universe as a file.
- [ ] Add a way to load a universe from a file.
- [x] Add MIDI support.
@ -51,16 +51,17 @@ To evaluate code, press `Ctrl+Enter` (no visible animation). This is true for ev
## UI
- [ ] Settings menu with all options.
- [x] Settings menu with all options.
- [ ] Color themes (dark/light), other colors.
- [ ] Font size and font family.
- [ ] Vim mode.
- [x] Font size.
- [x] Vim mode.
- [ ] Repair the current layout (aside + CodeMirror)
- [ ] Optimizations for smaller screens and mobile devices.
- [ ] Add a new "note" buffer for each universe (MarkDown)
- [ ] Find a way to visualize console logs somewhere
## Web Audio
- [ ] Support Faut DSP integration.
- [ ] Support Tone.js integration.
- [ ] WebAudio based engine.
- [x] WebAudio based engine.

View File

@ -164,9 +164,14 @@ export class UserAPI {
// Transport functions
// =============================================================
bpm(bpm: number): void {
bpm(bpm?: number): number {
if (bpm === undefined)
return this.app.clock.bpm
this.app.clock.bpm = bpm
return bpm
}
tempo = this.bpm
time_signature(numerator: number, denominator: number): void {
this.app.clock.time_signature = [numerator, denominator]
@ -210,12 +215,16 @@ export class UserAPI {
get bar(): number { return this.app.clock.time_position.bar }
get pulse(): number { return this.app.clock.time_position.pulse }
get beat(): number { return this.app.clock.time_position.beat }
get beats_since_origin(): number { return this.app.clock.beats_since_origin }
onbar(...bar: number[]): boolean {
return bar.some(b => b === this.app.clock.time_position.bar)
onbar(n: number, ...bar: number[]): boolean {
// n is acting as a modulo on the bar number
const bar_list = [...Array(n).keys()].map(i => i + 1);
console.log(bar_list)
console.log(bar.some(b => bar_list.includes(b % n)))
return bar.some(b => bar_list.includes(b % n))
}
// TODO: bugfix here
onbeat(...beat: number[]): boolean {
let final_pulses: boolean[] = []
beat.forEach(b => {
@ -235,7 +244,6 @@ export class UserAPI {
}
mod(...pulse: number[]): boolean { return pulse.some(p => this.app.clock.time_position.pulse % p === 0) }
modbar(...bar: number[]): boolean { return bar.some(b => this.app.clock.time_position.bar % b === 0) }
euclid(pulses: number, length: number, rotate: number=0): boolean {
@ -266,9 +274,8 @@ export class UserAPI {
// Small ZZFX interface for playing with this synth
zzfx = (...thing: number[]) => zzfx(...thing);
playSound = async (values: object) => {
sound = async (values: object) => {
await this.load;
webaudioOutput(sound(values), 0.01)
webaudioOutput(sound(values), 0.00)
}
}

View File

@ -38,6 +38,15 @@ export class Clock {
get beats_per_bar(): number { return this.time_signature[0]; }
get pulse_duration(): number {
return 60 / this.bpm / this.ppqn;
}
public convertPulseToSecond(n: number): number {
return n * this.pulse_duration
}
start(): void {
// Check if the clock is already running
if (this.transportNode?.state === 'running') {

View File

@ -11,28 +11,45 @@ export class TransportNode extends AudioWorkletNode {
/** @type {HTMLSpanElement} */
this.$clock = document.getElementById("clockviewer");
this.hasBeenEvaluated = false;
this.currentPulse = 0;
this.currentPulsePosition = 0;
this.nextPulsePosition = -1;
this.executionLatency = 0;
this.lastLatencies = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
this.indexOfLastLatencies = 0;
setInterval(() => this.ping(), 1000);
}
ping() {
this.port.postMessage({ type: "ping", t: performance.now() })
}
/** @type {(this: MessagePort, ev: MessageEvent<any>) => any} */
handleMessage = (message) => {
if (message.data && message.data.type === "bang") {
let info = this.convertTimeToBarsBeats(message.data.currentTime);
this.app.clock.time_position = { bar: info.bar, beat: info.beat, pulse: info.ppqn }
this.$clock.innerHTML = `[${info.bar} | ${info.beat} | ${zeroPad(info.ppqn, '2')}]`
if (message.data && message.data.type === "ping") {
const delay = performance.now() - message.data.t;
// console.log(delay);
} else if (message.data && message.data.type === "bang") {
let { futureTimeStamp, timeToNextPulse, nextPulsePosition } = this.convertTimeToNextBarsBeats(message.data.currentTime);
// Evaluate the global buffer only once per ppqn value
if (this.currentPulse !== info.ppqn) {
this.hasBeenEvaluated = false;
}
if (!this.hasBeenEvaluated) {
tryEvaluate( this.app, this.app.global_buffer );
if (this.nextPulsePosition !== nextPulsePosition) {
this.nextPulsePosition = nextPulsePosition;
setTimeout(() => {
const now = performance.now();
this.app.clock.time_position = futureTimeStamp;
this.$clock.innerHTML = `[${futureTimeStamp.bar} | ${futureTimeStamp.beat} | ${zeroPad(futureTimeStamp.pulse, '2')}]`;
tryEvaluate(
this.app,
this.app.global_buffer
);
this.hasBeenEvaluated = true;
this.currentPulse = info.ppqn;
this.app.api.midi_clock();
this.currentPulsePosition = nextPulsePosition;
const then = performance.now();
this.lastLatencies[this.indexOfLastLatencies] = then - now;
this.indexOfLastLatencies = (this.indexOfLastLatencies + 1) % this.lastLatencies.length;
const averageLatency = this.lastLatencies.reduce((a, b) => a + b) / this.lastLatencies.length;
this.executionLatency = averageLatency / 1000;
}, (timeToNextPulse + this.executionLatency) * 1000);
}
}
};
@ -57,4 +74,28 @@ export class TransportNode extends AudioWorkletNode {
this.app.clock.tick++
return { bar: barNumber, beat: beatWithinBar, ppqn: ppqnPosition };
}
convertTimeToNextBarsBeats(currentTime) {
const beatDuration = 60 / this.app.clock.bpm;
const beatNumber = (currentTime) / beatDuration;
const beatsPerBar = this.app.clock.time_signature[0];
this.currentPulsePosition = beatNumber * this.app.clock.ppqn;
const nextPulsePosition = Math.ceil(this.currentPulsePosition);
const timeToNextPulse = this.app.clock.convertPulseToSecond(this.nextPulsePosition - this.currentPulsePosition);
const futureBeatNumber = this.nextPulsePosition / this.app.clock.ppqn;
const futureBarNumber = futureBeatNumber / beatsPerBar;
const futureTimeStamp = {
bar: Math.floor(futureBarNumber) + 1,
beat: Math.floor(futureBeatNumber) % beatsPerBar + 1,
pulse: Math.floor(this.nextPulsePosition) % this.app.clock.ppqn
};
this.app.clock.tick++
return {
futureTimeStamp,
timeToNextPulse,
nextPulsePosition
};
}
}

View File

@ -8,7 +8,9 @@ class TransportProcessor extends AudioWorkletProcessor {
}
handleMessage = (message) => {
if (message.data === "start") {
if (message.data && message.data.type === "ping") {
this.port.postMessage(message.data);
} else if (message.data === "start") {
this.started = true;
} else if (message.data === "pause") {
this.started = false;

View File

@ -20,8 +20,8 @@ import {
template_universe,
template_universes,
} from "./AppSettings";
import { tryEvaluate } from "./Evaluator";
import { oneDark } from "@codemirror/theme-one-dark";
import { tryEvaluate } from "./Evaluator";
export class Editor {
@ -127,8 +127,6 @@ export class Editor {
// CodeMirror Management
// ================================================================================
console.log(this.settings)
this.fontSize = new Compartment();
this.vimModeCompartment = new Compartment();
const vimPlugin = this.settings.vimMode ? vim() : [];
@ -378,6 +376,8 @@ export class Editor {
}
}
});
tryEvaluate(this, this.universes[this.selected_universe.toString()].init)
}
get global_buffer() {
@ -404,49 +404,49 @@ export class Editor {
}
changeModeFromInterface(mode: "global" | "local" | "init") {
const interface_buttons: HTMLElement[] = [
this.local_button,
this.global_button,
this.init_button,
];
const interface_buttons: HTMLElement[] = [ this.local_button, this.global_button, this.init_button ];
let changeColor = (button: HTMLElement) => {
interface_buttons.forEach((button) => {
let svg = button.children[0] as HTMLElement;
if (svg.classList.contains("text-orange-300")) {
svg.classList.remove("text-orange-300");
svg.classList.add("text-white");
button.classList.remove("text-orange-300");
console.log(svg.classList)
console.log(button.classList)
}
});
button.children[0].classList.remove('text-white');
button.children[0].classList.add("text-orange-300");
button.classList.add("text-orange-300");
};
if (mode === this.editor_mode) return;
switch (mode) {
case "local":
if (this.local_script_tabs.classList.contains("hidden")) {
this.local_script_tabs.classList.remove("hidden");
}
this.currentFile.candidate = this.view.state.doc.toString();
changeColor(this.local_button);
this.editor_mode = "local";
changeColor(this.local_button);
break;
case "global":
if (!this.local_script_tabs.classList.contains("hidden")) {
this.local_script_tabs.classList.add("hidden");
}
this.currentFile.candidate = this.view.state.doc.toString();
changeColor(this.global_button);
this.editor_mode = "global";
changeColor(this.global_button);
break;
case "init":
if (!this.local_script_tabs.classList.contains("hidden")) {
this.local_script_tabs.classList.add("hidden");
}
this.currentFile.candidate = this.view.state.doc.toString();
this.editor_mode = "init";
changeColor(this.init_button);
this.changeToLocalBuffer(0);
this.editor_mode = "init";
break;
}
this.updateEditorView();
@ -546,6 +546,7 @@ export class Editor {
this.view.dispatch({
changes: { from: 0, insert: this.currentFile.candidate },
});
tryEvaluate(this, this.universes[this.selected_universe.toString()].init)
}
getCodeBlock(): string {
@ -686,4 +687,5 @@ window.addEventListener("beforeunload", () => {
app.currentFile.candidate = app.view.state.doc.toString();
app.currentFile.committed = app.view.state.doc.toString();
app.settings.saveApplicationToLocalStorage(app.universes, app.settings);
app.clock.stop()
});