907 lines
32 KiB
TypeScript
907 lines
32 KiB
TypeScript
/*
|
|
* Copyright (c) 2025 Robin E. R. Davies
|
|
* All rights reserved.
|
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*/
|
|
|
|
import React, { useEffect } from 'react';
|
|
import { styled } from '@mui/material/styles';
|
|
import LoopDialog from './LoopDialog';
|
|
import Box from '@mui/material/Box';
|
|
import Typography from '@mui/material/Typography';
|
|
import ButtonEx from './ButtonEx';
|
|
import IconButtonEx from './IconButtonEx';
|
|
import Pause from '@mui/icons-material/Pause';
|
|
import PlayArrow from '@mui/icons-material/PlayArrow';
|
|
import FastForward from '@mui/icons-material/FastForward';
|
|
import FastRewind from '@mui/icons-material/FastRewind';
|
|
import { PiPedalModelFactory, State } from './PiPedalModel';
|
|
import { pathFileNameOnly } from './FileUtils';
|
|
import ButtonBase from '@mui/material/ButtonBase';
|
|
import FilePropertyDialog from './FilePropertyDialog';
|
|
import JsonAtom from './JsonAtom';
|
|
import { UiFileProperty } from './Lv2Plugin';
|
|
import Divider from '@mui/material/Divider';
|
|
import useWindowSize from './UseWindowSize';
|
|
import { getAlbumArtUri } from './AudioFileMetadata';
|
|
import RepeatIcon from '@mui/icons-material/Repeat';
|
|
import Timebase, { LoopParameters, TimebaseUnits } from './Timebase';
|
|
import ControlSlider from './ControlSlider';
|
|
import { getTrackTitle } from './AudioFileMetadata';
|
|
|
|
|
|
let Player__seek = "http://two-play.com/plugins/toob-player#seek"
|
|
const AUDIO_FILE_PROPERTY_URI = "http://two-play.com/plugins/toob-player#audioFile";
|
|
const LOOP_PROPERTY_URI = "http://two-play.com/plugins/toob-player#loop";
|
|
class PluginState {
|
|
// Must match ProcessorState values in Lv2AudioFileProcessor.hpp
|
|
// Not an enum because it has to be json serializable.
|
|
static Idle = 0;
|
|
static Recording = 1;
|
|
static StoppingRecording = 2;
|
|
static CuePlayingThenPlay = 3;
|
|
static CuePlayingThenPause = 4;
|
|
static Paused = 5;
|
|
static Playing = 6;
|
|
static Error = 7;
|
|
};
|
|
|
|
const useWallpaper = false;
|
|
const WallPaper = styled('div')({
|
|
position: 'absolute',
|
|
width: '100%',
|
|
height: '100%',
|
|
top: 0,
|
|
left: 0,
|
|
overflow: 'hidden',
|
|
background: 'linear-gradient(rgb(255, 38, 142) 0%, rgb(255, 105, 79) 100%)',
|
|
transition: 'all 500ms cubic-bezier(0.175, 0.885, 0.32, 1.275) 0s',
|
|
'&::before': {
|
|
content: '""',
|
|
width: '140%',
|
|
height: '140%',
|
|
position: 'absolute',
|
|
top: '-40%',
|
|
right: '-50%',
|
|
background:
|
|
'radial-gradient(at center center, rgb(62, 79, 249) 0%, rgba(62, 79, 249, 0) 64%)',
|
|
},
|
|
'&::after': {
|
|
content: '""',
|
|
width: '140%',
|
|
height: '140%',
|
|
position: 'absolute',
|
|
bottom: '-50%',
|
|
left: '-30%',
|
|
background:
|
|
'radial-gradient(at center center, rgb(247, 237, 225) 0%, rgba(247, 237, 225, 0) 70%)',
|
|
transform: 'rotate(30deg)',
|
|
},
|
|
});
|
|
|
|
function tidyHundredths(value: number): string {
|
|
if (value === 0) {
|
|
return "";
|
|
} else if (value % 10 == 0) {
|
|
return `.${(value / 10).toString()}`;
|
|
} else {
|
|
return `.${value.toString()}`;
|
|
}
|
|
|
|
}
|
|
export function formatTimeCompact(timebase: Timebase, sampleRate: number, seconds: number): string {
|
|
switch (timebase.units) {
|
|
case TimebaseUnits.Samples:
|
|
{
|
|
return Math.round(seconds * sampleRate).toString();
|
|
}
|
|
break;
|
|
case TimebaseUnits.Seconds:
|
|
{
|
|
const t = Math.round(seconds * 100);
|
|
let hundredths = t % 100;
|
|
let secs = Math.floor(t / 100);
|
|
let minutes = Math.floor(secs / 60);
|
|
let hours = Math.floor(minutes / 60);
|
|
minutes = minutes % 60;
|
|
secs = secs % 60;
|
|
|
|
if (hours == 0) {
|
|
return `${minutes}:${secs.toString().padStart(2, '0')}${tidyHundredths(hundredths)}`;
|
|
} else {
|
|
return `${hours}:${minutes}:${secs.toString().padStart(2, '0')}${tidyHundredths(hundredths)}`;
|
|
}
|
|
break;
|
|
}
|
|
case TimebaseUnits.Beats:
|
|
{
|
|
let t = Math.round(seconds * timebase.tempo / 60.0 * 100.0);
|
|
let hundredths = t % 100;
|
|
let beats = Math.floor(t / 100.0);
|
|
let bars = Math.floor(beats / timebase.timeSignature.numerator);
|
|
let beat = (beats - bars * timebase.timeSignature.numerator);
|
|
|
|
return `${bars + 1}:${(beat + 1)}${tidyHundredths(hundredths)}`;
|
|
}
|
|
break;
|
|
default:
|
|
throw new Error("Unsupported timebase units");
|
|
}
|
|
}
|
|
|
|
function timebaseEqual(a: Timebase, b: Timebase): boolean {
|
|
if (a.units !== b.units) {
|
|
return false;
|
|
}
|
|
if (a.tempo !== b.tempo) {
|
|
return false;
|
|
}
|
|
if (a.timeSignature.numerator !== b.timeSignature.numerator) {
|
|
return false;
|
|
}
|
|
if (a.timeSignature.denominator !== b.timeSignature.denominator) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
function getAlbumLine(album: string, artist: string, albumArtist: string): string {
|
|
if (artist === "") {
|
|
artist = albumArtist;
|
|
}
|
|
let joiner = (artist !== "" && album !== "") ? " - " : "";
|
|
return album + joiner + artist;
|
|
|
|
}
|
|
|
|
interface WidgetProps {
|
|
noBorders: boolean;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
const WidgetBorders = styled('div')(({ theme }) => ({
|
|
|
|
padding: 16,
|
|
borderRadius: 16,
|
|
minWidth: 300,
|
|
maxWidth: 800,
|
|
width: "70%",
|
|
margin: 'auto',
|
|
position: 'relative',
|
|
zIndex: 1,
|
|
backgroundColor: 'rgba(255,255,255,0.4)',
|
|
boxShadow: "1px 4px 12px rgba(0,0,0,0.2)",
|
|
...theme.applyStyles('dark', {
|
|
backgroundColor: useWallpaper ? 'rgba(0,0,0,0.6)' : '#282828',
|
|
}),
|
|
}));
|
|
|
|
const WidgetNoBorders = styled('div')(({ theme }) => ({
|
|
|
|
padding: 16,
|
|
borderRadius: 0,
|
|
width: "100%",
|
|
height: "100%",
|
|
margin: 0,
|
|
position: 'relative',
|
|
zIndex: 1,
|
|
backgroundColor: 'rgba(255,255,255,0.4)',
|
|
...theme.applyStyles('dark', {
|
|
backgroundColor: useWallpaper ? 'rgba(0,0,0,0.6)' : theme.mainBackground,
|
|
}),
|
|
}));
|
|
// const WidgetNoWallpaper = styled('div')(({ theme }) => ({
|
|
|
|
// padding: 16,
|
|
// borderRadius: 0,
|
|
// width: "100%",
|
|
// height: "100%",
|
|
// margin: 0,
|
|
// position: 'relative',
|
|
// zIndex: 1,
|
|
|
|
// }));
|
|
|
|
|
|
function Widget(props: WidgetProps) {
|
|
// if (!useWallpaper) {
|
|
// return(<WidgetNoWallpaper>{props.children} </WidgetNoWallpaper>);
|
|
// }
|
|
if (props.noBorders) {
|
|
return (<WidgetNoBorders>{props.children} </WidgetNoBorders>);
|
|
} else {
|
|
return (<WidgetBorders>{props.children} </WidgetBorders>);
|
|
}
|
|
}
|
|
|
|
const CoverImage = styled('div')({
|
|
width: 60,
|
|
height: 60,
|
|
objectFit: 'cover',
|
|
overflow: 'hidden',
|
|
flexShrink: 0,
|
|
borderRadius: 8,
|
|
backgroundColor: 'rgba(0,0,0,0.08)',
|
|
'& > img': {
|
|
width: '100%',
|
|
},
|
|
});
|
|
|
|
|
|
|
|
export interface ToobPlayerControlProps {
|
|
instanceId: number;
|
|
extraControls: React.ReactElement[];
|
|
}
|
|
|
|
export default function ToobPlayerControl(
|
|
props: ToobPlayerControlProps
|
|
) {
|
|
|
|
const model = PiPedalModelFactory.getInstance();
|
|
const sampleRate = model.jackConfiguration.get().sampleRate == 0 ?
|
|
48000 :
|
|
model.jackConfiguration.get().sampleRate;
|
|
|
|
|
|
const defaultCoverArt = "/img/default_album.jpg";
|
|
const [serverConnected, setServerConnected] = React.useState(model.state.get() == State.Ready);
|
|
const [duration, setDuration] = React.useState(0.0);
|
|
const [start, setStart] = React.useState(0.0);
|
|
const [loopStart, setLoopStart] = React.useState(0.0);
|
|
const [loopEnable, setLoopEnable] = React.useState(false);
|
|
const [loopEnd, setLoopEnd] = React.useState(0.0);
|
|
const [pluginState, setPluginState] = React.useState(0.0);
|
|
const [coverArt, setCoverArt] = React.useState(defaultCoverArt);
|
|
const [audioFile, setAudioFile] = React.useState("");
|
|
const [position, setPosition] = React.useState(0.0);
|
|
//let position = 0;
|
|
const [title, setTitle] = React.useState("");
|
|
const [album, setAlbum] = React.useState("");
|
|
const [artist, setArtist] = React.useState("");
|
|
const [albumArtist, setAlbumArtist] = React.useState("");
|
|
const [showFileDialog, setShowFileDialog] = React.useState(false);
|
|
const [showLoopDialog, setShowLoopDialog] = React.useState(false);
|
|
const [timebase, setTimebase] = React.useState<Timebase>(
|
|
{
|
|
units: TimebaseUnits.Seconds,
|
|
tempo: 120.0,
|
|
timeSignature: { numerator: 4, denominator: 4 }
|
|
});
|
|
const [loopParameters, setLoopParameters] = React.useState<LoopParameters>({
|
|
start: 0.0,
|
|
loopEnable: false,
|
|
loopStart: 0.0,
|
|
loopEnd: 0.0
|
|
});
|
|
|
|
|
|
const [size] = useWindowSize();
|
|
const width = size.width;
|
|
const height = size.height;
|
|
|
|
const HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK = 500;
|
|
|
|
const useHorizontalLayout = height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK && width > 573 && width > height;
|
|
//const useVerticalScroll = width < 573;
|
|
const noBorders = width < 420 || height < 720;
|
|
let useQuadMixPanel = width < 720;
|
|
if (noBorders) {
|
|
useQuadMixPanel = width < 370;
|
|
}
|
|
|
|
function SelectFile() {
|
|
setShowFileDialog(true);
|
|
}
|
|
function onAudioFileChanged(path: string) {
|
|
setAudioFile(path);
|
|
if (path === "") {
|
|
setTitle("");
|
|
setAlbum("");
|
|
setArtist("");
|
|
setAlbumArtist("");
|
|
setCoverArt(defaultCoverArt);
|
|
return;
|
|
}
|
|
model.getAudioFileMetadata(path)
|
|
.then((metadata) => {
|
|
let coverArtUri = getAlbumArtUri(model, metadata, path);
|
|
setCoverArt(coverArtUri);
|
|
setTitle(getTrackTitle(path, metadata));
|
|
setAlbum(metadata.album);
|
|
setArtist(metadata.artist);
|
|
setAlbumArtist(metadata.albumArtist);
|
|
})
|
|
.catch((e) => {
|
|
setTitle("#error" + e.message);
|
|
setAlbum("");
|
|
});
|
|
|
|
}
|
|
function ControlCluster() {
|
|
return (
|
|
<Box
|
|
sx={(theme) => ({
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
mt: -1,
|
|
'& svg': {
|
|
color: '#000',
|
|
...theme.applyStyles('dark', {
|
|
color: '#fff',
|
|
}),
|
|
},
|
|
})}
|
|
>
|
|
<IconButtonEx tooltip="Previous track"
|
|
style={{ opacity: (pluginState === PluginState.Idle ? 0.2 : 0.7) }}
|
|
onClick={() => {
|
|
if (position > start + 3.0) {
|
|
model.sendPedalboardControlTrigger(
|
|
props.instanceId,
|
|
"stop",
|
|
1
|
|
);
|
|
} else {
|
|
onPreviousTrack();
|
|
}
|
|
}}
|
|
>
|
|
<FastRewind fontSize="large" />
|
|
</IconButtonEx>
|
|
<IconButtonEx
|
|
tooltip={paused ? "Play" : "Pause"}
|
|
|
|
style={{ opacity: (pluginState === PluginState.Idle ? 0.2 : 0.7) }}
|
|
onClick={() => {
|
|
if (pluginState != PluginState.Idle) {
|
|
if (paused) {
|
|
model.sendPedalboardControlTrigger(
|
|
props.instanceId,
|
|
"play",
|
|
1
|
|
);
|
|
} else {
|
|
model.sendPedalboardControlTrigger(
|
|
props.instanceId,
|
|
"pause",
|
|
1
|
|
);
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
{paused ? (
|
|
<PlayArrow sx={{ fontSize: '3rem' }} />
|
|
) : (
|
|
<Pause sx={{ fontSize: '3rem' }} />
|
|
)}
|
|
</IconButtonEx>
|
|
<IconButtonEx tooltip="Next track"
|
|
style={{ opacity: (pluginState == PluginState.Idle ? 0.2 : 0.7) }}
|
|
onClick={() => { onNextTrack(); }}
|
|
>
|
|
<FastForward fontSize="large" />
|
|
</IconButtonEx>
|
|
</Box>
|
|
|
|
);
|
|
}
|
|
function FilePanel() {
|
|
let textColor = pluginState == PluginState.Error ? "error" : "textPrimary";
|
|
|
|
return (
|
|
<ButtonBase style={{ display: "block", width: "100%", borderRadius: 10, textAlign: "left" }}
|
|
onClick={() => { SelectFile() }}
|
|
>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', padding: "2px" }}>
|
|
<CoverImage>
|
|
<img style={{ opacity: pluginState === PluginState.Idle ? 0.3 : 1.0 }}
|
|
onDragStart={(e) => {
|
|
e.preventDefault();
|
|
}}
|
|
alt="Cover Art"
|
|
src={
|
|
coverArt
|
|
}
|
|
/>
|
|
</CoverImage>
|
|
<Box sx={{ ml: 1.5, minWidth: 0 }}>
|
|
{pluginState === PluginState.Idle ? (
|
|
<Typography variant="caption" noWrap>
|
|
Tap to select
|
|
</Typography>
|
|
) : (
|
|
<div>
|
|
<Typography variant="body1" color={textColor} noWrap>
|
|
{titleLine}
|
|
</Typography>
|
|
<Typography variant="body2" color={textColor} noWrap sx={{}}>
|
|
{albumLine}
|
|
</Typography>
|
|
</div>
|
|
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
</ButtonBase>
|
|
);
|
|
|
|
}
|
|
function loopButtonText() {
|
|
if (start === 0 && !loopEnable) {
|
|
return "Set loop";
|
|
|
|
} else if (loopEnable) {
|
|
return `${formatTimeCompact(timebase, sampleRate, start)} [${formatTimeCompact(timebase, sampleRate, loopStart)} - ${formatTimeCompact(timebase, sampleRate, loopEnd)}]`;
|
|
} else {
|
|
return `Start: ${formatTimeCompact(timebase, sampleRate, start)}`;
|
|
}
|
|
}
|
|
function getUiFileProperty(uri: string): UiFileProperty {
|
|
let pedalboardItem = model.pedalboard.get().getItem(props.instanceId);
|
|
let uiPlugin = model.getUiPlugin(pedalboardItem.uri);
|
|
if (!uiPlugin) {
|
|
throw "uiPlugin not found.";
|
|
}
|
|
for (let property of uiPlugin.fileProperties) {
|
|
if (property.patchProperty === uri) {
|
|
return property;
|
|
}
|
|
}
|
|
throw "FileProperty not found.";
|
|
}
|
|
|
|
function onLoopPropertyChanged(loopSettingsJson: string) {
|
|
try {
|
|
if (loopSettingsJson === "") {
|
|
setTimebase(
|
|
{
|
|
units: TimebaseUnits.Seconds,
|
|
tempo: 120.0,
|
|
timeSignature: { numerator: 4, denominator: 4 }
|
|
})
|
|
let loopParameters = {
|
|
start: 0.0,
|
|
loopEnable: false,
|
|
loopStart: 0.0,
|
|
loopEnd: 0.0
|
|
};
|
|
setLoopParameters(loopParameters);
|
|
|
|
setStart(0.0);
|
|
setLoopEnable(false);
|
|
setLoopStart(0.0);
|
|
setLoopEnd(0.0);
|
|
return;
|
|
}
|
|
let atomObject = JSON.parse(loopSettingsJson);
|
|
let loopParameters: LoopParameters = atomObject.loopParameters as LoopParameters;
|
|
let newTimebase: Timebase | undefined = atomObject.timebase as (Timebase | undefined);;
|
|
if (newTimebase !== undefined) {
|
|
if (!timebaseEqual(timebase, newTimebase)) {
|
|
setTimebase(newTimebase);
|
|
}
|
|
setLoopParameters(loopParameters);
|
|
setLoopEnable(loopParameters.loopEnable);
|
|
setStart(loopParameters.start);
|
|
setLoopStart(loopParameters.loopStart);
|
|
setLoopEnd(loopParameters.loopEnd);
|
|
} else {
|
|
throw new Error("Invalid loop settings.");
|
|
}
|
|
} catch (e) {
|
|
console.warn("Unable to parse loop settings.");
|
|
setTimebase(
|
|
{
|
|
units: TimebaseUnits.Seconds,
|
|
tempo: 120.0,
|
|
timeSignature: { numerator: 4, denominator: 4 }
|
|
})
|
|
setLoopEnable(false);
|
|
setStart(0.0);
|
|
setLoopStart(0.0);
|
|
setLoopEnd(0.0);
|
|
}
|
|
|
|
}
|
|
function onNextTrack() {
|
|
model.getNextAudioFile(audioFile)
|
|
.then((file) => {
|
|
let json = JsonAtom._Path(file).asAny();
|
|
model.setPatchProperty(
|
|
props.instanceId,
|
|
AUDIO_FILE_PROPERTY_URI,
|
|
json);
|
|
})
|
|
.catch(() => {
|
|
});
|
|
|
|
}
|
|
function onPreviousTrack() {
|
|
model.getPreviousAudioFile(audioFile)
|
|
.then((file) => {
|
|
let json = JsonAtom._Path(file).asAny();
|
|
model.setPatchProperty(
|
|
props.instanceId,
|
|
AUDIO_FILE_PROPERTY_URI,
|
|
json);
|
|
})
|
|
.catch(() => {
|
|
});
|
|
}
|
|
function OnSeek(value: number) {
|
|
model.setPatchProperty(props.instanceId, Player__seek, value)
|
|
.then(() => {
|
|
}).catch((e) => {
|
|
console.warn("Seek error. " + e.toString());
|
|
});
|
|
}
|
|
function onStateChanged(value: State) {
|
|
setServerConnected(value === State.Ready);
|
|
}
|
|
useEffect(() => {
|
|
model.state.addOnChangedHandler(onStateChanged);
|
|
if (model.state.get() !== State.Ready) {
|
|
// wait for it.
|
|
return () => {
|
|
model.state.removeOnChangedHandler(onStateChanged);
|
|
}
|
|
}
|
|
let durationHandle = model.monitorPort(props.instanceId, "duration", 1.0 / 15,
|
|
(value) => {
|
|
setDuration(value);
|
|
}
|
|
);
|
|
let positionHandle = model.monitorPort(props.instanceId, "position", 1.0,
|
|
(value) => {
|
|
setPosition(value);
|
|
}
|
|
);
|
|
|
|
let pluginStateHandle = model.monitorPort(props.instanceId, "state", 1.0 / 1000.0,
|
|
(value) => {
|
|
setPluginState(value);
|
|
}
|
|
);
|
|
let filePropertyHandle = model.monitorPatchProperty(
|
|
props.instanceId,
|
|
AUDIO_FILE_PROPERTY_URI,
|
|
(instanceId: number, propertyUri: string, atomObject: any) => {
|
|
if (typeof (atomObject) === "object") {
|
|
let path = atomObject.value;
|
|
onAudioFileChanged(path);
|
|
} else if (typeof (atomObject) === "string") {
|
|
onAudioFileChanged(atomObject as string);
|
|
}
|
|
}
|
|
|
|
);
|
|
let loopPropertyHandle = model.monitorPatchProperty(
|
|
props.instanceId,
|
|
LOOP_PROPERTY_URI,
|
|
(instanceId: number, propertyUri: string, atomObject: any) => {
|
|
if (typeof (atomObject) === "string") {
|
|
onLoopPropertyChanged(atomObject as string);
|
|
}
|
|
}
|
|
|
|
);
|
|
|
|
model.getPatchProperty(props.instanceId, AUDIO_FILE_PROPERTY_URI)
|
|
.then((o) => {
|
|
let path = o.value;
|
|
onAudioFileChanged(path);
|
|
});
|
|
model.getPatchProperty(props.instanceId, LOOP_PROPERTY_URI)
|
|
.then((o) => {
|
|
onLoopPropertyChanged(o as string);
|
|
})
|
|
|
|
|
|
return () => {
|
|
model.state.removeOnChangedHandler(onStateChanged);
|
|
model.unmonitorPort(durationHandle);
|
|
model.unmonitorPort(pluginStateHandle);
|
|
model.unmonitorPort(positionHandle);
|
|
// model.unmonitorPort(loopEnableHandle);
|
|
// model.unmonitorPort(startHandle);
|
|
// model.unmonitorPort(loopStartHandle);
|
|
// model.unmonitorPort(loopEndHandle);
|
|
model.cancelMonitorPatchProperty(filePropertyHandle);
|
|
model.cancelMonitorPatchProperty(loopPropertyHandle);
|
|
};
|
|
},
|
|
[serverConnected]
|
|
);
|
|
const titleLine = title !== "" ? title : pathFileNameOnly(audioFile);
|
|
const albumLine = getAlbumLine(album, artist, albumArtist);
|
|
|
|
const paused = (pluginState === PluginState.Idle
|
|
|| pluginState === PluginState.CuePlayingThenPause
|
|
|| pluginState === PluginState.Paused
|
|
|| pluginState === PluginState.Error);
|
|
|
|
function handlePreview() {
|
|
if (paused) {
|
|
model.sendPedalboardControlTrigger(
|
|
props.instanceId,
|
|
"play",
|
|
1
|
|
);
|
|
} else {
|
|
model.sendPedalboardControlTrigger(
|
|
props.instanceId,
|
|
"stop",
|
|
1
|
|
);
|
|
}
|
|
}
|
|
function handleCancelPlaying() {
|
|
if (!paused) {
|
|
model.sendPedalboardControlTrigger(
|
|
props.instanceId,
|
|
"stop",
|
|
1
|
|
);
|
|
}
|
|
}
|
|
function LinearMixPanel() {
|
|
return (
|
|
<Box style={{ width: "100%" }} >
|
|
<div style={{ display: "flex", flex: "0 0 auto", justifyContent: "center", alignItems: 'center', flexFlow: "row nowrap" }}>
|
|
{props.extraControls}
|
|
</div>
|
|
</Box>
|
|
);
|
|
|
|
}
|
|
function QuadMixPanel() {
|
|
return (
|
|
<div style={{ display: "flex", width: "100%", flex: "0 0 auto", justifyContent: "center", alignItems: "center", flexFlow: "row nowrap" }}>
|
|
<div style={{ display: "flex", flex: "0 0 auto", gap: 8, alignItems: "center", flexFlow: "column nowrap" }}>
|
|
{props.extraControls[0]}
|
|
{props.extraControls[1]}
|
|
</div>
|
|
<div style={{ display: "flex", flex: "0 0 auto", gap: 8, alignItems: "center", flexFlow: "column nowrap" }}>
|
|
{props.extraControls[2]}
|
|
{props.extraControls[3]}
|
|
</div>
|
|
|
|
</div>
|
|
);
|
|
}
|
|
function SliderCluster() {
|
|
return (
|
|
|
|
<div style={{ position: "relative", display: "flex", flexFlow: "column nowrap" }}>
|
|
<div style={{ marginBottom: 16 }}>
|
|
<ControlSlider
|
|
instanceId={props.instanceId}
|
|
controlKey="position"
|
|
duration={duration}
|
|
onPreviewValue={(value) => {
|
|
}}
|
|
onValueChanged={(value) => {
|
|
OnSeek(value);
|
|
}}
|
|
/>
|
|
</div>
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
width: "100%",
|
|
display: "flex", flexFlow: "row nowrap", alignItems: "center",
|
|
justifyContent: "center",
|
|
bottom: 0
|
|
}}
|
|
>
|
|
<ButtonEx tooltip="Loop Settings" variant="dialogSecondary"
|
|
style={{
|
|
flex: "0 1 auto", maxWidth: 200,
|
|
textTransform: "none", padding: "4px 8px"
|
|
}}
|
|
disabled={audioFile === ""}
|
|
startIcon={(<RepeatIcon />)}
|
|
onClick={() => {
|
|
if (audioFile !== "") {
|
|
setShowLoopDialog(true);
|
|
}
|
|
}}
|
|
>
|
|
<Typography noWrap variant="caption">
|
|
{loopButtonText()
|
|
}
|
|
</Typography>
|
|
</ButtonEx>
|
|
</div>
|
|
</div >
|
|
);
|
|
}
|
|
|
|
|
|
function VerticalWidget() {
|
|
return (
|
|
<div style={{
|
|
position: "relative", width: "100%", height: "100%",
|
|
display: "flex", flexFlow: "column nowrap"
|
|
}} >
|
|
<div style={{ flex: "1 1 1px" }} />
|
|
|
|
<Widget noBorders={noBorders}>
|
|
{
|
|
FilePanel()
|
|
}
|
|
{SliderCluster()}
|
|
{
|
|
ControlCluster()
|
|
}
|
|
<Divider style={{ marginTop: 8, marginBottom: 8 }} />
|
|
{useQuadMixPanel ? QuadMixPanel()
|
|
:
|
|
LinearMixPanel()
|
|
}
|
|
</Widget>
|
|
<div style={{ flex: "2 2 1px" }} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function HorizontalWidget() {
|
|
return (
|
|
<div style={{
|
|
position: "relative", width: "100%", height: "100%",
|
|
display: "flex", flexFlow: "column nowrap"
|
|
}} >
|
|
|
|
<Widget noBorders={true}>
|
|
<div style={{
|
|
display: "flex", flexFlow: "row nowrap", alignItems: "center", justifyContent: "stretch",
|
|
width: "100%"
|
|
}}>
|
|
<div style={{
|
|
display: "flex", alignItems: "stretch", flexFlow: "column nowrap", flex: "1 1 100%"
|
|
|
|
}}>
|
|
<div style={{ display: "flex", flexFlow: "row nowrap", flex: "1 1 auto" }}>
|
|
<div style={{ position: "relative", display: "flex", flexFlow: "row nowrap", flex: "1 1 auto" }}>
|
|
{
|
|
FilePanel()
|
|
}
|
|
</div>
|
|
</div>
|
|
<div style={{ flex: "0 0 auto" }}>
|
|
{
|
|
SliderCluster()
|
|
}
|
|
</div>
|
|
</div>
|
|
<div style={{
|
|
position: "relative", display: "flex", flexFlow: "row nowrap", flex: "0 0 auto"
|
|
|
|
}}>
|
|
{
|
|
ControlCluster()
|
|
}
|
|
</div>
|
|
<div style={{ flex: "0 0 auto" }}>
|
|
{LinearMixPanel()}
|
|
</div>
|
|
</div>
|
|
|
|
|
|
</Widget>
|
|
<div style={{ flex: "2 2 1px" }} />
|
|
</div >
|
|
);
|
|
}
|
|
return (
|
|
<div id="toobPlayerViewFrame" style={{
|
|
width: '100%', height: "100%", overflow: 'hidden', position: 'relative',
|
|
}}>
|
|
{useWallpaper && (<WallPaper />)}
|
|
{
|
|
useHorizontalLayout ?
|
|
HorizontalWidget() : VerticalWidget()
|
|
}
|
|
{showLoopDialog && (
|
|
<LoopDialog isOpen={showLoopDialog}
|
|
sampleRate={
|
|
sampleRate
|
|
}
|
|
playing={!paused}
|
|
onPreview={() => { handlePreview(); }}
|
|
value={loopParameters}
|
|
onCancelPlaying={() => { handleCancelPlaying(); }}
|
|
timebase={timebase}
|
|
onTimebaseChange={(newTimebase) => {
|
|
setTimebase(newTimebase);
|
|
// model.setPatchProperty(
|
|
// props.instanceId,
|
|
// "timebase",
|
|
// newTimebase
|
|
// );
|
|
}}
|
|
|
|
onClose={() => {
|
|
setShowLoopDialog(false);
|
|
}}
|
|
onSetLoop={(loop: LoopParameters) => {
|
|
let loopSettings = {
|
|
timebase: timebase,
|
|
loopParameters: loop
|
|
};
|
|
model.setPatchProperty(
|
|
props.instanceId,
|
|
LOOP_PROPERTY_URI,
|
|
JSON.stringify(loopSettings));
|
|
|
|
setStart(loop.start);
|
|
setLoopEnable(loop.loopEnable);
|
|
setLoopStart(loop.loopStart);
|
|
setLoopEnd(loop.loopEnd);
|
|
}}
|
|
duration={duration}
|
|
/>
|
|
)}
|
|
{showFileDialog && (
|
|
<FilePropertyDialog open={showFileDialog}
|
|
fileProperty={getUiFileProperty(AUDIO_FILE_PROPERTY_URI)}
|
|
instanceId={props.instanceId}
|
|
selectedFile={audioFile}
|
|
onCancel={() => {
|
|
setShowFileDialog(false);
|
|
}}
|
|
onApply={(fileProperty, selectedFile) => {
|
|
model.setPatchProperty(
|
|
props.instanceId,
|
|
AUDIO_FILE_PROPERTY_URI,
|
|
JsonAtom._Path(selectedFile).asAny()
|
|
)
|
|
.then(() => {
|
|
|
|
})
|
|
.catch((error) => {
|
|
model.showAlert("Unable to complete the operation. " + error);
|
|
});
|
|
|
|
}}
|
|
onOk={(fileProperty, selectedFile) => {
|
|
|
|
model.setPatchProperty(
|
|
props.instanceId,
|
|
fileProperty.patchProperty,
|
|
JsonAtom._Path(selectedFile).asAny()
|
|
)
|
|
.then(() => {
|
|
|
|
})
|
|
.catch((error) => {
|
|
model.showAlert("Unable to complete the operation. " + error);
|
|
});
|
|
setShowFileDialog(false);
|
|
}
|
|
}
|
|
/>
|
|
|
|
)}
|
|
</div>
|
|
);
|
|
} |