const Sequencer = require('./sequencer');
/**
* Generates `{time, intensity, duration}` values to control the groove of a {@link Part}.
* @extends Sequencer
*/
class Rhythm extends Sequencer {
/**
* @param {Object} options
* @param {String|Iterable} options.pattern When a String, it can contain the following characters:
* - `"X"` → accented note
* - `"x"` → normal note
* - `"="` → tie
* - `"."` → rest
*
* Each characters' duration is determined by the `pulse` option.
* NOTE: Other characters are ignored and can be used to improve readability, for example `"X.x.|x==.|..x=|x=X="`
*
* When an {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#iterable|Iterable}
* of delta-start times, it represents the time between each note (with the first note always starting immediately).
* NOTE: Negative numbers can be used as rests, and the absolute value is the time until the next note.
* @param {Number} [options.pulse=1] The duration in beats of each character in a String `pattern`.
* Only relevant if the `pattern` option is a String.
* @param {Iterable} [options.intensities=[0.7]]
* Determines the note intensities in an Iterable `pattern`. Ignored if the `pattern` option is a String.
* @param {Iterable} [options.durations=time between notes]
* Determines the note durations in an Iterable `pattern`. Ignored if the `pattern` option is a String.
* @param {Iterable} [options.length=pattern length]
* Overrides the length of this rhythm to be different than the `pattern` length.
* Useful when this rhythm is `looped` or when using {@link Random.duration}s.
* @param {Iterable} [options.looped=false] If true, this rhythm will repeat infinitely. Note that delta-start times,
* intensities, and durations loop independently for Iterable `pattern`s, which creates less repetitive rhythms.
*/
constructor({ pattern=[], pulse=1, intensities, durations, length, looped=false } = {}) {
const times = [];
if (typeof pattern === 'string') {
length = length || pattern.length * pulse;
intensities = [];
durations = [];
let duration = null;
let count = 0;
for (const char of pattern) {
switch (char) {
case 'X':
times.push(pulse * count);
intensities.push(1);
if (duration) durations.push(duration); // previous duration
duration = pulse;
count++;
break;
case 'x':
times.push(pulse * count);
intensities.push(0.7);
if (duration) durations.push(duration); // previous duration
duration = pulse;
count++;
break;
case '=':
if (duration) duration += pulse;
count++;
break;
case '.':
if (duration) durations.push(duration);
duration = null;
count++;
break;
default:
}
}
if (duration) durations.push(duration);
}
else {
length = length || pattern.map(Math.abs).reduce((a,b) => a + b, 0);
const legatoDurations = [];
let time = 0;
let nextTime;
for (const value of pattern) {
nextTime = time + Math.abs(value);
const duration = (nextTime - time) * Math.sign(value);
if (duration > 0) {
times.push(time);
legatoDurations.push(duration);
} // else this is a rest
time = nextTime;
}
if (!durations) durations = legatoDurations;
}
intensities = intensities || [0.7];
super({ time: times, intensity: intensities, duration: durations }, { length, looped });
this.times = times;
this.intensities = intensities;
this.durations = durations;
}
/**
* Generates a Rhythm pattern string by evenly distributes the given number of pulses into the given total number of
* time units. Also known as a "Euclidean rhythm". Use the return value for the constructor's pattern option.
* @param pulses {number}
* @param total {number}
* @param {Object} options
* @param {Number} [options.shift=0] shifts the pattern (with wrap-around) by the given number of time units
* @param {Number} [options.rotation=0] shifts the pattern (with wrap-around) to the given pulse index
*/
static distribute(pulses, total, options={}) {
const { rotation, shift } = options;
let pattern = [];
let count = 0;
let nextPulse = Math.floor(++count/pulses * total);
for (let i=1; i<=total; i++) {
if (i < nextPulse) {
pattern.push('.'); // rest
} else {
pattern.push('x'); // pulse
nextPulse = Math.floor(++count/pulses * total);
}
}
pattern = pattern.reverse();
if (rotation) {
for (let i = 1; i <= rotation; i++) {
const slicePoint = pattern.indexOf('x', 1);
if (slicePoint > 0) pattern = pattern.slice(slicePoint).concat(pattern.slice(0, slicePoint));
else break;
}
for (let i = -1; i >= rotation; i--) {
const slicePoint = pattern.lastIndexOf('x');
if (slicePoint > 0) pattern = pattern.slice(slicePoint).concat(pattern.slice(0, slicePoint));
else break;
}
}
if (shift) {
const slicePoint = shift % pattern.length;
pattern = pattern.slice(slicePoint).concat(pattern.slice(0, slicePoint));
}
return pattern.join('');
}
}
module.exports = Rhythm;