Move file specific media info to the track

Fixes #712
This commit is contained in:
Jordan Eldredge 2018-12-29 12:29:10 -08:00
parent eb296fc182
commit b16fb3a2ae
18 changed files with 205 additions and 153 deletions

View file

@ -267,7 +267,19 @@ export function loadMediaFile(
if (metaData != null) {
const { artist, title, album } = metaData;
dispatch({ type: SET_MEDIA_TAGS, artist, title, album, id });
dispatch({
type: SET_MEDIA_TAGS,
artist,
title,
album,
// For now, we lie about these next three things.
// TODO: Ideally we would leave these as null and force a media data
// fetch when the user starts playing.
sampleRate: 44000,
bitrate: 192000,
numberOfChannels: 2,
id
});
} else if ("blob" in track) {
// Blobs can be loaded quickly
dispatch(fetchMediaTags(track.blob, id));

View file

@ -17,6 +17,7 @@ import {
import { MEDIA_STATUS } from "../constants";
import { openMediaFileDialog } from "./";
import { GetState, Dispatch, Dispatchable } from "../types";
import * as Selectors from "../selectors";
function playRandomTrack(): Dispatchable {
return (dispatch: Dispatch, getState: GetState) => {
@ -91,14 +92,16 @@ export function previous(): Dispatchable {
export function seekForward(seconds: number): Dispatchable {
return function(dispatch, getState) {
const { timeElapsed, length } = getState().media;
if (length == null) {
const state = getState();
const timeElapsed = Selectors.getTimeElapsed(state);
const duration = Selectors.getDuration(state);
if (duration == null) {
return;
}
const newTimeElapsed = timeElapsed + seconds;
dispatch({
type: SEEK_TO_PERCENT_COMPLETE,
percent: (newTimeElapsed / length) * 100
percent: (newTimeElapsed / duration) * 100
});
};
}

View file

@ -60,7 +60,6 @@ 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";
export const WINDOW_SIZE_CHANGED = "WINDOW_SIZE_CHANGED";
export const TOGGLE_WINDOW_SHADE_MODE = "TOGGLE_WINDOW_SHADE_MODE";
export const LOADED = "LOADED";

View file

@ -1,12 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import CharacterString from "../CharacterString";
const Kbps = props => (
<div id="kbps">
<CharacterString>{props.kbps}</CharacterString>
</div>
);
export default connect(state => ({ kbps: state.media.kbps }))(Kbps);

View file

@ -0,0 +1,22 @@
import React from "react";
import { connect } from "react-redux";
import CharacterString from "../CharacterString";
import { AppState } from "../../types";
import * as Selectors from "../../selectors";
interface StateProps {
kbps: string | null;
}
const Kbps = (props: StateProps) => (
<div id="kbps">
<CharacterString>{props.kbps || ""}</CharacterString>
</div>
);
function mapStateToProps(state: AppState): StateProps {
return { kbps: Selectors.getKbps(state) };
}
export default connect(mapStateToProps)(Kbps);

View file

@ -1,12 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import CharacterString from "../CharacterString";
const Khz = props => (
<div id="khz">
<CharacterString>{props.khz}</CharacterString>
</div>
);
export default connect(state => state.media)(Khz);

View file

@ -0,0 +1,22 @@
import React from "react";
import { connect } from "react-redux";
import CharacterString from "../CharacterString";
import { AppState } from "../../types";
import * as Selectors from "../../selectors";
interface StateProps {
khz: string | null;
}
const Khz = (props: StateProps) => (
<div id="khz">
<CharacterString>{props.khz || ""}</CharacterString>
</div>
);
function mapStateToProps(state: AppState): StateProps {
return { khz: Selectors.getKhz(state) };
}
export default connect(mapStateToProps)(Khz);

View file

@ -1,5 +1,6 @@
import React from "react";
import { connect } from "react-redux";
import { AppState, Dispatch } from "../../types";
import {
SEEK_TO_PERCENT_COMPLETE,
@ -7,13 +8,26 @@ import {
UNSET_FOCUS,
SET_SCRUB_POSITION
} from "../../actionTypes";
import * as Selectors from "../../selectors";
interface StateProps {
displayedPosition: number;
position: number;
}
interface DispatchProps {
seekToPercentComplete(e: React.MouseEvent<HTMLInputElement>): void;
setPosition(e: React.MouseEvent<HTMLInputElement>): void;
}
type Props = StateProps & DispatchProps;
const Position = ({
position,
seekToPercentComplete,
displayedPosition,
setPosition
}) => {
}: Props) => {
// In shade mode, the position slider shows up differently depending on if
// it's near the start, middle or end of its progress
let className = "";
@ -43,16 +57,15 @@ const Position = ({
);
};
const mapStateToProps = ({ media, userInput }) => {
let position;
if (media.length) {
position = (Math.floor(media.timeElapsed) / media.length) * 100;
} else {
position = 0;
}
const mapStateToProps = (state: AppState): StateProps => {
const duration = Selectors.getDuration(state);
const timeElapsed = Selectors.getTimeElapsed(state);
const userInputFocus = Selectors.getUserInputFocus(state);
const scrubPosition = Selectors.getUserInputScrubPosition(state);
const position = duration ? (Math.floor(timeElapsed) / duration) * 100 : 0;
const displayedPosition =
userInput.focus === "position" ? userInput.scrubPosition : position;
userInputFocus === "position" ? scrubPosition : position;
return {
displayedPosition,
@ -60,14 +73,20 @@ const mapStateToProps = ({ media, userInput }) => {
};
};
const mapDispatchToProps = dispatch => ({
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
seekToPercentComplete: e => {
dispatch({ type: SEEK_TO_PERCENT_COMPLETE, percent: e.target.value });
dispatch({
type: SEEK_TO_PERCENT_COMPLETE,
percent: Number((e.target as HTMLInputElement).value)
});
dispatch({ type: UNSET_FOCUS });
},
setPosition: e => {
dispatch({ type: SET_FOCUS, input: "position" });
dispatch({ type: SET_SCRUB_POSITION, position: e.target.value });
dispatch({
type: SET_SCRUB_POSITION,
position: Number((e.target as HTMLInputElement).value)
});
}
});

View file

@ -4,11 +4,12 @@ import { TimeMode, AppState, Dispatch } from "../../types";
import { getTimeObj } from "../../utils";
import * as Actions from "../../actionCreators";
import * as Selectors from "../../selectors";
import { TIME_MODE } from "../../constants";
interface StateProps {
timeElapsed: number;
length: number;
duration: number;
timeMode: TimeMode;
}
@ -18,12 +19,12 @@ interface DispatchProps {
const Time = ({
timeElapsed,
length,
duration,
timeMode,
toggleTimeMode
}: StateProps & DispatchProps) => {
const seconds =
timeMode === TIME_MODE.ELAPSED ? timeElapsed : length - timeElapsed;
timeMode === TIME_MODE.ELAPSED ? timeElapsed : duration - timeElapsed;
const timeObj = getTimeObj(seconds);
return (
@ -50,8 +51,10 @@ const Time = ({
};
const mapStateToProps = (state: AppState): StateProps => {
const { timeElapsed, length, timeMode } = state.media;
return { timeElapsed, length: length || 0, timeMode };
const timeElapsed = Selectors.getTimeElapsed(state);
const duration = Selectors.getDuration(state);
const { timeMode } = state.media;
return { timeElapsed, duration: duration || 0, timeMode };
};
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
toggleTimeMode: () => dispatch(Actions.toggleTimeMode())

View file

@ -1,4 +1,4 @@
import { AppState, Action } from "../types";
import { AppState, Action, Dispatchable, Dispatch } from "../types";
import React from "react";
import { connect } from "react-redux";
import classnames from "classnames";
@ -6,6 +6,7 @@ import { getTimeObj } from "../utils";
import { TOGGLE_TIME_MODE } from "../actionTypes";
import { TIME_MODE, MEDIA_STATUS } from "../constants";
import Character from "./Character";
import * as Selectors from "../selectors";
import "../../css/mini-time.css";
@ -26,15 +27,20 @@ const Background = () => (
</React.Fragment>
);
type StateProps = {
interface StateProps {
status: string | null;
timeMode: string;
timeElapsed: number;
length: number | null;
toggle: () => void;
};
}
const MiniTime = (props: StateProps) => {
interface DispatchProps {
toggle: () => void;
}
type Props = StateProps & DispatchProps;
const MiniTime = (props: Props) => {
let seconds = null;
// TODO: Clean this up: If stopped, just render the background, rather than
// rendering spaces twice.
@ -66,14 +72,14 @@ const MiniTime = (props: StateProps) => {
);
};
const mapStateToProps = (state: AppState) => ({
const mapStateToProps = (state: AppState): StateProps => ({
status: state.media.status,
timeMode: state.media.timeMode,
timeElapsed: state.media.timeElapsed,
length: state.media.length
timeElapsed: Selectors.getTimeElapsed(state),
length: Selectors.getDuration(state)
});
const mapDispatchToProps = (dispatch: (action: Action) => void) => ({
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
// TODO: move to actionCreators
toggle: () => {
dispatch({ type: TOGGLE_TIME_MODE });

View file

@ -147,16 +147,6 @@ export default class ElementSource {
return this._audio.currentTime;
}
getNumberOfChannels() {
return this._source.channelCount;
}
getSampleRate() {
// This is a lie. This is the sample rate of the context, not the
// underlying source media.
return this._context.sampleRate;
}
_setStatus(status: MediaStatus) {
this._status = status;
this._emitter.trigger("statusChange");

View file

@ -17,7 +17,6 @@ function createStereoPanner(context: AudioContext): StereoPannerNode {
export default class Media {
_emitter: Emitter;
_context: AudioContext;
_channels: number | null;
_balance: number;
_staticSource: AnalyserNode;
_preamp: GainNode;
@ -52,8 +51,6 @@ export default class Media {
document.body.addEventListener("click", resume, false);
document.body.addEventListener("keydown", resume, false);
}
// We don't currently know how many channels
this._channels = null;
this._balance = 0;
// The _source node has to be recreated each time it's stopped or
@ -152,11 +149,6 @@ export default class Media {
this._gainNode.connect(this._context.destination);
}
_setChannels(num: number | null) {
this._channels = num;
this._emitter.trigger("channelupdate");
}
getAnalyser() {
return this._analyser;
}
@ -178,14 +170,6 @@ export default class Media {
return (this.timeElapsed() / this.duration()) * 100;
}
channels() {
return this._channels == null ? 2 : this._channels;
}
sampleRate() {
return this._source.getSampleRate();
}
/* Actions */
async play() {
await this._source.play();
@ -248,7 +232,6 @@ export default class Media {
async loadFromUrl(url: string, autoPlay: boolean) {
this._emitter.trigger("waiting");
await this._source.loadUrl(url);
this._setChannels(null);
this._emitter.trigger("stopWaiting");
if (autoPlay) {
this.play();

View file

@ -17,7 +17,6 @@ import {
SET_EQ_ON,
PLAY_TRACK,
BUFFER_TRACK,
CHANNEL_COUNT_CHANGED,
LOAD_SERIALIZED_STATE
} from "./actionTypes";
import { next as nextTrack } from "./actionCreators";
@ -70,19 +69,12 @@ export default (media: Media) => (store: MiddlewareStore) => {
id,
type: SET_MEDIA,
kbps: "128",
khz: Math.round(media.sampleRate() / 1000).toString(),
channels: media.channels(),
khz: "44",
channels: 2,
length: media.duration()
});
});
media.on("channelupdate", () => {
store.dispatch({
type: CHANNEL_COUNT_CHANGED,
channels: media.channels()
});
});
return (next: Dispatch) => (action: Action) => {
const returnValue = next(action);
const state = store.getState();

View file

@ -14,7 +14,6 @@ import {
TOGGLE_TIME_MODE,
UPDATE_TIME_ELAPSED,
ADD_TRACK_FROM_URL,
CHANNEL_COUNT_CHANGED,
LOAD_SERIALIZED_STATE
} from "../actionTypes";
import { TIME_MODE, MEDIA_STATUS } from "../constants";
@ -23,12 +22,8 @@ import { MediaSerializedStateV1 } from "../serializedStates/v1Types";
export interface MediaState {
timeMode: TimeMode; // TODO: Convert this to an enum
timeElapsed: number;
length: number | null;
kbps: string | null;
khz: string | null;
volume: number;
balance: number;
channels: number | null; // TODO: Convert this to an enum
shuffle: boolean;
repeat: boolean;
status: MediaStatus | null; // TODO: Convert this to an enum
@ -37,15 +32,12 @@ export interface MediaState {
const defaultState = {
timeMode: TIME_MODE.ELAPSED,
timeElapsed: 0,
length: null, // Consider renaming to "duration"
kbps: null,
khz: null,
// The winamp ini file declares the default volume as "200".
// The UI seems to show a default volume near 78, which would
// math with the default value being 200 out of 255.
volume: Math.round((200 / 255) * 100),
balance: 0,
channels: null,
shuffle: false,
repeat: false,
// TODO: Enforce possible values
@ -66,8 +58,6 @@ const media = (
case STOP:
case IS_STOPPED:
return { ...state, status: MEDIA_STATUS.STOPPED };
case CHANNEL_COUNT_CHANGED:
return { ...state, channels: action.channels };
case TOGGLE_TIME_MODE:
const newMode =
state.timeMode === TIME_MODE.REMAINING
@ -79,28 +69,11 @@ const media = (
case ADD_TRACK_FROM_URL:
return {
...state,
timeElapsed: 0,
length: null,
kbps: null,
khz: null,
channels: null
timeElapsed: 0
};
case SET_MEDIA:
return {
...state,
length: action.length,
kbps: action.kbps,
khz: action.khz,
channels: action.channels
};
case SET_MEDIA_TAGS:
const { sampleRate, bitrate, numberOfChannels } = action;
const { kbps, khz, channels } = state;
return {
...state,
kbps: bitrate != null ? String(Math.round(bitrate / 1000)) : kbps,
khz: sampleRate != null ? String(Math.round(sampleRate / 1000)) : khz,
channels: numberOfChannels != null ? numberOfChannels : channels
...state
};
case SET_VOLUME:
return { ...state, volume: action.volume };

View file

@ -42,19 +42,6 @@ const tracks = (
[action.id]: newTrack
};
}
case SET_MEDIA_TAGS: {
return {
...state,
[action.id]: {
...state[action.id],
mediaTagsRequestStatus: MEDIA_TAG_REQUEST_STATUS.COMPLETE,
title: action.title,
artist: action.artist,
album: action.album,
albumArtUrl: action.albumArtUrl
}
};
}
case MEDIA_TAG_REQUEST_INITIALIZED:
return {
...state,
@ -80,6 +67,32 @@ const tracks = (
}
};
}
case SET_MEDIA_TAGS:
const track = state[action.id];
const {
sampleRate,
bitrate,
numberOfChannels,
title,
artist,
album,
albumArtUrl
} = action;
const { kbps, khz, channels } = track;
return {
...state,
[action.id]: {
...track,
mediaTagsRequestStatus: MEDIA_TAG_REQUEST_STATUS.COMPLETE,
title,
artist,
album,
albumArtUrl,
kbps: bitrate != null ? String(Math.round(bitrate / 1000)) : kbps,
khz: sampleRate != null ? String(Math.round(sampleRate / 1000)) : khz,
channels: numberOfChannels != null ? numberOfChannels : channels
}
};
default:
return state;
}

View file

@ -7,28 +7,40 @@ const defaultTracksState = {
title: "Llama Whipping Intro",
artist: "DJ Mike Llama",
duration: 5,
url: ""
url: "",
kbps: "128",
khz: "44",
channels: 2
},
"1": {
id: 1,
title: "Rock Is Dead",
artist: "Marilyn Manson",
duration: 191, // 3:11
url: ""
url: "",
kbps: "128",
khz: "44",
channels: 2
},
"2": {
id: 2,
title: "Spybreak! (Short One)",
artist: "Propellerheads",
duration: 240, // 4:00
url: ""
url: "",
kbps: "128",
khz: "44",
channels: 2
},
"3": {
id: 3,
title: "Bad Blood",
artist: "Ministry",
duration: 300, // 5:00
url: ""
url: "",
kbps: "128",
khz: "44",
channels: 2
}
};
@ -50,11 +62,7 @@ const initialState: DeepPartial<AppState> = {
},
media: {
status: "PLAYING",
kbps: "128",
khz: "44",
length: 5,
timeElapsed: 3,
channels: 2
timeElapsed: 3
},
playlist: {
trackOrder: [0, 1, 2, 3],

View file

@ -454,7 +454,17 @@ export const getBalance = (state: AppState) => state.media.balance;
export const getShuffle = (state: AppState) => state.media.shuffle;
export const getRepeat = (state: AppState) => state.media.repeat;
export const getChannels = (state: AppState) => state.media.channels;
export const getChannels = createSelector(
getCurrentTrack,
(track: PlaylistTrack | null): number | null => {
return track != null ? track.channels || null : null;
}
);
export const getTimeElapsed = (state: AppState): number => {
return state.media.timeElapsed;
};
export function getSerlializedState(state: AppState): SerializedStateV1 {
return {
version: 1,
@ -496,25 +506,33 @@ export const getStackedLayoutPositions = createSelector(
}
);
export const getUserInputFocus = (state: AppState): string | null => {
return state.userInput.focus;
};
export const getUserInputScrubPosition = (state: AppState): number => {
return state.userInput.scrubPosition;
};
// TODO: Make this a reselect selector
export const getMarqueeText = (state: AppState): string => {
const defaultText = "Winamp 2.91";
if (state.userInput.userMessage != null) {
return state.userInput.userMessage;
}
switch (state.userInput.focus) {
switch (getUserInputFocus(state)) {
case "balance":
return MarqueeUtils.getBalanceText(state.media.balance);
case "volume":
return MarqueeUtils.getVolumeText(state.media.volume);
case "position":
if (state.media.length == null) {
const duration = getDuration(state);
if (duration == null) {
// This probably can't ever happen.
return defaultText;
}
return MarqueeUtils.getPositionText(
state.media.length,
state.userInput.scrubPosition
duration,
getUserInputScrubPosition(state)
);
case "double":
return MarqueeUtils.getDoubleSizeModeText(state.display.doubled);
@ -539,6 +557,20 @@ export const getMarqueeText = (state: AppState): string => {
return defaultText;
};
export const getKbps = createSelector(
getCurrentTrack,
(track: PlaylistTrack | null): string | null => {
return track != null ? track.kbps || null : null;
}
);
export const getKhz = createSelector(
getCurrentTrack,
(track: PlaylistTrack | null): string | null => {
return track != null ? track.khz || null : null;
}
);
export function getDebugData(state: AppState) {
return {
...state,

View file

@ -168,10 +168,6 @@ export type Action =
| {
type: "IS_STOPPED";
}
| {
type: "CHANNEL_COUNT_CHANGED";
channels: number;
}
| {
type: "TOGGLE_TIME_MODE";
}
@ -526,6 +522,9 @@ export interface PlaylistTrack {
albumArtUrl?: string | null;
mediaTagsRequestStatus: MediaTagRequestStatus;
duration: number | null;
kbps?: string;
khz: string;
channels?: number;
}
export interface AppState {