diff --git a/Tone/event/Pattern.test.ts b/Tone/event/Pattern.test.ts index 2fd77115..9375fa26 100644 --- a/Tone/event/Pattern.test.ts +++ b/Tone/event/Pattern.test.ts @@ -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); + }); + }); }); }); diff --git a/Tone/event/Pattern.ts b/Tone/event/Pattern.ts index a5d9180c..0f2e0f1c 100644 --- a/Tone/event/Pattern.ts +++ b/Tone/event/Pattern.ts @@ -27,12 +27,17 @@ export class Pattern extends Loop> { /** * The pattern generator function */ - private _pattern: Iterator; + private _pattern: Iterator; + + /** + * 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 extends Loop> { 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 extends Loop> { * Internal function called when the notes should be called */ protected _tick(time: Seconds): void { - const value = this._pattern.next() as IteratorResult; - this._value = value.value; + const index = this._pattern.next() as IteratorResult; + this._index = index.value; + this._value = this._values[index.value]; this.callback(time, this._value); } @@ -107,6 +113,13 @@ export class Pattern extends Loop> { 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 extends Loop> { } set pattern(pattern) { this._type = pattern; - this._pattern = PatternGenerator(this._values, this._type); + this._pattern = PatternGenerator(this._values.length, this._type); } } diff --git a/Tone/event/PatternGenerator.test.ts b/Tone/event/PatternGenerator.test.ts index 841778e7..81b2acf1 100644 --- a/Tone/event/PatternGenerator.test.ts +++ b/Tone/event/PatternGenerator.test.ts @@ -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); diff --git a/Tone/event/PatternGenerator.ts b/Tone/event/PatternGenerator.ts index 8fcc3547..ba63fcbb 100644 --- a/Tone/event/PatternGenerator.ts +++ b/Tone/event/PatternGenerator.ts @@ -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(values: T[]): IterableIterator { +function* upPatternGen(numValues: number): IterableIterator { 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(values: T[]): IterableIterator { /** * Start at the last value and go down to 0 */ -function* downPatternGen(values: T[]): IterableIterator { - let index = values.length - 1; +function* downPatternGen(numValues: number): IterableIterator { + 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(values: T[]): IterableIterator { /** * Infinitely yield the generator */ -function* infiniteGen(values: T[], gen: typeof upPatternGen): IterableIterator { +function* infiniteGen(numValues: number, gen: typeof upPatternGen): IterableIterator { 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(values: T[], directionUp: boolean): IterableIterator { - let index = directionUp ? 0 : values.length - 1; +function* alternatingGenerator(numValues: number, directionUp: boolean): IterableIterator { + 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(values: T[], directionUp: boolean): IterableIt /** * Starting from the bottom move up 2, down 1 */ -function* jumpUp(values: T[]): IterableIterator { +function* jumpUp(numValues: number): IterableIterator { 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(values: T[]): IterableIterator { /** * Starting from the top move down 2, up 1 */ -function* jumpDown(values: T[]): IterableIterator { - let index = values.length - 1; +function* jumpDown(numValues: number): IterableIterator { + 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(values: T[]): IterableIterator { /** * Choose a random index each time */ -function* randomGen(values: T[]): IterableIterator { +function* randomGen(numValues: number): IterableIterator { 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(values: T[]): IterableIterator { +function* randomOnce(numValues: number): IterableIterator { // 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(values: T[]): IterableIterator { +function* randomWalk(numValues: number): IterableIterator { // 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(values: T[], pattern: PatternName = "up", index = 0): Iterator { +export function* PatternGenerator(numValues: number, pattern: PatternName = "up", index = 0): Iterator { // 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); } }