From b16fb3a2aee4891e27bbdb0e4400bdd7661e3cef Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Sat, 29 Dec 2018 12:29:10 -0800 Subject: [PATCH] Move file specific media info to the track Fixes #712 --- js/actionCreators/files.ts | 14 +++++- js/actionCreators/media.ts | 9 ++-- js/actionTypes.ts | 1 - js/components/MainWindow/Kbps.js | 12 ------ js/components/MainWindow/Kbps.tsx | 22 ++++++++++ js/components/MainWindow/Khz.js | 12 ------ js/components/MainWindow/Khz.tsx | 22 ++++++++++ .../MainWindow/{Position.js => Position.tsx} | 43 +++++++++++++------ js/components/MainWindow/Time.tsx | 13 +++--- js/components/MiniTime.tsx | 24 +++++++---- js/media/elementSource.ts | 10 ----- js/media/index.ts | 17 -------- js/mediaMiddleware.ts | 12 +----- js/reducers/media.ts | 33 ++------------ js/reducers/tracks.ts | 39 +++++++++++------ js/screenshotInitialState.ts | 26 +++++++---- js/selectors.ts | 42 +++++++++++++++--- js/types.ts | 7 ++- 18 files changed, 205 insertions(+), 153 deletions(-) delete mode 100644 js/components/MainWindow/Kbps.js create mode 100644 js/components/MainWindow/Kbps.tsx delete mode 100644 js/components/MainWindow/Khz.js create mode 100644 js/components/MainWindow/Khz.tsx rename js/components/MainWindow/{Position.js => Position.tsx} (53%) diff --git a/js/actionCreators/files.ts b/js/actionCreators/files.ts index 4980a083..98e632d6 100644 --- a/js/actionCreators/files.ts +++ b/js/actionCreators/files.ts @@ -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)); diff --git a/js/actionCreators/media.ts b/js/actionCreators/media.ts index 46b67efd..4a8c1b80 100644 --- a/js/actionCreators/media.ts +++ b/js/actionCreators/media.ts @@ -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 }); }; } diff --git a/js/actionTypes.ts b/js/actionTypes.ts index 891a82f7..9783752f 100644 --- a/js/actionTypes.ts +++ b/js/actionTypes.ts @@ -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"; diff --git a/js/components/MainWindow/Kbps.js b/js/components/MainWindow/Kbps.js deleted file mode 100644 index 3d0688a7..00000000 --- a/js/components/MainWindow/Kbps.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; -import { connect } from "react-redux"; - -import CharacterString from "../CharacterString"; - -const Kbps = props => ( -
- {props.kbps} -
-); - -export default connect(state => ({ kbps: state.media.kbps }))(Kbps); diff --git a/js/components/MainWindow/Kbps.tsx b/js/components/MainWindow/Kbps.tsx new file mode 100644 index 00000000..d7b3d87f --- /dev/null +++ b/js/components/MainWindow/Kbps.tsx @@ -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) => ( +
+ {props.kbps || ""} +
+); + +function mapStateToProps(state: AppState): StateProps { + return { kbps: Selectors.getKbps(state) }; +} + +export default connect(mapStateToProps)(Kbps); diff --git a/js/components/MainWindow/Khz.js b/js/components/MainWindow/Khz.js deleted file mode 100644 index 7d689b4f..00000000 --- a/js/components/MainWindow/Khz.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; -import { connect } from "react-redux"; - -import CharacterString from "../CharacterString"; - -const Khz = props => ( -
- {props.khz} -
-); - -export default connect(state => state.media)(Khz); diff --git a/js/components/MainWindow/Khz.tsx b/js/components/MainWindow/Khz.tsx new file mode 100644 index 00000000..8ddd9ab2 --- /dev/null +++ b/js/components/MainWindow/Khz.tsx @@ -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) => ( +
+ {props.khz || ""} +
+); + +function mapStateToProps(state: AppState): StateProps { + return { khz: Selectors.getKhz(state) }; +} + +export default connect(mapStateToProps)(Khz); diff --git a/js/components/MainWindow/Position.js b/js/components/MainWindow/Position.tsx similarity index 53% rename from js/components/MainWindow/Position.js rename to js/components/MainWindow/Position.tsx index 6daa353c..21434f45 100644 --- a/js/components/MainWindow/Position.js +++ b/js/components/MainWindow/Position.tsx @@ -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): void; + setPosition(e: React.MouseEvent): 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) + }); } }); diff --git a/js/components/MainWindow/Time.tsx b/js/components/MainWindow/Time.tsx index 96daca1e..51b315c5 100644 --- a/js/components/MainWindow/Time.tsx +++ b/js/components/MainWindow/Time.tsx @@ -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()) diff --git a/js/components/MiniTime.tsx b/js/components/MiniTime.tsx index 9c746b16..5a9f6aee 100644 --- a/js/components/MiniTime.tsx +++ b/js/components/MiniTime.tsx @@ -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 = () => ( ); -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 }); diff --git a/js/media/elementSource.ts b/js/media/elementSource.ts index a88b4c07..b48bd88e 100644 --- a/js/media/elementSource.ts +++ b/js/media/elementSource.ts @@ -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"); diff --git a/js/media/index.ts b/js/media/index.ts index 9d3858f3..4c4f5680 100644 --- a/js/media/index.ts +++ b/js/media/index.ts @@ -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(); diff --git a/js/mediaMiddleware.ts b/js/mediaMiddleware.ts index 31c67e56..9e2dc151 100644 --- a/js/mediaMiddleware.ts +++ b/js/mediaMiddleware.ts @@ -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(); diff --git a/js/reducers/media.ts b/js/reducers/media.ts index 03688a93..8f8d4446 100644 --- a/js/reducers/media.ts +++ b/js/reducers/media.ts @@ -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 }; diff --git a/js/reducers/tracks.ts b/js/reducers/tracks.ts index 42effa6e..38cfce71 100644 --- a/js/reducers/tracks.ts +++ b/js/reducers/tracks.ts @@ -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; } diff --git a/js/screenshotInitialState.ts b/js/screenshotInitialState.ts index 2e4a3698..6935fe8b 100644 --- a/js/screenshotInitialState.ts +++ b/js/screenshotInitialState.ts @@ -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 = { }, media: { status: "PLAYING", - kbps: "128", - khz: "44", - length: 5, - timeElapsed: 3, - channels: 2 + timeElapsed: 3 }, playlist: { trackOrder: [0, 1, 2, 3], diff --git a/js/selectors.ts b/js/selectors.ts index c401df53..d76d25a0 100644 --- a/js/selectors.ts +++ b/js/selectors.ts @@ -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, diff --git a/js/types.ts b/js/types.ts index d8928830..51f81ff9 100644 --- a/js/types.ts +++ b/js/types.ts @@ -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 {