diff --git a/.changeset/cheese-bread-love.md b/.changeset/cheese-bread-love.md new file mode 100644 index 0000000..c978a39 --- /dev/null +++ b/.changeset/cheese-bread-love.md @@ -0,0 +1,5 @@ +--- +'earwurm': minor +--- + +Remove all `static` members in favour of exported `tokens` object. diff --git a/.changeset/lucky-mugs-live.md b/.changeset/lucky-mugs-live.md new file mode 100644 index 0000000..be25859 --- /dev/null +++ b/.changeset/lucky-mugs-live.md @@ -0,0 +1,5 @@ +--- +'earwurm': minor +--- + +Replace `fadeMs` option with `transitions` boolean. diff --git a/MIGRATION.md b/MIGRATION.md index 3779422..5d28635 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,9 +1,23 @@ # Earwurm migration guide +## 0.7.0 + +- Replaced the `fadeMs?: number` option for `Earwurm`, `Stack`, and `Sound` with a simpler `transitions?: boolean` option. + - Defaults to `false`. + - If opted-into, it will provide an opinionated `200ms` “fade”. + - To fix: Replace all instances of `fadeMs: someNumber` with `transitions: true`. +- Removed some `static` members from `Earwurm` and `Stack`. + - Now exposing equivalent values on the exported `tokens` object. + - To fix, simply replace any instances of: + - `Earwurm.suspendAfterMs` with `tokens.suspendAfterMs`. + - `Earwurm.maxStackSize` or `Stack.maxStackSize` with `tokens.maxStackSize`. + ## 0.6.0 For more details on the released changes, please see [🐛 Various bug fixes](https://github.com/beefchimi/earwurm/pull/50). -- Rename all instances of `statechange` to `state`. +- Renamed all instances of `statechange` to `state`. - Example: `manager.on('statechange', () => {})` -- Replace any instances of `LibraryKeys` with the equivalent `StackIds[]`. + - To fix: Simply find all instances of `statechange` and rename it to `state`. +- Removed the exported `LibraryKeys` type. + - Simply find-and-replace any instances of `LibraryKeys` with the equivalent `StackIds[]`. diff --git a/README.md b/README.md index dccb40e..6801fe9 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ import type {ManagerConfig} from 'earwurm'; // Optionally configure some global settings. const customConfig: ManagerConfig = { - fadeMs: 200, + transitions: true, request: {}, }; diff --git a/docs/api.md b/docs/api.md index face521..6e771dd 100644 --- a/docs/api.md +++ b/docs/api.md @@ -125,14 +125,12 @@ manager.activeEvents; **Static members:** -```ts -// Retrieve the maximum `number` of sounds allowed to -// be contained within a `Stack` at once. -Earwurm.maxStackSize; +There are no static members. However, there are some relevant static values that can be retrieved from the exported `tokens` object: +```ts // Retrieve the total time (in milliseconds) that needs // to pass before the auto-suspension kicks in. -Earwurm.suspendAfterMs; +tokens.suspendAfterMs; ``` ## Stack API @@ -257,11 +255,12 @@ soundStack.activeEvents; **Static members:** +There are no static members. However, there are some relevant static values that can be retrieved from the exported `tokens` object: + ```ts // Retrieve the maximum `number` of sounds allowed to -// be contained within a `Stack` at once. This is -// identical to what can also be read from `Earwurm`. -Stack.maxStackSize; +// be contained within a `Stack` at once. +tokens.maxStackSize; ``` ## Sound API diff --git a/docs/examples-future.md b/docs/examples-future.md index 3dd4c55..b80e239 100644 --- a/docs/examples-future.md +++ b/docs/examples-future.md @@ -112,7 +112,7 @@ function handleQueueChange(keys: SoundId[]) { stack?.on('queuechange', (keys) => handleQueueChange(keys)); async function handleQueuedPlay() { - if (!stack || stack.keys.length >= Stack.maxStackSize) return; + if (!stack || stack.keys.length >= tokens.maxStackSize) return; const sound = await stack.prepare(); return sound; diff --git a/src/Earwurm.ts b/src/Earwurm.ts index e557f9d..99c0d88 100644 --- a/src/Earwurm.ts +++ b/src/Earwurm.ts @@ -1,7 +1,7 @@ import {EmittenCommon} from 'emitten'; import {getErrorMessage, linearRamp, unlockAudioContext} from './helpers'; -import {arrayShallowEquals, clamp, msToSec, secToMs} from './utilities'; +import {arrayShallowEquals, clamp} from './utilities'; import {tokens} from './tokens'; import type { @@ -17,24 +17,16 @@ import type { import {Stack} from './Stack'; export class Earwurm extends EmittenCommon { - static readonly maxStackSize = tokens.maxStackSize; - static readonly suspendAfterMs = tokens.suspendAfterMs; - - static readonly errorMessage = { - close: 'Failed to close the Earwurm AudioContext.', - resume: 'Failed to resume the Earwurm AudioContext.', - }; - - private _volume = 1; + private _vol = 1; private _mute = false; + private _trans = false; private _keys: StackId[] = []; private _state: ManagerState = 'suspended'; readonly #context = new AudioContext(); readonly #gainNode = this.#context.createGain(); - - readonly #fadeSec: number = 0; readonly #request: ManagerConfig['request']; + #library: Stack[] = []; #suspendId: TimeoutId = 0; #queuedResume = false; @@ -46,31 +38,46 @@ export class Earwurm extends EmittenCommon { constructor(config?: ManagerConfig) { super(); - this._volume = config?.volume ?? this._volume; - this.#fadeSec = config?.fadeMs ? msToSec(config.fadeMs) : this.#fadeSec; + this._vol = config?.volume ?? this._vol; + this._trans = Boolean(config?.transitions); this.#request = config?.request ?? undefined; this.#gainNode.connect(this.#context.destination); - this.#gainNode.gain.setValueAtTime(this._volume, this.#context.currentTime); + this.#gainNode.gain.setValueAtTime(this._vol, this.#context.currentTime); if (this._unlocked) this.#autoSuspend(); this.#context.addEventListener('statechange', this.#handleStateChange); } + private get transDuration() { + return this._trans ? tokens.transitionSec : 0; + } + + get transitions() { + return this._trans; + } + + set transitions(value: boolean) { + this._trans = value; + + this.#library.forEach((stack) => { + stack.transitions = value; + }); + } + get volume() { - return this._volume; + return this._vol; } set volume(value: number) { - const oldVolume = this._volume; + const oldVolume = this._vol; const newVolume = clamp(0, value, 1); - this._volume = newVolume; + if (oldVolume === newVolume) return; - if (oldVolume !== newVolume) { - this.emit('volume', newVolume); - } + this._vol = newVolume; + this.emit('volume', newVolume); if (this._mute) return; @@ -78,7 +85,7 @@ export class Earwurm extends EmittenCommon { linearRamp( this.#gainNode.gain, {from: oldVolume, to: newVolume}, - {from: currentTime, to: currentTime + this.#fadeSec}, + {from: currentTime, to: currentTime + this.transDuration}, ); } @@ -87,20 +94,19 @@ export class Earwurm extends EmittenCommon { } set mute(value: boolean) { - if (this._mute !== value) { - this.emit('mute', value); - } + if (this._mute === value) return; this._mute = value; + this.emit('mute', value); - const fromValue = value ? this._volume : 0; - const toValue = value ? 0 : this._volume; + const fromValue = value ? this._vol : 0; + const toValue = value ? 0 : this._vol; const {currentTime} = this.#context; linearRamp( this.#gainNode.gain, {from: fromValue, to: toValue}, - {from: currentTime, to: currentTime + this.#fadeSec}, + {from: currentTime, to: currentTime + this.transDuration}, ); } @@ -145,7 +151,7 @@ export class Earwurm extends EmittenCommon { newKeys.push(id); const newStack = new Stack(id, path, this.#context, this.#gainNode, { - fadeMs: secToMs(this.#fadeSec), + transitions: this._trans, request: this.#request, }); @@ -205,10 +211,7 @@ export class Earwurm extends EmittenCommon { ); }) .catch((error) => { - this.emit('error', [ - Earwurm.errorMessage.close, - getErrorMessage(error), - ]); + this.emit('error', [tokens.error.close, getErrorMessage(error)]); }); this.empty(); @@ -227,7 +230,7 @@ export class Earwurm extends EmittenCommon { if (this.#suspendId) clearTimeout(this.#suspendId); - this.#suspendId = setTimeout(this.#handleSuspend, Earwurm.suspendAfterMs); + this.#suspendId = setTimeout(this.#handleSuspend, tokens.suspendAfterMs); } #autoResume() { @@ -238,10 +241,7 @@ export class Earwurm extends EmittenCommon { if (this._state === 'suspended' || this._state === 'interrupted') { this.#context.resume().catch((error) => { - this.emit('error', [ - Earwurm.errorMessage.resume, - getErrorMessage(error), - ]); + this.emit('error', [tokens.error.resume, getErrorMessage(error)]); }); } @@ -265,9 +265,7 @@ export class Earwurm extends EmittenCommon { this.#library = library; this._keys = newKeys; - if (!identicalKeys) { - this.emit('library', newKeys, oldKeys); - } + if (!identicalKeys) this.emit('library', newKeys, oldKeys); } #setState(value: ManagerState) { diff --git a/src/Sound.ts b/src/Sound.ts index 39823e6..670db02 100644 --- a/src/Sound.ts +++ b/src/Sound.ts @@ -1,7 +1,7 @@ import {EmittenCommon} from 'emitten'; import {linearRamp} from './helpers'; -import {clamp, msToSec, progressPercentage} from './utilities'; +import {clamp, progressPercentage} from './utilities'; import {tokens} from './tokens'; import type { SoundId, @@ -12,14 +12,14 @@ import type { } from './types'; export class Sound extends EmittenCommon { - private _volume = 1; + private _vol = 1; private _mute = false; + private _trans = false; private _speed = 1; private _state: SoundState = 'created'; readonly #source: AudioBufferSourceNode; readonly #gainNode: GainNode; - readonly #fadeSec: number = 0; readonly #progress = { elapsed: 0, remaining: 0, @@ -41,15 +41,15 @@ export class Sound extends EmittenCommon { ) { super(); - this._volume = config?.volume ?? this._volume; - this.#fadeSec = config?.fadeMs ? msToSec(config.fadeMs) : this.#fadeSec; + this._vol = config?.volume ?? this._vol; + this._trans = Boolean(config?.transitions); this.#gainNode = this.context.createGain(); this.#source = this.context.createBufferSource(); this.#source.buffer = buffer; this.#source.connect(this.#gainNode).connect(this.destination); - this.#gainNode.gain.setValueAtTime(this._volume, this.context.currentTime); + this.#gainNode.gain.setValueAtTime(this._vol, this.context.currentTime); this.#progress.remaining = this.#source.buffer.duration; // The `ended` event is fired either when the sound has played its full duration, @@ -61,19 +61,30 @@ export class Sound extends EmittenCommon { return this.activeEvents.some((event) => event === 'progress'); } + private get transDuration() { + return this._trans ? tokens.transitionSec : 0; + } + + get transitions() { + return this._trans; + } + + set transitions(value: boolean) { + this._trans = value; + } + get volume() { - return this._volume; + return this._vol; } set volume(value: number) { - const oldVolume = this._volume; + const oldVolume = this._vol; const newVolume = clamp(0, value, 1); - this._volume = newVolume; + if (oldVolume === newVolume) return; - if (oldVolume !== newVolume) { - this.emit('volume', newVolume); - } + this._vol = newVolume; + this.emit('volume', newVolume); if (this._mute) return; @@ -81,7 +92,7 @@ export class Sound extends EmittenCommon { linearRamp( this.#gainNode.gain, {from: oldVolume, to: newVolume}, - {from: currentTime, to: currentTime + this.#fadeSec}, + {from: currentTime, to: currentTime + this.transDuration}, ); } @@ -90,20 +101,19 @@ export class Sound extends EmittenCommon { } set mute(value: boolean) { - if (this._mute !== value) { - this.emit('mute', value); - } + if (this._mute === value) return; this._mute = value; + this.emit('mute', value); - const fromValue = value ? this._volume : 0; - const toValue = value ? 0 : this._volume; + const fromValue = value ? this._vol : 0; + const toValue = value ? 0 : this._vol; const {currentTime} = this.context; linearRamp( this.#gainNode.gain, {from: fromValue, to: toValue}, - {from: currentTime, to: currentTime + this.#fadeSec}, + {from: currentTime, to: currentTime + this.transDuration}, ); } @@ -139,7 +149,7 @@ export class Sound extends EmittenCommon { linearRamp( this.#source.playbackRate, {from: oldSpeed, to: newSpeed}, - {from: currentTime, to: currentTime + this.#fadeSec}, + {from: currentTime, to: currentTime + this.transDuration}, ); */ diff --git a/src/Stack.ts b/src/Stack.ts index 67e8f90..8d826c4 100644 --- a/src/Stack.ts +++ b/src/Stack.ts @@ -6,7 +6,7 @@ import { linearRamp, scratchBuffer, } from './helpers'; -import {arrayShallowEquals, clamp, msToSec, secToMs} from './utilities'; +import {arrayShallowEquals, clamp} from './utilities'; import {tokens} from './tokens'; import type { @@ -22,8 +22,6 @@ import type { import {Sound} from './Sound'; export class Stack extends EmittenCommon { - static readonly maxStackSize = tokens.maxStackSize; - static readonly #loadError = ( id: StackId, path: string, @@ -33,13 +31,13 @@ export class Stack extends EmittenCommon { message: [`Failed to load: ${path}`, getErrorMessage(error)], }); - private _volume = 1; + private _vol = 1; private _mute = false; + private _trans = false; private _keys: SoundId[] = []; private _state: StackState = 'idle'; readonly #gainNode: GainNode; - readonly #fadeSec: number = 0; readonly #request: StackConfig['request']; #totalSoundsCreated = 0; @@ -54,29 +52,44 @@ export class Stack extends EmittenCommon { ) { super(); - this._volume = config?.volume ?? this._volume; - this.#fadeSec = config?.fadeMs ? msToSec(config.fadeMs) : this.#fadeSec; + this._vol = config?.volume ?? this._vol; + this._trans = Boolean(config?.transitions); this.#request = config?.request ?? undefined; this.#gainNode = this.context.createGain(); this.#gainNode.connect(this.destination); - this.#gainNode.gain.setValueAtTime(this._volume, this.context.currentTime); + this.#gainNode.gain.setValueAtTime(this._vol, this.context.currentTime); + } + + private get transDuration() { + return this._trans ? tokens.transitionSec : 0; + } + + get transitions() { + return this._trans; + } + + set transitions(value: boolean) { + this._trans = value; + + this.#queue.forEach((sound) => { + sound.transitions = value; + }); } get volume() { - return this._volume; + return this._vol; } set volume(value: number) { - const oldVolume = this._volume; + const oldVolume = this._vol; const newVolume = clamp(0, value, 1); - this._volume = newVolume; + if (oldVolume === newVolume) return; - if (oldVolume !== newVolume) { - this.emit('volume', newVolume); - } + this._vol = newVolume; + this.emit('volume', newVolume); if (this._mute) return; @@ -84,7 +97,7 @@ export class Stack extends EmittenCommon { linearRamp( this.#gainNode.gain, {from: oldVolume, to: newVolume}, - {from: currentTime, to: currentTime + this.#fadeSec}, + {from: currentTime, to: currentTime + this.transDuration}, ); } @@ -93,20 +106,19 @@ export class Stack extends EmittenCommon { } set mute(value: boolean) { - if (this._mute !== value) { - this.emit('mute', value); - } + if (this._mute === value) return; this._mute = value; + this.emit('mute', value); - const fromValue = value ? this._volume : 0; - const toValue = value ? 0 : this._volume; + const fromValue = value ? this._vol : 0; + const toValue = value ? 0 : this._vol; const {currentTime} = this.context; linearRamp( this.#gainNode.gain, {from: fromValue, to: toValue}, - {from: currentTime, to: currentTime + this.#fadeSec}, + {from: currentTime, to: currentTime + this.transDuration}, ); } @@ -131,25 +143,18 @@ export class Stack extends EmittenCommon { } pause() { - this.#queue.forEach((sound) => { - sound.pause(); - }); - + this.#queue.forEach((sound) => sound.pause()); return this; } stop() { - this.#queue.forEach((sound) => { - sound.stop(); - }); - + this.#queue.forEach((sound) => sound.stop()); return this; } teardown() { this.stop(); this.empty(); - return this; } @@ -185,19 +190,19 @@ export class Stack extends EmittenCommon { #create(id: SoundId, buffer: AudioBuffer) { const newSound = new Sound(id, buffer, this.context, this.#gainNode, { - fadeMs: secToMs(this.#fadeSec), + transitions: this._trans, }); newSound.on('state', this.#handleSoundState); newSound.once('ended', this.#handleSoundEnded); - // We do not filter out identical `id` values, + // TODO: We do not filter out identical `id` values, // so duplicate custom ids are possible... which means // identical ids could get wrongfully captured by any // `queue/key` filtering. const newQueue = [...this.#queue, newSound]; - const upperBound = newQueue.length - Stack.maxStackSize; + const upperBound = newQueue.length - tokens.maxStackSize; const outOfBounds = upperBound > 0 ? newQueue.slice(0, upperBound) : []; const outOfBoundsIds = outOfBounds.map(({id}) => id); @@ -219,9 +224,7 @@ export class Stack extends EmittenCommon { this.#queue = value; this._keys = newKeys; - if (!identicalKeys) { - this.emit('queue', newKeys, oldKeys); - } + if (!identicalKeys) this.emit('queue', newKeys, oldKeys); } #setState(value: StackState) { diff --git a/src/tests/Abstract.test.ts b/src/tests/Abstract.test.ts index 72328f8..c316017 100644 --- a/src/tests/Abstract.test.ts +++ b/src/tests/Abstract.test.ts @@ -1,7 +1,7 @@ import {afterEach, describe, it, expect, vi} from 'vitest'; -import {msToSec} from '../utilities'; import {Sound} from '../Sound'; +import {tokens} from '../tokens'; import type {SoundConfig, SoundEventMap} from '../types'; // This test covers any shared implementation between @@ -40,29 +40,28 @@ describe('Abstract implementation', () => { it.todo('sets gain to 0 when muted'); it.todo('sets gain to volume when un-muted'); - it('fades to new value', async () => { + it('transitions to new value', async () => { const mockOptions: SoundConfig = { volume: 0.6, - fadeMs: 100, + transitions: true, }; - const soundWithFade = new Sound( - 'TestMuteFade', + const soundWithTrans = new Sound( + 'TestMuteTrans', defaultAudioBuffer, defaultContext, defaultAudioNode, {...mockOptions}, ); - const fadeSec = msToSec(mockOptions.fadeMs ?? 0); - const endTime = defaultContext.currentTime + fadeSec; + const endTime = defaultContext.currentTime + tokens.transitionSec; const spyGainRamp = vi.spyOn( AudioParam.prototype, 'linearRampToValueAtTime', ); - soundWithFade.mute = true; + soundWithTrans.mute = true; // TODO: Check that `gain.value` is `mockOptions.volume`. // Then, `advanceTimersToNextTimer()` and check that @@ -179,14 +178,14 @@ describe('Abstract implementation', () => { expect(spyGainRamp).not.toBeCalledTimes(2); }); - it('fades to new volume', async () => { + it('transitions to new volume', async () => { const mockOptions: SoundConfig = { volume: 0.6, - fadeMs: 100, + transitions: true, }; - const soundWithFade = new Sound( - 'TestVolumeFade', + const soundWithTrans = new Sound( + 'TestVolumeTrans', defaultAudioBuffer, defaultContext, defaultAudioNode, @@ -194,15 +193,14 @@ describe('Abstract implementation', () => { ); const newValue = 0.8; - const fadeSec = msToSec(mockOptions.fadeMs ?? 0); - const endTime = defaultContext.currentTime + fadeSec; + const endTime = defaultContext.currentTime + tokens.transitionSec; const spyGainRamp = vi.spyOn( AudioParam.prototype, 'linearRampToValueAtTime', ); - soundWithFade.volume = newValue; + soundWithTrans.volume = newValue; // TODO: Check that `gain.value` is `mockOptions.volume`. // Then, `advanceTimersToNextTimer()` and check that diff --git a/src/tests/Earwurm.test.ts b/src/tests/Earwurm.test.ts index 18619e9..2b0db9d 100644 --- a/src/tests/Earwurm.test.ts +++ b/src/tests/Earwurm.test.ts @@ -31,11 +31,8 @@ describe('Earwurm component', () => { it('is initialized with default values', async () => { expect(mockManager).toBeInstanceOf(Earwurm); - // Class static properties - expect(Earwurm).toHaveProperty('maxStackSize', tokens.maxStackSize); - expect(Earwurm).toHaveProperty('suspendAfterMs', tokens.suspendAfterMs); - // Instance properties + expect(mockManager).toHaveProperty('transitions', false); expect(mockManager).toHaveProperty('volume', 1); expect(mockManager).toHaveProperty('mute', false); expect(mockManager).toHaveProperty('unlocked', false); @@ -54,6 +51,35 @@ describe('Earwurm component', () => { // `unlocked` getter is covered in `unlock()` test. // describe('unlocked', () => {}); + describe('transitions', () => { + it('allows `set` and `get`', async () => { + expect(mockManager.transitions).toBe(false); + mockManager.transitions = true; + expect(mockManager.transitions).toBe(true); + }); + + it('updates equivalent prop on all contained Stacks', async () => { + const stackIds = mockEntries.map(({id}) => id); + mockManager.add(...mockEntries); + + stackIds.forEach((id) => { + expect(mockManager.get(id)?.transitions).toBe(false); + }); + + mockManager.transitions = true; + + stackIds.forEach((id) => { + expect(mockManager.get(id)?.transitions).toBe(true); + }); + + mockManager.transitions = false; + + stackIds.forEach((id) => { + expect(mockManager.get(id)?.transitions).toBe(false); + }); + }); + }); + describe('keys', () => { it('contains ids of each active Stack', async () => { expect(mockManager.keys).toHaveLength(0); @@ -293,10 +319,10 @@ describe('Earwurm component', () => { ); }); - // TODO: Figure out how best to read `fadeMs` and `request` from Stack. - it.skip('passes `fadeMs` and `request` to Stack', async () => { + // TODO: Figure out how best to read `transitions` and `request` from Stack. + it.skip('passes `transitions` and `request` to Stack', async () => { const mockConfig: ManagerConfig = { - fadeMs: 100, + transitions: true, request: { integrity: 'foo', method: 'bar', @@ -528,13 +554,7 @@ describe('Earwurm component', () => { }); expect(() => mockManager.teardown()).toThrowError(mockErrorMessage); - - /* - expect(spyError).toBeCalledWith([ - Earwurm.errorMessage.close, - mockErrorMessage, - ]); - */ + // expect(spyError).toBeCalledWith([tokens.error.close, mockErrorMessage]); }); it('removes any event listeners', async () => { diff --git a/src/tests/Sound.test.ts b/src/tests/Sound.test.ts index 76c8537..b3d0dd8 100644 --- a/src/tests/Sound.test.ts +++ b/src/tests/Sound.test.ts @@ -27,6 +27,7 @@ describe('Sound component', () => { it('is initialized with default values', async () => { expect(testSound).toBeInstanceOf(Sound); + expect(testSound).toHaveProperty('transitions', false); expect(testSound).toHaveProperty('volume', 1); expect(testSound).toHaveProperty('mute', false); expect(testSound).toHaveProperty('speed', 1); @@ -48,6 +49,21 @@ describe('Sound component', () => { // `volume` accessor is covered in `Abstract.test.ts`. // describe('volume', () => {}); + describe('transitions', () => { + it('allows `set` and `get`', async () => { + const testSound = new Sound( + 'TestTransitions', + defaultAudioBuffer, + defaultContext, + defaultAudioNode, + ); + + expect(testSound.transitions).toBe(false); + testSound.transitions = true; + expect(testSound.transitions).toBe(true); + }); + }); + describe('speed', () => { const testSound = new Sound( 'TestSpeed', diff --git a/src/tests/Stack.test.ts b/src/tests/Stack.test.ts index 5309601..5b7103d 100644 --- a/src/tests/Stack.test.ts +++ b/src/tests/Stack.test.ts @@ -30,10 +30,8 @@ describe('Stack component', () => { it('is initialized with default values', async () => { expect(mockStack).toBeInstanceOf(Stack); - // Class static properties - expect(Stack).toHaveProperty('maxStackSize', tokens.maxStackSize); - // Instance properties + expect(mockStack).toHaveProperty('transitions', false); expect(mockStack).toHaveProperty('volume', 1); expect(mockStack).toHaveProperty('mute', false); expect(mockStack).toHaveProperty('keys', []); @@ -48,6 +46,38 @@ describe('Stack component', () => { // `volume` accessor is covered in `Abstract.test.ts`. // describe('volume', () => {}); + describe('transitions', () => { + it('allows `set` and `get`', async () => { + expect(mockStack.transitions).toBe(false); + mockStack.transitions = true; + expect(mockStack.transitions).toBe(true); + }); + + it('updates equivalent prop on all contained Sounds', async () => { + const soundIds = ['One', 'Two', 'Three']; + + for (const id of soundIds) { + await mockStack.prepare(id); + } + + soundIds.forEach((id) => { + expect(mockStack.get(id)?.transitions).toBe(false); + }); + + mockStack.transitions = true; + + soundIds.forEach((id) => { + expect(mockStack.get(id)?.transitions).toBe(true); + }); + + mockStack.transitions = false; + + soundIds.forEach((id) => { + expect(mockStack.get(id)?.transitions).toBe(false); + }); + }); + }); + describe('keys', () => { it('contains ids of each unexpired Sound', async () => { const mockDurationHalf = Math.floor(mockData.playDurationMs / 2); @@ -414,14 +444,13 @@ describe('Stack component', () => { describe('#create()', () => { const mockStackId = 'TestCreate'; - const mockFadeMs = 8; const mockConstructorArgs: StackConstructor = [ mockStackId, mockData.audio, defaultContext, defaultAudioNode, - {fadeMs: mockFadeMs}, + {transitions: true}, ]; it('constructs Sound', async () => { @@ -450,7 +479,7 @@ describe('Stack component', () => { // assert that it has been called with the expected parameters. // We still need to test for `options`. - // expect(sound).toHaveProperty('options.fadeMs', mockFadeMs); + // expect(sound).toHaveProperty('options.transitions', true); }); it('registers `state` multi-listener on Sound', async () => { @@ -474,12 +503,12 @@ describe('Stack component', () => { const spyEnded: SoundEventMap['ended'] = vi.fn((_ended) => {}); // Fill the `queue` up with the exact max number of Sounds. - const pendingSounds = arrayOfLength(Stack.maxStackSize).map( + const pendingSounds = arrayOfLength(tokens.maxStackSize).map( async (_index) => await testStack.prepare(), ); const sounds = await Promise.all(pendingSounds); - const additionalSoundsCount = Math.floor(Stack.maxStackSize / 2); + const additionalSoundsCount = Math.floor(tokens.maxStackSize / 2); sounds.forEach((sound) => { // We won't know what the exactly order of Sounds will be, @@ -509,7 +538,7 @@ describe('Stack component', () => { await Promise.all(additionalSounds); - expect(testStack.keys).toHaveLength(Stack.maxStackSize); + expect(testStack.keys).toHaveLength(tokens.maxStackSize); expect(spyEnded).toBeCalledTimes(additionalSoundsCount); }); }); diff --git a/src/tokens.ts b/src/tokens.ts index ab5c8ad..a5632ce 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -1,6 +1,15 @@ export const tokens = { - maxStackSize: 8, + // Global + error: { + close: 'Failed to close the Earwurm AudioContext.', + resume: 'Failed to resume the Earwurm AudioContext.', + }, + transitionSec: 0.2, + // Earwurm (manager) suspendAfterMs: 30000, + // Stack + maxStackSize: 8, + // Sound minStartTime: 0.0001, minSpeed: 0.25, maxSpeed: 4, diff --git a/src/types.ts b/src/types.ts index 4f13aef..afe31c7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,7 +28,7 @@ export type ManagerEventMap = { export interface ManagerConfig { volume?: number; - fadeMs?: number; + transitions?: boolean; request?: RequestInit; } @@ -59,7 +59,7 @@ export type StackEventMap = { export interface StackConfig { volume?: number; - fadeMs?: number; + transitions?: boolean; request?: RequestInit; } @@ -97,10 +97,9 @@ export type SoundEventMap = { mute: (muted: boolean) => void; speed: (rate: number) => void; progress: (event: SoundProgressEvent) => void; - // loop(ended: boolean): void; }; export interface SoundConfig { volume?: number; - fadeMs?: number; + transitions?: boolean; }