mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-28 12:36:35 +00:00
Handle mono audio files
This commit is contained in:
parent
9f5e5aaa71
commit
a1d73c2f34
5 changed files with 111 additions and 11 deletions
|
|
@ -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";
|
||||
|
|
|
|||
53
js/media/detectChannels.js
Normal file
53
js/media/detectChannels.js
Normal 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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue