227 lines
9.1 KiB
TypeScript
227 lines
9.1 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()) },
|
|
];
|
|
|
|
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>
|
|
);
|
|
};
|