337 lines
9.7 KiB
TypeScript
337 lines
9.7 KiB
TypeScript
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>
|
|
);
|
|
};
|