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

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>
);
};