This commit is contained in:
2023-09-02 22:23:07 +03:00
6 changed files with 157 additions and 18 deletions

View File

@ -32,6 +32,7 @@ export async function loadSamples() {
),
registerZZFXSounds(),
samples("github:Bubobubobubobubo/Topos-Samples/main"),
samples("github:Bubobubobubobubo/Topos-Atari/main"),
]);
}
@ -148,6 +149,28 @@ export class UserAPI {
silence = this.stop;
hush = this.stop;
// =============================================================
// Time warp functions
// =============================================================
public warp = (n: number): void => {
/**
* Time-warp the clock by using the tick you wish to jump to.
*/
this.app.clock.tick = n;
this.app.clock.time_position = this.app.clock.convertTicksToTimeposition(n);
};
public beat_warp = (beat: number): void => {
/**
* Time-warp the clock by using the tick you wish to jump to.
*/
this.app.clock.tick = beat * this.app.clock.ppqn;
this.app.clock.time_position = this.app.clock.convertTicksToTimeposition(
beat * this.app.clock.ppqn
);
};
// =============================================================
// Mouse functions
// =============================================================
@ -910,7 +933,7 @@ export class UserAPI {
/**
* Returns the current number of pulses elapsed since origin of time
*/
return this.app.clock.pulses_since_origin + 1;
return this.app.clock.pulses_since_origin + 1;
};
nominator = (): number => {
@ -918,7 +941,7 @@ export class UserAPI {
* Returns the current nominator of the time signature
*/
return this.app.clock.time_signature[0];
}
};
meter = (): number => {
/**
@ -935,20 +958,25 @@ export class UserAPI {
public mod = (...n: number[]): boolean => {
const results: boolean[] = n.map(
(value) => this.app.clock.pulses_since_origin % Math.floor(value * this.ppqn()) === 0
(value) =>
this.app.clock.pulses_since_origin % Math.floor(value * this.ppqn()) ===
0
);
return results.some((value) => value === true);
};
public modpulse = (...n: number[]): boolean => {
const results: boolean[] = n.map((value) => this.app.clock.pulses_since_origin % value === 0);
const results: boolean[] = n.map(
(value) => this.app.clock.pulses_since_origin % value === 0
);
return results.some((value) => value === true);
};
modp = this.modpulse;
public modbar = (...n: number[]): boolean => {
const results: boolean[] = n.map(
(value) => this.app.clock.time_position.bar % Math.floor(value * this.ppqn()) === 0
(value) =>
this.app.clock.time_position.bar % Math.floor(value * this.ppqn()) === 0
);
return results.some((value) => value === true);
};
@ -1000,7 +1028,7 @@ export class UserAPI {
return final_pulses.some((p) => p == true);
};
oncount = (beats: number[]|number, count: number): boolean => {
oncount = (beats: number[] | number, count: number): boolean => {
/**
* Returns true if the current beat is in the given list of beats.
*
@ -1010,11 +1038,11 @@ export class UserAPI {
* @param beat - The beats to check
* @returns True if the current beat is in the given list of beats
*/
if(typeof beats === "number") beats = [beats];
if (typeof beats === "number") beats = [beats];
const origin = this.app.clock.pulses_since_origin;
let final_pulses: boolean[] = [];
beats.forEach((b) => {
b = b<1 ? 0 : b-1;
b = b < 1 ? 0 : b - 1;
const beatInTicks = Math.ceil(b * this.ppqn());
const meterPosition = origin % (this.ppqn() * count);
return final_pulses.push(meterPosition === beatInTicks);

View File

@ -59,6 +59,15 @@ export class Clock {
});
}
convertTicksToTimeposition(ticks: number): TimePosition {
const beatsPerBar = this.app.clock.time_signature[0];
const ppqnPosition = ticks % this.app.clock.ppqn;
const beatNumber = Math.floor(ticks / this.app.clock.ppqn);
const barNumber = Math.floor(beatNumber / beatsPerBar);
const beatWithinBar = Math.floor(beatNumber % beatsPerBar);
return { bar: barNumber, beat: beatWithinBar, pulse: ppqnPosition };
}
get ticks_before_new_bar(): number {
/**
* This function returns the number of ticks separating the current moment

View File

@ -18,8 +18,7 @@ export class TransportNode extends AudioWorkletNode {
this.app.clock.tick++
const futureTimeStamp = this.convertTicksToTimeposition(this.app.clock.tick);
//console.log("BANG", this.logicalTime, futureTimeStamp);
const futureTimeStamp = this.app.clock.convertTicksToTimeposition(this.app.clock.tick);
this.app.clock.time_position = futureTimeStamp;
tryEvaluate(this.app, this.app.global_buffer);
@ -27,14 +26,6 @@ export class TransportNode extends AudioWorkletNode {
}
};
convertTicksToTimeposition(ticks) {
const beatsPerBar = this.app.clock.time_signature[0];
const ppqnPosition = (ticks % this.app.clock.ppqn);
const beatNumber = Math.floor(ticks / this.app.clock.ppqn);
const barNumber = Math.floor(beatNumber / beatsPerBar);
const beatWithinBar = Math.floor(beatNumber % beatsPerBar);
return {bar: barNumber, beat: beatWithinBar, pulse: ppqnPosition};
}
start() {
this.port.postMessage("start");

View File

@ -47,6 +47,8 @@ export const samples = (application: Editor): string => {
Audio samples are dynamically loaded from the web. By default, Topos is providing some samples coming from the classic [Dirt-Samples](https://github.com/tidalcycles/Dirt-Samples) but also from the [Topos-Samples](https://github.com/Bubobubobubobubo/Topos-Samples) repository. You can contribute to the latter if you want to share your samples with the community! For each sample folder, we are indicating how many of them are available in parentheses.
The samples starting with <ic>ST</ic> are coming from [this wonderful collection](https://archive.org/details/AmigaSoundtrackerSamplePacksst-xx) of Ultimate Tracker Atari ST audio samples released by Karsten Obarski. They are very high-pitched as was usual in the tracker era. Pitch them down using <ic>.speed(0.5)</ic>.
## Available audio samples

View File

@ -223,7 +223,53 @@ prob(80)::mod(.5) && sound('hh').out()
true
)}
## Time Warping
Time is cool. But it's even cooler when you can manipulate it to your liking. Think about jumping back or forward in time. Think about looping a specific part of your current pattern or song. This is all possible thanks to two simple functions: <ic>warp(n: number)</ic> and <ic>beat_warp(n: number)</ic>. They are both very easy to use and very powerful. Let's see how they work.
- <ic>warp(n: number)</ic>: this function jumps to the _n_ tick of the clock. <ic>1</ic> is the first pulsation ever, and the number keeps increasing to the infinite.
${makeExample(
"Jumping back and forth in time",
`
// Obscure Shenanigans - Bubobubobubo
mod([1/4,1/8,1/16].div(8)):: sound('sine')
.freq([100,50].div(16) + 50 * ($(1)%10))
.gain(0.5).room(0.9).size(0.9)
.sustain(0.1).out()
mod(1) :: sound('kick').out()
mod(2) :: sound('dr').n(5).out()
div(3) :: mod([.25,.5].div(.5)) :: sound('dr')
.n([8,9].pick()).gain([.8,.5,.25,.1,.0].div(.25)).out()
// Time is elastic now!
mod(.25) :: warp([12, 48, 24, 1, 120, 30].pick())
`,
true
)}
- <ic>beat_warp(beat: number)</ic>: this function jumps to the _n_ beat of the clock. The first beat is <ic>1</ic>.
${makeExample(
"Jumping back and forth with beats",
`
// Resonance bliss - Bubobubobubo
mod(.25)::snd('arpy')
.note(30 + [0,3,7,10].beat())
.cutoff(usine(.5) * 5000).resonance(10).gain(0.3)
.end(0.8).room(0.9).size(0.9).n(0).out();
mod([.25,.125].div(2))::snd('arpy')
.note(30 + [0,3,7,10].beat())
.cutoff(usine(.5) * 5000).resonance(20).gain(0.3)
.end(0.8).room(0.9).size(0.9).n(3).out();
mod(.5) :: snd('arpy').note(
[30, 33, 35].repeatAll(4).div(1) - [12,0].div(0.5)).out()
// Comment me to stop warping!
mod(1) :: beat_warp([2,4,5,10,11].pick())
`,
true
)}
## Larger time divisions
Now you know how to play some basic rhythmic music but you are a bit stuck in a one-bar long loop. Let's see how we can think about time flowing on longer periods. The functions you are going to learn now are _very fundamental_ and all the fun comes from mastering them. **Read and experiment a lot with the following examples**.

View File

@ -1,5 +1,68 @@
export const examples = [
`
// Race day - Bubobubobubo
bpm(125);
mod(.5) :: sound('STB6')
.n(irand(1,10)).speed(0.5).rel(1)
.sus(0.1).out()
rhythm(div(4) ? 1 : .5, 5, 8) :: sound('kick').out()
rhythm(div(2) ? .5 : .25, 7, 8) :: sound('click')
.vel(0.1 + utriangle(.25)).n(irand(1,5)).out()
rhythm(.5, 2, 8) :: sound('snare').out()
`,
`
// Structure et approximation - Bubobubobubo
mod(.25) :: sound('zzfx').zzfx(
// Randomized chaos :)
[
rand(1,5),,rand(500,1000),rand(.01, 0.02),,
rand(.01, .05),irand(1,12),rand(0,8),,
irand(0,200),-411,rand(0, 1),,,irand(-20, 40),,
.43,irand(1,20)
]).room(0.4).size(0.15).cutoff(500 + usine() * 8000)
.vel(0.1).gain(toss() ? .5 : .125)
.delay(toss() ? 0.5 : 0).delayt(0.045).delayfb(0.1).out()
rhythm(.5, toss() ? 5 : 7, 12) :: sound('kick').n(13).out()
rhythm(toss() ? .25 : .5, div(2) ? 3 : 5, 12) :: sound(
toss() ? 'snare' : 'cp').n(5).out()
rhythm(div(2) ? .5 : .25, div(4) ? 8 : 11, 12) :: sound('hat')
.orbit(3).room(0.5).size(0.5).n(0).out()
`,
`
// Part-Dieu - Bubobubobubo
bpm(90);
mod(rarely(12) ? .5 : .25) :: sound('ST22')
.note([30, 30, 30, 31].repeatAll(8).div(.5))
.cut(1).n([19, 21].div(.75))
.cutoff(irand(200, 5000))
.resonance(rand(0.2,0.8))
.room(0.9).size(1).orbit(2)
.speed(0.25).vel(0.3).end(0.5)
.out()
mod(.5) :: snd('dr')
.n([0, 0, 0, 0, 2, 8].beat())
.gain(1).out()
mod(div(2) ? 1 : 0.75) :: snd('bd').n(2).out()
mod(4) :: snd('snare').n(5)
.delay(0.5).delayt(bpm() / 60 / 8)
.delayfb(0.25).out()
`,
`
// Atarism - Bubobubobubo
bpm(85);
let modifier = [.5, 1, 2].div(8);
let othermod = [1, .5, 4].div(4);
mod(modifier / 2):: sound('STA9').n([0,2].div(.5)).speed(0.5).vel(0.5).out()
mod(.5)::sound('STA9').n([0, 20].div(.5)).speed([1,1.5].repeatAll(4).beat() / 4)
.cutoff(500 + usine(.25) * 3000).vel(0.5).out()
mod(modifier / 2):: sound('STA9')
.n([0,7].div(.5)).speed(div(othermod) ? 2 :4).vel(0.45).out()
rhythm(.25, 3, 8, 1) :: sound('STA9')
.note([30, 33].pick()).n(32).speed(0.5).out()
rhythm(othermod, 5, 8) :: sound('dr').n([0,1,2].beat()).out()
mod(1) :: sound('kick').vel(1).out()
`,
`
// Ancient rhythms - Bubobubobubo
mod(1)::snd('kick').out();
mod(2)::snd('sd').room(0.9).size(0.9).out();