npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

rhythmical

v0.1.6

Published

parse and manipulate musical rhythms in javascript

Downloads

10

Readme

rhythmical

This lib is able to parse and manipulate musical rhythms by using nested arrays and algebraic fractions.

State

This lib is a work in progress.

Example Showcase

Here are some example usages of the lib, to get you hooked.

Example 1: Rendering for playback

Lets spell the famous bolero rhythm in nested notation:

const bolero = [
  // bar 1
  [
    [1, [1, 1, 1]], // beat 1
    [1, [1, 1, 1]], // beat 2
    [1, 1] // beat 3
  ],
  // bar 2
  [
    [1, [1, 1, 1]], // beat 1
    [1, [1, 1, 1]], // beat 2
    [[1, 1, 1], [1, 1, 1]] // beat 3
  ]
];

Compared with the standard rhythm notation:

bolero

  • it is a direct representation in array format
  • you could even see the inner brackets as beams
  • no need to specify any durations / time signatures

We can now render it and play it back with Tone.js:

import { Rhythm } from 'rhythmical';

// render events
const events = Rhythm.render(bolero, 3);

// play back with tonal
var synth = new Tone.MembraneSynth().toMaster();
const part = new Tone.Part(
  (time, event) => synth.triggerAttackRelease('C2', event.duration, time),
  events // <- the events are used here
).start(0);
part.loop = true;
part.loopEnd = 3;
Tone.Transport.start('+1');

Example 2: Edit Groupings

Lets take the afrobell in 4/4:

const afrobell4 = [[2, 0, 2], [0, 1, 2], [0, 2, 0], [2, 0, 1]];

afrobell4

We can remove the triplet groupings with ungroup:

const afrobell = Rhythm.ungroup(afrobell4);

...which returns the "raw" afrobell pattern:

[2, 0, 2, 0, 1, 2, 0, 2, 0, 2, 0, 1];

afrobell0

We can now apply another grouping:

const afrobell3 = Rhythm.group(afrobell, 3);
[[2, 0, 2, 0], [1, 2, 0, 2], [0, 2, 0, 1]];

afrobell3

Example 3: Insert

You can insert rhythms into one another. Take the rhythm of a famous rock song riff:

const smoke = [[1, 1, 1.5, [0, 1]], [[0, 1], [0, 1], 2, 0]];

smoke

As you may know, the riff of the original is 4 bars long. Let's build the next two bars, reusing the first bar:

const smokeOn = Rhythm.insert([[0, 7], 0, 0, 0], smoke.concat(smoke[0]));

yields:

[
  [1, 1, 1.5, [0, 1]], // bar 1
  [[0, 1], [0, 1], 2, 0], // bar 2
  [1, 1, 1.5, [0, 1]], // bar 3 = copied bar 1
  [[0, 7], 0, 0, 0] // inserted bar 4
];

smoke

Note: The first argument is expected to be ungrouped, while the second argument is expected to have groups. This allows you to reuse rhythmic blocks independent of time signatures.

insert at beat

The insert method has a third argument, which specifies the beat of insertion:

Rhythm.insert([3, 4], [[1, 2, 0, 0]], 2); // from left
Rhythm.insert([3, 4], [[1, 2, 0, 0]], -2); // from right

both yield

[[1, 2, 3, 4]];

Design Goals

  • Keep it functional: inputs and outputs can be transformed using function chaining
  • Keep it immutable: arrays/objects never change
  • Keep it static: no class instances => outputs are primitives, objects or arrays
  • strictly typed: typescript first
  • Influenced by Tonal, Tidal Cycles and Impro-Visor

API

NestedRhythm

A NestedRhythm is an easy way to notate rhythms:

