This commit is contained in:
2023-08-21 19:45:06 +03:00
9 changed files with 595 additions and 417 deletions

View File

@ -2,7 +2,6 @@ import { Pitch, Chord, Rest, Event, cachedPattern, seededRandom } from "zifferjs
import { MidiConnection } from "./IO/MidiConnection";
import { tryEvaluate } from "./Evaluator";
import { DrunkWalk } from "./Utils/Drunk";
import { LRUCache } from "lru-cache";
import { scale } from "./Scales";
import { Editor } from "./main";
import { Sound } from "./Sound";
@ -11,25 +10,16 @@ import {
samples,
initAudioOnFirstClick,
registerSynthSounds,
soundMap,
// @ts-ignore
} from "superdough";
// This is an LRU cache used for storing persistent patterns
const cache = new LRUCache({ max: 1000, ttl: 1000 * 60 * 5 });
interface ControlChange {
channel: number;
control: number;
value: number;
}
interface Pattern<T> {
pattern: any[];
options: {
[key: string]: T;
};
}
/**
* This is an override of the basic "includes" method.
*/
@ -43,16 +33,13 @@ Array.prototype.in = function <T>(this: T[], value: T): boolean {
};
async function loadSamples() {
const ds = "https://raw.githubusercontent.com/felixroos/dough-samples/main/";
// const ds = "https://raw.githubusercontent.com/felixroos/dough-samples/main/";
return Promise.all([
initAudioOnFirstClick(),
samples("github:Bubobubobubobubo/Topos-Samples/main"),
samples(`${ds}/tidal-drum-machines.json`),
samples(`${ds}/piano.json`),
samples(`${ds}/Dirt-Samples.json`),
samples(`${ds}/EmuSP12.json`),
samples(`${ds}/vcsl.json`),
registerSynthSounds(),
samples("github:tidalcycles/Dirt-Samples/master").then(() =>
registerSynthSounds()
),
]);
}
@ -67,7 +54,7 @@ export class UserAPI {
*/
private variables: { [key: string]: any } = {};
private iterators: { [key: string]: any } = {};
private counters: { [key: string]: any } = {};
private _drunk: DrunkWalk = new DrunkWalk(-100, 100, false);
public randomGen = Math.random;
public currentSeed: string|undefined = undefined;
@ -77,7 +64,16 @@ export class UserAPI {
load: samples;
constructor(public app: Editor) {
this.load = samples("github:tidalcycles/Dirt-Samples/master");
//this.load = samples("github:tidalcycles/Dirt-Samples/master");
}
_reportError = (error: any): void => {
console.log(error)
if (!this.app.show_error) {
this.app.error_line.innerHTML = error as string;
this.app.error_line.classList.remove('hidden');
setInterval(() => this.app.error_line.classList.add('hidden'), 2000)
}
}
// =============================================================
@ -91,6 +87,23 @@ export class UserAPI {
return this.app.audioContext.currentTime;
};
public play = (): void => {
this.app.setButtonHighlighting("play", true);
this.app.clock.start();
};
public pause = (): void => {
this.app.setButtonHighlighting("pause", true);
this.app.clock.pause();
};
public stop = (): void => {
this.app.setButtonHighlighting("stop", true);
this.app.clock.stop();
};
silence = this.stop;
hush = this.stop;
// =============================================================
// Mouse functions
// =============================================================
@ -320,56 +333,60 @@ export class UserAPI {
};
// =============================================================
// Iterator related functions
// Counter related functions
// =============================================================
public iterator = (name: string, limit?: number, step?: number): number => {
public counter = (
name: string | number,
limit?: number,
step?: number
): number => {
/**
* Returns the current value of an iterator, and increments it by the step value.
* Returns the current value of a counter, and increments it by the step value.
*
* @param name - The name of the iterator
* @param limit - The upper limit of the iterator
* @param step - The step value of the iterator
* @returns The current value of the iterator
* @param name - The name of the counter
* @param limit - The upper limit of the counter
* @param step - The step value of the counter
* @returns The current value of the counter
*/
if (!(name in this.iterators)) {
// Create new iterator with default step of 1
this.iterators[name] = {
if (!(name in this.counters)) {
// Create new counter with default step of 1
this.counters[name] = {
value: 0,
step: step ?? 1,
limit,
};
} else {
// Check if limit has changed
if (this.iterators[name].limit !== limit) {
if (this.counters[name].limit !== limit) {
// Reset value to 0 and update limit
this.iterators[name].value = 0;
this.iterators[name].limit = limit;
this.counters[name].value = 0;
this.counters[name].limit = limit;
}
// Check if step has changed
if (this.iterators[name].step !== step) {
if (this.counters[name].step !== step) {
// Update step
this.iterators[name].step = step ?? this.iterators[name].step;
this.counters[name].step = step ?? this.counters[name].step;
}
// Increment existing iterator by step value
this.iterators[name].value += this.iterators[name].step;
this.counters[name].value += this.counters[name].step;
// Check for limit overshoot
if (
this.iterators[name].limit !== undefined &&
this.iterators[name].value > this.iterators[name].limit
this.counters[name].limit !== undefined &&
this.counters[name].value > this.counters[name].limit
) {
this.iterators[name].value = 0;
this.counters[name].value = 0;
}
}
// Return current iterator value
return this.iterators[name].value;
return this.counters[name].value;
};
$ = this.iterator;
$ = this.counter;
// =============================================================
// Drunk mechanism
@ -466,20 +483,11 @@ export class UserAPI {
// Sequencer related functions
// =============================================================
private _sequence_key_generator(pattern: any[]) {
/**
* Generates a key for the sequence function.
*
* @param input - The input to generate a key for
* @returns A key for the sequence function
*/
// Make the pattern base64
return btoa(JSON.stringify(pattern));
}
public slice = (chunk: number): boolean => {
const time_pos = this.epulse();
const current_chunk = Math.floor(time_pos / chunk);
const current_chunk = Math.floor(
time_pos / Math.floor(chunk * this.ppqn())
);
return current_chunk % 2 === 0;
};
@ -493,98 +501,12 @@ export class UserAPI {
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();
const slice_count = Math.floor(timepos / chunk_size);
const slice_count = Math.floor(
timepos / Math.floor(chunk_size * this.ppqn())
);
return elements[slice_count % elements.length];
};
public seqmod = (...input: any[]) => {
if (cache.has(this._sequence_key_generator(input))) {
let sequence = cache.get(
this._sequence_key_generator(input)
) as Pattern<any>;
sequence.options.currentIteration++;
if (sequence.options.currentIteration === sequence.options.nextTarget) {
sequence.options.index++;
sequence.options.nextTarget =
input[sequence.options.index % input.length];
sequence.options.currentIteration = 0;
}
cache.set(this._sequence_key_generator(input), {
pattern: input as any[],
options: sequence.options,
});
return sequence.options.currentIteration === 0;
} else {
let pattern_options = {
index: -1,
nextTarget: this.app.clock.ticks_before_new_bar,
currentIteration: 0,
};
if (typeof input[input.length - 1] === "object") {
pattern_options = {
...input.pop(),
...(pattern_options as object),
};
}
// pattern_options.currentIteration++;
// TEST
pattern_options.nextTarget = this.app.clock.ticks_before_new_bar;
if (pattern_options.currentIteration === pattern_options.nextTarget) {
pattern_options.index++;
pattern_options.nextTarget =
input[pattern_options.index % input.length];
pattern_options.currentIteration = 0;
}
cache.set(this._sequence_key_generator(input), {
pattern: input as any[],
options: pattern_options,
});
return pattern_options.currentIteration === 0;
}
};
public seq = (...input: any[]) => {
/**
* Returns a value in a sequence stored using an LRU Cache.
* The sequence is stored in the cache with an hash identifier
* made from a base64 encoding of the pattern. The pattern itself
* is composed of the pattern itself (a list of arbitrary typed
* values) and a set of options (an object) detailing how the pattern
* should be iterated on.
*
* @param input - The input to generate a key for
* Note that the last element of the input can be an object
* containing options for the sequence function.
* @returns A value in a sequence stored using an LRU Cache
*/
if (cache.has(this._sequence_key_generator(input))) {
let sequence = cache.get(
this._sequence_key_generator(input)
) as Pattern<any>;
sequence.options.index += 1;
cache.set(this._sequence_key_generator(input), sequence);
return sequence.pattern[sequence.options.index % sequence.pattern.length];
} else {
let pattern_options = { index: 0 };
if (typeof input[input.length - 1] === "object") {
pattern_options = { ...input.pop(), ...(pattern_options as object) };
}
cache.set(this._sequence_key_generator(input), {
pattern: input as any[],
options: pattern_options,
});
return cache.get(this._sequence_key_generator(input));
}
};
pick = <T>(...array: T[]): T => {
/**
* Returns a random element from an array.
@ -940,19 +862,6 @@ export class UserAPI {
return final_pulses.some((p) => p == true);
};
stop = (): void => {
/**
* Stops the clock.
*
* @see silence
* @see hush
*/
this.app.clock.pause();
this.app.setButtonHighlighting("pause", true);
};
silence = this.stop;
hush = this.stop;
prob = (p: number): boolean => {
/**
* Returns true p% of the time.
@ -977,7 +886,7 @@ export class UserAPI {
return this.randomGen() > 0.5;
};
min = (...values: number[]): number => {
public min = (...values: number[]): number => {
/**
* Returns the minimum value of a list of numbers.
*
@ -987,7 +896,7 @@ export class UserAPI {
return Math.min(...values);
};
max = (...values: number[]): number => {
public max = (...values: number[]): number => {
/**
* Returns the maximum value of a list of numbers.
*
@ -997,6 +906,20 @@ export class UserAPI {
return Math.max(...values);
};
public mean = (...values: number[]): number => {
/**
* Returns the mean of a list of numbers.
*
* @param values - The list of numbers
* @returns The mean value of the list of numbers
*/
const sum = values.reduce(
(accumulator, currentValue) => accumulator + currentValue,
0
);
return sum / values.length;
};
limit = (value: number, min: number, max: number): number => {
/**
* Limits a value between a minimum and a maximum.
@ -1036,30 +959,24 @@ export class UserAPI {
};
public mod = (...n: number[]): boolean => {
const results: boolean[] = n.map((value) => this.epulse() % value === 0);
const results: boolean[] = n.map(
(value) => this.epulse() % Math.floor(value * this.ppqn()) === 0
);
return results.some((value) => value === true);
};
public modbar = (...n: number[]): boolean => {
const results: boolean[] = n.map((value) => this.bar() % value === 0);
const results: boolean[] = n.map(
(value) => this.bar() % Math.floor(value * this.ppqn()) === 0
);
return results.some((value) => value === true);
};
// mod = (...pulse: number[]): boolean => {
// /**
// * Returns true if the current pulse is a modulo of any of the given pulses.
// *
// * @param pulse - The pulse to check for
// * @returns True if the current pulse is a modulo of any of the given pulses
// */
// return pulse.some((p) => this.app.clock.time_position.pulse % p === 0);
// };
// =============================================================
// Rythmic generators
// =============================================================
euclid = (
public euclid = (
iterator: number,
pulses: number,
length: number,
@ -1076,6 +993,7 @@ export class UserAPI {
*/
return this._euclidean_cycle(pulses, length, rotate)[iterator % length];
};
ec = this.euclid;
_euclidean_cycle(
pulses: number,
@ -1216,20 +1134,30 @@ export class UserAPI {
return (this.triangle(freq, offset) + 1) / 2;
};
square = (freq: number = 1, offset: number = 0): number => {
square = (
freq: number = 1,
offset: number = 0,
duty: number = 0.5
): number => {
/**
* Returns a square wave between -1 and 1.
* Returns a square wave with a specified duty cycle between -1 and 1.
*
* @returns A square wave between -1 and 1
* @returns A square wave with a specified duty cycle between -1 and 1
* @see saw
* @see triangle
* @see sine
* @see noise
*/
return this.saw(freq, offset) > 0 ? 1 : -1;
const period = 1 / freq;
const t = (Date.now() / 1000 + offset) % period;
return t / period < duty ? 1 : -1;
};
usquare = (freq: number = 1, offset: number = 0): number => {
usquare = (
freq: number = 1,
offset: number = 0,
duty: number = 0.5
): number => {
/**
* Returns a square wave between 0 and 1.
*
@ -1238,7 +1166,7 @@ export class UserAPI {
* @returns A square wave between 0 and 1
* @see square
*/
return (this.square(freq, offset) + 1) / 2;
return (this.square(freq, offset, duty) + 1) / 2;
};
noise = (): number => {
@ -1268,8 +1196,9 @@ export class UserAPI {
sound = (sound: string) => {
return new Sound(sound, this.app);
};
snd = this.sound;
samples = samples;
soundMap = soundMap;
log = console.log;