Skip to content

Instantly share code, notes, and snippets.

@thomastuts
Last active February 14, 2016 00:33
Show Gist options
  • Save thomastuts/582bb5106e1f0dcee04c to your computer and use it in GitHub Desktop.
Save thomastuts/582bb5106e1f0dcee04c to your computer and use it in GitHub Desktop.

I'm working on an application that plays audio clips. The audio clips that are currently playing are stored in the Redux store (you can play more than one clip at once). Everything is working just fine, but I'm wondering what the cleanest way to play/stop the audio would be. (See below JS files for the full source of my store and audio manager file)

Currently, I am triggering the play/stop actions from within the store itself, but this feels kind of wrong:

// store.js

// ...
case 'AUDIO_PLAY':
      const audioObject = createAudioObject({
        src: action.payload.url,
      });
      state.currentlyPlaying = state.currentlyPlaying.concat([audioObject]);
      playAudio(audioObject);
      return state;
case 'AUDIO_STOP':
      state.currentlyPlaying = state.currentlyPlaying.filter(s => {
        const isStoppedAudio = s._src !== action.payload;

        if (isStoppedAudio) {
          stopAudio(s);
        }

        return !isStoppedAudio;
      });
      return state;

Fixing the playing of audio outside of the store is pretty easy: listen to the store for any new changes, and play the sound if it is not already playing, like this:

// audio-manager.js

store.subscribe(() => {
  const { currentlyPlaying } = store.getState();
  
  for (const sound of currentlyPlaying) {
    if (!sound.playing()) {
      sound.play();
    }
  }
});

However, stopping the sound is proving to be a little trickier. Obviously, when I remove the sound from the currentlyPlaying array, the store listener in the audio manager won't know that it needs to stop that particular audio file, because at that point, it will already be gone. This is why I am stopping the audio in the store itself before I remove it from the array. This seems counter-intuitive though, since stores should only be concerned with storing data, and not doing anything with it.

The only way I can think of keeping the stop logic in the audio manager is to create separate actions (AUDIO_STOPPING and AUDIO_STOPPED), and store the sound that should be stopped in another part of the state that can still be accessed in the store. After stopping the sound, it would dispatch the AUDIO_STOPPED action that actually completely removes the audio from the state. Would this solution be more 'Redux-y' than using the store to trigger audio playing and stopping?

// audio-manager.js
import { Howl } from 'howler';
import store from '../store';
export function createAudioObject({ src, loop }) {
return new Howl({
src: [src],
loop,
onend: () => store.dispatch({ type: 'AUDIO_STOP', payload: src }),
});
}
export function stopAudio(audio) {
audio.stop();
}
export function playAudio(audio) {
audio.play();
}
// store.js
import { createStore } from 'redux';
import { MANIFEST } from './data/audio';
import { createAudioObject, playAudio, stopAudio } from './audio/audio-manager';
const defaultState = {
manifest: MANIFEST,
currentlyPlaying: [],
};
function audio(state = defaultState, action) {
switch (action.type) {
case 'AUDIO_PLAY':
const audioObject = createAudioObject({
src: action.payload.url,
});
state.currentlyPlaying = state.currentlyPlaying.concat([audioObject]);
playAudio(audioObject);
return state;
case 'AUDIO_STOP':
state.currentlyPlaying = state.currentlyPlaying.filter(s => {
const isStoppedAudio = s._src !== action.payload;
if (isStoppedAudio) {
stopAudio(s);
}
return !isStoppedAudio;
});
return state;
default:
return state;
}
}
export default createStore(audio);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment