npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@mediamonks/channels

v1.1.0

Published

Channel-based sound player

Downloads

859

Readme

Channels

Channels is a channel based sound player for the web.

Installation

npm install @mediamonks/channels

Quick start

import { Channels } from '@mediamonks/channels';

//  create {name: string} list of filenames without extension 
const soundFiles = ['sound1', 'sound2'].map(name => ({
    name,
}));

// create a Channels instance, extension and path are required
const channelsInstance = new Channels({
    soundsExtension: 'mp3',
    soundsPath: 'static/audio/',
    sounds: soundFiles, // not required, can be set later
});

// load files
await channelsInstance.loadSounds();

// play a sound
channelsInstance.play('sound1');

// play it on a channel
channelsInstance.createChannel('background-music');
channelsInstance.play('sound2', {channel: 'background-music'});

// the exact same through the actual channel
const myChannel = channelsInstance.createChannel('ui-sounds');
myChannel.play('sound2');

// stop a sound
const sound1 = channelsInstance.play('sound1');
sound1.stop();

// stop all sounds
channelsInstance.stopAll();

// stop all sounds on a channel 
channelsInstance.stopAll({channel: 'background-music'});
myChannel.stopAll(); // or through the actual channel

// set main volume, or for a channel
channelsInstance.setVolume(0.5);
myChannel.setVolume(0.5);

Getting started

Before we can do anything, an instance of Channels has to be created. Two parameters are required: the location of the sound files, and the file extension to use:

new Channels({
    soundsPath: 'location/of/your/files',
    soundsExtension: 'mp3',
})

Don't create more than one Channels instance.

Optionally, an audioContext can be passed in the constructor options. If omitted, one will be created in the Channels constructor.

new Channels({
    soundsPath,
    soundsExtension,
    audioContext: myAudioContext,
})

React

For React projects, there is a use-channels hook to easily work with a Channels instance.

Suspended state

An AudioContext created without user interaction (for example a click) will be in the suspended state, in which no sound can be produced. This can happen for example if a Channels instance is created on page landing without supplying a (non-suspended) audioContext, since one will be created then automatically.

Creating a Channels instance this way is fine by itself, just make sure to resume the context once on user interaction before playing any sounds.

const onClick = async () => {
    await channelsInstance.resumeContext();
}

TLDR: The audioContext that is used must have been created or resumed on user interaction.

To check whether the context is suspended: channelsInstance.contextIsSuspended

Loading files

Channels uses the sample-manager for dealing with files, and creates an instance of it named sampleManager.

channelsInstance.sampleManager

The easiest way to load files is to supply a list of objects with a name property, matching the filenames without extension. The file extension has to be set when creating the Channels object (which allows for an easy switch to different filetypes on certain clients).

// - soundfiles/sound1.mp3
// - soundfiles/sound2.mp3
const soundFiles = [{name: 'sound1'}, {name: 'sound2'}];

// list can be used when instantiating Channels 
const channelsInstance = new Channels({
    soundsPath: 'soundfiles/',
    soundsExtension: 'mp3',
    sounds: soundFiles,
})

// or can be set at a later stage
channelsInstance.sampleManager.addSamples(soundFiles);

// either way, loading can be done like so:
await channelsInstance.loadSounds();

// optionally, keep track of progress
await channelsInstance.loadSounds((progress) => {
    // ...
});

The loadSounds method is an alias for sampleManager.loadAllSamples

For more info on how to define sound files, please refer to the sample-manager page.

Playing a sound

When a sound has been loaded, it can be played by referring to its unique name:

channelsInstance.play('sound');

A second argument can be passed with optional properties:

channelsInstance.play('sound', {
    volume: 0.5,
    channel: 'channel1',
    loop: true,
    fadeInTime: 2,
    pan: 1, // between -1 and 1
    startTimeOffset: 0, // in seconds
    effects: {
        preVolume: myEffectsChain,
    }
});

The play function returns a reference to the playing sound, containing various methods to interact with the sound.

const sound = channelsInstance.play('sound');
sound.setVolume(0.5);

Stopping a sound

Stopping a sound can be done by calling stop() on the playing sound reference.

const playingSound = channelsInstance.play('sound');
playingSound.stop();

Sounds can be faded out before stopping by providing a fadeOutTime

