veza/apps/web/src/context/AudioContext.tsx

338 lines
9.7 KiB
TypeScript
Raw Normal View History

import React, {
createContext,
useContext,
useState,
useEffect,
useRef,
} from 'react';
import { Track } from '../types';
export interface VisualizerSettings {
mode: 'waveform' | 'spectrogram' | 'bars' | 'off';
color: string;
sensitivity: number;
}
interface AudioContextType {
currentTrack: Track | null;
isPlaying: boolean;
queue: Track[];
history: Track[];
progress: number; // 0-100
currentTime: number;
duration: number;
volume: number;
isMuted: boolean;
shuffle: boolean;
repeatMode: 'off' | 'all' | 'one';
playbackRate: number;
pitchCorrection: boolean;
visualizerSettings: VisualizerSettings;
autoplay: boolean;
// Actions
playTrack: (track: Track, context?: Track[]) => void;
togglePlay: () => void;
nextTrack: () => void;
prevTrack: () => void;
seek: (percent: number) => void;
setVolume: (val: number) => void;
toggleMute: () => void;
toggleShuffle: () => void;
toggleRepeat: () => void;
setPlaybackRate: (rate: number) => void;
togglePitchCorrection: () => void;
setVisualizerSettings: (settings: VisualizerSettings) => void;
addToQueue: (track: Track) => void;
removeFromQueue: (trackId: string) => void;
playNext: (track: Track) => void;
reorderQueue: (fromIndex: number, toIndex: number) => void;
clearQueue: () => void;
toggleAutoplay: () => void;
}
const AudioContext = createContext<AudioContextType | undefined>(undefined);
export const useAudio = () => {
const context = useContext(AudioContext);
if (!context) throw new Error('useAudio must be used within AudioProvider');
return context;
};
// Mock Data for Initial State
const mockTracks: Track[] = [
{
id: '1',
title: 'Neon Nightrider',
artist: 'Cyber_Punk_OST',
album: 'Night City Vol.1',
duration: '3:45',
durationSec: 225,
plays: 12000,
like_count: 3400,
coverUrl: 'https://picsum.photos/id/55/400/400',
isPremium: true,
waveformData: Array.from({ length: 100 }, () => Math.random()),
lyrics: [
{ time: 10, text: 'Neon lights flickering...' },
{ time: 15, text: 'Driving through the cyber city' },
{ time: 20, text: 'Bass dropping heavy on the pavement' },
],
},
{
id: '2',
title: 'Glitch in the Matrix',
artist: 'Null Pointer',
album: 'System Failure',
duration: '4:20',
durationSec: 260,
plays: 8500,
like_count: 2100,
coverUrl: 'https://picsum.photos/id/58/400/400',
waveformData: Array.from({ length: 100 }, () => Math.random()),
},
{
id: '3',
title: 'Tokyo Drift (Lofi)',
artist: 'Sakura Beats',
album: 'Chillhop Essentials',
duration: '2:55',
durationSec: 175,
plays: 45000,
like_count: 12000,
coverUrl: 'https://picsum.photos/id/60/400/400',
isPremium: true,
waveformData: Array.from({ length: 100 }, () => Math.random()),
},
{
id: '4',
title: 'Neural Link',
artist: 'Mainframe',
album: 'AI Dreams',
duration: '5:10',
durationSec: 310,
plays: 2300,
like_count: 450,
coverUrl: 'https://picsum.photos/id/70/200/200',
waveformData: Array.from({ length: 100 }, () => Math.random()),
},
{
id: '5',
title: 'Synthwave Sunset',
artist: 'Retro Boy',
album: 'Analog Memories',
duration: '3:30',
durationSec: 210,
plays: 1200,
like_count: 300,
coverUrl: 'https://picsum.photos/id/80/200/200',
waveformData: Array.from({ length: 100 }, () => Math.random()),
},
] as unknown as Track[];
export const AudioProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [currentTrack, setCurrentTrack] = useState<Track | null>(mockTracks[0]);
const [queue, setQueue] = useState<Track[]>(mockTracks.slice(1));
const [history, setHistory] = useState<Track[]>([]);
const [isPlaying, setIsPlaying] = useState(false);
const [progress, setProgress] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
const [volume, setVolumeState] = useState(80);
const [isMuted, _setIsMuted] = useState(false);
const [shuffle, setShuffle] = useState(false);
const [repeatMode, setRepeatMode] = useState<'off' | 'all' | 'one'>('off');
// Phase 8 & 9 New States
const [playbackRate, setPlaybackRate] = useState(1.0);
const [pitchCorrection, setPitchCorrection] = useState(true);
const [visualizerSettings, setVisualizerSettings] =
useState<VisualizerSettings>({
mode: 'waveform',
color: '#66FCF1',
sensitivity: 50,
});
const [autoplay, setAutoplay] = useState(true);
const audioInterval = useRef<number | null>(null);
// Simulation of Audio Playback
useEffect(() => {
if (isPlaying && currentTrack) {
audioInterval.current = window.setInterval(() => {
setCurrentTime((prev) => {
if (prev >= currentTrack.durationSec) {
// Track finished
if (repeatMode === 'one') {
return 0; // Restart
}
if (queue.length > 0 || autoplay) {
nextTrack(); // Auto next logic handled there
} else {
setIsPlaying(false);
return prev;
}
return 0;
}
// Adjust increment based on playback rate
return prev + 1 * playbackRate;
});
}, 1000 / playbackRate); // Interval adjusts to speed
} else {
if (audioInterval.current) clearInterval(audioInterval.current);
}
return () => {
if (audioInterval.current) clearInterval(audioInterval.current);
};
}, [isPlaying, currentTrack, repeatMode, playbackRate, queue, autoplay]);
// Sync Progress Percentage
useEffect(() => {
if (currentTrack) {
setProgress((currentTime / currentTrack.durationSec) * 100);
}
}, [currentTime, currentTrack]);
const playTrack = (track: Track, context?: Track[]) => {
if (currentTrack && currentTrack.id !== track.id) {
setHistory((prev) => [...prev, currentTrack]);
}
setCurrentTrack(track);
if (context) {
const trackIndex = context.findIndex((t) => t.id === track.id);
if (trackIndex !== -1) {
setQueue(context.slice(trackIndex + 1));
}
}
setIsPlaying(true);
setCurrentTime(0);
};
const togglePlay = () => setIsPlaying(!isPlaying);
const nextTrack = () => {
if (queue.length > 0) {
const next = shuffle
? queue[Math.floor(Math.random() * queue.length)]
: queue[0];
setHistory((prev) => (currentTrack ? [...prev, currentTrack] : prev));
if (repeatMode !== 'all') {
setQueue((prev) => prev.filter((t) => t.id !== next.id));
} else {
setQueue((prev) => [...prev.filter((t) => t.id !== next.id), next]);
}
setCurrentTrack(next);
setCurrentTime(0);
setIsPlaying(true);
} else if (autoplay) {
// Mock Autoplay logic: Pick random track from "Network"
// In real app, this fetches recommendation
const randomMock =
mockTracks[Math.floor(Math.random() * mockTracks.length)];
// Don't add to history if it's autoplay transition usually, but for mock simplicty:
setHistory((prev) => (currentTrack ? [...prev, currentTrack] : prev));
setCurrentTrack({
...randomMock,
id: `auto-${Date.now()}`,
title: `Autoplay: ${randomMock.title}`,
});
setCurrentTime(0);
setIsPlaying(true);
} else {
setIsPlaying(false);
setCurrentTime(0);
}
};
const prevTrack = () => {
if (currentTime > 3) {
setCurrentTime(0);
} else if (history.length > 0) {
const prev = history[history.length - 1];
setQueue((prevQ) => (currentTrack ? [currentTrack, ...prevQ] : prevQ));
setHistory((prevH) => prevH.slice(0, -1));
setCurrentTrack(prev);
setCurrentTime(0);
setIsPlaying(true);
}
};
const seek = (percent: number) => {
if (currentTrack) {
const newTime = (percent / 100) * currentTrack.durationSec;
setCurrentTime(newTime);
setProgress(percent);
}
};
const setVolume = (val: number) => setVolumeState(val);
const toggleMute = () => setIsPlaying((prev) => !prev); // Simplified mock
const toggleShuffle = () => setShuffle(!shuffle);
const toggleRepeat = () => {
const modes: ('off' | 'all' | 'one')[] = ['off', 'all', 'one'];
const next = modes[(modes.indexOf(repeatMode) + 1) % modes.length];
setRepeatMode(next);
};
const togglePitchCorrection = () => setPitchCorrection(!pitchCorrection);
const toggleAutoplay = () => setAutoplay(!autoplay);
const addToQueue = (track: Track) => setQueue((prev) => [...prev, track]);
const playNext = (track: Track) => setQueue((prev) => [track, ...prev]);
const removeFromQueue = (id: string) =>
setQueue((prev) => prev.filter((t) => t.id !== id));
const clearQueue = () => setQueue([]);
const reorderQueue = (fromIndex: number, toIndex: number) => {
const result = Array.from(queue);
const [removed] = result.splice(fromIndex, 1);
result.splice(toIndex, 0, removed);
setQueue(result);
};
return (
<AudioContext.Provider
value={{
currentTrack,
isPlaying,
queue,
history,
progress,
currentTime,
duration: currentTrack?.durationSec || 0,
volume,
isMuted,
shuffle,
repeatMode,
playbackRate,
pitchCorrection,
visualizerSettings,
autoplay,
playTrack,
togglePlay,
nextTrack,
prevTrack,
seek,
setVolume,
toggleMute,
toggleShuffle,
toggleRepeat,
setPlaybackRate,
togglePitchCorrection,
setVisualizerSettings,
toggleAutoplay,
addToQueue,
removeFromQueue,
playNext,
reorderQueue,
clearQueue,
}}
>
{children}
</AudioContext.Provider>
);
};