various improvements

This commit is contained in:
2023-10-15 00:59:49 +02:00
parent a7a38a4f1e
commit ce7596ae5f
7 changed files with 193 additions and 74 deletions

View File

@ -199,21 +199,24 @@
</div>
<!-- This modal is used for settings -->
<div id="modal-settings" class="invisible bg-neutral-900 bg-opacity-50 flex justify-center items-center absolute top-0 right-0 bottom-0 left-0">
<div class="grid grid-col-3 w-full">
<div class="bg-white rounded-lg mx-48 my-48 space-y-8 px-8">
<div id="modal-settings" class="invisible
bg-neutral-900 bg-opacity-50 flex
absolute justify-center items-center
top-0 bottom-0 overflow-y-auto
overflow-x-auto w-screen">
<div class="grid lg:grid-col-3 grid-col-1">
<div class="bg-white rounded-lg lg:mx-48 mx-0 lg:my-48 my-0 lg:space-y-8 space-y-2 lg:px-8 px-2 mt-24">
<h1 class="mt-12 font-semibold rounded bg-gray-800 justify-center text-center text-white mx-4 text-xl border-b border-gray-300 py-4">Topos Application Settings</h1>
<div class="flex flex-row mr-4 ml-4">
<div class="lg:flex lg:flex-row mr-4 ml-4">
<!-- Font Size Selection -->
<div class="bg-gray-200 rounded-lg w-full pt-2">
<div class="bg-gray-200 rounded-lg lg:w-full w-auto pt-2 pb-1 mb-2">
<p class="font-bold text-xl ml-4 pb-4 underline underline-offset-4">Font Settings</p>
<div class="mb-6 mx-4 font-semibold">
<label for="default-input" class="block mb-2 ml-1 font-normal">Size:</label>
<input type="text" id="font-size-input" type="number" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
</div>
<label for="font" class="block ml-5 mb-2 font-medium">Font:</label>
<select id="font-family" class="bg-gray-50 w-5/6 ml-4 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
<select id="font-family" class="bg-gray-50 w-2/3 ml-4 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
<option selected>Choose a font</option>
<option value="US">IBM Plex Mono</option>
<option value="CA">Victor Mono</option>
@ -221,7 +224,7 @@
</select>
</div>
<!-- Editor mode selection -->
<div class="bg-gray-200 rounded-lg ml-4 w-full pt-2">
<div class="bg-gray-200 rounded-lg lg:ml-4 lg:w-full w-auto pt-2 pb-1 mb-2">
<p class="font-bold text-xl ml-4 pb-4 underline underline-offset-4">Editor options</p>
<!-- Checkboxes -->
<div class="pr-4">
@ -248,7 +251,7 @@
</div>
</div>
<div class="bg-gray-200 rounded-lg ml-4 w-full pt-2">
<div class="bg-gray-200 rounded-lg lg:ml-4 lg:w-full w-auto pt-2 pb-1 mb-2">
<p class="font-bold text-xl ml-4 pb-4 underline underline-offset-4">File Management</p>
<div class="flex flex-col space-y-2">
<button id="download-universes" class="bg-gray-800 hover:bg-gray-900 text-white font-bold py-4 px-2 rounded-lg inline-flex items-center mx-4">
@ -347,7 +350,7 @@
</div>
<div class="flex space-x-6 border-t border-gray-200 rounded-b dark:border-gray-600 mx-4 border-spacing-y-4">
<div class="flex space-x-6 border-t border-gray-200 rounded-b dark:border-gray-600 mx-4 border-spacing-y-4 pb-2">
<button id="close-settings-button" data-modal-hide="defaultModal" type="button" class="hover:bg-gray-700 bg-gray-800 mt-4 mb-4 text-white focus:ring-4 font-medium rounded-lg text-sm px-5 py-2.5 text-center">OK</button>
</div>
</div>
@ -454,4 +457,5 @@
</template>
</body>
<p id="timeviewer" class="rounded-lg px-2 py-2 font-bold bg-white cursor-textpointer-events-none select-none text-black text-sm absolute bottom-2 right-2"></p>
<p id="fillviewer" class="invisible rounded-lg px-2 py-2 font-bold bg-white cursor-textpointer-events-none select-none text-black text-sm absolute right-2 bottom-12">/////// Fill ///////</p>
</html>

View File