playingSound.stop({ fadeOutTime: 2 });

Channels

Channels are a way of grouping sounds that are played. They have their own volume and optional effects, and their output connects to the main output. They are completely optional and might not be needed at all, since sounds can also be played without a channel.

The reason to create a channel is to easily manage a group of sounds, for example to:

  • change their volume
  • apply effects
  • fade out
  • stop all of them

Creating a channel

The only thing needed to create a channel is a unique name:

channelsInstance.createChannel('my-channel');

Second parameter can be used for some optional properties.

channelsInstance.createChannel('my-channel',{
    type: 'monophonic',
    volume: 0.5,
    pan: 1,
    effects: {
        preVolume: myEffectsChain,
    }
});

Check the Audio Effects section for more information about the effects option.

A reference to a channel is returned when creating it, or can be retrieved afterwards.

const myChannel = channelsInstance.createChannel('my-channel');

const myChannel = channelsInstance.getChannel('my-channel');

Monophonic vs polyphonic

A Channel can be either polyphonic or monophonic, which defines how many sounds can be played simultaneously on a channel:

  • A monophonic channel can play one sound at a time. When playing a sound on such a channel, all other sounds on that channel will be stopped
  • A polyphonic channel has no restrictions

The term monophonic is used loosely. Since sounds can fade in and out, even on a monophonic channel multiple sounds may be audible at the same time.

This type can be set during creation. When no type is given, the default polyphonic is used.

channelsInstance.createChannel('monophonic-channel', {type: "monophonic"});
channelsInstance.createChannel('polyphonic-channel');

Using a monophonic channel can be very helpful when creating a background music layer where the music loop needs to be changed now and then.

Playing a sound on a channel

There are two ways to play a sound on a channel. First of all, directly on the channelsInstance:

channelsInstance.play('mysound', { channel: 'mychannel'});

Or, if you happen to have a reference to an actual channel:

myChannel.play('my-sound');

All options for the play() method can still be used, except for the (in this context useless) channel prop.

myChannel.play('sound', {
    volume: 0.5,
    loop: true,
    fadeInTime: 2,
    pan: 1,
    effects: {
        preVolume: myEffectsChain,
    }
});

Stopping all sounds on a channel

channelsInstance.stopAll({channel: 'channel-name'});
// or:
myChannel.stopAll();

// examples above will override any fade outs.
// if you do want those, set 'immediate' to false:
channelsInstance.stopAll({ channel: 'channel-name', immediate: false });
myChannel.stopAll({ immediate: false });

Default play/stop options

Channels can have default options to use when calling play() or stop() for sounds playing on that channel. This can be used for example to create a channel on which every sound automatically always loops when played.

These default options are the combination of the options for play() and stop(), without the channel.

// options for play
const sound = channelsInstance.play({
    loop: true,
    fadeInTime: 1,
    volume: 0.5,
    channel: 'my-channel',
    pan: -1,
    effects: {
        preVolume: myEffectsChain,
    }
});

// options for stop
sound.stop({
    fadeOutTime: 1,
})

// all above props combined (except channel) can be used
const defaultStartStopProps = {
    loop: true,
    fadeInTime: 1,
    volume: 0.5,
    fadeOutTime: 1,
    pan: -1,
    effects: {
        preVolume: myEffectsChain,
    }
};
// can be set on creation as part of channel options
channelsInstance.createChannel('my-channel', { defaultStartStopProps });

// or can be set directly on a channel
myChannel.defaultStartStopProps = defaultStartStopProps;

Passing props toplay() or stop() will override the defaultStartStopProps of a channel.

Default props (in combination with a monophonic channel) can be very helpful when creating a background music layer with music loops that need to change now and then:

const channel = channelsInstance.createChannel('background-music', {
    type: 'monophonic',
    defaultStartStopProps: { fadeInTime: 2, fadeOutTime: 2, loop: true },
});
// start a loop
channel.play('loop1');

// starting 2nd loop some time later, loop1 will fade out, loop2 will fade in 
channel.play('loop2');

Signal modifiers

A SignalModifier is something that allows the audio signal to be changed, for example to set the volume. These SignalModifiers exist three places:

  1. On a sound
  2. On a channel
  3. On the main output

