Skip to content

Commit abb73e6

Browse files
committed
Make effects modular
1 parent 1be824e commit abb73e6

8 files changed

Lines changed: 951 additions & 251 deletions

File tree

cypress.config.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
const { defineConfig } = require('cypress')
1+
const { defineConfig } = require('cypress');
22

33
module.exports = defineConfig({
4-
projectId: 'b9jn8v',
5-
e2e: {
6-
// We've imported your old cypress plugins here.
7-
// You may want to clean this up later by importing these.
8-
setupNodeEvents(on, config) {
9-
return require('./cypress/plugins/index.js')(on, config)
10-
},
11-
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
12-
},
13-
})
4+
projectId: 'b9jn8v',
5+
e2e: {
6+
// We've imported your old cypress plugins here.
7+
// You may want to clean this up later by importing these.
8+
setupNodeEvents (on, config) {
9+
return require('./cypress/plugins/index.js')(on, config);
10+
},
11+
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
12+
},
13+
});

cypress/e2e/actions/pause.spec.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ context ('Pause', function () {
3636
cy.get ('text-box').contains ('One');
3737

3838
cy.proceed ();
39+
cy.wait(100); // Add a small delay to ensure async operations complete
3940
cy.wrap (this.monogatari).invoke ('state', 'music').should ('deep.equal', [{ statement: 'play music theme', paused: true }]);
4041
cy.wrap (this.monogatari).invoke ('history', 'music').should ('deep.equal', ['play music theme']);
4142
cy.wrap (this.monogatari).invoke ('mediaPlayers', 'music').should ('have.length', 1);
@@ -89,6 +90,7 @@ context ('Pause', function () {
8990
cy.get ('text-box').contains ('One');
9091

9192
cy.proceed ();
93+
cy.wait(100); // Add a small delay to ensure async operations complete
9294
cy.wrap (this.monogatari).invoke ('state', 'music').should ('deep.equal', [{ statement: 'play music theme loop', paused: true }, { statement: 'play music subspace loop', paused: true }]);
9395
cy.wrap (this.monogatari).invoke ('history', 'music').should ('deep.equal', ['play music theme loop', 'play music subspace loop']);
9496
cy.wrap (this.monogatari).invoke ('mediaPlayers', 'music').should ('have.length', 2);
@@ -143,6 +145,7 @@ context ('Pause', function () {
143145
cy.get ('text-box').contains ('One');
144146

145147
cy.proceed ();
148+
cy.wait(100); // Add a small delay to ensure async operations complete
146149
cy.wrap (this.monogatari).invoke ('state', 'music').should ('deep.equal', [{ statement: 'play music theme', paused: true }]);
147150
cy.wrap (this.monogatari).invoke ('history', 'music').should ('deep.equal', ['play music theme']);
148151
cy.wrap (this.monogatari).invoke ('mediaPlayers', 'music').should ('have.length', 1);

cypress/plugins/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@
1717
*/
1818
// eslint-disable-next-line no-unused-vars
1919
module.exports = (on, config) => {
20-
// `on` is used to hook into various events Cypress emits
21-
// `config` is the resolved Cypress config
22-
}
20+
// `on` is used to hook into various events Cypress emits
21+
// `config` is the resolved Cypress config
22+
};

src/actions/Pause.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ export class Pause extends Action {
4949
state[this.type] = prev.map((item) => {
5050
if (typeof item.statement === 'string') {
5151
const [play, type, media] = item.statement.split(' ');
52-
if (media === this.media) {
52+
// If this.media is undefined, pause all items
53+
// If this.media is defined, only pause matching items
54+
if (this.media === undefined || media === this.media) {
5355
return { ...item, paused: true };
5456
}
5557
}
@@ -94,7 +96,9 @@ export class Pause extends Action {
9496
if (typeof item.statement === 'string') {
9597
const [play, type, media] = item.statement.split (' ');
9698

97-
if (media === this.media) {
99+
// If this.media is undefined, unpause all items
100+
// If this.media is defined, only unpause matching items
101+
if (this.media === undefined || media === this.media) {
98102
return { ...item, paused: false };
99103
}
100104
}

src/actions/Play.js

Lines changed: 57 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ export class Play extends Action {
3434
this.engine.audioContext = new (window.AudioContext || window.webkitAudioContext)();
3535
}
3636

37+
// Initialize AudioPlayer worklets for the audio context
38+
AudioPlayer.initialize(this.engine.audioContext).catch(error => {
39+
console.warn('Failed to initialize AudioPlayer worklets:', error);
40+
});
41+
3742
this.engine.history ('music');
3843
this.engine.history ('sound');
3944
this.engine.history ('voice');
@@ -171,81 +176,6 @@ export class Play extends Action {
171176
this.engine.state ({ voice : [] });
172177
}
173178

174-
/**
175-
* Prepare the needed values to run the fade function on the given player
176-
*
177-
* @param {string} fadeTime - The time it will take the audio to reach it's maximum audio
178-
* @param {AudioPlayer} player - The AudioPlayer object to modify
179-
*
180-
* @return {Promise} - This promise will resolve once the fadeIn has ended
181-
*/
182-
static fadeIn (fadeTime, player) {
183-
const time = parseFloat (fadeTime.match (/\d*(\.\d*)?/));
184-
const increments = time / 0.1;
185-
186-
let targetVolume = player.volume;
187-
188-
if (player.dataset.volumePercentage) {
189-
const percentage = parseInt(player.dataset.volumePercentage);
190-
targetVolume = (percentage / 100) * player.volume;
191-
}
192-
193-
const volume = targetVolume / increments;
194-
const interval = (1000 * time) / increments;
195-
const expected = Date.now () + interval;
196-
197-
player.volume = 0;
198-
199-
player.dataset.fade = 'in';
200-
player.dataset.maxVolume = targetVolume;
201-
202-
if (Math.sign (volume) === 1) {
203-
return new Promise ((resolve, reject) => {
204-
setTimeout (() => {
205-
Play.fade (player, volume, targetVolume, interval, expected, resolve);
206-
}, interval);
207-
});
208-
} else {
209-
// If the volume is set to zero or not valid, the fade effect is disabled
210-
// to prevent errors
211-
return Promise.resolve ();
212-
}
213-
}
214-
215-
/**
216-
* Fade the player's audio on small iterations until it reaches the maximum value for it
217-
*
218-
* @param {AudioPlayer} player The AudioPlayer to which the audio will fadeIn
219-
* @param {number} volume The amount to increase the volume on each iteration
220-
* @param {number} interval The time in milliseconds between each iteration
221-
* @param {Date} expected The expected time the next iteration will happen
222-
* @param {function} resolve The resolve function of the promise returned by the fadeIn function
223-
*
224-
* @return {void}
225-
*/
226-
static fade (player, volume, targetVolume, interval, expected, resolve) {
227-
const now = Date.now () - expected; // the drift (positive for overshooting)
228-
229-
if (now > interval) {
230-
// something really bad happened. Maybe the browser (tab) was inactive?
231-
// possibly special handling to avoid futile "catch up" run
232-
}
233-
234-
if (player.volume < targetVolume && player.dataset.fade === 'in') {
235-
if (player.volume + volume > targetVolume) {
236-
player.volume = targetVolume;
237-
delete player.dataset.fade;
238-
resolve ();
239-
} else {
240-
player.volume += volume;
241-
expected += interval;
242-
setTimeout (() => {
243-
Play.fade (player, volume, targetVolume, interval, expected, resolve);
244-
}, Math.max (0, interval - now)); // take into account drift
245-
}
246-
}
247-
}
248-
249179
constructor ([ action, type, media, ...props ]) {
250180
super ();
251181
this.type = type;
@@ -279,7 +209,7 @@ export class Play extends Action {
279209
}
280210
}
281211

282-
async createAudioPlayer () {
212+
async createAudioPlayer (paused = false) {
283213
const audioContext = this.engine.audioContext;
284214
const gainNode = audioContext.createGain ();
285215
gainNode.connect (audioContext.destination);
@@ -290,7 +220,46 @@ export class Play extends Action {
290220
const arrayBuffer = await response.arrayBuffer ();
291221
const audioBuffer = await audioContext.decodeAudioData (arrayBuffer);
292222

293-
return new AudioPlayer(audioContext, audioBuffer, gainNode);
223+
// Parse effects from props
224+
const effects = this.parseEffects();
225+
226+
return new AudioPlayer(audioContext, audioBuffer, {
227+
outputNode: gainNode,
228+
effects: effects,
229+
paused: paused,
230+
});
231+
}
232+
233+
parseEffects () {
234+
const availableEffects = AudioPlayer.effects ();
235+
236+
const effects = {};
237+
238+
for (const [id, config] of Object.entries (availableEffects)) {
239+
const index = this.props.indexOf (id);
240+
241+
if (index === -1) {
242+
continue;
243+
}
244+
245+
const params = {};
246+
247+
// Parse parameters based on the effect's parameter list
248+
for (let i = 0; i < config.params.length; i++) {
249+
const paramName = config.params[i];
250+
const paramValue = this.props[index + 1 + i];
251+
252+
if (paramValue !== undefined) {
253+
// Try to parse as number first, fallback to string
254+
const numValue = parseFloat (paramValue);
255+
params[paramName] = isNaN (numValue) ? paramValue : numValue;
256+
}
257+
}
258+
259+
effects[id] = params;
260+
}
261+
262+
return effects;
294263
}
295264

296265
willApply () {
@@ -310,7 +279,7 @@ export class Play extends Action {
310279

311280
// Create player if it doesn't exist yet
312281
if (this.player === null) {
313-
this.player = await this.createAudioPlayer();
282+
this.player = await this.createAudioPlayer(paused);
314283
this.engine.mediaPlayer (this.type, this.mediaKey, this.player);
315284
}
316285

@@ -334,25 +303,26 @@ export class Play extends Action {
334303
};
335304

336305
if (fadePosition > -1) {
337-
Play.fadeIn (this.props[fadePosition + 1], this.player);
306+
const fadeTime = this.props[fadePosition + 1];
307+
const duration = parseFloat(fadeTime.match(/\d*(\.\d*)?/)[0]);
308+
this.player.fadeIn(duration);
338309
}
339310

340-
if (paused === true) {
341-
// Do not start playback, but set the player as paused
342-
this.player.isPaused = true;
343-
this.player.isPlaying = false;
344-
this.player.hasEnded = false;
311+
if (paused === true) {
345312
return Promise.resolve();
346-
} else {
347-
return this.player.play ();
348313
}
314+
315+
return this.player.play ();
316+
349317
}
350318
else if (this.player instanceof Array) {
351319
const promises = [];
352320
for (const player of this.player) {
353321
if (player.paused && !player.ended) {
354322
if (fadePosition > -1) {
355-
Play.fadeIn (this.props[fadePosition + 1], player);
323+
const fadeTime = this.props[fadePosition + 1];
324+
const duration = parseFloat(fadeTime.match(/\d*(\.\d*)?/)[0]);
325+
player.fadeIn(duration);
356326
}
357327
promises.push (player.play ());
358328
}

src/actions/Stop.js

Lines changed: 10 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -7,77 +7,6 @@ export class Stop extends Action {
77
return action === 'stop';
88
}
99

10-
/**
11-
* Prepare the needed values to run the fade function on the given player
12-
*
13-
* @param {string} fadeTime - The time it will take the audio to reach 0
14-
* @param {AudioPlayer} player - The AudioPlayer object to modify
15-
*
16-
* @return {Promise} - This promise will resolve once the fadeOut has ended
17-
*/
18-
static fadeOut (fadeTime, player) {
19-
const time = parseFloat (fadeTime.match (/\d*(\.\d*)?/));
20-
const increments = time / 0.1;
21-
22-
// Get the current volume (considering volume percentage if set)
23-
let currentVolume = player.volume;
24-
if (player.dataset.volumePercentage) {
25-
const percentage = parseInt(player.dataset.volumePercentage);
26-
currentVolume = (percentage / 100) * player.volume;
27-
}
28-
29-
const volume = currentVolume / increments;
30-
const interval = (1000 * time) / increments;
31-
const expected = Date.now () + interval;
32-
33-
player.dataset.fade = 'out';
34-
35-
if (Math.sign (volume) === 1) {
36-
return new Promise ((resolve, reject) => {
37-
setTimeout (() => {
38-
Stop.fade (player, volume, interval, expected, resolve);
39-
}, interval);
40-
});
41-
} else {
42-
// If the volume is set to zero or not valid, the fade effect is disabled
43-
// to prevent errors
44-
return Promise.resolve ();
45-
}
46-
}
47-
48-
/**
49-
* Fade the player's audio on small iterations until it reaches 0
50-
*
51-
* @param {AudioPlayer} player The AudioPlayer to which the audio will fadeOut
52-
* @param {number} volume The amount to decrease the volume on each iteration
53-
* @param {number} interval The time in milliseconds between each iteration
54-
* @param {Date} expected The expected time the next iteration will happen
55-
* @param {function} resolve The resolve function of the promise returned by the fadeOut function
56-
*
57-
* @return {void}
58-
*/
59-
static fade (player, volume, interval, expected, resolve) {
60-
const now = Date.now () - expected; // the drift (positive for overshooting)
61-
62-
if (now > interval) {
63-
// something really bad happened. Maybe the browser (tab) was inactive?
64-
// possibly special handling to avoid futile "catch up" run
65-
}
66-
67-
if (player.volume > 0 && player.dataset.fade === 'out') {
68-
if ((player.volume - volume) < 0) {
69-
player.volume = 0;
70-
resolve ();
71-
} else {
72-
player.volume -= volume;
73-
expected += interval;
74-
setTimeout (() => {
75-
Stop.fade (player, volume, interval, expected, resolve);
76-
}, Math.max (0, interval - now)); // take into account drift
77-
}
78-
}
79-
}
80-
8110
constructor ([ action, type, media, ...props ]) {
8211
super ();
8312

@@ -112,21 +41,24 @@ export class Stop extends Action {
11241
if (typeof this.player === 'object' && !(this.player instanceof AudioPlayer)) {
11342
if (fadePosition > -1) {
11443
for (const player of this.player) {
115-
Stop.fadeOut (this.props[fadePosition + 1], player).then (() => {
116-
this.engine.removeMediaPlayer (this.type, player.dataset.key);
44+
const fadeTime = this.props[fadePosition + 1];
45+
const duration = parseFloat(fadeTime.match(/\d*(\.\d*)?/)[0]);
46+
player.fadeOut(duration).then(() => {
47+
this.engine.removeMediaPlayer(this.type, player.dataset.key);
11748
});
11849
}
11950
} else {
120-
this.engine.removeMediaPlayer (this.type);
51+
this.engine.removeMediaPlayer(this.type);
12152
}
12253
} else {
123-
12454
if (fadePosition > -1) {
125-
Stop.fadeOut (this.props[fadePosition + 1], this.player).then (() => {
126-
this.engine.removeMediaPlayer (this.type, this.media);
55+
const fadeTime = this.props[fadePosition + 1];
56+
const duration = parseFloat(fadeTime.match(/\d*(\.\d*)?/)[0]);
57+
this.player.fadeOut(duration).then(() => {
58+
this.engine.removeMediaPlayer(this.type, this.media);
12759
});
12860
} else {
129-
this.engine.removeMediaPlayer (this.type, this.media);
61+
this.engine.removeMediaPlayer(this.type, this.media);
13062
}
13163
}
13264
return Promise.resolve ();

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,4 +317,7 @@ export * from './lib/ScreenComponent';
317317
export * from './lib/MenuComponent';
318318
export * from './lib/FancyError';
319319

320+
import AudioPlayer from './lib/AudioPlayer';
321+
export { AudioPlayer };
322+
320323
export default Monogatari;

0 commit comments

Comments
 (0)