diff --git a/src/API.ts b/src/API.ts index 6de4545..b2c555c 100644 --- a/src/API.ts +++ b/src/API.ts @@ -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); diff --git a/src/Clock.ts b/src/Clock.ts index e50029d..aca16bc 100644 --- a/src/Clock.ts +++ b/src/Clock.ts @@ -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 diff --git a/src/TransportNode.js b/src/TransportNode.js index a515b5e..64c2fef 100644 --- a/src/TransportNode.js +++ b/src/TransportNode.js @@ -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"); diff --git a/src/documentation/samples.ts b/src/documentation/samples.ts index 1b3221c..35720ea 100644 --- a/src/documentation/samples.ts +++ b/src/documentation/samples.ts @@ -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 ST 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 .speed(0.5). + ## Available audio samples diff --git a/src/documentation/time.ts b/src/documentation/time.ts index a0c5ae4..ef5bddb 100644 --- a/src/documentation/time.ts +++ b/src/documentation/time.ts @@ -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: warp(n: number) and beat_warp(n: number). They are both very easy to use and very powerful. Let's see how they work. + +- warp(n: number): this function jumps to the _n_ tick of the clock. 1 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 +)} +- beat_warp(beat: number): this function jumps to the _n_ beat of the clock. The first beat is 1. + +${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**. diff --git a/src/examples/excerpts.ts b/src/examples/excerpts.ts index 3614b9e..0196b72 100644 --- a/src/examples/excerpts.ts +++ b/src/examples/excerpts.ts @@ -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();