@ -1255,6 +1255,13 @@ export class UserAPI {
denominator = this.meter;
// =============================================================
// Fill
// =============================================================
public fill = (): boolean => this.app.fill;
// =============================================================
// Time Filters
// =============================================================
@ -1308,6 +1315,14 @@ export class UserAPI {
};
p = this.pulse;
public tick = (tick: number | number[], offset: number = 0): boolean => {
const nArray = Array.isArray(tick) ? tick : [tick];
const results: boolean[] = nArray.map(
(value) => (this.app.clock.time_position.pulse === value + offset)
);
return results.some((value) => value === true)
}
// =============================================================
// Modulo based time filters

View File

@ -148,6 +148,24 @@ export class Clock {
}
}
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 nudgedTime = time + nudge;
const nextTickTime = Math.ceil(nudgedTime /
pulseDuration) * pulseDuration;
const remainingTime = nextTickTime - nudgedTime;
return remainingTime;
}
public convertPulseToSecond(n: number): number {
/**
* Converts a pulse to a second.

View File

@ -57,6 +57,7 @@ export class SoundEvent extends AudibleEvent {
this.updateValue("pitchJumpTime", value);
public pjt = this.pitchJumpTime;
public lfo = (value: number) => this.updateValue("lfo", value);
public znoise = (value: number) => this.updateValue("znoise", value);
public noise = (value: number) => this.updateValue("noise", value);
public zmod = (value: number) => this.updateValue("zmod", value);
public zcrush = (value: number) => this.updateValue("zcrush", value);
@ -109,6 +110,13 @@ export class SoundEvent extends AudibleEvent {
this.release(r);
return this;
};
public ad = (a: number, d: number) => {
this.attack(a);
this.decay(d);
this.sustain(0.0);
this.release(0.0);
return this;
}
// Lowpass filter
public lpenv = (value: number) => this.updateValue("lpenv", value);

View File

@ -1,5 +1,5 @@
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
import { key_shortcut, makeExampleFactory } from "../Documentation";
// @ts-ignore
export const interaction = (application: Editor): string => {
@ -9,6 +9,17 @@ 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).
## Fill
By pressing the ${key_shortcut('Alt')} key, you can trigger the <ic>Fill</ic> mode which can either be <ic>true</ic> or <ic>false</ic>. The fill will be set to <ic>true</ic> as long as the key is held. Try pressing ${key_shortcut('Alt')} when playing this example:
${makeExample(
"Claping twice as fast with fill",
`
beat(fill() ? 1/4 : 1/2)::sound('cp').out()
`, true)
}
## 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 a far away universe; you can't expect a quick response, but the messages will be received and processed eventually.
@ -23,49 +34,49 @@ MIDI input can be enabled in the settings panel. Once you have done that, you ca
* <ic>buffer_note(channel?: number)</ic>: 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()
`,
false
)}
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()
`,
false
)}
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()
`,
false
)}
false
)}
${makeExample(
"Play buffered note",
`
"Play buffered note",
`
beat(1) && buffer() && sound('sine').note(buffer_note()).out()
`,
false
)}
false
)}
### MIDI CC Input
@ -75,16 +86,16 @@ Currently supported methods for CC input are:
* <ic>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,8 +108,8 @@ beat(last_cc(74)/127*.5) :: sound('sine')
.sustain(last_cc(74)/127*.25)
.out()
`,
false
)}
false
)}
### Run scripts with MIDI
@ -120,16 +131,16 @@ You can get the current position of the mouse on the screen by using the followi
- <ic>mouseY()</ic>: the vertical position of the mouse on the screen (as a floating point number).
${makeExample(
"FM Synthesizer controlled using the mouse",
`
"FM Synthesizer controlled using the mouse",
`
beat(.25) :: sound('sine')
.fmi(mouseX() / 100)
.fmh(mouseY() / 100)
.vel(0.2)
.room(0.9).out()
`,
true
)}
true
)}
Current mouse position can also be used to generate notes:
@ -138,8 +149,8 @@ Current mouse position can also be used to generate notes:
${makeExample(
"The same synthesizer, with note control!",
`
"The same synthesizer, with note control!",
`
beat(.25) :: sound('sine')
.fmi(mouseX() / 100)
.note(noteX())
@ -147,8 +158,8 @@ beat(.25) :: sound('sine')
.vel(0.2)
.room(0.9).out()
`,
true
)}
true
)}
## Scale output for lighted keys
@ -157,16 +168,16 @@ Topos can output scales to external keyboards lighted keys using the following f
- <ic>show_scale(key: string, scale: string|int, channel?: number, port?: string|number, soundOff?: boolean): void</ic>: sends the scale as midi on messages to specified port and channel to light the keys of external keyboard. If soundOff is true, all sound off message will be sent after every note on message. This can be useful with some keyboards not supporting external channel for lightning or routing for the midi in to suppress the sound from incoming note on messages.
${makeExample(
"Show scale on external keyboard",
`show_scale("F","aeolian",0,4)`,
true
)}
"Show scale on external keyboard",
`show_scale("F","aeolian",0,4)`,
true
)}
${makeExample(
"Hide scale",
`hide_scale("F","aeolian",0,4)`,
true
)}
"Hide scale",
`hide_scale("F","aeolian",0,4)`,
true
)}
`

View File

@ -20,6 +20,17 @@ beat(.5) && snd(['sine', 'triangle', 'sawtooth', 'square'].beat()).freq(100).out
true
)}
Note that you can also use noise if you do not want to use a periodic oscillator:
${makeExample(
"Listening to the different types of noise",
`
beat(.5) && snd(['brown', 'pink', 'white'].beat()).adsr(0,.1,0,0).out()
`,
true
)}
Two functions are primarily used to control the frequency of the synthesizer:
- <ic>freq(hz: number)</ic>: sets the frequency of the oscillator.
- <ic>note(note: number|string)</ic>: sets the MIDI note of the oscillator (MIDI note converted to hertz).
@ -76,6 +87,24 @@ beat(1) :: sound('triangle')
true
)}
## Noise
A certain amount of brown noise can be added by using the <ic>.noise</ic> key:
${makeExample(
"Different vibrato settings",
`
bpm(140);
beat(1) :: sound('triangle')
.freq(400).release(0.2)
.noise([0.2,0.4,0.5].bar())
.vib([1/2, 1, 2, 4].beat())
.vibmod([1,2,4,8].beat(2))
.out()`,
true
)}
## Controlling the amplitude
Controlling the amplitude and duration of the sound can be done using various techniques. The most important thing to learn is probably how set the amplitude (volume) of your synthesizer:
@ -131,6 +160,19 @@ beat(0.5) :: sound('wt_piano')
true
)}
- <ic>ad(attack: number, decay: number)</ic>: sets the attack and decay phases, setting sustain and release to <ic>0</ic>.
${makeExample(
"Two segment envelope",
`
beat(0.5) :: sound('wt_piano')
.cutoff(1000 + usine() * 4000)
.freq(100)
.ad(0, .2)
.out()
`,
true
)}
## Substractive synthesis using filters

View File

@ -34,23 +34,23 @@ import showdownHighlight from "showdown-highlight";
import { makeStringExtensions } from "./StringExtensions";
// Broadcast that you're opening a page.
localStorage.openpages = Date.now();
window.addEventListener(
"storage",
function (e) {
if (e.key == "openpages") {
// Listen if anybody else is opening the same page!
localStorage.page_available = Date.now();
}
if (e.key == "page_available") {
document.getElementById("all")!.classList.add("invisible");
alert(
"Topos is already opened in another tab. Close this tab now to prevent data loss."
);
}
},
false
);
// localStorage.openpages = Date.now();
// window.addEventListener(
// "storage",
// function (e) {
// if (e.key == "openpages") {
// // Listen if anybody else is opening the same page!
// localStorage.page_available = Date.now();
// }
// if (e.key == "page_available") {
// document.getElementById("all")!.classList.add("invisible");
// alert(
// "Topos is already opened in another tab. Close this tab now to prevent data loss."
// );
// }
// },
// false
// );
const classMap = {
h1: "text-white lg:text-4xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 bg-neutral-900 rounded-lg py-2 px-2",
@ -88,6 +88,7 @@ const bindings = Object.keys(classMap).map((key) => ({
export class Editor {
universes: Universes = template_universes;
fill: boolean = false;
selected_universe: string;
local_index: number = 1;
editor_mode: "global" | "local" | "init" | "notes" = "global";
@ -125,6 +126,11 @@ export class Editor {
"topos-logo"
) as HTMLElement;
// Fillviewer
fill_viewer: HTMLElement = document.getElementById(
"fillviewer"
) as HTMLElement;
// Transport elements
play_buttons: HTMLButtonElement[] = [
document.getElementById("play-button-1") as HTMLButtonElement,
@ -396,6 +402,21 @@ export class Editor {
// ================================================================================
// Application event listeners
// ================================================================================
//
document.addEventListener('keydown', (event) => {
if (event.altKey) {
event.preventDefault();
this.fill = true;
this.fill_viewer.classList.remove("invisible");
}
});
document.addEventListener('keyup', (event) => {
if (event.key === 'Alt') {
this.fill = false;
this.fill_viewer.classList.add("invisible");
}
});
window.addEventListener("keydown", (event: KeyboardEvent) => {
if (event.key === "Tab") {
@ -1008,13 +1029,13 @@ export class Editor {
updateKnownUniversesView = () => {
let itemTemplate = document.getElementById("ui-known-universe-item-template") as HTMLTemplateElement;
if(!itemTemplate){
if (!itemTemplate) {
console.warn("Missing template #ui-known-universe-item-template")
return
}
let existing_universes = document.getElementById("existing-universes")
if(!existing_universes){
if (!existing_universes) {
console.warn("Missing element #existing-universes")
return
}