Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const Router = () => (
<Route exact={true} path={routes.player(":id")}>
<PlayerPage />
</Route>
<Route exact={true} path={"/next/player"}>
<Route exact={true} path={routes.ribbonPlayer(":id")}>
<Player2Page />
</Route>
<Route path={routes.about()} exact={true} component={AboutPage} />
Expand Down
2 changes: 1 addition & 1 deletion src/app/assets/stop-circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 3 additions & 7 deletions src/app/assets/stop.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/app/components/explore/ExplorePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const ExplorePanel: React.FC<Props> = ({ audioset }) => {
title={audioset.meta.title}
backTo={audioset.meta.parent_path}
visuals={
<PanelVisuals audioset={audioset} activeClipId={playing.clipId} />
<PanelVisuals audioset={audioset} activeClipIds={[playing.clipId]} />
}
>
<div className="sidebar sm:pr-3">
Expand Down
18 changes: 13 additions & 5 deletions src/app/components/explore/PanelVisuals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,31 @@ import Spinner from "../Spinner";

type Props = {
audioset: Audioset;
activeClipId: string;
activeClipIds: string[];
};

const PanelVisuals: React.FC<Props> = ({ audioset, activeClipId }) => {
const PanelVisuals: React.FC<Props> = ({ audioset, activeClipIds }) => {
const { ref, width } = useDimensions<HTMLImageElement>();
if (audioset.visuals.mode !== "panel") return null;

const ratio = width / audioset.visuals.image.size.width;

const clip = audioset.index.clipById[activeClipId];

const radius = Math.floor(120 * ratio);

return (
<div className="h-full w-full flex flex-col items-start relative">
<img ref={ref} src={audioset.visuals.image.url} alt="fondo" />
{clip && <PlayingClip clip={clip} ratio={ratio} radius={radius} />}
{activeClipIds.map((clipId) => {
const clip = audioset.index.clipById[clipId];
return clip ? (
<PlayingClip
key={clip.id}
clip={clip}
ratio={ratio}
radius={radius}
/>
) : null;
})}
</div>
);
};
Expand Down
72 changes: 72 additions & 0 deletions src/app/components/next/Clip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { useState, useEffect } from "react";
import Spinner from "../Spinner";
import { ReactComponent as PlayIcon } from "../../assets/play-circle.svg";
import { Clip } from "../../../audioset";
import { AudioSampler } from "../../hooks/useAudioSampler";
import { ClipState } from "./player";

export type ClipProps = {
clip: Clip;
color: string;
keyboard: string;
state?: ClipState;
sampler: AudioSampler;

toggle: () => void;
};

const ClipView: React.FC<ClipProps> = ({
clip,
color,
keyboard,
state,
toggle,
sampler,
}) => {
const [hover, setHover] = useState(false);

const isRunning = state?.state === "start";

useEffect(() => {
if (state) {
const sample = sampler.clips[clip.id];
if (state.state === "start") sample.start(state.time);
else if (state.state === "stop") sample.stop(state.time);
}
}, [state, sampler, clip.id]);

return (
<button
key={clip.id}
className="ml-2 w-1/6 ratio rounded overflow-hidden relative"
onClick={toggle}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
<img
className={isRunning ? "opacity-75" : "opacity-100"}
alt="cover"
width="300"
height="300"
src={clip.resources.cover.thumb}
/>
<svg viewBox="0 0 1 1" />
<div className="absolute inset-0 flex items-center justify-center">
{hover ? (
isRunning ? (
<div className="w-6 h-6 bg-black rounded-sm" />
) : (
<PlayIcon className="w-12 h-12 text-black mt-2" />
)
) : isRunning ? (
<Spinner color={color} />
) : (
<div className="text-white bg-black w-8 h-8 uppercase rounded-full leading-8 text-center opacity-50">
{keyboard}
</div>
)}
</div>
</button>
);
};
export default ClipView;
73 changes: 73 additions & 0 deletions src/app/components/next/PlayerRibbon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { useReducer, useEffect, useState, useCallback } from "react";
import Layout from "../../components/layout/Layout";
import { Audioset } from "../../../audioset";
import Track from "./Track";
import player, { initialState, tick } from "./player";
import { getActiveAudioContext } from "../../../lib/active-audio-context";
import { IAudioContext } from "standardized-audio-context";
import { useAudioSampler } from "../../hooks/useAudioSampler";
import PanelVisuals from "../explore/PanelVisuals";

type Props = {
audioset: Audioset;
};

const PlayerRibbon: React.FC<Props> = ({ audioset }) => {
const [ctx, setContext] = useState<IAudioContext | null>(null);
const [state, dispatch] = useReducer(player, initialState());
const sampler = useAudioSampler(audioset);

const _tick = useCallback(() => {
if (!ctx) return;
const commands = tick(ctx.currentTime, state);
if (commands.length) {
dispatch({ type: "run", commands });
}
}, [ctx, state]);

useEffect(() => {
getActiveAudioContext().then(setContext);
}, []);

useEffect(() => {
const id = setInterval(_tick, 100);
return () => clearInterval(id);
}, [_tick]);

return (
<Layout
title={audioset.meta.title}
backTo="/"
visuals={
audioset.visuals.mode === "panel" && (
<PanelVisuals
audioset={audioset}
activeClipIds={state.activeClipIds}
/>
)
}
>
<div className="h-full noselect">
{!ctx && <div>Click to start</div>}
{audioset?.tracks.map((track) => (
<Track
key={track.id}
track={track}
sampler={sampler}
audioset={audioset}
state={state.tracks[track.id] || {}}
toggle={(clipId: string) =>
dispatch({
type: "trigger",
trigger: "toggle",
clipId,
trackId: track.id,
})
}
/>
))}
</div>
</Layout>
);
};
export default PlayerRibbon;
34 changes: 34 additions & 0 deletions src/app/components/next/Slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React, { useState } from "react";
import { useGestureResponder } from "react-gesture-responder";

type Props = {
label: string;
left: number;
onChange: (value: number) => void;
};

const Slider: React.FC<Props> = ({ label, left, onChange }) => {
const [deltaX, setDeltaX] = useState(0);

const { bind } = useGestureResponder({
onStartShouldSet: () => true,
onRelease: () => {
onChange(Math.floor(deltaX + left));
setDeltaX(0);
},
onMove: ({ delta }) => {
setDeltaX(delta[0]);
},
});
return (
<div className="ml-2 flex items-center flex-grow relative" {...bind}>
<label className="font-normal">{label}</label>
<div
className="absolute inset-y-0 right-0 bg-gray-dark bg-opacity-25"
style={{ left: Math.floor(deltaX + left) + "px" }}
/>
</div>
);
};

export default Slider;
77 changes: 77 additions & 0 deletions src/app/components/next/Track.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React, { useState } from "react";
import { Audioset, Track as TrackData } from "../../../audioset";
import { AudioSampler } from "../../hooks/useAudioSampler";
import Slider from "./Slider";
import { TrackState } from "./player";
import Clip from "./Clip";

export type Props = {
audioset: Audioset;
track: TrackData;
state: TrackState;
sampler: AudioSampler;
toggle: (clipId: string) => void;
};

const Track: React.FC<Props> = ({
track,
audioset,
toggle,
state,
sampler,
}) => {
const [isOpen, setOpen] = useState(true);
const [left, setLeft] = useState(30);

const current = Object.values(state).find(
(clipState) => clipState.state === "start"
);

// useEffect(() => {
// if (current) setOpen(false);
// }, [current]);

const label = track.name; // clipId ? audioset.index.clipById[clipId].title : track.name;

return (
<div className="flex p-2" style={{ backgroundColor: track.color }}>
<button
key={track.id}
className="w-1/6 ratio bg-gray-light rounded bg-opacity-50 overflow-hidden"
onClick={() => {
current && setOpen(!isOpen);
}}
>
{current && (
<img
alt="cover"
width="300"
height="300"
src={audioset.index.clipById[current.clipId].resources.cover.thumb}
/>
)}
<svg viewBox="0 0 1 1" />
</button>
{isOpen ? (
track.clipIds.map((id) => {
const clip = audioset.index.clipById[id];
return (
<Clip
key={id}
clip={clip}
sampler={sampler}
color={track.color}
state={state[id]}
keyboard={clip.keyMap}
toggle={() => toggle(id)}
/>
);
})
) : (
<Slider label={label} left={left} onChange={setLeft} />
)}
</div>
);
};

export default Track;
Loading