Mixing and UI tweaks, mebbe.
This commit is contained in:
parent
254ce13b88
commit
a6b24072b8
9 changed files with 795 additions and 189 deletions
|
@ -13,6 +13,7 @@
|
|||
"@types/react-dom": "16.9.4",
|
||||
"@types/react-redux": "^7.1.5",
|
||||
"@types/webpack-env": "^1.14.1",
|
||||
"between.js": "^0.1.2-fix.2",
|
||||
"lodash": "^4.17.15",
|
||||
"qs": "^6.9.1",
|
||||
"react": "^0.0.0-experimental-38dd17ab9",
|
||||
|
|
39
src/App.css
39
src/App.css
|
@ -38,13 +38,15 @@
|
|||
}
|
||||
|
||||
.sp-col {
|
||||
display: block;
|
||||
height: 80%;
|
||||
display: flex;
|
||||
height: 60%;
|
||||
overflow-y: scroll;
|
||||
border: 1px solid black;
|
||||
margin-left: .2vw;
|
||||
margin-right: .2vw;
|
||||
padding: .2vw;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.sp-main-col{
|
||||
|
@ -52,7 +54,7 @@
|
|||
}
|
||||
|
||||
.sp-col-inner {
|
||||
height: 99%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sp-track {
|
||||
|
@ -70,7 +72,7 @@
|
|||
}
|
||||
|
||||
.sp-track-active {
|
||||
background-color: #10c998;
|
||||
background-color: #10c998 !important;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
|
@ -120,6 +122,31 @@ button{
|
|||
}
|
||||
|
||||
.player div {
|
||||
height: 16%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.sp-player-button {
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.sp-mixer-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sp-mixer-buttons-backdrop {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background-color: #78acf1;
|
||||
}
|
||||
|
||||
.sp-mixer-buttons button {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: 1px solid black;
|
||||
z-index: 100;
|
||||
}
|
||||
|
|
424
src/lib/between.d.ts
vendored
Normal file
424
src/lib/between.d.ts
vendored
Normal file
|
@ -0,0 +1,424 @@
|
|||
// Stolen from https://github.com/sasha240100/between.js/blob/master/types/Between.d.ts
|
||||
// Declare your modules properly, people!
|
||||
|
||||
declare module "between.js" {
|
||||
/**
|
||||
* Loop mode
|
||||
*/
|
||||
export type LoopMode = "repeat" | "bounce";
|
||||
export type EventEmmit = "start" | "update" | "complete";
|
||||
|
||||
/**
|
||||
* A collection of easing methods defining ease-in ease-out curves.
|
||||
*/
|
||||
export interface Easing extends Function {
|
||||
/**
|
||||
* Linear easing.
|
||||
*
|
||||
* @class Easing.Linear
|
||||
*/
|
||||
Linear: {
|
||||
/**
|
||||
* Ease-in.
|
||||
*
|
||||
* @method Easing.Linear#In
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} k^2.
|
||||
*/
|
||||
None: Function;
|
||||
};
|
||||
|
||||
/**
|
||||
* Quadratic easing.
|
||||
*
|
||||
* @class Easing.Quadratic
|
||||
*/
|
||||
Quadratic: {
|
||||
/**
|
||||
* Ease-in.
|
||||
*
|
||||
* @method Easing.Quadratic#In
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} k^2.
|
||||
*/
|
||||
In: Function;
|
||||
|
||||
/**
|
||||
* Ease-out.
|
||||
*
|
||||
* @method Easing.Quadratic#Out
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} k* (2-k).
|
||||
*/
|
||||
Out: Function;
|
||||
|
||||
/**
|
||||
* Ease-in/out.
|
||||
*
|
||||
* @method Easing.Quadratic#InOut
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
InOut: Function;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cubic easing.
|
||||
*
|
||||
* @class Easing.Cubic
|
||||
*/
|
||||
Cubic: {
|
||||
/**
|
||||
* Cubic ease-in.
|
||||
*
|
||||
* @method Easing.Cubic#In
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
In: Function;
|
||||
|
||||
/**
|
||||
* Cubic ease-out.
|
||||
*
|
||||
* @method Easing.Cubic#Out
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
Out: Function;
|
||||
|
||||
/**
|
||||
* Cubic ease-in/out.
|
||||
*
|
||||
* @method Easing.Cubic#InOut
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
InOut: Function;
|
||||
};
|
||||
|
||||
/**
|
||||
* Quartic easing.
|
||||
*
|
||||
* @class Easing.Quartic
|
||||
*/
|
||||
Quartic: {
|
||||
/**
|
||||
* Quartic ease-in.
|
||||
*
|
||||
* @method Easing.Quartic#In
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
In: Function;
|
||||
|
||||
/**
|
||||
* Quartic ease-out.
|
||||
*
|
||||
* @method Easing.Quartic#Out
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
Out: Function;
|
||||
|
||||
/**
|
||||
* Quartic ease-in/out.
|
||||
*
|
||||
* @method Easing.Quartic#InOut
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
InOut: Function;
|
||||
};
|
||||
|
||||
/**
|
||||
* Quintic easing.
|
||||
*
|
||||
* @class Easing.Quintic
|
||||
*/
|
||||
Quintic: {
|
||||
/**
|
||||
* Quintic ease-in.
|
||||
*
|
||||
* @method Easing.Quintic#In
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
In: Function;
|
||||
|
||||
/**
|
||||
* Quintic ease-out.
|
||||
*
|
||||
* @method Easing.Quintic#Out
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
Out: Function;
|
||||
|
||||
/**
|
||||
* Quintic ease-in/out.
|
||||
*
|
||||
* @method Easing.Quintic#InOut
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
InOut: Number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sinusoidal easing.
|
||||
*
|
||||
* @class Easing.Sinusoidal
|
||||
*/
|
||||
Sinusoidal: {
|
||||
/**
|
||||
* Sinusoidal ease-in.
|
||||
*
|
||||
* @method Easing.Sinusoidal#In
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
In: Function;
|
||||
|
||||
/**
|
||||
* Sinusoidal ease-out.
|
||||
*
|
||||
* @method Easing.Sinusoidal#Out
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
Out: Function;
|
||||
|
||||
/**
|
||||
* Sinusoidal ease-in/out.
|
||||
*
|
||||
* @method Easing.Sinusoidal#InOut
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
InOut: Function;
|
||||
};
|
||||
|
||||
/**
|
||||
* Exponential easing.
|
||||
*
|
||||
* @class Easing.Exponential
|
||||
*/
|
||||
Exponential: {
|
||||
/**
|
||||
* Exponential ease-in.
|
||||
*
|
||||
* @method Easing.Exponential#In
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
In: Function;
|
||||
|
||||
/**
|
||||
* Exponential ease-out.
|
||||
*
|
||||
* @method Easing.Exponential#Out
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
Out: Function;
|
||||
|
||||
/**
|
||||
* Exponential ease-in/out.
|
||||
*
|
||||
* @method Easing.Exponential#InOut
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
InOut: Function;
|
||||
};
|
||||
|
||||
/**
|
||||
* Circular easing.
|
||||
*
|
||||
* @class Easing.Circular
|
||||
*/
|
||||
Circular: {
|
||||
/**
|
||||
* Circular ease-in.
|
||||
*
|
||||
* @method Easing.Circular#In
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
In: Function;
|
||||
|
||||
/**
|
||||
* Circular ease-out.
|
||||
*
|
||||
* @method Easing.Circular#Out
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
Out: Function;
|
||||
|
||||
/**
|
||||
* Circular ease-in/out.
|
||||
*
|
||||
* @method Easing.Circular#InOut
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
InOut: Function;
|
||||
};
|
||||
|
||||
/**
|
||||
* Elastic easing.
|
||||
*
|
||||
* @class Easing.Elastic
|
||||
*/
|
||||
Elastic: {
|
||||
/**
|
||||
* Elastic ease-in.
|
||||
*
|
||||
* @method Easing.Elastic#In
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
In: Function;
|
||||
|
||||
/**
|
||||
* Elastic ease-out.
|
||||
*
|
||||
* @method Easing.Elastic#Out
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
Out: Function;
|
||||
|
||||
/**
|
||||
* Elastic ease-in/out.
|
||||
*
|
||||
* @method Easing.Elastic#InOut
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
InOut: Function;
|
||||
};
|
||||
|
||||
/**
|
||||
* Back easing.
|
||||
*
|
||||
* @class Easing.Back
|
||||
*/
|
||||
Back: {
|
||||
/**
|
||||
* Back ease-in.
|
||||
*
|
||||
* @method Easing.Back#In
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
In: Function;
|
||||
|
||||
/**
|
||||
* Back ease-out.
|
||||
*
|
||||
* @method Easing.Back#Out
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
Out: Function;
|
||||
|
||||
/**
|
||||
* Back ease-in/out.
|
||||
*
|
||||
* @method Easing.Back#InOut
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
InOut: Function;
|
||||
};
|
||||
|
||||
/**
|
||||
* Bounce easing.
|
||||
*
|
||||
* @class Easing.Bounce
|
||||
*/
|
||||
Bounce: {
|
||||
/**
|
||||
* Bounce ease-in.
|
||||
*
|
||||
* @method Easing.Bounce#In
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
In: Function;
|
||||
|
||||
/**
|
||||
* Bounce ease-out.
|
||||
*
|
||||
* @method Easing.Bounce#Out
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
Out: Function;
|
||||
|
||||
/**
|
||||
* Bounce ease-in/out.
|
||||
*
|
||||
* @method Easing.Bounce#InOut
|
||||
* @param {number} k - The value to be tweened.
|
||||
* @returns {number} The tweened value.
|
||||
*/
|
||||
InOut: Function;
|
||||
};
|
||||
}
|
||||
|
||||
export class Between {
|
||||
/**
|
||||
* Creates a new Between instance
|
||||
* @param from start
|
||||
* @param to end
|
||||
*/
|
||||
constructor(
|
||||
from: Number | Object | [Number | Object],
|
||||
to: Number | Object | [Number | Object]
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets duration
|
||||
* @param duration duration in ms
|
||||
*/
|
||||
time(duration: Number): this;
|
||||
|
||||
/**
|
||||
* Set loop mode and repeat times
|
||||
* @param mode the loop mode
|
||||
* @param repeatTime if not defined, then treats as endless
|
||||
*/
|
||||
loop(mode: LoopMode, repeatTime?: Number): this;
|
||||
|
||||
/**
|
||||
* Set easing function
|
||||
* @param easing Easing type
|
||||
*/
|
||||
easing(easing: Easing | Function): this;
|
||||
|
||||
/**
|
||||
* Adds event listener
|
||||
* @param eventName
|
||||
* @param callback
|
||||
*/
|
||||
on(eventName: EventEmmit, callback: Function): this;
|
||||
|
||||
/**
|
||||
* Pauses
|
||||
*/
|
||||
pause(): this;
|
||||
|
||||
/**
|
||||
* Starts (if was paused)
|
||||
*/
|
||||
play(): this;
|
||||
|
||||
/**
|
||||
* Returns `true` if paused
|
||||
*/
|
||||
isPaused: boolean;
|
||||
}
|
||||
}
|
209
src/mixer/state.ts
Normal file
209
src/mixer/state.ts
Normal file
|
@ -0,0 +1,209 @@
|
|||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import * as Between from "between.js";
|
||||
import { PlanItem } from "../showplanner/state";
|
||||
import { Track, MYRADIO_NON_API_BASE } from "../api";
|
||||
import { AppThunk } from "../store";
|
||||
|
||||
console.log(Between);
|
||||
|
||||
const audioContext = new AudioContext();
|
||||
const playerSources: MediaElementAudioSourceNode[] = [];
|
||||
const playerGains: GainNode[] = [];
|
||||
const playerGainTweens: Between.Between[] = [];
|
||||
// TODO
|
||||
// const destination = audioContext.createWebcastSource(4096, 2);
|
||||
const destination = audioContext.createDynamicsCompressor();
|
||||
destination.connect(audioContext.destination);
|
||||
|
||||
type PlayerStateEnum = "playing" | "paused" | "stopped";
|
||||
type VolumePresetEnum = "off" | "bed" | "full";
|
||||
|
||||
interface PlayerState {
|
||||
loadedItem: PlanItem | Track | null;
|
||||
loading: boolean;
|
||||
state: PlayerStateEnum;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
interface MixerState {
|
||||
players: PlayerState[];
|
||||
}
|
||||
|
||||
const mixerState = createSlice({
|
||||
name: "Player",
|
||||
initialState: {
|
||||
players: [
|
||||
{
|
||||
loadedItem: null,
|
||||
loading: false,
|
||||
state: "stopped",
|
||||
volume: 1
|
||||
},
|
||||
{
|
||||
loadedItem: null,
|
||||
loading: false,
|
||||
state: "stopped",
|
||||
volume: 1
|
||||
},
|
||||
{
|
||||
loadedItem: null,
|
||||
loading: false,
|
||||
state: "stopped",
|
||||
volume: 1
|
||||
}
|
||||
]
|
||||
} as MixerState,
|
||||
reducers: {
|
||||
loadItem(
|
||||
state,
|
||||
action: PayloadAction<{ player: number; item: PlanItem | Track }>
|
||||
) {
|
||||
state.players[action.payload.player].loadedItem =
|
||||
action.payload.item;
|
||||
state.players[action.payload.player].loading = true;
|
||||
},
|
||||
itemLoadComplete(state, action: PayloadAction<{ player: number }>) {
|
||||
state.players[action.payload.player].loading = false;
|
||||
},
|
||||
setPlayerState(
|
||||
state,
|
||||
action: PayloadAction<{ player: number; state: PlayerStateEnum }>
|
||||
) {
|
||||
state.players[action.payload.player].state = action.payload.state;
|
||||
},
|
||||
setPlayerVolume(
|
||||
state,
|
||||
action: PayloadAction<{ player: number; volume: number }>
|
||||
) {
|
||||
state.players[action.payload.player].volume = action.payload.volume;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default mixerState.reducer;
|
||||
|
||||
export const load = (player: number, item: PlanItem | Track): AppThunk => (
|
||||
dispatch,
|
||||
getState
|
||||
) => {
|
||||
if (typeof playerSources[player] !== "undefined") {
|
||||
if (!playerSources[player].mediaElement.paused) {
|
||||
// already playing, don't kill playback
|
||||
return;
|
||||
}
|
||||
}
|
||||
dispatch(mixerState.actions.loadItem({ player, item }));
|
||||
const el = new Audio();
|
||||
el.crossOrigin = "use-credentials";
|
||||
if ("album" in item) {
|
||||
// track
|
||||
el.src =
|
||||
MYRADIO_NON_API_BASE +
|
||||
"/NIPSWeb/secure_play?recordid=" +
|
||||
item.album.recordid +
|
||||
"&trackid=" +
|
||||
item.trackid;
|
||||
} else if ("type" in item && item.type == "aux") {
|
||||
el.src =
|
||||
MYRADIO_NON_API_BASE +
|
||||
"/NIPSWeb/managed_play?managedid=" +
|
||||
item.managedid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Unsure how to handle this!\r\n\r\n" + JSON.stringify(item)
|
||||
);
|
||||
}
|
||||
el.oncanplay = () => {
|
||||
dispatch(mixerState.actions.itemLoadComplete({ player }));
|
||||
};
|
||||
el.load();
|
||||
const sauce = audioContext.createMediaElementSource(el);
|
||||
const gain = audioContext.createGain();
|
||||
gain.gain.value = getState().mixer.players[player].volume;
|
||||
sauce.connect(gain);
|
||||
gain.connect(destination);
|
||||
console.log("Connected to", destination);
|
||||
playerSources[player] = sauce;
|
||||
playerGains[player] = gain;
|
||||
};
|
||||
|
||||
export const play = (player: number): AppThunk => dispatch => {
|
||||
try {
|
||||
console.log("PLAY YOU FASCIST")
|
||||
playerSources[player].mediaElement.play();
|
||||
dispatch(
|
||||
mixerState.actions.setPlayerState({ player, state: "playing" })
|
||||
);
|
||||
playerSources[player].mediaElement.addEventListener(
|
||||
"ended",
|
||||
function() {
|
||||
dispatch(
|
||||
mixerState.actions.setPlayerState({
|
||||
player,
|
||||
state: "stopped"
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
console.log("nothing selected/loaded");
|
||||
}
|
||||
};
|
||||
|
||||
export const pause = (player: number): AppThunk => dispatch => {
|
||||
try {
|
||||
if (playerSources[player].mediaElement.paused) {
|
||||
playerSources[player].mediaElement.play();
|
||||
dispatch(
|
||||
mixerState.actions.setPlayerState({ player, state: "playing" })
|
||||
);
|
||||
} else {
|
||||
playerSources[player].mediaElement.pause();
|
||||
dispatch(
|
||||
mixerState.actions.setPlayerState({ player, state: "paused" })
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
console.log("nothing selected/loaded");
|
||||
}
|
||||
};
|
||||
|
||||
export const stop = (player: number): AppThunk => dispatch => {
|
||||
try {
|
||||
playerSources[player].mediaElement.pause();
|
||||
playerSources[player].mediaElement.currentTime = 0;
|
||||
dispatch(
|
||||
mixerState.actions.setPlayerState({ player, state: "stopped" })
|
||||
);
|
||||
} catch {
|
||||
console.log("nothing selected/loaded");
|
||||
}
|
||||
};
|
||||
|
||||
export const setVolume = (
|
||||
player: number,
|
||||
level: VolumePresetEnum
|
||||
): AppThunk => (dispatch, getState) => {
|
||||
let volume: number;
|
||||
switch (level) {
|
||||
case "off":
|
||||
volume = 0;
|
||||
break;
|
||||
case "bed":
|
||||
volume = 0.25;
|
||||
break;
|
||||
case "full":
|
||||
volume = 1;
|
||||
break;
|
||||
}
|
||||
const currentLevel = getState().mixer.players[player].volume;
|
||||
playerGainTweens[player] = new (Between as any)(currentLevel, volume)
|
||||
.on("update", (value: number) => {
|
||||
console.log(value);
|
||||
dispatch(mixerState.actions.setPlayerVolume({ player, volume }));
|
||||
if (playerGains[player]) {
|
||||
playerGains[player].gain.value = value;
|
||||
}
|
||||
})
|
||||
.time(1000);
|
||||
};
|
|
@ -1,11 +1,11 @@
|
|||
import { combineReducers } from "@reduxjs/toolkit";
|
||||
|
||||
import ShowplanReducer from "./showplanner/state";
|
||||
import PlayerReducer from "./showplanner/player/state";
|
||||
import MixerReducer from "./mixer/state";
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
showplan: ShowplanReducer,
|
||||
player: PlayerReducer
|
||||
mixer: MixerReducer
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof rootReducer>;
|
||||
|
|
|
@ -31,23 +31,33 @@ import {
|
|||
removeItem
|
||||
} from "./state";
|
||||
|
||||
import * as PlayerState from "./player/state";
|
||||
import * as PlayerState from "../mixer/state";
|
||||
|
||||
import playLogo from '../assets/icons/play.svg'
|
||||
import pauseLogo from '../assets/icons/pause.svg'
|
||||
import stopLogo from '../assets/icons/stop.svg'
|
||||
import playLogo from "../assets/icons/play.svg";
|
||||
import pauseLogo from "../assets/icons/pause.svg";
|
||||
import stopLogo from "../assets/icons/stop.svg";
|
||||
|
||||
const CML_CACHE: { [recordid_trackid: string]: Track } = {};
|
||||
|
||||
const TS_ITEM_MENU_ID = "SongMenu";
|
||||
|
||||
function Item({ item: x, index, column }: { item: PlanItem | Track; index: number; column: number }) {
|
||||
function Item({
|
||||
item: x,
|
||||
index,
|
||||
column
|
||||
}: {
|
||||
item: PlanItem | Track;
|
||||
index: number;
|
||||
column: number;
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const id = itemId(x);
|
||||
const isReal = "timeslotitemid" in x;
|
||||
const isGhost = "ghostid" in x;
|
||||
|
||||
const playerState = useSelector((state: RootState) => state.player.players[column]);
|
||||
const playerState = useSelector(
|
||||
(state: RootState) => state.mixer.players[column]
|
||||
);
|
||||
|
||||
function triggerClick() {
|
||||
if (column > -1) {
|
||||
|
@ -61,12 +71,20 @@ function Item({ item: x, index, column }: { item: PlanItem | Track; index: numbe
|
|||
<div
|
||||
ref={provided.innerRef}
|
||||
key={id}
|
||||
className={`sp-track ${(playerState.loadedItem !== null && itemId(playerState.loadedItem) === id) ? "sp-track-active" : ""}`}
|
||||
className={`sp-track ${
|
||||
playerState.loadedItem !== null &&
|
||||
itemId(playerState.loadedItem) === id
|
||||
? "sp-track-active"
|
||||
: ""
|
||||
}`}
|
||||
onClick={triggerClick}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<ContextMenuTrigger id={isReal ? TS_ITEM_MENU_ID : ""} collect={() => ({ id })}>
|
||||
<ContextMenuTrigger
|
||||
id={isReal ? TS_ITEM_MENU_ID : ""}
|
||||
collect={() => ({ id })}
|
||||
>
|
||||
{x.title}
|
||||
{"artist" in x && " - " + x.artist}
|
||||
<code>
|
||||
|
@ -80,33 +98,43 @@ function Item({ item: x, index, column }: { item: PlanItem | Track; index: numbe
|
|||
}
|
||||
|
||||
function Player({ id }: { id: number }) {
|
||||
const playerState = useSelector((state: RootState) => state.player.players[id]);
|
||||
const playerState = useSelector(
|
||||
(state: RootState) => state.mixer.players[id]
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
<div style={{height:"16%"}} className="player">
|
||||
{playerState.loadedItem == null && (<div>No Media Selected</div>)}
|
||||
{(playerState.loadedItem !== null && playerState.loading == false) && (<div>{playerState.loadedItem.title}</div>)}
|
||||
<div className="player">
|
||||
{playerState.loadedItem == null && <div>No Media Selected</div>}
|
||||
{playerState.loadedItem !== null && playerState.loading == false && (
|
||||
<div>{playerState.loadedItem.title}</div>
|
||||
)}
|
||||
{playerState.loading && <b>LOADING</b>}
|
||||
<div className="mediaButtons" style={{height:"100%"}}>
|
||||
<button
|
||||
onClick={() => dispatch(PlayerState.play(id))}
|
||||
className={playerState.state === "playing" ? "sp-state-playing" : ""}
|
||||
>
|
||||
<img src={playLogo} style={{height:"10vh"}}/>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => dispatch(PlayerState.pause(id))}
|
||||
className={playerState.state === "paused" ? "sp-state-paused" : ""}
|
||||
>
|
||||
<img src={pauseLogo} style={{height:"10vh"}}/>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => dispatch(PlayerState.stop(id))}
|
||||
className={playerState.state === "stopped" ? "sp-state-stopped" : ""}
|
||||
>
|
||||
<img src={stopLogo} style={{height:"10vh"}}/>
|
||||
</button>
|
||||
<div className="mediaButtons">
|
||||
<button
|
||||
onClick={() => dispatch(PlayerState.play(id))}
|
||||
className={playerState.state === "playing" ? "sp-state-playing" : ""}
|
||||
>
|
||||
<img src={playLogo} className="sp-player-button" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => dispatch(PlayerState.pause(id))}
|
||||
className={playerState.state === "paused" ? "sp-state-paused" : ""}
|
||||
>
|
||||
<img src={pauseLogo} className="sp-player-button" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => dispatch(PlayerState.stop(id))}
|
||||
className={playerState.state === "stopped" ? "sp-state-stopped" : ""}
|
||||
>
|
||||
<img src={stopLogo} className="sp-player-button" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="sp-mixer-buttons">
|
||||
<div className="sp-mixer-buttons-backdrop" style={{ width: playerState.volume * 100 + "%" }}></div>
|
||||
<button onClick={() => dispatch(PlayerState.setVolume(id, "off"))}>Off</button>
|
||||
<button onClick={() => dispatch(PlayerState.setVolume(id, "bed"))}>Bed</button>
|
||||
<button onClick={() => dispatch(PlayerState.setVolume(id, "full"))}>Full</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -129,7 +157,12 @@ function Column({ id, data }: { id: number; data: PlanItem[] }) {
|
|||
.filter(x => x.channel === id)
|
||||
.sort((a, b) => a.weight - b.weight)
|
||||
.map((x, index) => (
|
||||
<Item key={itemId(x)} item={x} index={index} column={id} />
|
||||
<Item
|
||||
key={itemId(x)}
|
||||
item={x}
|
||||
index={index}
|
||||
column={id}
|
||||
/>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
|
@ -184,28 +217,28 @@ function CentralMusicLibrary() {
|
|||
function LibraryColumn() {
|
||||
const [sauce, setSauce] = useState("None");
|
||||
return (
|
||||
<div className="sp-col" style={{height:"48%", marginBottom:"1%"}}>
|
||||
<select
|
||||
style={{ width: "100%" }}
|
||||
value={sauce}
|
||||
onChange={e => setSauce(e.target.value)}
|
||||
>
|
||||
<option value={"None"} disabled>
|
||||
Choose a library
|
||||
</option>
|
||||
<option value={"CentralMusicLibrary"}>Central Music Library</option>
|
||||
</select>
|
||||
{sauce === "CentralMusicLibrary" && <CentralMusicLibrary />}
|
||||
</div>
|
||||
<div className="sp-col" style={{ height: "48%", marginBottom: "1%" }}>
|
||||
<select
|
||||
style={{ width: "100%" }}
|
||||
value={sauce}
|
||||
onChange={e => setSauce(e.target.value)}
|
||||
>
|
||||
<option value={"None"} disabled>
|
||||
Choose a library
|
||||
</option>
|
||||
<option value={"CentralMusicLibrary"}>Central Music Library</option>
|
||||
</select>
|
||||
{sauce === "CentralMusicLibrary" && <CentralMusicLibrary />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MixingInterface(){
|
||||
function MixingInterface() {
|
||||
const [sauce, setSauce] = useState("None");
|
||||
return (
|
||||
<div className="sp-col" style={{height:"48%", overflowY:"visible"}}>
|
||||
<h1>Mixing Interface</h1>
|
||||
</div>
|
||||
<div className="sp-col" style={{ height: "48%", overflowY: "visible" }}>
|
||||
<h1>Mixing Interface</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -275,9 +308,12 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) {
|
|||
}
|
||||
return (
|
||||
<div className="sp-container">
|
||||
<div style={{height:"10%"}}>
|
||||
<div style={{ height: "10%" }}>
|
||||
<h1>baps3 ayy lmao</h1>
|
||||
<img src="https://ury.org.uk/images/logo.png" style={{height:"6%", right:"2%", position:"absolute", top:"2%"}}/>
|
||||
<img
|
||||
src="https://ury.org.uk/images/logo.png"
|
||||
style={{ height: "6%", right: "2%", position: "absolute", top: "2%" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="sp-status">
|
||||
{planSaving && <em>Plan saving...</em>}
|
||||
|
@ -292,9 +328,9 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) {
|
|||
<Column id={0} data={showplan} />
|
||||
<Column id={1} data={showplan} />
|
||||
<Column id={2} data={showplan} />
|
||||
<div className="sp-main-col" style={{marginRight:".2%"}}>
|
||||
<LibraryColumn />
|
||||
<MixingInterface />
|
||||
<div className="sp-main-col" style={{ marginRight: ".2%" }}>
|
||||
<LibraryColumn />
|
||||
<MixingInterface />
|
||||
</div>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
import "../../lib/webcast";
|
||||
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { PlanItem } from "../state";
|
||||
import { Track, MYRADIO_NON_API_BASE } from "../../api";
|
||||
import { AppThunk } from "../../store";
|
||||
|
||||
/// <reference path="webcast.d.ts" />
|
||||
|
||||
const audioContext = new AudioContext();
|
||||
const playerSources: MediaElementAudioSourceNode[] = [];
|
||||
const playerGains: GainNode[] = [];
|
||||
// TODO
|
||||
// const destination = audioContext.createWebcastSource(4096, 2);
|
||||
const destination = audioContext.createDynamicsCompressor();
|
||||
destination.connect(audioContext.destination);
|
||||
|
||||
type PlayerStateEnum = "playing" | "paused" | "stopped";
|
||||
|
||||
interface SinglePlayerState {
|
||||
loadedItem: PlanItem | Track | null
|
||||
loading: boolean;
|
||||
state: PlayerStateEnum;
|
||||
}
|
||||
|
||||
interface PlayerState {
|
||||
players: SinglePlayerState[];
|
||||
}
|
||||
|
||||
const playerState = createSlice({
|
||||
name: "Player",
|
||||
initialState: {
|
||||
players: [{
|
||||
loadedItem: null,
|
||||
loading: false,
|
||||
state: "stopped"
|
||||
}, {
|
||||
loadedItem: null,
|
||||
loading: false,
|
||||
state: "stopped"
|
||||
}, {
|
||||
loadedItem: null,
|
||||
loading: false,
|
||||
state: "stopped"
|
||||
}]
|
||||
} as PlayerState,
|
||||
reducers: {
|
||||
loadItem(state, action: PayloadAction<{ player: number, item: PlanItem | Track }>) {
|
||||
state.players[action.payload.player].loadedItem = action.payload.item
|
||||
state.players[action.payload.player].loading = true;
|
||||
},
|
||||
itemLoadComplete(state, action: PayloadAction<{ player: number}>) {
|
||||
state.players[action.payload.player].loading = false;
|
||||
},
|
||||
setPlayerState(state, action: PayloadAction<{ player: number, state: PlayerStateEnum }>) {
|
||||
state.players[action.payload.player].state = action.payload.state;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default playerState.reducer;
|
||||
|
||||
export const load = (player: number, item: PlanItem | Track): AppThunk => dispatch => {
|
||||
if (typeof playerSources[player] !== "undefined") {
|
||||
if (!playerSources[player].mediaElement.paused) {
|
||||
// already playing, don't kill playback
|
||||
return;
|
||||
}
|
||||
}
|
||||
dispatch(playerState.actions.loadItem({ player, item }));
|
||||
const el = new Audio();
|
||||
el.crossOrigin = "use-credentials";
|
||||
if ("album" in item) {
|
||||
// track
|
||||
el.src = MYRADIO_NON_API_BASE + "/NIPSWeb/secure_play?recordid=" + item.album.recordid + "&trackid=" + item.trackid;
|
||||
} else if ("type" in item && item.type == "aux") {
|
||||
el.src = MYRADIO_NON_API_BASE + "/NIPSWeb/managed_play?managedid=" + item.managedid;
|
||||
} else {
|
||||
throw new Error("Unsure how to handle this!\r\n\r\n" + JSON.stringify(item));
|
||||
}
|
||||
el.oncanplay = () => {
|
||||
dispatch(playerState.actions.itemLoadComplete({ player }));
|
||||
}
|
||||
el.load();
|
||||
const sauce = audioContext.createMediaElementSource(el);
|
||||
const gain = audioContext.createGain();
|
||||
sauce.connect(gain);
|
||||
gain.connect(destination);
|
||||
console.log("Connected to", destination);
|
||||
playerSources[player] = sauce;
|
||||
playerGains[player] = gain;
|
||||
}
|
||||
|
||||
export const play = (player: number): AppThunk => dispatch => {
|
||||
try{
|
||||
playerSources[player].mediaElement.play();
|
||||
dispatch(playerState.actions.setPlayerState({ player, state: "playing" }));
|
||||
playerSources[player].mediaElement.addEventListener("ended", function(){dispatch(playerState.actions.setPlayerState({ player, state: "stopped" }));})
|
||||
} catch {
|
||||
console.log("nothing selected/loaded");
|
||||
}
|
||||
};
|
||||
|
||||
export const pause = (player: number): AppThunk => dispatch => {
|
||||
try{
|
||||
if (playerSources[player].mediaElement.paused) {
|
||||
playerSources[player].mediaElement.play();
|
||||
dispatch(playerState.actions.setPlayerState({ player, state: "playing" }));
|
||||
} else {
|
||||
playerSources[player].mediaElement.pause();
|
||||
dispatch(playerState.actions.setPlayerState({ player, state: "paused" }));
|
||||
}
|
||||
} catch {
|
||||
console.log("nothing selected/loaded");
|
||||
}
|
||||
};
|
||||
|
||||
export const stop = (player: number): AppThunk => dispatch => {
|
||||
try{
|
||||
playerSources[player].mediaElement.pause();
|
||||
playerSources[player].mediaElement.currentTime = 0;
|
||||
dispatch(playerState.actions.setPlayerState({ player, state: "stopped" }));
|
||||
} catch {
|
||||
console.log("nothing selected/loaded");
|
||||
}
|
||||
};
|
37
yarn.lock
37
yarn.lock
|
@ -2138,6 +2138,19 @@ bcrypt-pbkdf@^1.0.0:
|
|||
dependencies:
|
||||
tweetnacl "^0.14.3"
|
||||
|
||||
between.js@^0.1.2-fix.2:
|
||||
version "0.1.2-fix.2"
|
||||
resolved "https://registry.yarnpkg.com/between.js/-/between.js-0.1.2-fix.2.tgz#3cdc2b728decf7fc1c3c66042afe98cbaedb3259"
|
||||
integrity sha512-9rkmzYDM6fW6LV99qD9cF17IjRRIQrOFKnZYuR1W2vqPGJtc9Y3UGs+UH6FJ0xqNYv0i9wRu+ZSqG6gM1xX1eg==
|
||||
dependencies:
|
||||
color "^3.0.0"
|
||||
color-string "^1.5.2"
|
||||
easing-functions "^1.0.1"
|
||||
lerp "^1.0.3"
|
||||
minivents "^2.2.0"
|
||||
raf "^3.4.0"
|
||||
rollup-plugin-alias "^1.4.0"
|
||||
|
||||
big.js@^5.2.2:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
||||
|
@ -3519,6 +3532,11 @@ duplexify@^3.4.2, duplexify@^3.6.0:
|
|||
readable-stream "^2.0.0"
|
||||
stream-shift "^1.0.0"
|
||||
|
||||
easing-functions@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/easing-functions/-/easing-functions-1.0.1.tgz#66eaadafb4593212b700e867553d876009848ef4"
|
||||
integrity sha512-5eHZ+InobokmzyYTs3BtL+bNoKVQiDF1iQoh1FQ0pWNAhhgqA2/o9z4LX/G6fq0jWFu16LdAxidxQi/fnRqfdw==
|
||||
|
||||
ecc-jsbn@~0.1.1:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
|
||||
|
@ -5991,6 +6009,11 @@ left-pad@^1.3.0:
|
|||
resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"
|
||||
integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==
|
||||
|
||||
lerp@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/lerp/-/lerp-1.0.3.tgz#a18c8968f917896de15ccfcc28d55a6b731e776e"
|
||||
integrity sha1-oYyJaPkXiW3hXM/MKNVaa3Med24=
|
||||
|
||||
leven@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
|
||||
|
@ -6364,6 +6387,11 @@ minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0:
|
|||
safe-buffer "^5.1.2"
|
||||
yallist "^3.0.0"
|
||||
|
||||
minivents@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minivents/-/minivents-2.2.0.tgz#559492edc04bc00b62168850af5b8ca06d5e088b"
|
||||
integrity sha1-VZSS7cBLwAtiFohQr1uMoG1eCIs=
|
||||
|
||||
minizlib@^1.2.1:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
|
||||
|
@ -8091,7 +8119,7 @@ raf-schd@^4.0.2:
|
|||
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0"
|
||||
integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==
|
||||
|
||||
raf@3.4.1:
|
||||
raf@3.4.1, raf@^3.4.0:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
|
||||
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
|
||||
|
@ -8721,6 +8749,13 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
|
|||
hash-base "^3.0.0"
|
||||
inherits "^2.0.1"
|
||||
|
||||
rollup-plugin-alias@^1.4.0:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/rollup-plugin-alias/-/rollup-plugin-alias-1.5.2.tgz#f15a1cc8ee0debf74ab5c2bb68a944a66b568411"
|
||||
integrity sha512-ODeZXhTxpD48sfcYLAFc1BGrsXKDj7o1CSNH3uYbdK3o0NxyMmaQPTNgW+ko+am92DLC8QSTe4kyxTuEkI5S5w==
|
||||
dependencies:
|
||||
slash "^3.0.0"
|
||||
|
||||
rsvp@^4.8.4:
|
||||
version "4.8.5"
|
||||
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
|
||||
|
|
Loading…
Reference in a new issue