Handle mono audio files

This commit is contained in:
Jordan Eldredge 2018-04-08 14:48:28 -07:00
parent 9f5e5aaa71
commit a1d73c2f34
5 changed files with 111 additions and 11 deletions

View file

@ -65,3 +65,4 @@ export const MEDIA_TAG_REQUEST_FAILED = "MEDIA_TAG_REQUEST_FAILED";
export const NETWORK_CONNECTED = "NETWORK_CONNECTED";
export const NETWORK_DISCONNECTED = "NETWORK_DISCONNECTED";
export const UPDATE_WINDOW_POSITIONS = "UPDATE_WINDOW_POSITIONS";
export const CHANNEL_COUNT_CHANGED = "CHANNEL_COUNT_CHANGED";

View file

@ -0,0 +1,53 @@
// Get the max value from an array-like
const max = arr => arr.reduce((acc, val) => Math.max(acc, val));
function createAnalysers(source) {
const { context } = source;
const splitter = context.createChannelSplitter();
const leftGain = context.createGain();
const rightGain = context.createGain();
const leftAnalyser = context.createAnalyser();
const rightAnalyser = context.createAnalyser();
source.connect(splitter);
splitter.connect(leftGain, 0);
splitter.connect(rightGain, 1);
leftGain.connect(leftAnalyser);
rightGain.connect(rightAnalyser);
return { left: leftAnalyser, right: rightAnalyser };
}
const MAX_CALLS = 1000;
export default async function detectChannels(source) {
return new Promise((resolve, reject) => {
const { left, right } = createAnalysers(source);
const dataArray = new Uint8Array(left.frequencyBinCount);
let maxLeft = 0;
let maxRight = 0;
let calls = 0;
const intervalHandle = setInterval(() => {
left.getByteFrequencyData(dataArray);
maxLeft = Math.max(maxLeft, max(dataArray));
right.getByteFrequencyData(dataArray);
maxRight = Math.max(maxRight, max(dataArray));
if (maxLeft > 0) {
if (maxRight > 0) {
resolve(2);
} else {
resolve(1);
}
clearInterval(intervalHandle);
}
if (calls >= MAX_CALLS) {
reject();
}
calls++;
}, 20);
});
}

View file

@ -1,6 +1,7 @@
/* Emulate the native <audio> element with Web Audio API */
import { BANDS } from "../constants";
import ElementSource from "./elementSource";
import detectChannels from "./detectChannels";
export default class Media {
constructor() {
@ -32,10 +33,12 @@ export default class Media {
timeupdate: function() {},
visualizerupdate: function() {},
ended: function() {},
fileLoaded: function() {}
fileLoaded: function() {},
channelupdate: function() {}
};
// We don't currently know how many channels
this._channels = null;
this._balance = 0;
this.name = null;
// The _source node has to be recreated each time it's stopped or
// paused, so we don't create it here. Instead we create this dummy
@ -60,6 +63,10 @@ export default class Media {
// Create the analyser node for the visualizer
this._analyser = this._context.createAnalyser();
this._analyser.fftSize = 2048;
// TODO: Tune these to something that looks like Winamp
this._analyser.minDecibels = -90;
this._analyser.maxDecibels = -10;
this._analyser.smoothingTimeConstant = 0.8;
// Create the gain node for the volume control
this._gainNode = this._context.createGain();
@ -154,8 +161,29 @@ export default class Media {
this._chanMerge.connect(this._analyser);
this._gainNode.connect(this._context.destination);
window.media = this;
}
_setChannels(num) {
const assumedChannels = num == null ? 2 : num;
this._chanSplit.disconnect();
this._chanSplit.connect(this._leftGain, 0);
// If we only have one channel, use it for both left and right.
this._chanSplit.connect(this._rightGain, assumedChannels === 1 ? 0 : 1);
this._channels = num;
this._callbacks.channelupdate();
}
_makeMono() {
this._setChannels(1);
}
_makeStereo() {
this._setChannels(2);
}
_resetChannels() {
this._setChannels(null);
}
/* Properties */
duration() {
return this._source.getDuration();
@ -174,7 +202,7 @@ export default class Media {
}
channels() {
return this._source.getNumberOfChannels();
return this._channels == null ? 2 : this._channels;
}
sampleRate() {
@ -182,8 +210,17 @@ export default class Media {
}
/* Actions */
play() {
this._source.play();
async play() {
await this._source.play();
if (this._channels == null) {
detectChannels(this._staticSource)
.then(channels => {
this._setChannels(channels);
})
.catch(() => {
this._setChannels(null);
});
}
}
pause() {
@ -259,10 +296,10 @@ export default class Media {
}
// Used only for the initial load, since it must have a CORS header
async loadFromUrl(url, fileName, autoPlay) {
this.name = fileName;
async loadFromUrl(url, autoPlay) {
this._callbacks.waiting();
await this._source.loadUrl(url);
this._resetChannels();
this._callbacks.stopWaiting();
if (autoPlay) {
this.play();

View file

@ -15,7 +15,8 @@ import {
SET_EQ_OFF,
SET_EQ_ON,
PLAY_TRACK,
BUFFER_TRACK
BUFFER_TRACK,
CHANNEL_COUNT_CHANGED
} from "./actionTypes";
import { next as nextTrack } from "./actionCreators";
import { getCurrentTrackId } from "./selectors";
@ -63,6 +64,13 @@ export default media => store => {
});
});
media.addEventListener("channelupdate", () => {
store.dispatch({
type: CHANNEL_COUNT_CHANGED,
channels: media.channels()
});
});
return next => action => {
// TODO: Consider doing this after the action, and using the state as the source of truth.
switch (action.type) {
@ -87,14 +95,12 @@ export default media => store => {
case PLAY_TRACK:
media.loadFromUrl(
store.getState().playlist.tracks[action.id].url,
action.name,
true
);
break;
case BUFFER_TRACK:
media.loadFromUrl(
store.getState().playlist.tracks[action.id].url,
action.name,
false
);
break;

View file

@ -11,7 +11,8 @@ import {
TOGGLE_SHUFFLE,
TOGGLE_TIME_MODE,
UPDATE_TIME_ELAPSED,
ADD_TRACK_FROM_URL
ADD_TRACK_FROM_URL,
CHANNEL_COUNT_CHANGED
} from "../actionTypes";
const media = (state, action) => {
@ -44,6 +45,8 @@ const media = (state, action) => {
case STOP:
case IS_STOPPED:
return { ...state, status: "STOPPED" };
case CHANNEL_COUNT_CHANGED:
return { ...state, channels: action.channels };
case TOGGLE_TIME_MODE:
const newMode = state.timeMode === "REMAINING" ? "ELAPSED" : "REMAINING";
return { ...state, timeMode: newMode };