diff --git a/package.json b/package.json
index ae199c4..6b14fe9 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,7 @@
"tailwindcss": "^3.3.3",
"tone": "^14.8.49",
"vite-plugin-markdown": "^2.1.0",
- "zifferjs": "^0.0.9",
+ "zifferjs": "^0.0.10",
"zzfx": "^1.2.0"
}
}
diff --git a/src/API.ts b/src/API.ts
index 7690c02..cb06d71 100644
--- a/src/API.ts
+++ b/src/API.ts
@@ -483,7 +483,7 @@ export class UserAPI {
// Sequencer related functions
// =============================================================
- public slice = (chunk: number): boolean => {
+ public div = (chunk: number): boolean => {
const time_pos = this.epulse();
const current_chunk = Math.floor(
time_pos / Math.floor(chunk * this.ppqn())
@@ -491,13 +491,13 @@ export class UserAPI {
return current_chunk % 2 === 0;
};
- public barslice = (chunk: number): boolean => {
+ public divbar = (chunk: number): boolean => {
const time_pos = this.bar() - 1;
const current_chunk = Math.floor(time_pos / chunk);
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 elements = args.slice(1); // Get the rest of the arguments as an array
const timepos = this.epulse();
@@ -578,9 +578,10 @@ export class UserAPI {
*/
return this.randomGen() * (max - min) + min;
};
- irand = this.randI
+ irand = this.randI;
rI = this.randI;
r = this.rand;
+ ir = this.randI;
seed = (seed: string | number): void => {
/**
@@ -704,55 +705,91 @@ export class UserAPI {
// 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
+ *
+ * @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 => {
- /**
- * Returns true 50% of the time.
- *
- * @returns True 50% of the time
- */
- return this.randomGen() > 0.5;
- };
-
- public rarely = (): boolean => {
+ public scarcely = (sec: number = 15): boolean => {
/**
* Returns true 25% of the time.
- *
+ *
+ * @param sec - The time frame in seconds
* @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.
- *
+ *
+ * @param sec - The time frame in seconds
* @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.
- *
+ *
+ * @param sec - The time frame in seconds
* @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 => {
/**
* Returns the value of a dice roll with n sides.
- *
+ *
* @param sides - The number of sides on the dice
* @returns The value of a dice roll with n sides
*/
@@ -965,12 +1002,19 @@ export class UserAPI {
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 => {
const results: boolean[] = n.map(
(value) => this.bar() % Math.floor(value * this.ppqn()) === 0
);
return results.some((value) => value === true);
};
+ bmod = this.modbar;
// =============================================================
// Rythmic generators
diff --git a/src/Documentation.ts b/src/Documentation.ts
index e0b10f9..452081b 100644
--- a/src/Documentation.ts
+++ b/src/Documentation.ts
@@ -143,7 +143,12 @@ Some functions can be leveraged to play rhythms without thinking too much about
\`\`\`
- onbar(...values: number[]): returns true if the bar is currently equal to any of the specified values.
-- modbar(...values: number[]): returns true if the bar is currently a multiple of any of the specified values.
+- modbar(...values: number[]) or bmod(...): returns true if the bar is currently a multiple of any of the specified values.
+- modpulse(...values: number[]) or pmod(...): returns true if the pulse is currently a multiple of any of the specified values.
+
+- div(chunk: number): returns true for every pulse in intervals of given number of beats
+- divbar(chunk: number): returns true for every pulse in intervals of given number of bars
+- divseq(...values: number[]): returns true for every pulse in intervals of given number of beats returning different value each time.
## 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()
\`\`\`
-### Note chaining
+## Note chaining
The note(number) 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
- mouseX(): the horizontal position of the mouse on the screen (as a floating point number).
- mouseY(): the vertical position of the mouse on the screen (as a floating point number).
+Current mouse position can also be used to generate notes:
+
+- noteX(): returns a MIDI note number (0-127) based on the horizontal position of the mouse on the screen.
+- noteY(): returns a MIDI note number (0-127) based on the vertical position of the mouse on the screen.
+
## 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.
@@ -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.
+- rand(min: number, max:number): returns a random number between min and max. Shorthand _r()_.
+- irand(min: number, max:number): returns a random integer between min and max. Shorthands _ir()_ or _rI()_.
- prob(p: number): return true _p_% of time, false in other cases.
- toss(): throwing a coin. Head (true) or tails (false).
+- seed(val: number|string): 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:
+
+- odds(n: number, sec?: number): returns true for every n (odds) (eg. 1/4 = 0.25) in given seconds (sec)
+- almostNever(sec?: number): returns true 0.1% in given seconds (sec)
+- rarely(sec?: number): returns true 1% in given seconds (sec)
+- scaresly(sec?: number): returns true 10% in given seconds (sec)
+- sometimes(sec?: number): returns true 50% in given seconds (sec)
+- often(sec?: number): returns true 75% in given seconds (sec)
+- frequently(sec?: number): returns true 90% in given seconds (sec)
+- almostAlways(sec?: number): returns true 99% in given seconds (sec)
## Math functions
diff --git a/src/Event.ts b/src/Event.ts
index f1732b6..a5da575 100644
--- a/src/Event.ts
+++ b/src/Event.ts
@@ -12,23 +12,39 @@ export class Event {
}
}
- sometimesBy = (probability: number, func: Function): Event => {
+ odds = (probability: number, func: Function): Event => {
if(this.randomGen() < probability) {
return this.modify(func);
}
return this;
}
- sometimes = (func: Function): Event => {
- return this.sometimesBy(0.5, func);
+ almostNever = (func: Function): Event => {
+ return this.odds(0.025, func);
}
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 => {
- 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 => {
diff --git a/src/IO/MidiConnection.ts b/src/IO/MidiConnection.ts
index b238a66..33669fa 100644
--- a/src/IO/MidiConnection.ts
+++ b/src/IO/MidiConnection.ts
@@ -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
* the note off message after the specified duration.
@@ -151,9 +151,12 @@ export class MidiConnection{
// Send Note On
output.send(noteOnMessage);
+ if(bend) this.sendPitchBend(bend, channel, port);
+
// Schedule Note Off
const timeoutId = setTimeout(() => {
output.send(noteOffMessage);
+ if(bend) this.sendPitchBend(8192, channel, port);
delete this.scheduledNotes[noteNumber];
}, (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.
*
@@ -195,7 +198,8 @@ export class MidiConnection{
if (channel < 0 || channel > 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) {
const lsb = value & 0x7F;
const msb = (value >> 7) & 0x7F;
diff --git a/src/Note.ts b/src/Note.ts
index b087a66..bfe380a 100644
--- a/src/Note.ts
+++ b/src/Note.ts
@@ -1,6 +1,8 @@
import { Event } from './Event';
import { type Editor } from './main';
import { MidiConnection } from "./IO/MidiConnection";
+import { freqToMidi, resolvePitchBend } from 'zifferjs';
+
export class Note extends Event {
values: { [key: string]: any };
midiConnection: MidiConnection;
@@ -49,9 +51,15 @@ export class Note extends Event {
}
}
- // TODO: Add bend
freq = (value: number): this => {
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;
}
@@ -73,7 +81,7 @@ export class Note extends Event {
const velocity = this.values.velocity ? this.values.velocity : 100;
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();
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.getCurrentMidiPortIndex();
- if (bend) this.midiConnection.sendPitchBend(bend, channel);
- this.midiConnection.sendMidiNote(note, channel, velocity, duration, port);
- if (bend) this.midiConnection.sendPitchBend(8192, channel);
+ this.midiConnection.sendMidiNote(note, channel, velocity, duration, port, bend);
}
}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index f5c0699..32cc326 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1441,10 +1441,10 @@ yaml@^2.1.1:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==
-zifferjs@^0.0.9:
- version "0.0.9"
- resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.9.tgz#47037cee6dd161838dd236bdbc3eda9b099e2281"
- integrity sha512-XS/JAc9nkmoiRaT/YFuX7r1ROvApQnY5BxOKyenAeDATvKZ80sIoXUw48U27KTsuJIsiPInNm5RieJGCJkoVmQ==
+zifferjs@^0.0.10:
+ version "0.0.10"
+ resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.10.tgz#b8c2617f5fc8fb4422f311702785c47b752a920e"
+ integrity sha512-kaMWRZcsAXXpPFjDoVtS3sQ5bZs+S7t3ejd8+WZV/nc52y/vXe/QcKAjT+jYCHGq8J1WMCITDn6OnVfswqJ8Ig==
dependencies:
lru-cache "^10.0.0"