-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
beat and beat track and some tests - general improvements
- Loading branch information
1 parent
5bfe155
commit 4392bc4
Showing
15 changed files
with
673 additions
and
151 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { expect, it } from 'vitest' | ||
import { AudioContext as Mock } from 'standardized-audio-context-mock' | ||
import type { Playable } from './interfaces/playable' | ||
import type { Connectable } from './interfaces/connectable' | ||
import { BeatTrack } from '@/beat-track' | ||
|
||
function createBeatTrack() { | ||
const context = new Mock() as unknown as AudioContext | ||
const sounds: (Playable & Connectable)[] = [] | ||
return new BeatTrack(context, sounds) | ||
} | ||
|
||
it('exists', () => { | ||
expect(BeatTrack).toBeTruthy() | ||
}) | ||
|
||
it('can be created', () => { | ||
const track = createBeatTrack() | ||
expect(track).toBeTruthy() | ||
}) | ||
|
||
it(`remembers beats' 'active' state when numBeats changes`, () => { | ||
const beatTrack = createBeatTrack() | ||
let [beat1, beat2, beat3] = beatTrack.beats | ||
|
||
beat1.active = true | ||
beat3.active = true | ||
|
||
beatTrack.numBeats = 6 | ||
|
||
beat1 = beatTrack.beats[0] | ||
beat2 = beatTrack.beats[1] | ||
beat3 = beatTrack.beats[2] | ||
|
||
expect(beat1.active).toBe(true) | ||
expect(beat2.active).toBe(false) | ||
expect(beat3.active).toBe(true) | ||
|
||
beatTrack.numBeats = 4 | ||
|
||
beat1 = beatTrack.beats[0] | ||
beat2 = beatTrack.beats[1] | ||
beat3 = beatTrack.beats[2] | ||
|
||
expect(beat1.active).toBe(true) | ||
expect(beat2.active).toBe(false) | ||
expect(beat3.active).toBe(true) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import { Beat } from './beat' | ||
import type { Connectable } from './interfaces/connectable' | ||
import type { Playable } from './interfaces/playable' | ||
import { Sampler } from './sampler' | ||
|
||
const beatBank = new WeakMap() | ||
|
||
/** | ||
* An instance of this class has an array of "sounds" (comprised of one or multiple | ||
* audio sources, if multiple are provided, they are played in a round-robin fashion) | ||
* and provides methods to play that sound repeatedly, mixed with "rests," in a | ||
* rhythmic way. An instance of this class behaves very similarly to a "lane" on a drum machine. | ||
* | ||
* @class BeatTrack | ||
* @extends Sampler | ||
* | ||
* @todo need a way to stop a BeatTrack once it's started. Maybe by creating | ||
* the times in advance and not calling play until it's the next beat in the | ||
* queue? | ||
*/ | ||
export class BeatTrack extends Sampler { | ||
constructor(private audioContext: AudioContext, sounds: (Playable & Connectable)[], opts?: { numBeats?: number, duration?: number }) { | ||
super(sounds) | ||
if (opts?.numBeats) { | ||
this.numBeats = opts.numBeats | ||
} | ||
if (opts?.duration) { | ||
this.duration = opts.duration | ||
} | ||
} | ||
|
||
/** | ||
* @property numBeats | ||
* | ||
* Determines the number of beats in a BeatTrack instance. | ||
*/ | ||
public numBeats = 4 | ||
|
||
/** | ||
* @property duration | ||
* | ||
* If specified, Determines length of time, in milliseconds, before isPlaying | ||
* and currentTimeIsPlaying are automatically switched back to false after | ||
* having been switched to true for each beat. 100ms is used by default. | ||
* | ||
* @default 100 | ||
*/ | ||
public duration = 100 | ||
|
||
/** | ||
* @property beats | ||
* | ||
* Computed property. An array of Beat instances. The number of Beat instances | ||
* in the array is always the same as the `numBeats` property. If 'numBeats' | ||
* or duration changes. This property will be recomputed, but any beats that | ||
* previously existed are reused so that they will maintain their `active` | ||
* state. | ||
*/ | ||
public get beats(): Beat[] { | ||
let beats = [] | ||
let numBeats = this.numBeats | ||
let existingBeats | ||
|
||
if (beatBank.has(this)) { | ||
existingBeats = beatBank.get(this) | ||
numBeats = numBeats - existingBeats.length | ||
} | ||
|
||
for (let i = 0; i < numBeats; i++) { | ||
const beat = new Beat(this.audioContext, { | ||
duration: this.duration, | ||
playIn: this.playIn.bind(this), | ||
play: this.play.bind(this), | ||
}) | ||
|
||
beats.push(beat) | ||
} | ||
|
||
if (existingBeats) { | ||
beats = existingBeats.concat(beats) | ||
} | ||
|
||
beatBank.set(this, beats) | ||
|
||
return beats | ||
} | ||
|
||
/** | ||
* @method playBeats | ||
* | ||
* Calls play on all Beat instances in the beats array. | ||
* | ||
* @param {number} bpm The tempo at which the beats should be played. | ||
* @param noteType {number} The (rhythmic) length of each beat. Fractions | ||
* are suggested here so that it's easy to reason about. For example, for | ||
* eighth notes, pass in `1/8`. | ||
*/ | ||
public playBeats(bpm: number, noteType: number): void { | ||
this.callPlayMethodOnBeats('playIn', bpm, noteType) | ||
} | ||
|
||
/** | ||
* @method playActiveBeats | ||
* | ||
* Calls play on `active` Beat instances in the beats array. Any beat that | ||
* is not marked active is effectively a "rest". | ||
* | ||
* @param {number} bpm The tempo at which the beats and rests should be played. | ||
* @param noteType {number} The (rhythmic) length of each beat/rest. Fractions | ||
* are suggested here so that it's easy to reason about. For example, for | ||
* eighth notes, pass in `1/8`. | ||
*/ | ||
public playActiveBeats(bpm: number, noteType: number): void { | ||
this.callPlayMethodOnBeats('ifActivePlayIn', bpm, noteType) | ||
} | ||
|
||
/** | ||
* @method callPlayMethodOnBeats | ||
* | ||
* The underlying method behind playBeats and playActiveBeats. | ||
* | ||
* @param {string} method The method that should be called on each beat. | ||
* @param {number} bpm The tempo that should be used to calculate the length | ||
* of a beat/rest. | ||
* @param noteType {number} The (rhythmic) length of each beat/rest that should | ||
* be used to calculate the length of a beat/rest in seconds. | ||
*/ | ||
private callPlayMethodOnBeats(method: 'ifActivePlayIn' | 'playIn', bpm: number, noteType: number = 1 / 4): void { | ||
// http://bradthemad.org/guitar/tempo_explanation.php | ||
const duration = (240 * noteType) / bpm | ||
this.beats.forEach((beat, idx) => beat[method](idx * duration)) | ||
} | ||
} |
Oops, something went wrong.