Overall structure

Everything in Channels connects to the main output SignalModifier, which is the final step before going to the actual output. A channel has its own SignalModifer, which connects to the main SignalModifier. In the following image, the SignalModifiers are the blue blocks:

Sounds can be played either on a channel, or directly on the main output.

Sound structure

Sounds themselves also have a SignalModifier:

SignalModifier structure

A SignalModifier always contains the following nodes:

  • a GainNode for setting volume
  • a separate GainNode for applying fades
  • a StereoPannerNode to pan the sound left or right

Optionally, custom effects chains can be added before or after these nodes.

Modifying the signal

The three places that have a SignalModifier (sound, channel or main output) all have a set of methods implemented:

const myChannel = channelsInstance.getChannel('my-channel');

// on a channel
myChannel.setVolume(0.5); 
myChannel.getVolume();
myChannel.mute();
myChannel.unmute();
myChannel.fadeOut(1); // time in seconds
myChannel.fadeIn(1);
myChannel.setPan(1); // value between -1 and 1
myChannel.getPan();

// all these also exist on a playing sound
const playingSound = channelsInstance.play('my-sound');
playingSound.setVolume(0.5);

// and on the library instance (to target the main output)
channelsInstance.setVolume(0.5);

Volume values should be 0 or higher. Keep in mind that going beyond 1 might result in digital clipping.

When calling mute() the volume will be set to 0, with the additional effect that the previous volume value will be stored and used when calling unmute()

Pan values should be between -1 (left) and 1 (right).

Events

There are a few things in Channels that dispatch events. First of all: volume and pan changes on either a channel, a sound or the main instance.

// listening to main volume changes
channelsInstance.addEventListener('VOLUME_CHANGE', (event) => {
    console.log(event.data.volume);
})
// or a channel
channel.addEventListener('VOLUME_CHANGE', (event) => {
    console.log(event.data.volume);
})
// or a playing sound
playingSound.addEventListener('VOLUME_CHANGE', (event) => {
    console.log(event.data.volume);
})

// all these three also dispatch pan changes
channel.addEventListener('PAN_CHANGE', (event) => {
    console.log(event.data.pan);
})

When using React, there are some hooks available to work with volume or pan changes.

Apart from that, the main instance notifies when the list of channels or playing sounds updates:

channelsInstance.addEventListener('CHANNELS_CHANGE', () => {
    console.log(channelsInstance.getChannels());
})

channelsInstance.addEventListener('PLAYING_SOUNDS_CHANGE', () => {
    console.log(channelsInstance.getPlayingSounds());
})

Audio effects

Effects can be placed in the chain of a SignalModifier and are therefor available on the three places that have a SignalModifier:

  • on the main output
  • on a channel
  • on a playing sound

In all of these cases, effects can be inserted before and/or after the volume is applied.

Effects have to be defined as an EffectsChain, which is an object with an input and an output (both of type AudioNode). An effect can be a single node (with input and output pointing to the same AudioNode), or a long chain or multiple nodes.

Once an EffectsChain has been created, it can be used in the preVolume or postVolume prop of the effects prop.

const filter = audioContext.createBiquadFilter();
const myEffectsChain = {
    input: filter,
    output: filter,
}
// setting an effect on the main output, before the volume
const channelsInstance = new Channels({
    soundsExtension,
    soundsPath,
    effects: {
        preVolume: myEffectsChain
    },
});

// setting it on a channel, after the volume
channelsInstance.createChannel('effect-channel', {
    effects: {
        postVolume: myEffectsChain,
    } 
})

// on a sound, before and after the volume
channelsInstance.play('my-sound', {
    effects: {
        preVolume: myEffectsChain,
        postVolume: myOtherEffectsChain,
    }
})

Both input and output use the in/out with index 0 to connect, which will work for nearly all cases. If for some reason you need to use a different index, you can add a GainNode before/after your effects as a solution.

Use <audio> or <video> output

It is possible to route the audio from an <audio> or <video> element into Channels, for example to apply effects or to control their volume along with other sounds.

To do so, use the connectMediaElement() method on either a channel or the main instance:

// connect output to a channel
myChannel.connectMediaElement(myVideoElement);

// or to the main output
channelsInstance.connectMediaElement(myVideoElement);