Add Pattern.index

Pattern.index returns the current index of the pattern. This method
previously existed but was lost after the TypeScript conversion.
This commit is contained in:
Yifan Mai 2022-01-04 21:12:40 -08:00
parent aad2d8c890
commit 205c438511
4 changed files with 116 additions and 92 deletions

View file

@ -89,19 +89,21 @@ describe("Pattern", () => {
it("is invoked after it's started", () => {
let invoked = false;
return Offline(({ transport }) => {
const values = ["a", "b", "c"];
let index = 0;
const pattern = new Pattern((() => {
invoked = true;
expect(pattern.value).to.equal(index);
expect(pattern.value).to.equal(values[index]);
expect(pattern.index).to.equal(index);
index++;
}), [0, 1, 2]).start(0);
}), values).start(0);
transport.start();
}, 0.2).then(() => {
expect(invoked).to.be.true;
});
});
it("passes in the scheduled time and pattern index to the callback", () => {
it("passes in the scheduled time and pattern note to the callback", () => {
let invoked = false;
return Offline(({ transport }) => {
const startTime = 0.05;
@ -109,6 +111,8 @@ describe("Pattern", () => {
expect(time).to.be.a("number");
expect(time - startTime).to.be.closeTo(0.3, 0.01);
expect(note).to.be.equal("a");
expect(pattern.value).to.equal("a");
expect(pattern.index).to.be.equal(0);
invoked = true;
}), ["a"], "up");
transport.start(startTime);
@ -121,16 +125,38 @@ describe("Pattern", () => {
it("passes in the next note of the pattern", () => {
let counter = 0;
return Offline(({ transport }) => {
const values = ["a", "b", "c"];
const pattern = new Pattern(((time, note) => {
expect(note).to.equal(counter % 3);
expect(note).to.equal(values[counter % 3]);
expect(pattern.value).to.equal(values[counter % 3]);
expect(pattern.index).to.be.equal(counter % 3);
counter++;
}), [0, 1, 2], "up").start(0);
}), values, "up").start(0);
pattern.interval = "16n";
transport.start(0);
}, 0.7).then(() => {
expect(counter).to.equal(6);
});
});
it("can modify the pattern type and values", () => {
let counter = 0;
return Offline(({ transport }) => {
const values = ["a", "b", "c"];
const pattern = new Pattern(((time, note) => {
expect(note).to.equal(values[counter % 3]);
expect(pattern.value).to.equal(values[counter % 3]);
expect(pattern.index).to.be.equal(counter % 3);
counter++;
}), ["a"], "down").start(0);
pattern.interval = "16n";
pattern.pattern = "up";
pattern.values = values;
transport.start(0);
}, 0.7).then(() => {
expect(counter).to.equal(6);
});
});
});
});

View file

@ -27,12 +27,17 @@ export class Pattern<ValueType> extends Loop<PatternOptions<ValueType>> {
/**
* The pattern generator function
*/
private _pattern: Iterator<ValueType>;
private _pattern: Iterator<number>;
/**
* The current index
*/
private _index?: number;
/**
* The current value
*/
private _value?: ValueType;
private _value?: ValueType;
/**
* Hold the pattern type
@ -67,7 +72,7 @@ export class Pattern<ValueType> extends Loop<PatternOptions<ValueType>> {
this.callback = options.callback;
this._values = options.values;
this._pattern = PatternGenerator(options.values, options.pattern);
this._pattern = PatternGenerator(options.values.length, options.pattern);
this._type = options.pattern;
}
@ -83,8 +88,9 @@ export class Pattern<ValueType> extends Loop<PatternOptions<ValueType>> {
* Internal function called when the notes should be called
*/
protected _tick(time: Seconds): void {
const value = this._pattern.next() as IteratorResult<ValueType>;
this._value = value.value;
const index = this._pattern.next() as IteratorResult<ValueType>;
this._index = index.value;
this._value = this._values[index.value];
this.callback(time, this._value);
}
@ -107,6 +113,13 @@ export class Pattern<ValueType> extends Loop<PatternOptions<ValueType>> {
return this._value;
}
/**
* The current index of the pattern.
*/
get index(): number | undefined {
return this._index;
}
/**
* The pattern type. See Tone.CtrlPattern for the full list of patterns.
*/
@ -115,7 +128,7 @@ export class Pattern<ValueType> extends Loop<PatternOptions<ValueType>> {
}
set pattern(pattern) {
this._type = pattern;
this._pattern = PatternGenerator(this._values, this._type);
this._pattern = PatternGenerator(this._values.length, this._type);
}
}

