This commit is contained in:
2023-08-22 13:12:37 +02:00
7 changed files with 141 additions and 47 deletions

View File

@ -37,7 +37,7 @@
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"tone": "^14.8.49", "tone": "^14.8.49",
"vite-plugin-markdown": "^2.1.0", "vite-plugin-markdown": "^2.1.0",
"zifferjs": "^0.0.9", "zifferjs": "^0.0.10",
"zzfx": "^1.2.0" "zzfx": "^1.2.0"
} }
} }

View File

@ -483,7 +483,7 @@ export class UserAPI {
// Sequencer related functions // Sequencer related functions
// ============================================================= // =============================================================
public slice = (chunk: number): boolean => { public div = (chunk: number): boolean => {
const time_pos = this.epulse(); const time_pos = this.epulse();
const current_chunk = Math.floor( const current_chunk = Math.floor(
time_pos / Math.floor(chunk * this.ppqn()) time_pos / Math.floor(chunk * this.ppqn())
@ -491,13 +491,13 @@ export class UserAPI {
return current_chunk % 2 === 0; return current_chunk % 2 === 0;
}; };
public barslice = (chunk: number): boolean => { public divbar = (chunk: number): boolean => {
const time_pos = this.bar() - 1; const time_pos = this.bar() - 1;
const current_chunk = Math.floor(time_pos / chunk); const current_chunk = Math.floor(time_pos / chunk);
return current_chunk % 2 === 0; return current_chunk % 2 === 0;
}; };
public seqslice = (...args: any): any => { public divseq = (...args: any): any => {
const chunk_size = args[0]; // Get the first argument (chunk size) const chunk_size = args[0]; // Get the first argument (chunk size)
const elements = args.slice(1); // Get the rest of the arguments as an array const elements = args.slice(1); // Get the rest of the arguments as an array
const timepos = this.epulse(); const timepos = this.epulse();
@ -578,9 +578,10 @@ export class UserAPI {
*/ */
return this.randomGen() * (max - min) + min; return this.randomGen() * (max - min) + min;
}; };
irand = this.randI irand = this.randI;
rI = this.randI; rI = this.randI;
r = this.rand; r = this.rand;
ir = this.randI;
seed = (seed: string | number): void => { seed = (seed: string | number): void => {
/** /**
@ -704,55 +705,91 @@ export class UserAPI {
// Probability functions // Probability functions
// ============================================================= // =============================================================
public almostNever = (): boolean => { public odds = (n: number, sec: number = 15): boolean => {
/**
* Returns true n% of the time.
*
* @param n - The probability of returning true. 1/4 = 25% = 0.25, 80/127 = 62.9% = 0.6299212598425197, etc...
* @param sec - The time frame in seconds
* @returns True n% of the time
*/
return this.randomGen() < n*this.ppqn()/(this.ppqn()*sec);
};
public almostNever = (sec: number = 15): boolean => {
/**
* Returns true 2.5% of the time in given time frame.
*
* @param sec - The time frame in seconds
* @returns True 2.5% of the time
*/
return this.randomGen() < 0.025*this.ppqn()/(this.ppqn()*sec);
};
public rarely = (sec: number = 15): boolean => {
/** /**
* Returns true 10% of the time. * Returns true 10% of the time.
* *
* @returns True 10% of the time * @param sec - The time frame in seconds
* @returns True 10% of the time.
*/ */
return this.randomGen() > 0.9; return this.randomGen() < 0.1*this.ppqn()/(this.ppqn()*sec);
}; };
public sometimes = (): boolean => { public scarcely = (sec: number = 15): boolean => {
/**
* Returns true 50% of the time.
*
* @returns True 50% of the time
*/
return this.randomGen() > 0.5;
};
public rarely = (): boolean => {
/** /**
* Returns true 25% of the time. * Returns true 25% of the time.
* *
* @param sec - The time frame in seconds
* @returns True 25% of the time * @returns True 25% of the time
*/ */
return this.randomGen() > 0.75; return this.randomGen() < 0.25*this.ppqn()/(this.ppqn()*sec);
}; };
public often = (): boolean => { public sometimes = (sec: number = 15): boolean => {
/**
* Returns true 50% of the time.
*
* @param sec - The time frame in seconds
* @returns True 50% of the time
*/
return this.randomGen() < 0.5*this.ppqn()/(this.ppqn()*sec);
};
public often = (sec: number = 15): boolean => {
/** /**
* Returns true 75% of the time. * Returns true 75% of the time.
* *
* @param sec - The time frame in seconds
* @returns True 75% of the time * @returns True 75% of the time
*/ */
return this.randomGen() > 0.25; return this.randomGen() < 0.75*this.ppqn()/(this.ppqn()*sec);
}; };
public almostAlways = (): boolean => { public frequently = (sec: number = 15): boolean => {
/** /**
* Returns true 90% of the time. * Returns true 90% of the time.
* *
* @param sec - The time frame in seconds
* @returns True 90% of the time * @returns True 90% of the time
*/ */
return this.randomGen() > 0.1; return this.randomGen() < 0.9*this.ppqn()/(this.ppqn()*sec);
};
public almostAlways = (sec: number = 15): boolean => {
/**
* Returns true 98.5% of the time.
*
* @param sec - The time frame in seconds
* @returns True 98.5% of the time
*/
return this.randomGen() < 0.985*this.ppqn()/(this.ppqn()*sec);
}; };
public dice = (sides: number): number => { public dice = (sides: number): number => {
/** /**
* Returns the value of a dice roll with n sides. * Returns the value of a dice roll with n sides.
* *
* @param sides - The number of sides on the dice * @param sides - The number of sides on the dice
* @returns The value of a dice roll with n sides * @returns The value of a dice roll with n sides
*/ */
@ -965,12 +1002,19 @@ export class UserAPI {
return results.some((value) => value === true); return results.some((value) => value === true);
}; };
public modpulse = (...n: number[]): boolean => {
const results: boolean[] = n.map((value) => this.epulse() % value === 0);
return results.some((value) => value === true);
};
pmod = this.modpulse;
public modbar = (...n: number[]): boolean => { public modbar = (...n: number[]): boolean => {
const results: boolean[] = n.map( const results: boolean[] = n.map(
(value) => this.bar() % Math.floor(value * this.ppqn()) === 0 (value) => this.bar() % Math.floor(value * this.ppqn()) === 0
); );
return results.some((value) => value === true); return results.some((value) => value === true);
}; };
bmod = this.modbar;
// ============================================================= // =============================================================
// Rythmic generators // Rythmic generators

View File

@ -143,7 +143,12 @@ Some functions can be leveraged to play rhythms without thinking too much about
\`\`\` \`\`\`
- <icode>onbar(...values: number[])</icode>: returns <icode>true</icode> if the bar is currently equal to any of the specified values. - <icode>onbar(...values: number[])</icode>: returns <icode>true</icode> if the bar is currently equal to any of the specified values.
- <icode>modbar(...values: number[])</icode>: returns <icode>true</icode> if the bar is currently a multiple of any of the specified values. - <icode>modbar(...values: number[]) or bmod(...)</icode>: returns <icode>true</icode> if the bar is currently a multiple of any of the specified values.
- <icode>modpulse(...values: number[]) or pmod(...)</icode>: returns <icode>true</icode> if the pulse is currently a multiple of any of the specified values.
- <icode>div(chunk: number)</icode>: returns <icode>true</icode> for every pulse in intervals of given number of beats
- <icode>divbar(chunk: number)</icode>: returns <icode>true</icode> for every pulse in intervals of given number of bars
- <icode>divseq(...values: number[])</icode>: returns <icode>true</icode> for every pulse in intervals of given number of beats returning different value each time.
## Rhythm generators ## Rhythm generators
@ -200,7 +205,7 @@ You can use Topos to play MIDI thanks to the [WebMIDI API](https://developer.moz
sometimes() && mod(.25) && note(seqbeat(64, 67, 69) + 24).duration(0.05).out() sometimes() && mod(.25) && note(seqbeat(64, 67, 69) + 24).duration(0.05).out()
\`\`\` \`\`\`
### Note chaining ## Note chaining
The <icode>note(number)</icode> function can be chained to _specify_ a midi note more. For instance, you can add a duration, a velocity, a channel, etc...: The <icode>note(number)</icode> function can be chained to _specify_ a midi note more. For instance, you can add a duration, a velocity, a channel, etc...:
@ -431,6 +436,11 @@ You can get the current position of the mouse on the screen by using the followi
- <icode>mouseX()</icode>: the horizontal position of the mouse on the screen (as a floating point number). - <icode>mouseX()</icode>: the horizontal position of the mouse on the screen (as a floating point number).
- <icode>mouseY()</icode>: the vertical position of the mouse on the screen (as a floating point number). - <icode>mouseY()</icode>: the vertical position of the mouse on the screen (as a floating point number).
Current mouse position can also be used to generate notes:
- <icode>noteX()</icode>: returns a MIDI note number (0-127) based on the horizontal position of the mouse on the screen.
- <icode>noteY()</icode>: returns a MIDI note number (0-127) based on the vertical position of the mouse on the screen.
## Low Frequency Oscillators ## Low Frequency Oscillators
Low Frequency Oscillators (_LFOs_) are an important piece in any digital audio workstation or synthesizer. Topos implements some basic waveforms you can play with to automatically modulate your paremeters. Low Frequency Oscillators (_LFOs_) are an important piece in any digital audio workstation or synthesizer. Topos implements some basic waveforms you can play with to automatically modulate your paremeters.
@ -473,8 +483,22 @@ Low Frequency Oscillators (_LFOs_) are an important piece in any digital audio w
There are some simple functions to play with probabilities. There are some simple functions to play with probabilities.
- <icode>rand(min: number, max:number)</icode>: returns a random number between <icode>min</icode> and <icode>max</icode>. Shorthand _r()_.
- <icode>irand(min: number, max:number)</icode>: returns a random integer between <icode>min</icode> and <icode>max</icode>. Shorthands _ir()_ or _rI()_.
- <icode>prob(p: number)</icode>: return <icode>true</icode> _p_% of time, <icode>false</icode> in other cases. - <icode>prob(p: number)</icode>: return <icode>true</icode> _p_% of time, <icode>false</icode> in other cases.
- <icode>toss()</icode>: throwing a coin. Head (<icode>true</icode>) or tails (<icode>false</icode>). - <icode>toss()</icode>: throwing a coin. Head (<icode>true</icode>) or tails (<icode>false</icode>).
- <icode>seed(val: number|string)</icode>: sets the seed of the random number generator. You can use a number or a string. The same seed will always return the same sequence of random numbers.
Chance operators returning a boolean value are also available:
- <icode>odds(n: number, sec?: number)</icode>: returns true for every n (odds) (eg. 1/4 = 0.25) in given seconds (sec)
- <icode>almostNever(sec?: number)</icode>: returns true 0.1% in given seconds (sec)
- <icode>rarely(sec?: number)</icode>: returns true 1% in given seconds (sec)
- <icode>scaresly(sec?: number)</icode>: returns true 10% in given seconds (sec)
- <icode>sometimes(sec?: number)</icode>: returns true 50% in given seconds (sec)
- <icode>often(sec?: number)</icode>: returns true 75% in given seconds (sec)
- <icode>frequently(sec?: number)</icode>: returns true 90% in given seconds (sec)
- <icode>almostAlways(sec?: number)</icode>: returns true 99% in given seconds (sec)
## Math functions ## Math functions

View File

@ -12,23 +12,39 @@ export class Event {
} }
} }
sometimesBy = (probability: number, func: Function): Event => { odds = (probability: number, func: Function): Event => {
if(this.randomGen() < probability) { if(this.randomGen() < probability) {
return this.modify(func); return this.modify(func);
} }
return this; return this;
} }
sometimes = (func: Function): Event => { almostNever = (func: Function): Event => {
return this.sometimesBy(0.5, func); return this.odds(0.025, func);
} }
rarely = (func: Function): Event => { rarely = (func: Function): Event => {
return this.sometimesBy(0.1, func); return this.odds(0.1, func);
}
scarcely = (func: Function): Event => {
return this.odds(0.25, func);
}
sometimes = (func: Function): Event => {
return this.odds(0.5, func);
} }
often = (func: Function): Event => { often = (func: Function): Event => {
return this.sometimesBy(0.9, func); return this.odds(0.75, func);
}
frequently = (func: Function): Event => {
return this.odds(0.9, func);
}
almostAlways = (func: Function): Event => {
return this.odds(0.985, func);
} }
modify = (func: Function): Event => { modify = (func: Function): Event => {

View File

@ -129,7 +129,7 @@ export class MidiConnection{
}); });
} }
public sendMidiNote(noteNumber: number, channel: number, velocity: number, duration: number, port: number|string = this.currentOutputIndex): void { public sendMidiNote(noteNumber: number, channel: number, velocity: number, duration: number, port: number|string = this.currentOutputIndex, bend: number|undefined = undefined): void {
/** /**
* Sending a MIDI Note on/off message with the same note number and channel. Automatically manages * Sending a MIDI Note on/off message with the same note number and channel. Automatically manages
* the note off message after the specified duration. * the note off message after the specified duration.
@ -151,9 +151,12 @@ export class MidiConnection{
// Send Note On // Send Note On
output.send(noteOnMessage); output.send(noteOnMessage);
if(bend) this.sendPitchBend(bend, channel, port);
// Schedule Note Off // Schedule Note Off
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
output.send(noteOffMessage); output.send(noteOffMessage);
if(bend) this.sendPitchBend(8192, channel, port);
delete this.scheduledNotes[noteNumber]; delete this.scheduledNotes[noteNumber];
}, (duration - 0.02) * 1000); }, (duration - 0.02) * 1000);
@ -181,7 +184,7 @@ export class MidiConnection{
} }
} }
public sendPitchBend(value: number, channel: number): void { public sendPitchBend(value: number, channel: number, port: number|string = this.currentOutputIndex): void {
/** /**
* Sends a MIDI Pitch Bend message to the currently selected MIDI output. * Sends a MIDI Pitch Bend message to the currently selected MIDI output.
* *
@ -195,7 +198,8 @@ export class MidiConnection{
if (channel < 0 || channel > 15) { if (channel < 0 || channel > 15) {
console.error('Invalid MIDI channel. Channel must be in the range 0-15.'); console.error('Invalid MIDI channel. Channel must be in the range 0-15.');
} }
const output = this.midiOutputs[this.currentOutputIndex]; if(typeof port === 'string') port = this.getMidiOutputIndex(port);
const output = this.midiOutputs[port];
if (output) { if (output) {
const lsb = value & 0x7F; const lsb = value & 0x7F;
const msb = (value >> 7) & 0x7F; const msb = (value >> 7) & 0x7F;

View File

@ -1,6 +1,8 @@
import { Event } from './Event'; import { Event } from './Event';
import { type Editor } from './main'; import { type Editor } from './main';
import { MidiConnection } from "./IO/MidiConnection"; import { MidiConnection } from "./IO/MidiConnection";
import { freqToMidi, resolvePitchBend } from 'zifferjs';
export class Note extends Event { export class Note extends Event {
values: { [key: string]: any }; values: { [key: string]: any };
midiConnection: MidiConnection; midiConnection: MidiConnection;
@ -49,9 +51,15 @@ export class Note extends Event {
} }
} }
// TODO: Add bend
freq = (value: number): this => { freq = (value: number): this => {
this.values['freq'] = value; this.values['freq'] = value;
const midiNote = freqToMidi(value);
if(midiNote % 1 !== 0) {
this.values['note'] = Math.floor(midiNote);
this.values['bend'] = resolvePitchBend(midiNote)[1];
} else {
this.values['note'] = midiNote;
}
return this; return this;
} }
@ -73,7 +81,7 @@ export class Note extends Event {
const velocity = this.values.velocity ? this.values.velocity : 100; const velocity = this.values.velocity ? this.values.velocity : 100;
const duration = this.values.duration ? const duration = this.values.duration ?
this.values.duration * Math.floor(this.app.clock.pulse_duration * this.app.api.ppqn()) : this.values.duration * this.app.clock.pulse_duration * this.app.api.ppqn() :
this.app.clock.pulse_duration * this.app.api.ppqn(); this.app.clock.pulse_duration * this.app.api.ppqn();
const bend = this.values.bend ? this.values.bend : undefined; const bend = this.values.bend ? this.values.bend : undefined;
@ -82,9 +90,7 @@ export class Note extends Event {
this.midiConnection.getMidiOutputIndex(this.values.port) : this.midiConnection.getMidiOutputIndex(this.values.port) :
this.midiConnection.getCurrentMidiPortIndex(); this.midiConnection.getCurrentMidiPortIndex();
if (bend) this.midiConnection.sendPitchBend(bend, channel); this.midiConnection.sendMidiNote(note, channel, velocity, duration, port, bend);
this.midiConnection.sendMidiNote(note, channel, velocity, duration, port);
if (bend) this.midiConnection.sendPitchBend(8192, channel);
} }
} }

View File

@ -1441,10 +1441,10 @@ yaml@^2.1.1:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ== integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==
zifferjs@^0.0.9: zifferjs@^0.0.10:
version "0.0.9" version "0.0.10"
resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.9.tgz#47037cee6dd161838dd236bdbc3eda9b099e2281" resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.10.tgz#b8c2617f5fc8fb4422f311702785c47b752a920e"
integrity sha512-XS/JAc9nkmoiRaT/YFuX7r1ROvApQnY5BxOKyenAeDATvKZ80sIoXUw48U27KTsuJIsiPInNm5RieJGCJkoVmQ== integrity sha512-kaMWRZcsAXXpPFjDoVtS3sQ5bZs+S7t3ejd8+WZV/nc52y/vXe/QcKAjT+jYCHGq8J1WMCITDn6OnVfswqJ8Ig==
dependencies: dependencies:
lru-cache "^10.0.0" lru-cache "^10.0.0"