Giter Club home page Giter Club logo

gig's Introduction

gig

๐Ÿ”‰ 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.

Sections

Install

$ npm i slurmulon/gig

Usage

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()

Documentation

Options

source

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.

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')
    ]
  `
})

audio

Specifies the audio data to synchronize the musical bach.json data with.

  • Type: String, Blob, Array
  • Required: false (may be inherited from source headers)
import { Gig } from 'gig'

const gig = new Gig({
  source: { /* ... */ },
  audio: 'http://api.madhax.io/track/q2IBRPmMq9/audio/mp3'
})

loop

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
})

stateless

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
})

Methods

play()

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()

start()

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 the start() and play() 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()

stop()

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)

pause()

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)

resume()

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)

kill()

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)

mute()

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)

moment(duration, is)

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')

check(status)

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

Getters

Gig extends bach-js's Music class and provides additional getters that are specific to real-time playback.

state

Provides the beat, elements and events found at the playback cursor (step).

  • beat: The beat present at the duration (from Gig.beats)
  • elems List of elements (by id) playing at the duration (from Gig.elements)
  • play: List of elements that should begin playing at the duration
  • stop: List of elements that should stop playing at the duration

prev

Provides the beat, elements and events found at the previous playback cursor (step).

next

Provides the beat, elements and events found at the next playback cursor (step).

cursor

Determines the cyclic/relative playback cursor (step), never exceeding the total length of the track.

current

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.

place

Determines the global/absolute playback cursor (step), strictly based on elapsed monotonic time.

unit

Determines the base duration unit to use via the stateless configuration option.

Returns ms when stateless and step when stateful.

first

Determines if the cursor is on the first step of the track.

last

Determines if the cursor is on the last step of the track.

elapsed

Determines the amount of time (in ms) that's elapsed since the track started playing.

progress

The progress of the track's overall playback, modulated to 1 (e.g. 1.2 -> 0.2).

completion

The run-time completion of the track's overall playback.

The same as progress but can overflow 1, meaning the track has looped.

iterations

Determines the number of times the track's playback has looped/repeated.

repeating

Determines if the track's playback has already looped/repeated.

limit

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.

metronome

Provides the current pulse beat under the context of a looping metronome.

updated

Determines if the current step's beat has changed from the previous step's beat.

Events

A Gig object emits events for each of its transitional behaviors, extending Node's EventEmitter API:

  • start: The internal clock has been instantiated and invoked
  • play: The audio has finished loading and begins playing
  • stop: The audio and clock have been stopped and deconstructed
  • pause: The audio and clock have been paused
  • resume: The audio and clock have been resumed
  • mute: The audio has been muted
  • seek: The position of the track (both data and audio) has been modified
  • loop: The track has looped
  • step: The clock has progressed a single step (bach's quantized unit of iteration)
  • stop:beat: The beat that was just playing has ended
  • play:beat: The next beat in the queue has begun playing
  • update: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!

Timers

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

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
})

Stateful

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.

Interface

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)!

Implementation

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.

Roadmap

  • Replace howler with tone.js
  • Unit and integration tests
  • Seek functionality
  • Tempo adjustment (required in bach-js or bach core)

License

Copyright ยฉ Erik Vavro. All rights reserved.

Licensed under the MIT License.

gig's People

Contributors

dependabot[bot] avatar slurmulon avatar

Stargazers

 avatar  avatar

Watchers

 avatar

gig's Issues

Support contextual collections

We need to be able to nest intervals that process collections of elements. For example:

@Time = 4|4

:Scales = [
  2 -> Scale('C Lydian')
  2 -> Scale('G Ionian')
]

:Chords = [
  2 -> Chord('Cmaj7')
  2 -> Chord('Gmaj7')
]

!Play {
  :Scales
  :Chords
}

In the Set ({}), the :Scales and :Chords will overlap and will be played at the same time.

In the nested List ([]), each Chord or Scale will be played for 2 measures at a time using their own contextual/internal timers.

The simplest way I know to do this is to introduce a recursive "context" abstraction.

This will also require changes to bach, such that Sets can be defined without a duration value.

Consume Bach @Audio header

Instead of accepting audio as an argument to the Track constructor, we should instead parse bach's @Audio header, automatically following the URL to the audio bytestream and loading it into memory.

This does not have to initially support streaming audio, although it does sound like a useful feature for the future.

Rename project to either `gig` or `gigue`

I just renamed this module to bach-player, but I realized that this is annoying for development (always have to tab for the project name) and also makes it ambiguous what platform this player actually applies to (default is JS, but not everybody would know that).

I've concluded that it's better to give the project a more generic and abstract name than a straight forward one.

gig means a band performance, while gigue is a lively piece of Baroque music, which Bach wrote a lot of.

gig is unsurprisingly taken on NPM (we could just scope to an organization or something), but gigue is awkward to pronounce (like "jeeg")

[Refactor] Improve accuracy of timer

Right now the timer backing juke is not sufficient because it is prone to drift (it's based on setInterval, or rather a modified version of it that tries to remain accurate but doesn't really work).

The timer found in heartbeets may be more accurate than stateful-dynamic-interval because it is using the requestAnimationFrame feature of modern browsers.

We may also want to try worker-timers as well, since it spins up instances of setInterval and setTimeout in Web Worker scripts and can run in parallel, therefore improving the accuracy.

Support seek to position

We first need to first support translating a time position in a track (e.g. 00:30) into the closest beat and measure cursors.

We then need to calculate the size of the interval at the beat and measure cursors. The size of the interval will be offset by the difference between the standard/default interval and the time remaining until the next interval.

More formally, if C is the timestamp found at the seeked position, E is the scheduled/expected invocation timestamp of the next interval, and R is the amount of time remaining (in ms) until the next interval, then:

R = E - C

Support Rest element

We need to support Rest elements.

This actually might work already due to how the module is written, but at the very least this needs to be verified + tested.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.