View file

@ -13,22 +13,14 @@ describe("PatternGenerator", () => {
context("API", () => {
it("can be constructed with an array and type", () => {
const pattern = PatternGenerator([0, 1, 2, 3], "down");
it("can be constructed with an number and type", () => {
const pattern = PatternGenerator(4, "down");
expect(getArrayValues(pattern, 10)).to.deep.equal([3, 2, 1, 0, 3, 2, 1, 0, 3, 2]);
});
it("can be resized smaller when the index is after the previous length", () => {
const values = [0, 1, 2, 3, 4];
const pattern = PatternGenerator(values);
expect(pattern.next().value).to.equal(0);
values.shift();
expect(pattern.next().value).to.equal(2);
});
it("throws an error with an empty array", () => {
it("throws an error with a number less than 1", () => {
expect(() => {
const pattern = PatternGenerator([]);
const pattern = PatternGenerator(0);
pattern.next();
}).to.throw(Error);
});
@ -37,54 +29,54 @@ describe("PatternGenerator", () => {
context("Patterns", () => {
it("does the up pattern", () => {
const pattern = PatternGenerator([0, 1, 2, 3], "up");
const pattern = PatternGenerator(4, "up");
expect(getArrayValues(pattern, 6)).to.deep.equal([0, 1, 2, 3, 0, 1]);
});
it("does the down pattern", () => {
const pattern = PatternGenerator([0, 1, 2, 3], "down");
const pattern = PatternGenerator(4, "down");
expect(getArrayValues(pattern, 6)).to.deep.equal([3, 2, 1, 0, 3, 2]);
});
it("does the upDown pattern", () => {
const pattern = PatternGenerator([0, 1, 2, 3], "upDown");
const pattern = PatternGenerator(4, "upDown");
expect(getArrayValues(pattern, 10)).to.deep.equal([0, 1, 2, 3, 2, 1, 0, 1, 2, 3]);
});
it("does the downUp pattern", () => {
const pattern = PatternGenerator([0, 1, 2, 3], "downUp");
const pattern = PatternGenerator(4, "downUp");
expect(getArrayValues(pattern, 10)).to.deep.equal([3, 2, 1, 0, 1, 2, 3, 2, 1, 0]);
});
it("does the alternateUp pattern", () => {
const pattern = PatternGenerator([0, 1, 2, 3, 4], "alternateUp");
const pattern = PatternGenerator(5, "alternateUp");
expect(getArrayValues(pattern, 10)).to.deep.equal([0, 2, 1, 3, 2, 4, 3, 0, 2, 1]);
});
it("does the alternateDown pattern", () => {
const pattern = PatternGenerator([0, 1, 2, 3, 4], "alternateDown");
const pattern = PatternGenerator(5, "alternateDown");
expect(getArrayValues(pattern, 10)).to.deep.equal([4, 2, 3, 1, 2, 0, 1, 4, 2, 3]);
});
it("outputs random elements from the values", () => {
const values = [0, 1, 2, 3, 4];
const pattern = PatternGenerator(values, "random");
const numValues = 5;
const pattern = PatternGenerator(numValues, "random");
for (let i = 0; i < 10; i++) {
expect(values.indexOf(pattern.next().value)).to.not.equal(-1);
expect(pattern.next().value).to.be.at.least(0).and.at.most(numValues - 1);
}
});
it("does randomOnce pattern", () => {
const pattern = PatternGenerator([4, 5, 6, 7, 8], "randomOnce");
expect(getArrayValues(pattern, 10).sort()).to.deep.equal([4, 4, 5, 5, 6, 6, 7, 7, 8, 8]);
const pattern = PatternGenerator(5, "randomOnce");
expect(getArrayValues(pattern, 10).sort()).to.deep.equal([0, 0, 1, 1, 2, 2, 3, 3, 4, 4]);
});
it("randomly walks up or down 1 step without repeating", () => {
const values = [0, 1, 2, 3, 4];
const pattern = PatternGenerator(values, "randomWalk");
let currentIndex = values.indexOf(pattern.next().value);
const pattern = PatternGenerator(5, "randomWalk");
let currentIndex = pattern.next().value;
for (let i = 0; i < 10; i++) {
const nextIndex = values.indexOf(pattern.next().value);
const nextIndex = pattern.next().value;
expect(currentIndex).to.not.equal(nextIndex);
// change always equals 1
expect(Math.abs(currentIndex - nextIndex)).to.equal(1);

View file

@ -9,11 +9,11 @@ export type PatternName = "up" | "down" | "upDown" | "downUp" | "alternateUp" |
/**
* Start at the first value and go up to the last
*/
function* upPatternGen<T>(values: T[]): IterableIterator<T> {
function* upPatternGen<T>(numValues: number): IterableIterator<number> {
let index = 0;
while (index < values.length) {
index = clampToArraySize(index, values);
yield values[index];
while (index < numValues) {
index = clamp(index, 0, numValues - 1);
yield index;
index++;
}
}
@ -21,11 +21,11 @@ function* upPatternGen<T>(values: T[]): IterableIterator<T> {
/**
* Start at the last value and go down to 0
*/
function* downPatternGen<T>(values: T[]): IterableIterator<T> {
let index = values.length - 1;
function* downPatternGen<T>(numValues: number): IterableIterator<number> {
let index = numValues - 1;
while (index >= 0) {
index = clampToArraySize(index, values);
yield values[index];
index = clamp(index, 0, numValues - 1);
yield index;
index--;
}
}
@ -33,30 +33,23 @@ function* downPatternGen<T>(values: T[]): IterableIterator<T> {
/**
* Infinitely yield the generator
*/
function* infiniteGen<T>(values: T[], gen: typeof upPatternGen): IterableIterator<T> {
function* infiniteGen<T>(numValues: number, gen: typeof upPatternGen): IterableIterator<number> {
while (true) {
yield* gen(values);
yield* gen(numValues);
}
}
/**
* Make sure that the index is in the given range
*/
function clampToArraySize(index: number, values: any[]): number {
return clamp(index, 0, values.length - 1);
}
/**
* Alternate between two generators
*/
function* alternatingGenerator<T>(values: T[], directionUp: boolean): IterableIterator<T> {
let index = directionUp ? 0 : values.length - 1;
function* alternatingGenerator<T>(numValues: number, directionUp: boolean): IterableIterator<number> {
let index = directionUp ? 0 : numValues - 1;
while (true) {
index = clampToArraySize(index, values);
yield values[index];
index = clamp(index, 0, numValues - 1);
yield index;
if (directionUp) {
index++;
if (index >= values.length - 1) {
if (index >= numValues - 1) {
directionUp = false;
}
} else {
@ -71,12 +64,12 @@ function* alternatingGenerator<T>(values: T[], directionUp: boolean): IterableIt
/**
* Starting from the bottom move up 2, down 1
*/
function* jumpUp<T>(values: T[]): IterableIterator<T> {
function* jumpUp<T>(numValues: number): IterableIterator<number> {
let index = 0;
let stepIndex = 0;
while (index < values.length) {
index = clampToArraySize(index, values);
yield values[index];
while (index < numValues) {
index = clamp(index, 0, numValues - 1);
yield index;
stepIndex++;
index += (stepIndex % 2 ? 2 : -1);
}
@ -85,12 +78,12 @@ function* jumpUp<T>(values: T[]): IterableIterator<T> {
/**
* Starting from the top move down 2, up 1
*/
function* jumpDown<T>(values: T[]): IterableIterator<T> {
let index = values.length - 1;
function* jumpDown<T>(numValues: number): IterableIterator<number> {
let index = numValues - 1;
let stepIndex = 0;
while (index >= 0) {
index = clampToArraySize(index, values);
yield values[index];
index = clamp(index, 0, numValues - 1);
yield index;
stepIndex++;
index += (stepIndex % 2 ? -2 : 1);
}
@ -99,78 +92,78 @@ function* jumpDown<T>(values: T[]): IterableIterator<T> {
/**
* Choose a random index each time
*/
function* randomGen<T>(values: T[]): IterableIterator<T> {
function* randomGen<T>(numValues: number): IterableIterator<number> {
while (true) {
const randomIndex = Math.floor(Math.random() * values.length);
yield values[randomIndex];
const randomIndex = Math.floor(Math.random() * numValues);
yield randomIndex;
}
}
/**
* Randomly go through all of the values once before choosing a new random order
*/
function* randomOnce<T>(values: T[]): IterableIterator<T> {
function* randomOnce<T>(numValues: number): IterableIterator<number> {
// create an array of indices
const copy: number[] = [];
for (let i = 0; i < values.length; i++) {
for (let i = 0; i < numValues; i++) {
copy.push(i);
}
while (copy.length > 0) {
// random choose an index, and then remove it so it's not chosen again
const randVal = copy.splice(Math.floor(copy.length * Math.random()), 1);
const index = clampToArraySize(randVal[0], values);
yield values[index];
const index = clamp(randVal[0], 0, numValues - 1);
yield index;
}
}
/**
* Randomly choose to walk up or down 1 index in the values array
*/
function* randomWalk<T>(values: T[]): IterableIterator<T> {
function* randomWalk<T>(numValues: number): IterableIterator<number> {
// randomly choose a starting index in the values array
let index = Math.floor(Math.random() * values.length);
let index = Math.floor(Math.random() * numValues);
while (true) {
if (index === 0) {
index++; // at bottom of array, so force upward step
} else if (index === values.length - 1) {
} else if (index === numValues - 1) {
index--; // at top of array, so force downward step
} else if (Math.random() < 0.5) { // else choose random downward or upward step
index--;
} else {
index++;
}
yield values[index];
yield index;
}
}
/**
* PatternGenerator returns a generator which will iterate over the given array
* of values and yield the items according to the passed in pattern
* @param values An array of values to iterate over
* PatternGenerator returns a generator which will yield numbers between 0 and numValues
* according to the passed in pattern that can be used as indexes into an array of size numValues.
* @param numValues The size of the array to emit indexes for
* @param pattern The name of the pattern use when iterating over
* @param index Where to start in the offset of the values array
*/
export function* PatternGenerator<T>(values: T[], pattern: PatternName = "up", index = 0): Iterator<T> {
export function* PatternGenerator(numValues: number, pattern: PatternName = "up", index = 0): Iterator<number> {
// safeguards
assert(values.length > 0, "The array must have more than one value in it");
assert(numValues >= 1, "The number of values must be at least one");
switch (pattern) {
case "up" :
yield* infiniteGen(values, upPatternGen);
yield* infiniteGen(numValues, upPatternGen);
case "down" :
yield* infiniteGen(values, downPatternGen);
yield* infiniteGen(numValues, downPatternGen);
case "upDown" :
yield* alternatingGenerator(values, true);
yield* alternatingGenerator(numValues, true);
case "downUp" :
yield* alternatingGenerator(values, false);
yield* alternatingGenerator(numValues, false);
case "alternateUp":
yield* infiniteGen(values, jumpUp);
yield* infiniteGen(numValues, jumpUp);
case "alternateDown":
yield* infiniteGen(values, jumpDown);
yield* infiniteGen(numValues, jumpDown);
case "random":
yield* randomGen(values);
yield* randomGen(numValues);
case "randomOnce":
yield* infiniteGen(values, randomOnce);
yield* infiniteGen(numValues, randomOnce);
case "randomWalk":
yield* randomWalk(values);
yield* randomWalk(numValues);
}
}