🔉 Bach player for JS
bach
is a semantic music notation with a focus on human readability and productivity.
gig
consumes and synchronizes bach
tracks with audio data (or any kind of data) in a browser or browser-like environment.
See gig
in action by using bach-editor
, a minimal web-based editor for writing and playing bach
tracks.
Example bach
tracks can be found at https://codebach.tech/#/examples.
$ npm i slurmulon/gig
Simply provide your bach
track as either a string (UTF-8) or a valid bach.json
object.
import { Gig } from 'gig'
const gig = new Gig({
source: `
@tempo = 134
play! [
1/2 -> chord('Am')
1/2 -> chord('G')
3/8 -> chord('F')
5/8 -> chord('D')
]
`
})
gig.play()
Defines the core musical data of the track in bach.json
.
If provided as a string, bach
will be compiled upon instantiation.
If provided an object, it will be validated as proper bach.json
.
- Type:
string
orbach.json
- Required:
true
import { Gig } from 'gig'
const gig = new Gig({
source: `
@tempo = 150
@meter = 5|8
play! [
3/8 -> {
Scale('D dorian')
Chord('Dm9')
}
2/8 -> Chord('Am9')
]
`
})
Specifies the audio data to synchronize the musical bach.json
data with.
- Type:
String
,Blob
,Array
- Required:
false
(may be inherited fromsource
headers)
import { Gig } from 'gig'
const gig = new Gig({
source: { /* ... */ },
audio: 'http://api.madhax.io/track/q2IBRPmMq9/audio/mp3'
})
Determines if the audio and music data should loop forever.
- Type:
Boolean
- Required:
false
- Default:
false
import { Gig } from 'gig'
const gig = new Gig({
source: { /* ... */ },
audio: 'http://api.madhax.io/track/q2IBRPmMq9/audio/mp3',
loop: true
})
Determines if the iteration cursor is stateless (true
) or stateful (false
).
Changing this value is not recommended unless you know what you're doing.
If you set stateless: false
, you must provide a custom timer
that manually sets gig.index
to the current step
(i.e. bach
's unit of iteration).
See the Timers section for more detailed information.
- Type:
Boolean
- Required:
false
- Default:
true
import { Gig } from 'gig'
const gig = new Gig({
source: { /* ... */ },
audio: 'http://api.madhax.io/track/q2IBRPmMq9/audio/mp3',
stateless: true
})
Loads the audio data and kicks off the internal synchronization clock once everything is ready.
import { Gig } from 'gig'
import source from './lullaby.bach.json'
const gig = new Gig({ source })
gig.play()
Instantiates a new clock from the provided timer, acting as the primary synchronization mechanism between the music and audio data.
Warning
This method is primarily for internal use, and
play()
is usually the method you want to use instead.Until
play()
is called, no audio will play at all, and, if there's any delay between thestart()
andplay()
calls, the internal clock will get out of sync!
import { Gig } from 'gig'
import source from './lullaby.bach.json'
const gig = new Gig({ source })
gig.start()
Stops the audio and synchronization clock. Does not allow either of them to be resumed.
import { Gig } from 'gig'
import source from './lullaby.bach.json'
const gig = new Gig({ source })
gig.play()
setTimeout(() => {
gig.stop()
}, 1000)
Pauses the audio and synchronization clock. May be resumed at any point via the resume()
method.
import { Gig } from 'gig'
import source from './lullaby.bach.json'
const gig = new Gig({ source })
gig.play()
setTimeout(() => {
gig.pause()
}, 1000)
Resumes a previously paused audio synchronization clock.
import { Gig } from 'gig'
import source from './lullaby.bach.json'
const gig = new Gig({ source })
gig.play()
setTimeout(() => {
gig.pause()
setTimeout(() => {
gig.resume()
}, 1000)
}, 1000)
Stops the synchronization clock, audio, and removes all even listeners and artifacts.
This is particularly useful in reactive systems such as Vue and React.
import { Gig } from 'gig'
import source from './lullaby.bach.json'
const gig = new Gig({ source })
gig.play()
setTimeout(() => {
gig.kill()
}, 1000)
Mutes the track audio. Has no effect on the synchronization clock.
import { Gig } from 'gig'
import source from './lullaby.bach.json'
const gig = new Gig({ source })
gig.play()
setTimeout(() => {
gig.mute()
}, 1000)
Determines when a duration occurs (in milliseconds) relative to the run-time origin.
import { Gig } from 'gig'
import source from './lullaby.bach.json'
const gig = new Gig({ source })
gig.moment(4, 'step')
gig.moment(12, 'pulse')
gig.moment(2.5, 'bar')
gig.moment(30, 'second')
Determines if playback matches the provided status (as a string).
The supported statuses are:
pristine
: Playback has not changed since instantiation.playing
: Playback is currently active.stopped
: Playback is stopped.paused
: Playback is paused and may be resumed later.killed
: Playback has been killed and all listeners and artifacts have been removed.
import { Gig } from 'gig'
import source from './lullaby.bach.json'
const gig = new Gig({ source })
gig.play()
gig.check('playing') // true
gig.stop()
gig.check('stopped') // true
Gig
extends bach-js
's Music
class and provides additional getters that are specific to real-time playback.
Provides the beat, elements and events found at the playback cursor (step).
beat
: The beat present at the duration (fromGig.beats
)elems
List of elements (by id) playing at the duration (fromGig.elements
)play
: List of elements that should begin playing at the durationstop
: List of elements that should stop playing at the duration
Provides the beat, elements and events found at the previous playback cursor (step).
Provides the beat, elements and events found at the next playback cursor (step).
Determines the cyclic/relative playback cursor (step), never exceeding the total length of the track.
Determines the global/absolute playback cursor (step), potentially exceeding the total length of the track.
Uses the stateless
configuration option to determine if the value is derived from an imperative state or a monotonic timer.
Determines the global/absolute playback cursor (step), strictly based on elapsed monotonic time.
Determines the base duration unit to use via the stateless
configuration option.
Returns ms
when stateless and step
when stateful.
Determines if the cursor is on the first step of the track.
Determines if the cursor is on the last step of the track.
Determines the amount of time (in ms
) that's elapsed since the track started playing.
The progress of the track's overall playback, modulated to 1 (e.g. 1.2 -> 0.2).
The run-time completion of the track's overall playback.
The same as progress
but can overflow 1
, meaning the track has looped.
Determines the number of times the track's playback has looped/repeated.
Determines if the track's playback has already looped/repeated.
Determines the limit of steps to restrict playback to.
If the loop
configuration option is true
, the limit becomes Math.Infinity
.
Otherwise the limit
matches the total duration of the track.
Provides the current pulse beat under the context of a looping metronome.
Determines if the current step's beat has changed from the previous step's beat.
A Gig
object emits events for each of its transitional behaviors, extending Node's EventEmitter
API:
start
: The internal clock has been instantiated and invokedplay
: The audio has finished loading and begins playingstop
: The audio and clock have been stopped and deconstructedpause
: The audio and clock have been pausedresume
: The audio and clock have been resumedmute
: The audio has been mutedseek
: The position of the track (both data and audio) has been modifiedloop
: The track has loopedstep
: The clock has progressed a single step (bach
's quantized unit of iteration)stop:beat
: The beat that was just playing has endedplay:beat
: The next beat in the queue has begun playingupdate:status
: The playback status has generally changed (i.e.paused
,resumed
, etc.)
Subscribe to events using the on
method:
const gig = new Gig({ /* ... */ })
gig.on('play:beat', beat => console.log('starting to play beat', beat))
gig.on('stop:beat', beat => console.log('finished playing beat', beat))
gig.play()
Warning
Be sure to unsubscribe to events using the
off
method when you're no longer using them in order to avoid memory leaks!
Because the timing needs of each music application are different, gig
allows you to provide your own custom timers.
gig
supports both stateless monotic timers (default) and stateful interval timers.
It's recommended to use a stateless monotic timer since they are immune to drift, however stateful intervals are more ideal under certain circumstances.
Stateless timers are those which determine state based on a monotonic timestamp.
By default gig
is stateless and uses a cross-platform timer based onrequestAnimationFrame
and performance.now()
, a high-resolution monotonic timestamp.
If you want to customize the default timer, such as providing a function to call on each frame/tick, you can import the clock directly:
import { Gig, clock } from 'gig'
const tick = time => console.log('tick', time)
const timer = gig => clock(gig, tick)
const gig = new Gig({
source: 'play! []',
timer
})
If your application has requires a less aggressive iteration mechanism than polling/frame-spotting (such as requestAnimationFrame
), you can provide gig
with a stateful timer.
The following example uses stateful-dynamic-interval
, a stateless timer that wraps setTimeout
with state controls.
Since it already conforms to the expected timer interface, it requires practically no customization:
import { setStatefulDynterval } from 'stateful-dynamic-interval'
import { Gig } from 'gig'
const clock = gig => setStatefulDynterval(gig.step.bind(gig), {
wait: gig.interval,
immediate: true
})
const gig = new Gig({
source: 'play! []',
clock,
stateless: false
})
gig.play()
However, because stateful-dynamic-interval
uses setTimeout
behind the scenes, drift between audio (or any other synchronization points) will inevitably grow, and playback will eventually become misaligned.
This is due to the single-threaded nature of JavaScript and the generally low precision of setTimeout
and setInterval
. Read Chris Wilson's article "A Tale of Two Clocks: Scheduling Web Audio for Precision" for detailed information on this limitation and a tutorial on how to create a more accurate clock in JavaScript.
This limitation becomes particularly prominent in web applications that loop audio forever or play otherwise "long" streams of audio information.
Because most applications are concerned with accurate synchronization over time, gig
establishes a driftless monotonic timer as its default, and its recommended to only detract from the default if you have to.
Timers are provided as a factory function (accepting the current gig
instance) which is expected to return an object with the following interface:
interface GigTimer {
(gig: Gig): GigTimer
stop()
pause()
resume()
}
Your timer must call gig.step()
as its interval callback/action. Otherwise gig
has no way to know when each step should be called (after all, that's the job of the timer)!
Timers must invoke their first step immediately, unlike the behavior of setInterval
where a full interval takes place before the first step is run. This constraint ultimately makes aligning the music with the audio much simpler.
The best example of a timer implementation is gig
's default monotonic clock, which can be found in src/timer.js
.
- Replace
howler
withtone.js
- Unit and integration tests
- Seek functionality
- Tempo adjustment (required in
bach-js
orbach
core)
Copyright © Erik Vavro. All rights reserved.
Licensed under the MIT License.