From 0c21770eaa30b2fcfc1294b82d5d57c919778f50 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sat, 7 Oct 2023 20:09:19 +0200 Subject: [PATCH] maintenance and audio nudging bar --- index.html | 18 ++++++- src/API.ts | 51 +++++++++++-------- src/Clock.ts | 4 ++ src/TransportNode.js | 6 ++- src/TransportProcessor.js | 84 +++++++++++++++++--------------- src/documentation/code.ts | 34 ++++++------- src/documentation/engine.ts | 2 +- src/documentation/interaction.ts | 84 ++++++++++++++++---------------- src/documentation/keyboard.ts | 2 +- src/main.ts | 11 ++++- 10 files changed, 173 insertions(+), 123 deletions(-) diff --git a/index.html b/index.html index ad692e1..78a1dd2 100644 --- a/index.html +++ b/index.html @@ -304,7 +304,23 @@ - + + +
+

Audio Output nudge

+
+ + 0 +
+
+
diff --git a/src/API.ts b/src/API.ts index 83a41a3..0d42586 100644 --- a/src/API.ts +++ b/src/API.ts @@ -448,17 +448,17 @@ export class UserAPI { this.MidiConnection.panic(); }; - public active_note_events = (channel?: number): MidiNoteEvent[]|undefined => { + public active_note_events = (channel?: number): MidiNoteEvent[] | undefined => { /** * @returns A list of currently active MIDI notes */ let events; - if(channel) { + if (channel) { events = this.MidiConnection.activeNotesFromChannel(channel); } else { events = this.MidiConnection.activeNotes; } - if(events.length>0) return events + if (events.length > 0) return events else return undefined; } @@ -469,12 +469,12 @@ export class UserAPI { return this.MidiConnection.activeNotes.length > 0; } - public active_notes = (channel?: number): number[]|undefined => { + public active_notes = (channel?: number): number[] | undefined => { /** * @returns A list of currently active MIDI notes */ const notes = this.active_note_events(channel); - if(notes && notes.length > 0) return notes.map((e) => e.note); + if (notes && notes.length > 0) return notes.map((e) => e.note); else return undefined; } @@ -485,16 +485,16 @@ export class UserAPI { this.MidiConnection.activeNotes = []; } - public sticky_notes = (channel?: number): number[]|undefined => { + public sticky_notes = (channel?: number): number[] | undefined => { /** * * @param channel * @returns */ let notes; - if(channel) notes = this.MidiConnection.stickyNotesFromChannel(channel); + if (channel) notes = this.MidiConnection.stickyNotesFromChannel(channel); else notes = this.MidiConnection.stickyNotes; - if(notes.length > 0) return notes.map((e) => e.note); + if (notes.length > 0) return notes.map((e) => e.note); else return undefined; } @@ -509,19 +509,19 @@ export class UserAPI { /** * Return true if there is last note event */ - if(channel) return this.MidiConnection.findNoteFromBufferInChannel(channel) !== undefined; + if (channel) return this.MidiConnection.findNoteFromBufferInChannel(channel) !== undefined; else return this.MidiConnection.noteInputBuffer.length > 0; } - public buffer_event = (channel?: number): MidiNoteEvent|undefined => { + public buffer_event = (channel?: number): MidiNoteEvent | undefined => { /** * @returns Returns latest unlistened note event */ - if(channel) return this.MidiConnection.findNoteFromBufferInChannel(channel); + if (channel) return this.MidiConnection.findNoteFromBufferInChannel(channel); else return this.MidiConnection.noteInputBuffer.shift(); } - public buffer_note = (channel?: number): number|undefined => { + public buffer_note = (channel?: number): number | undefined => { /** * @returns Returns latest received note */ @@ -529,11 +529,11 @@ export class UserAPI { return note ? note.note : undefined; } - public last_note_event = (channel?: number): MidiNoteEvent|undefined => { + public last_note_event = (channel?: number): MidiNoteEvent | undefined => { /** * @returns Returns last received note */ - if(channel) return this.MidiConnection.lastNoteInChannel[channel]; + if (channel) return this.MidiConnection.lastNoteInChannel[channel]; else return this.MidiConnection.lastNote; } @@ -549,8 +549,8 @@ export class UserAPI { /** * @returns Returns last received cc */ - if(channel) { - if(this.MidiConnection.lastCCInChannel[channel]) { + if (channel) { + if (this.MidiConnection.lastCCInChannel[channel]) { return this.MidiConnection.lastCCInChannel[channel][control]; } else return 64; } @@ -561,15 +561,15 @@ export class UserAPI { /** * Return true if there is last cc event */ - if(channel) return this.MidiConnection.findCCFromBufferInChannel(channel) !== undefined; + if (channel) return this.MidiConnection.findCCFromBufferInChannel(channel) !== undefined; else return this.MidiConnection.ccInputBuffer.length > 0; } - public buffer_cc = (channel?: number): MidiCCEvent|undefined => { + public buffer_cc = (channel?: number): MidiCCEvent | undefined => { /** * @returns Returns latest unlistened cc event */ - if(channel) return this.MidiConnection.findCCFromBufferInChannel(channel); + if (channel) return this.MidiConnection.findCCFromBufferInChannel(channel); else return this.MidiConnection.ccInputBuffer.shift(); } @@ -913,6 +913,19 @@ export class UserAPI { // 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 bpm = (n?: number): number => { /** * Sets or returns the current bpm. diff --git a/src/Clock.ts b/src/Clock.ts index 6e8bb5c..97b3f22 100644 --- a/src/Clock.ts +++ b/src/Clock.ts @@ -126,6 +126,10 @@ export class Clock { return this._bpm; } + set nudge(nudge: number) { + this.transportNode?.setNudge(nudge); + } + set bpm(bpm: number) { if (bpm > 0 && this._bpm !== bpm) { this._bpm = bpm; diff --git a/src/TransportNode.js b/src/TransportNode.js index 4af3380..12d9e66 100644 --- a/src/TransportNode.js +++ b/src/TransportNode.js @@ -13,7 +13,7 @@ export class TransportNode extends AudioWorkletNode { /** @type {(this: MessagePort, ev: MessageEvent) => any} */ handleMessage = (message) => { if (message.data && message.data.type === "bang") { - if(this.app.settings.send_clock) this.app.api.MidiConnection.sendMidiClock(); + if (this.app.settings.send_clock) this.app.api.MidiConnection.sendMidiClock(); this.app.clock.tick++; const futureTimeStamp = this.app.clock.convertTicksToTimeposition( this.app.clock.tick @@ -45,6 +45,10 @@ export class TransportNode extends AudioWorkletNode { this.port.postMessage({ type: "ppqn", value: ppqn }); } + setNudge(nudge) { + this.port.postMessage({ type: "nudge", value: nudge }); + } + stop() { this.port.postMessage("stop"); } diff --git a/src/TransportProcessor.js b/src/TransportProcessor.js index 0542ca7..ad09d09 100644 --- a/src/TransportProcessor.js +++ b/src/TransportProcessor.js @@ -1,47 +1,51 @@ class TransportProcessor extends AudioWorkletProcessor { - constructor(options) { - super(options); - this.port.addEventListener("message", this.handleMessage); - this.port.start(); - 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 === "start") { - this.started = true; - } else if(message.data === "pause") { - this.started = false; - } else if(message.data === "stop") { - this.started = false; - } else if(message.data.type === 'bpm') { - this.bpm = message.data.value; - this.currentPulsePosition = 0; - } else if(message.data.type === 'ppqn') { - this.ppqn = message.data.value; - this.currentPulsePosition = 0; - } - }; + 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; + } - process(inputs, outputs, parameters) { - if (this.started) { - const beatNumber = currentTime / (60 / this.bpm); - const currentPulsePosition = Math.ceil(beatNumber * this.ppqn); - if(currentPulsePosition > this.currentPulsePosition) { - this.currentPulsePosition = currentPulsePosition; - this.port.postMessage({ type: "bang" }); - } - } - return true; + handleMessage = (message) => { + 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; + } else if (message.data === "stop") { + this.started = false; + } else if (message.data.type === 'bpm') { + this.bpm = message.data.value; + this.currentPulsePosition = 0; + } else if (message.data.type === 'ppqn') { + this.ppqn = message.data.value; + this.currentPulsePosition = 0; + } else if (message.data.type === 'nudge') { + this.nudge = message.data.value } + } + + process(inputs, outputs, parameters) { + if (this.started) { + const adjustedCurrentTime = currentTime + (this.nudge / 1000); + 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" }); + } + } + return true; + } } registerProcessor( - "transport", - TransportProcessor -); \ No newline at end of file + "transport", + TransportProcessor +); diff --git a/src/documentation/code.ts b/src/documentation/code.ts index 1ed3f25..f6d8456 100644 --- a/src/documentation/code.ts +++ b/src/documentation/code.ts @@ -23,7 +23,7 @@ The code you enter in any of the scripts is evaluated in strict mode. This tells - **about variables:** the state of your variables is not kept between iterations. If you write let a = 2 and change the value later on, the value will be reset to 2 after each run! There are other ways to deal with variables and to share variables between scripts! Some variables like **iterators** can keep their state between iterations because they are saved **with the file itself**. - **about errors and printing:** your code will crash! Don't worry, it will hopefully try to crash in the most gracious way possible. To check if your code is erroring, you will have to open the dev console with ${key_shortcut( "Ctrl + Shift + I" - )}. You cannot directly use console.log('hello, world') in the interface. You will have to open the console as well to see your messages being printed there! + )}. You cannot directly use console.log('hello, world') in the interface but you can use log(message) to print a one line message. You will have to open the console as well to see your messages being printed there! - **about new syntax:** sometimes, we have taken liberties with the JavaScript syntax in order to make it easier/faster to write on stage. && can also be written :: or -> because it is faster to type or better for the eyes! ## Common idioms @@ -31,8 +31,8 @@ The code you enter in any of the scripts is evaluated in strict mode. This tells There are some techniques that Topos players are using to keep their JavaScript short and tidy. Don't try to write the shortest possible code but use shortcuts when it makes sense. It's sometimes very comforting to take time to write utilities and scripts that you will often reuse. Take a look at the following examples: ${makeExample( - "Shortening your if conditions", - ` + "Shortening your if conditions", + ` // The && symbol (overriden by :: in Topos) is very often used for conditions! beat(.75) :: snd('linnhats').n([1,4,5].beat()).out() beat(1) :: snd('bd').out() @@ -42,41 +42,41 @@ beat(1) :: snd('bd').out() //// beat(1) :: snd('bd').out() `, - true -)} + true + )} ${makeExample( - "More complex conditions using ?", - ` + "More complex conditions using ?", + ` // The ? symbol can be used to write a if/true/false condition beat(4) ? snd('kick').out() : beat(2) :: snd('snare').out() // (true) ? log('very true') : log('very false') `, - false -)} + false + )} ${makeExample( - "Using not and other short symbols", - ` + "Using not and other short symbols", + ` // The ! symbol can be used to reverse a condition beat(4) ? snd('kick').out() : beat(2) :: snd('snare').out() !beat(2) :: beat(0.5) :: snd('clap').out() `, - false -)} + false + )} ## About crashes and bugs Things will crash, that's also part of the show. You will learn progressively to avoid mistakes and to write safer code. Do not hesitate to kill the page or to stop the transport if you feel overwhelmed by an algorithm blowing up. There are no safeties in place to save you. This is to ensure that you have all the available possible room to write bespoke code and experiment with your ideas through code. ${makeExample( - "This example will crash! Who cares?", - `// This is crashing. Open your console! + "This example will crash! Who cares?", + `// This is crashing. Open your console! qjldfqsdklqsjdlkqjsdlqkjdlksjd `, - false -)} + false + )} `; }; diff --git a/src/documentation/engine.ts b/src/documentation/engine.ts index 6c2d6b2..a72967e 100644 --- a/src/documentation/engine.ts +++ b/src/documentation/engine.ts @@ -221,7 +221,7 @@ beat(1) :: sound('kick').shape(0.35).out()`, ## Filters -There are three basic filters: a _lowpass_, _highpass_ and _bandpass_ filters with rather soft slope. Each of them can take up to two arguments. You can also use only the _cutoff_ frequency and the resonance will stay to its default nominal value. +There are three basic filters: a _lowpass_, _highpass_ and _bandpass_ filters with rather soft slope. Each of them can take up to two arguments. You can also use only the _cutoff_ frequency and the resonance will stay to its default nominal value. You will learn more about the usage of filters in the synths page! | Method | Alias | Description | |------------|-------|-----------------------------------------| diff --git a/src/documentation/interaction.ts b/src/documentation/interaction.ts index ed41198..ca2aa99 100644 --- a/src/documentation/interaction.ts +++ b/src/documentation/interaction.ts @@ -9,13 +9,13 @@ export const interaction = (application: Editor): string => { Topos can interact with the physical world or react to events coming from outside the system (_MIDI_, physical control, etc). -## Midi input +## MIDI input -Topos can use MIDI input to estimate the bpm from incoming clock messages and to control sounds with incoming note and cc messages. Sending MIDI messages to Topos is like sending messages to far away universe; you can't expect a quick response, but the messages will be received and processed eventually. +Topos can use MIDI input to estimate the BPM from incoming Clock messages and to control sounds with incoming note and CC messages. Sending MIDI messages to Topos is like sending messages to a far away universe; you can't expect a quick response, but the messages will be received and processed eventually. -### Note input +### Note Input -Midi input can be enabled in the settings panel. Once you have done that, you can use the following functions to control values. All methods have channel parameter as optional value to receive only notes from a certain channel: +MIDI input can be enabled in the settings panel. Once you have done that, you can use the following functions to control values. All methods have channel parameter as optional value to receive only notes from a certain channel: * active_notes(channel?: number): returns array of the active notes / pressed keys as an array of MIDI note numbers (0-127). Returns undefined if no notes are active. * sticky_notes(channel?: number): returns array of the last pressed keys as an array of MIDI note numbers (0-127). Notes are added and removed from the list with the "Note on"-event. Returns undefined if no keys have been pressed. * last_note(channel?: number): returns the last note that has been received. Returns 60 if no other notes have been received. @@ -23,68 +23,68 @@ Midi input can be enabled in the settings panel. Once you have done that, you ca * buffer_note(channel?: number): returns last unread note that has been received. Note is fetched and removed from start of the buffer once this is called. Returns undefined if no notes have been received. ${makeExample( - "Play active notes as chords", - ` + "Play active notes as chords", + ` beat(1) && active_notes() && sound('sine').chord(active_notes()).out() `, - true -)} + true + )} ${makeExample( - "Play active notes as arpeggios", - ` + "Play active notes as arpeggios", + ` beat(0.25) && active_notes() && sound('juno').note( active_notes().beat(0.5)+[12,24].beat(0.25) ).cutoff(300 + usine(1/4) * 2000).out() `, - true -)} + false + )} ${makeExample( - "Play continous arpeggio with sticky notes", - ` + "Play continous arpeggio with sticky notes", + ` beat(0.25) && sticky_notes() && sound('arp') .note(sticky_notes().palindrome().beat(0.25)).out() `, - true -)} + false + )} ${makeExample( - "Play last note", - ` + "Play last note", + ` beat(0.5) && sound('sawtooth').note(last_note()) .vib([1, 3, 5].beat(1)) .vibmod([1,3,2,4].beat(2)).out() `, - true -)} + false + )} ${makeExample( - "Play buffered note", - ` + "Play buffered note", + ` beat(1) && buffer() && sound('sine').note(buffer_note()).out() `, - true -)} + false + )} -### Midi CC input +### MIDI CC Input -Midi CC messages can be used to control any value in Topos. Midi input can be defined in Settings and last received CC message can be used to control any numeric value within Topos. +Midi CC messages can be used to control any value in Topos. MIDI input can be defined in Settings and last received CC message can be used to control any numeric value within Topos. Currently supported methods for CC input are: * last_cc(control: number, channel?: number): Returns last received CC value for given control number (and optional channel). By default last CC value is last value from ANY channel or 64 if no CC messages have been received. ${makeExample( - "Play notes with cc", - ` + "Play notes with cc", + ` beat(0.5) && sound('arp').note(last_cc(74)).out() `, - true -)} + true + )} ${makeExample( - "Control everything with CCs", - ` + "Control everything with CCs", + ` beat(0.5) :: sound('sine') .freq(last_cc(75)*3) .cutoff(last_cc(76)*2*usine()) @@ -97,19 +97,22 @@ beat(last_cc(74)/127*.5) :: sound('sine') .sustain(last_cc(74)/127*.25) .out() `, - true -)} + false + )} -### Run scripts with mini note messages and channels +### Run scripts with MIDI -Midi note messages with channels can also be used to run scripts. This can be enabled in the settings panel by setting "Route channels to scripts". +MIDI note messages with channels can also be used to trigger scripts. +This can be enabled in the settings panel by setting _Route channels to scripts_. -### Midi clock +### MIDI clock Synchronisation -Topos can controlled from external hadware or software using Midi clock messages. To enable this feature, you need to connect a MIDI input as Midi Clock in the settings panel. Once you have done that, Topos will listen to incoming clock messages and will use them to estimate the current bpm. Topos will also listen to Start, Stop and Continue messages to start and stop the evaluation. Different MIDI devices can send clock at different resolution, define Clock PPQN in settings to match the resolution of your device. +Topos can controlled from external hadware or software using MIDI clock messages. To enable this feature, you need to connect a MIDI input as Midi Clock in the settings panel. +Once you have done that, Topos will listen to incoming Clock messages and will use them to estimate the current BPM. Topos will also listen to Start, Stop and Continue messages to start and stop the evaluation. +Different MIDI devices can send clock at different resolution, define Clock PPQN in settings to match the resolution of your device. -## Mouse input +## Mouse Input You can get the current position of the mouse on the screen by using the following functions: @@ -146,8 +149,5 @@ beat(.25) :: sound('sine') `, true )} - - - ` } diff --git a/src/documentation/keyboard.ts b/src/documentation/keyboard.ts index 6a1604e..ebe012b 100644 --- a/src/documentation/keyboard.ts +++ b/src/documentation/keyboard.ts @@ -4,7 +4,7 @@ export const shortcuts = (): string => { return ` # Keybindings -Topos is made to be controlled entirely with a keyboard. It is recommanded to stop using the mouse as much as possible when you are _live coding_. Some of the keybindings might not work like expected on Windows/Linux. They all work on MacOS. A fix is on the way. Here is a list of the most important keybindings: +Topos is made to be controlled entirely with a keyboard. It is recommanded to stop using the mouse as much as possible when you are _live coding_. Here is a list of the most important keybindings: ## Transport diff --git a/src/main.ts b/src/main.ts index a70eb15..43d111e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -240,7 +240,7 @@ export class Editor { "midi-channels-scripts" ) as HTMLInputElement; - midi_clock_ppqn: HTMLSelectElement = document.getElementById( + midi_clock_ppqn: HTMLSelectElement = document.getElementById( "midi-clock-ppqn-input" ) as HTMLSelectElement; @@ -262,6 +262,11 @@ export class Editor { "share-button" ) as HTMLElement; + // Audio nudge range + audio_nudge_range: HTMLInputElement = document.getElementById( + "audio_nudge" + ) as HTMLInputElement; + // Error line error_line: HTMLElement = document.getElementById( "error_line" @@ -593,6 +598,10 @@ export class Editor { } }); + this.audio_nudge_range.addEventListener("input", () => { + this.clock.nudge = parseInt(this.audio_nudge_range.value); + }) + this.upload_universe_button.addEventListener("click", () => { const fileInput = document.createElement("input"); fileInput.type = "file";