const fourToTheFlour = ['A', 'C', 'E', 'G'];
const waltz = ['C', ['E', 'E'], ['G', 'G']];
const swingCymbal = [1, [2, 0, 1], 1, [2, 0, 1]];
const swingHihat = [0, 1, 0, 1];
  • By using just nested arrays, you can express any musical rhythm
  • The notation is very similar to normal musical notation
  • Similar concept also used by TidalCycles (or see Tone#Sequence)
  • The actual content can be any type

To be able to play a rhythm we need:

  • absolute time
  • absolute duration

We can use Rhythm.render to calculate that:

Rhythm.render

render(NestedRhythm, duration)

Turns a NestedRhythm to a flat array of TimedEvent:

Rhythm.render(['C', ['E', 'G'], 'B', 'D'], 4);

yields:

[
  { value: 'C', time: 0, duration: 1, path: [[0, 4]] },
  { value: 'E', time: 1, duration: 0.5, path: [[1, 4], [0, 2]] },
  { value: 'G', time: 1.5, duration: 0.5, path: [[1, 4], [1, 2]] },
  { value: 'B', time: 2, duration: 1, path: [[2, 4]] },
  { value: 'D', time: 3, duration: 1, path: [[3, 4]] }
];
  • array is now one dimensional
  • time and duration are calculated based on the path fractions

Using numbers as duration

Using numbers, we can adjust the duration:

Rhythm.render([1, [0, 3], 0, [0, 1]], 4);
[
  { value: 1, time: 0, duration: 1, path: [[0, 4]] },
  { value: 2, time: 1, duration: 1, path: [[1, 4], [0, 2]] },
  { value: 2, time: 2, duration: 1, path: [[3, 4]] }
  { value: 1, time: 3.5, duration: 0.5, path: [[3, 4]] }
];
  • duration is now multiplied by value

Rhythm.flat

Converts a NestedRhythm to a one dimensional Array of FlatEvent. This is like render but without the absolute calculations:

const swingCymbal = [
  1, // one
  [2, 0, 1], // two with "swing" off
  1, // three
  [2, 0, 1] // four with "swing" off
];
Rhythm.flat(swingCymbal);

outputs

[
  { value: 1, path: [[0, 4]] },
  { value: 2, path: [[1, 4], [0, 3]] },
  { value: 0, path: [[1, 4], [1, 3]] },
  { value: 1, path: [[1, 4], [2, 3]] },
  { value: 1, path: [[2, 4]] },
  { value: 2, path: [[3, 4], [0, 3]] },
  { value: 0, path: [[3, 4], [1, 3]] },
  { value: 1, path: [[3, 4], [2, 3]] }
];

FlatEvent

A FlatEvent consists of

  • path: path of fractions to keep the nesting information
  • value: the original value

Rhythm.time

Calculates time of path fractions:

  Rhythm.time([[0, 2], [1, 2]])); // yields 0.25
  // = (0 + 1/2) / 2
  Rhythm.time([[1, 2], [0, 2]])); // yields 0.5
  // = (1 + 0/2) / 2
  Rhythm.time([[0, 1], [3, 4]])); // yields 0.75
  // = (0 + 3/4) / 1
  Rhythm.time([[0, 1], [0, 1]])); // yields 0
  // = (0 + 0/1) / 1
  expect(Rhythm.time([[1, 4], [1, 4]], 4)).toEqual(1.25);
  // = 4 * (1 + 1/4) / 4
  • the second argument is the time of the whole sequence. If the unit is seconds, we would have a 4s measure, with 4 beats in the bar => 60bpm.

Rhythm.duration

Calculates the duration of a given path:

Rhythm.duration([[0,4], [0,3]], 4)); // 4* 4/3 = 1/3
Rhythm.duration([[0,4], [0,2]], 4)); // 4* 4/2 = 1/2
  • This method can be used to determine the length of a note
  • the first one could be the length of a triplet in 60bpm
  • the second one could be the length of an eights in 60bpm

Rhythm.calculate

Calculates time and duration from a FlatEvent. This method is used by render:

const calculated = Rhythm.flat([1, [0, 3], 0, 1])
  .map(Rhythm.calculate(4))
  .map(({ time, duration }) => ({ value, time, duration }));

yields

[
  { value: 1, time: 0, duration: 1 },
  { value: 0, time: 1, duration: 0 },
  { value: 3, time: 1.5, duration: 1.5 },
  { value: 0, time: 2, duration: 0 },
  { value: 1, time: 3, duration: 1 }
];
  • the time value is now the absolute time inside the defined 4s
  • the duration is the length of the note inside the subdivision

This format can be used easily to schedule playback, e.g. using Tone.js (see Example 1 at the top)

Playback

The following function could be used to play notes with Tone.js:

function playNotes(notes, cycle, synth) {
  const events = Rhythm.render(notes, cycle);
  const part = new Tone.Part((time, event) => {
    synth.triggerAttackRelease(event.value, event.duration, time);
  }, events).start(0);
  part.loop = true;
  part.loopEnd = cycle;
  Tone.Transport.start('+1');
}

var pluck = new Tone.PluckSynth().toMaster();
playNotes(['C3', 'D3', ['E3', 'G3']], 2, pluck);

Polyrythms

Polyrhythm = different pulse, same duration

playNotes(['C3', 'E3', 'G3'], 2, synth);
playNotes(['C2', 0, 'G2', 0], 2, synth2);
  • The two Rhythms will be played in the same amount of time.
  • Having two different pulses (3 and 4), this will create a polyrhythm

Polymeter

Polymeter = same pulse, different length

playNotes(['C3', 'E3', 'G3'], 3, synth);
playNotes(['C2', 0, 'G2', 0], 4, synth2);
  • The two Rhythms will be played in the same amount of time.
  • Having two different pulses (3 and 4), this will create a polyrhythm

Rhythm.spm()

A little helper function to get the seconds per measure:

// polymeter
playNotes(['C3', 'E3', 'G3'], Rhythm.spm(120, 3), synth);
playNotes(['C2', 0, 'G2', 0], Rhythm.spm(120, 4), synth2);
// polyrhythm
playNotes(['C3', 'E3', 'G3'], Rhythm.spm(120, 4), synth);
playNotes(['C2', 0, 'G2', 0], Rhythm.spm(120, 4), synth2);

Rhythm.combine

addGroove(string[]): {[chord: string]: number[]}

addGroove(['C7', 'F7']);

yields

{
  "C7": [2, 0],
  "F7": [0, 2]
}

where each chord is mapped to its personal "track". Playing all the tracks at the same time should return a a seamless rhythm.