waveframe 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -82,6 +82,8 @@ interface WaveformConfig {
82
82
  type PlayerState = {
83
83
  /** Whether the audio is currently playing */
84
84
  isPlaying: boolean;
85
+ /** Whether the audio is stalled (waiting for data) */
86
+ isStalled: boolean;
85
87
  /** The current playback time in seconds */
86
88
  currentTime: number;
87
89
  /** The total duration of the track in seconds */
@@ -172,7 +174,7 @@ declare class PlayerCore {
172
174
  }
173
175
 
174
176
  /**
175
- * Represents the complete state of the Waveframe engine, combining playback and analysis.
177
+ * Represents the complete state of the Waveframe controller, combining playback and analysis.
176
178
  */
177
179
  type EngineState = PlayerState & {
178
180
  /** The current set of generated or provided waveform peaks (0-1 range) */
@@ -194,21 +196,21 @@ type EngineListener = (state: EngineState) => void;
194
196
  *
195
197
  * @example
196
198
  * ```typescript
197
- * const engine = new WaveframeEngine();
199
+ * const controller = new WaveframeController();
198
200
  *
199
201
  * // Load from URL (automatic analysis if peaks omitted)
200
- * engine.load('https://example.com/audio.mp3');
202
+ * controller.load('https://example.com/audio.mp3');
201
203
  *
202
204
  * // Load from Blob with pre-computed peaks
203
- * engine.load(myBlob, [0.1, 0.5, 0.8]);
205
+ * controller.load(myBlob, [0.1, 0.5, 0.8]);
204
206
  *
205
207
  * // Subscription
206
- * const unsubscribe = engine.subscribe((state) => {
208
+ * const unsubscribe = controller.subscribe((state) => {
207
209
  * console.log('Current time:', state.currentTime);
208
210
  * });
209
211
  * ```
210
212
  */
211
- declare class WaveframeEngine {
213
+ declare class WaveframeController {
212
214
  private player;
213
215
  private analyzer;
214
216
  private listeners;
@@ -216,7 +218,7 @@ declare class WaveframeEngine {
216
218
  private _media;
217
219
  private _objectUrl;
218
220
  /**
219
- * Creates a new instance of the WaveframeEngine.
221
+ * Creates a new instance of the WaveframeController.
220
222
  * Initializes internal PlayerCore and PeakAnalyzer.
221
223
  */
222
224
  constructor();
@@ -239,6 +241,14 @@ declare class WaveframeEngine {
239
241
  * Useful for `useSyncExternalStore`.
240
242
  */
241
243
  getSnapshot(): EngineState;
244
+ /**
245
+ * Returns the current engine state.
246
+ */
247
+ get state(): EngineState;
248
+ /**
249
+ * Resets the audio player and analyzer, clearing state and current media.
250
+ */
251
+ reset(): void;
242
252
  /**
243
253
  * Revokes any existing Object URLs to prevent memory leaks.
244
254
  */
@@ -263,11 +273,11 @@ declare class WaveframeEngine {
263
273
  /**
264
274
  * Toggles playback between playing and paused.
265
275
  */
266
- togglePlay(): void;
276
+ togglePlay(): Promise<void>;
267
277
  /**
268
278
  * Starts audio playback.
269
279
  */
270
- play(): void;
280
+ play(): Promise<void>;
271
281
  /**
272
282
  * Pauses audio playback.
273
283
  */
@@ -288,7 +298,7 @@ declare class WaveframeEngine {
288
298
  */
289
299
  setMuted(muted: boolean): void;
290
300
  /**
291
- * Disposes of the engine, pausing playback and clearing all listeners and resources.
301
+ * Disposes of the controller, pausing playback and clearing all listeners and resources.
292
302
  */
293
303
  dispose(): void;
294
304
  }
@@ -361,10 +371,10 @@ interface WaveframePlayerProps {
361
371
  */
362
372
  theme?: WaveframeTheme;
363
373
  /**
364
- * Optional WaveframeEngine instance for external control.
365
- * If provided, the player will sync with this engine instead of creating its own.
374
+ * Optional WaveframeController instance for external control.
375
+ * If provided, the player will sync with this controller instead of creating its own.
366
376
  */
367
- engine?: WaveframeEngine;
377
+ controller?: WaveframeController;
368
378
  }
369
379
  /**
370
380
  * The standard "all-in-one" Waveframe player component.
@@ -375,17 +385,37 @@ interface WaveframePlayerProps {
375
385
  declare const WaveframePlayer: React$1.FC<WaveframePlayerProps>;
376
386
 
377
387
  interface WaveformProps {
378
- peaks: number[];
379
- currentTime: number;
380
- duration: number;
381
- waveColor: string;
382
- progressColor: string;
383
- height: number;
384
- onSeek: (percentage: number) => void;
388
+ /** The WaveframeController instance managing audio state */
389
+ controller: WaveframeController;
390
+ /** Optional pre-computed peaks. If omitted, peaks from the controller are used. */
391
+ peaks?: number[];
392
+ /** Color of the background waveform bars */
393
+ waveColor?: string;
394
+ /** Color of the progress (played) waveform bars */
395
+ progressColor?: string;
396
+ /** Total height of the waveform in pixels */
397
+ height?: number;
398
+ /**
399
+ * Resolution of the waveform.
400
+ * 'auto' matches the container width (1 bar per pixel).
401
+ * A number sets a fixed number of bars.
402
+ */
385
403
  resolution?: number | 'auto';
404
+ /** Width of each bar in pixels (if resolution is 'auto') */
386
405
  barWidth?: number;
406
+ /** Gap between bars in pixels (if resolution is 'auto') */
387
407
  barGap?: number;
408
+ /** Overall amplitude multiplier (default 1.0) */
409
+ amplitude?: number;
410
+ /** Non-linear power scale to increase detail (e.g. 0.7-0.9 reduces 'sausage' look). Default 1.0 (linear). */
411
+ powerScale?: number;
388
412
  }
413
+ /**
414
+ * A "smart" waveform component that visualizes audio progress and allows seeking.
415
+ *
416
+ * It subscribes directly to the provided WaveframeController for high-frequency
417
+ * progress updates, ensuring the parent component remains immune to re-renders.
418
+ */
389
419
  declare const Waveform: React$1.FC<WaveformProps>;
390
420
 
391
421
  /**
@@ -436,77 +466,61 @@ declare class PeakAnalyzer {
436
466
  interface UseWaveframeOptions {
437
467
  /** Optional pre-computed peaks to skip automatic analysis */
438
468
  peaks?: number[];
439
- /** Optional external engine instance for shared playback across components */
440
- engine?: WaveframeEngine;
469
+ /** Optional external controller instance for shared playback across components */
470
+ controller?: WaveframeController;
471
+ /** Whether to automatically start playback when media is loaded */
472
+ autoPlay?: boolean;
441
473
  }
442
474
  /**
443
- * A headless hook that provides full control over the Waveframe engine.
475
+ * A headless hook that manages the lifecycle of a WaveframeController.
444
476
  *
445
- * It manages the engine's lifecycle, loads the provided media, and returns
446
- * the current state along with playback controls.
477
+ * It returns a stable controller instance that can be passed to components
478
+ * like <Waveform /> or used for custom playback logic.
447
479
  *
448
480
  * @param media The audio source (URL string or Blob/File object).
449
- * @param options Additional configuration and an optional external engine.
481
+ * @param options Additional configuration and an optional external controller.
450
482
  *
451
483
  * @example
452
484
  * ```tsx
453
- * const { state, togglePlay, seek } = useWaveframe('https://example.com/audio.mp3');
485
+ * const { controller, state } = useWaveframe('https://example.com/audio.mp3');
454
486
  *
455
487
  * return (
456
488
  * <div>
457
- * <button onClick={togglePlay}>{state.isPlaying ? 'Pause' : 'Play'}</button>
458
- * <div onClick={(e) => seek(0.5)}>Seek to Middle</div>
489
+ * <Waveform controller={controller} />
490
+ * <button onClick={() => controller.togglePlay()}>
491
+ * {state.isPlaying ? 'Pause' : 'Play'}
492
+ * </button>
459
493
  * </div>
460
494
  * );
461
495
  * ```
462
496
  */
463
497
  declare const useWaveframe: (media: string | Blob | undefined, options?: UseWaveframeOptions) => {
464
- /** The current reactive state of the engine */
498
+ /** The stable WaveframeController instance */
499
+ controller: WaveframeController;
500
+ /** The reactive engine state */
465
501
  state: EngineState;
466
- /** The raw WaveframeEngine instance for advanced usage */
467
- engine: WaveframeEngine;
468
- /** Toggles playback between playing and paused */
469
- togglePlay: () => void;
470
- /** Starts audio playback */
471
- play: () => void;
472
- /** Pauses audio playback */
473
- pause: () => void;
474
- /** Seeks to a specific percentage (0-1) */
475
- seek: (percentage: number) => void;
476
- /** Sets the playback volume (0-1) */
477
- setVolume: (v: number) => void;
478
- /** Mutes or unmutes the audio */
479
- setMuted: (m: boolean) => void;
480
- /** Manually triggers a re-analysis of the current media */
481
- analyze: (samples?: number) => Promise<void>;
482
502
  };
483
503
 
484
504
  /**
485
- * A React hook that synchronizes a WaveframeEngine's state with a React component.
505
+ * A React hook that synchronizes a WaveframeController's state with a React component.
486
506
  *
487
- * It uses `useSyncExternalStore` for high-performance updates, ensuring that
488
- * the component only re-renders when the engine's state snapshot actually changes.
507
+ * It supports an optional selector function to subscribe to specific parts of the state,
508
+ * preventing unnecessary re-renders when unrelated state changes.
489
509
  *
490
- * @param engine The WaveframeEngine instance to subscribe to.
491
- * @returns The current EngineState (isPlaying, currentTime, peaks, etc.).
510
+ * @param controller The WaveframeController instance to subscribe to.
511
+ * @param selector An optional function to select a specific slice of the state.
512
+ * @returns The selected state or the full EngineState if no selector is provided.
492
513
  *
493
514
  * @example
494
515
  * ```tsx
495
- * const MyPlayer = ({ engine }: { engine: WaveframeEngine }) => {
496
- * const { isPlaying, currentTime, duration } = useWaveframeStore(engine);
516
+ * // Subscribe only to isPlaying
517
+ * const isPlaying = useWaveframeStore(controller, state => state.isPlaying);
497
518
  *
498
- * return (
499
- * <div>
500
- * <button onClick={() => engine.togglePlay()}>
501
- * {isPlaying ? 'Pause' : 'Play'}
502
- * </button>
503
- * <p>{currentTime.toFixed(2)} / {duration.toFixed(2)}</p>
504
- * </div>
505
- * );
506
- * };
519
+ * // Subscribe to the full state
520
+ * const state = useWaveframeStore(controller);
507
521
  * ```
508
522
  */
509
- declare const useWaveframeStore: (engine: WaveframeEngine) => EngineState;
523
+ declare function useWaveframeStore<T = EngineState>(controller: WaveframeController, selector?: (state: EngineState) => T): T;
510
524
 
511
525
  declare const useResampledPeaks: (peaks: number[], targetCount: number) => number[];
512
526
 
@@ -559,4 +573,4 @@ declare const resamplePeaks: (peaks: number[], targetCount: number) => number[];
559
573
  */
560
574
  declare const highlightCode: (code: string) => string[];
561
575
 
562
- export { type EngineListener, type EngineState, PeakAnalyzer, PlayerCore, type PlayerListener, type PlayerState, type Resolution, type TrackInfo, type UseWaveframeOptions, Waveform, type WaveformConfig, WaveframeEngine, WaveframePlayer, type WaveframePlayerProps, type WaveframeTheme, formatTime, generatePeaks, highlightCode, loadAudioToMemory, resamplePeaks, revokeAudioMemory, useResampledPeaks, useResizeObserver, useWaveframe, useWaveframeStore };
576
+ export { type EngineListener, type EngineState, PeakAnalyzer, PlayerCore, type PlayerListener, type PlayerState, type Resolution, type TrackInfo, type UseWaveframeOptions, Waveform, type WaveformConfig, WaveframeController, WaveframePlayer, type WaveframePlayerProps, type WaveframeTheme, formatTime, generatePeaks, highlightCode, loadAudioToMemory, resamplePeaks, revokeAudioMemory, useResampledPeaks, useResizeObserver, useWaveframe, useWaveframeStore };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  "use client";
2
- import {memo,useRef,useEffect,useState,useMemo,useSyncExternalStore}from'react';import {jsxs,jsx}from'react/jsx-runtime';var T=class{audio;listeners=new Set;_state;constructor(){this.audio=new Audio,this._state={isPlaying:false,currentTime:0,duration:0,volume:1,muted:false,error:null},this.initListeners();}initListeners(){this.audio.addEventListener("play",()=>this.updateState({isPlaying:true,error:null})),this.audio.addEventListener("pause",()=>this.updateState({isPlaying:false})),this.audio.addEventListener("timeupdate",()=>this.updateState({currentTime:this.audio.currentTime})),this.audio.addEventListener("durationchange",()=>this.updateState({duration:this.audio.duration})),this.audio.addEventListener("volumechange",()=>this.updateState({volume:this.audio.volume,muted:this.audio.muted})),this.audio.addEventListener("ended",()=>this.updateState({isPlaying:false})),this.audio.addEventListener("error",()=>{let t=this.audio.error,e="Unknown audio error";if(t)switch(t.code){case t.MEDIA_ERR_ABORTED:e="Playback aborted";break;case t.MEDIA_ERR_NETWORK:e="Network error";break;case t.MEDIA_ERR_DECODE:e="Audio decoding failed";break;case t.MEDIA_ERR_SRC_NOT_SUPPORTED:e="Audio format not supported";break}this.updateState({isPlaying:false,error:e});});}updateState(t){this._state={...this._state,...t},this.notify();}notify(){this.listeners.forEach(t=>t(this._state));}subscribe(t){return this.listeners.add(t),()=>this.listeners.delete(t)}get state(){return this._state}setSource(t){this.audio.src=t,this.audio.load(),this.updateState({error:null,currentTime:0,duration:0});}async play(){try{await this.audio.play();}catch(t){let e=t instanceof Error?t.message:"Playback failed";throw this.updateState({isPlaying:false,error:e}),t}}pause(){this.audio.pause();}async togglePlay(){if(this._state.isPlaying)this.pause();else try{await this.play();}catch{}}seek(t){this.audio.currentTime=t;}setVolume(t){this.audio.volume=t;}setMuted(t){this.audio.muted=t;}dispose(){this.pause(),this.audio.src="",this.listeners.clear();}};var R=class{audioCtx=null;constructor(){}getContext(){return this.audioCtx||(this.audioCtx=new(window.AudioContext||window.webkitAudioContext)),this.audioCtx}async generatePeaks(t,e=512){try{let o;if(typeof t=="string"){let c=await fetch(t);if(!c.ok)throw new Error(`Failed to fetch audio: ${c.statusText}`);o=await c.arrayBuffer();}else o=await t.arrayBuffer();let n=(await this.getContext().decodeAudioData(o)).getChannelData(0),s=Math.floor(n.length/e),l=[];for(let c=0;c<e;c++){let u=0,g=c*s,d=g+s;for(let y=g;y<d;y++){let S=Math.abs(n[y]);S>u&&(u=S);}l.push(u);}let x=Math.max(...l);return l.map(c=>c/(x||1))}catch(o){throw console.error("PeakAnalyzer Error:",o),o}}dispose(){this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null);}};var W=class{player;analyzer;listeners=new Set;_state;_media=null;_objectUrl=null;constructor(){this.player=new T,this.analyzer=new R,this._state={...this.player.state,peaks:[],isAnalyzing:false,error:null},this.player.subscribe(t=>{this.updateState({...t});});}updateState(t){this._state={...this._state,...t},this.notify();}notify(){this.listeners.forEach(t=>t(this._state));}subscribe(t){return this.listeners.add(t),()=>this.listeners.delete(t)}getSnapshot(){return this._state}revokeOldSource(){this._objectUrl&&(URL.revokeObjectURL(this._objectUrl),this._objectUrl=null);}load(t,e){if(this._media!==t){this.revokeOldSource(),this._media=t;let i;typeof t=="string"?i=t:(this._objectUrl=URL.createObjectURL(t),i=this._objectUrl),this.player.setSource(i);let a=e&&e.length>0;this.updateState({peaks:e||[],isAnalyzing:false,error:null}),a||this.analyze();}else e&&e.length!==this._state.peaks.length&&this.updateState({peaks:e});}async analyze(t=512){if(!this._media){this.updateState({error:"No media loaded to analyze"});return}this.updateState({isAnalyzing:true,error:null});try{let e=await this.analyzer.generatePeaks(this._media,t);this.updateState({peaks:e,isAnalyzing:!1});}catch(e){this.updateState({isAnalyzing:false,error:e instanceof Error?e.message:"Analysis failed"});}}togglePlay(){this.player.togglePlay();}play(){this.player.play();}pause(){this.player.pause();}seek(t){let{duration:e}=this._state;e&&this.player.seek(t*e);}setVolume(t){this.player.setVolume(t);}setMuted(t){this.player.setMuted(t);}dispose(){this.revokeOldSource(),this.player.dispose(),this.analyzer.dispose(),this.listeners.clear();}};var q=r=>useSyncExternalStore(t=>r.subscribe(t),()=>r.getSnapshot());var Q=(r,t={})=>{let{peaks:e,engine:o}=t,i=useMemo(()=>o||new W,[o]),a=o||i,n=q(a);return useEffect(()=>{r&&a.load(r,e);},[a,r,e]),useEffect(()=>()=>{o||i.dispose();},[i,o]),{state:n,engine:a,togglePlay:()=>a.togglePlay(),play:()=>a.play(),pause:()=>a.pause(),seek:s=>a.seek(s),setVolume:s=>a.setVolume(s),setMuted:s=>a.setMuted(s),analyze:s=>a.analyze(s)}};var At=async(r,t=512)=>{let e=new R;try{return await e.generatePeaks(r,t)}finally{e.dispose();}},Lt=async r=>{let e=await(await fetch(r)).blob();return URL.createObjectURL(e)},jt=r=>{r&&r.startsWith("blob:")&&URL.revokeObjectURL(r);};var H=r=>{if(isNaN(r))return "0:00";let t=Math.floor(r/60),e=Math.floor(r%60);return `${t}:${e.toString().padStart(2,"0")}`},Y=(r,t)=>{if(r.length===0)return [];if(r.length===t)return r;let e=new Array(t),o=r.length/t;if(o>1)for(let i=0;i<t;i++){let a=0,n=Math.floor(i*o),s=Math.floor((i+1)*o);for(let l=n;l<s;l++)r[l]>a&&(a=r[l]);e[i]=a;}else for(let i=0;i<t;i++){let a=i*o,n=Math.floor(a),s=Math.min(n+1,r.length-1),l=a-n;e[i]=r[n]+(r[s]-r[n])*l;}return e},Wt=r=>r.split(`
3
- `).map(t=>{let e=t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;"),o={},i=0,a=(n,s)=>{let l=`__TOKEN_${i++}__`;return o[l]=`<span class="${s}">${n}</span>`,l};return e=e.replace(/("(?:[^"\\]|\\.)*")/g,n=>a(n,"text-[#ce9178]")),e=e.replace(/\b(\d+(\.\d+)?)\b/g,n=>a(n,"text-[#b5cea8]")),e=e.replace(/\b(WaveframePlayer)\b/g,n=>a(n,"text-[#4ec9b0]")),e=e.replace(/\b([a-z][a-zA-Z0-9]+)(?==)/g,n=>a(n,"text-[#9cdcfe]")),e=e.replace(/(&lt;|&gt;|\{|\}|\/|:|,)/g,'<span class="text-gray-500">$1</span>'),Object.entries(o).forEach(([n,s])=>{e=e.replace(n,s);}),e});var tt=(r,t)=>useMemo(()=>Y(r,t),[r,t]);var U=r=>{let[t,e]=useState(0);return useEffect(()=>{if(!r.current)return;let o=new ResizeObserver(i=>{for(let a of i)e(a.contentRect.width);});return o.observe(r.current),()=>o.disconnect()},[r]),t};var I=memo(({artworkUrl:r,title:t,isLoading:e})=>jsxs("div",{className:"relative flex-shrink-0 w-32 h-32 md:w-40 md:h-40 overflow-hidden rounded-[var(--wf-artwork-rounded,0.75rem)] shadow-lg group/artwork",children:[jsx("div",{className:`w-full h-full transition-all duration-700 ${e?"blur-md scale-110":""}`,children:r?jsx("img",{src:r,alt:t,className:"w-full h-full object-cover transition-transform duration-500 group-hover/artwork:scale-110"}):jsx("div",{className:"w-full h-full bg-gradient-to-br from-[var(--wf-placeholder-from,#fb923c)] to-[var(--wf-placeholder-to,#ec4899)] flex items-center justify-center",children:jsx("svg",{className:"w-16 h-16 text-white opacity-50",fill:"currentColor",viewBox:"0 0 24 24",children:jsx("path",{d:"M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"})})})}),e&&jsx("div",{className:"absolute inset-0 flex items-center justify-center bg-black/10 backdrop-blur-[1px]",children:jsx("div",{className:"w-8 h-8 border-4 border-white/30 border-t-white rounded-full animate-spin"})})]}));I.displayName="ArtworkOverlay";var K=memo(({peaks:r,currentTime:t,duration:e,waveColor:o,progressColor:i,height:a,onSeek:n,resolution:s="auto",barWidth:l=2,barGap:x=1})=>{let c=useRef(null),u=useRef(null),g=useRef(null),d=U(g);useEffect(()=>{let f=c.current,b=u.current;if(!f||!b)return;let h=f.getContext("2d"),p=b.getContext("2d");if(!h||!p)return;let k=window.devicePixelRatio||1,E=f.getBoundingClientRect(),M=E.width*k,N=E.height*k;[f,b].forEach(w=>{(w.width!==M||w.height!==N)&&(w.width=M,w.height=N);}),(()=>{if(r.length===0)return;let{width:w,height:m}=f;h.clearRect(0,0,w,m),p.clearRect(0,0,w,m);let B=r.length,A=w/B,P=typeof s=="number"?A*.7:l*k,D=typeof s=="number"?A*.3:x*k;h.lineCap="round",h.lineWidth=P,p.lineCap="round",p.lineWidth=P,r.forEach((L,C)=>{if(L<=0)return;let j=C*(P+D)+P/2,X=L*m*.8,$=(m-X)/2,Z=$+X;h.beginPath(),h.strokeStyle=o,h.moveTo(j,$),h.lineTo(j,Z),h.stroke(),p.beginPath(),p.strokeStyle=i,p.moveTo(j,$),p.lineTo(j,Z),p.stroke();});})();},[r,o,i,s,l,x,a]);let y=f=>{if(g.current&&e){let b=g.current.getBoundingClientRect(),h=f.clientX-b.left,p=Math.max(0,Math.min(1,h/b.width));n(p);}},S=e?t/e*100:0;return jsxs("div",{ref:g,className:"relative w-full cursor-pointer overflow-hidden",style:{height:`${a}px`},onClick:y,children:[jsx("canvas",{ref:c,className:"absolute inset-0 w-full h-full"}),jsx("div",{className:"absolute inset-0 h-full overflow-hidden transition-[width] duration-100 ease-linear pointer-events-none",style:{width:`${S}%`},children:jsx("canvas",{ref:u,className:"absolute h-full",style:{width:`${d}px`}})})]})});K.displayName="Waveform";var ut=memo(({media:r,peaks:t,artwork:e,title:o,artist:i,waveColor:a,progressColor:n,height:s=80,className:l="",style:x,resolution:c="auto",barWidth:u=2,barGap:g=1,theme:d,engine:y})=>{let{state:S,togglePlay:f,seek:b}=Q(r,{peaks:t,engine:y}),{isPlaying:h,currentTime:p,duration:k,peaks:E,isAnalyzing:M}=S,[N,O]=useState(typeof e=="string"?e:void 0);useEffect(()=>{if(e instanceof Blob){let C=URL.createObjectURL(e);return O(C),()=>URL.revokeObjectURL(C)}else O(e);},[e]);let w=useRef(null),m=U(w),B=useMemo(()=>typeof c=="number"?c:m>0?Math.max(1,Math.floor(m/(u+g))):E.length||1,[c,m,u,g,E.length]),A=tt(E,B),P=useMemo(()=>a||(d?d.bg==="#ffffff"?"#e5e7eb":"#374151":"#e5e7eb"),[a,d]),D=n||d?.primary||"#3b82f6",L=useMemo(()=>({...{"--wf-bg-color":d?.bg||"white","--wf-border-color":d?.border||"#f3f4f6","--wf-title-color":d?.text||"#111827","--wf-artist-color":d?.text||"#6b7280","--wf-time-color":d?.text||"#9ca3af","--wf-play-btn-bg":d?.primary||"#3b82f6","--wf-placeholder-from":d?.primary||"#fb923c","--wf-placeholder-to":d?.bg||"#ec4899"},...x}),[d,x]);return jsxs("div",{className:`group relative flex flex-col md:flex-row items-stretch gap-6 p-6 bg-[var(--wf-bg-color,white)] border border-[var(--wf-border-color,#f3f4f6)] rounded-[var(--wf-rounded,1rem)] shadow-xl hover:shadow-2xl transition-all duration-300 overflow-hidden ${l}`,style:L,children:[jsx(I,{artworkUrl:N,title:o,isLoading:M}),jsxs("div",{className:"flex-1 w-full flex flex-col min-w-0",children:[jsxs("div",{className:"flex items-center gap-4 mb-6",children:[jsx("button",{onClick:f,className:"w-12 h-12 md:w-14 md:h-14 flex-shrink-0 flex items-center justify-center rounded-full bg-[var(--wf-play-btn-bg,#3b82f6)] text-white shadow-[0_4px_12px_rgba(0,0,0,0.15)] hover:shadow-[0_6px_16px_rgba(0,0,0,0.2)] transition-all hover:scale-105 active:scale-95 cursor-pointer border-none outline-none group/play",children:h?jsx("svg",{className:"w-6 h-6 md:w-7 md:h-7",fill:"currentColor",viewBox:"0 0 24 24",children:jsx("path",{d:"M6 19h4V5H6v14zm8-14v14h4V5h-4z"})}):jsx("svg",{className:"w-6 h-6 md:w-7 md:h-7 ml-1",fill:"currentColor",viewBox:"0 0 24 24",children:jsx("path",{d:"M8 5v14l11-7z"})})}),jsxs("div",{className:"flex-1 flex flex-col min-w-0",children:[jsxs("div",{className:"flex items-center justify-between gap-4",children:[i&&jsx("p",{className:"text-[10px] md:text-xs font-bold uppercase text-[var(--wf-artist-color,#6b7280)] opacity-60 tracking-[0.1em] line-clamp-1",children:i}),jsxs("div",{className:"text-[10px] font-mono text-[var(--wf-time-color,#9ca3af)] tabular-nums flex-shrink-0",children:[H(p)," / ",H(k)]})]}),o&&jsx("h3",{className:"text-lg md:text-xl font-black text-[var(--wf-title-color,#111827)] tracking-tight line-clamp-1 mt-0.5 leading-tight",children:o})]})]}),jsx("div",{className:"mt-auto",ref:w,children:jsx(K,{peaks:A,currentTime:p,duration:k,waveColor:P,progressColor:D,height:s,onSeek:b,resolution:c,barWidth:u,barGap:g})})]})]})});ut.displayName="WaveframePlayer";export{R as PeakAnalyzer,T as PlayerCore,K as Waveform,W as WaveframeEngine,ut as WaveframePlayer,H as formatTime,At as generatePeaks,Wt as highlightCode,Lt as loadAudioToMemory,Y as resamplePeaks,jt as revokeAudioMemory,tt as useResampledPeaks,U as useResizeObserver,Q as useWaveframe,q as useWaveframeStore};//# sourceMappingURL=index.js.map
2
+ import {memo,useRef,useMemo,useEffect,useState,useSyncExternalStore}from'react';import {jsxs,jsx}from'react/jsx-runtime';var M=class{audio;listeners=new Set;_state;constructor(){this.audio=new Audio,this._state={isPlaying:false,isStalled:false,currentTime:0,duration:0,volume:1,muted:false,error:null},this.initListeners();}initListeners(){this.audio.addEventListener("play",()=>this.updateState({isPlaying:true,error:null})),this.audio.addEventListener("pause",()=>this.updateState({isPlaying:false})),this.audio.addEventListener("waiting",()=>this.updateState({isStalled:true})),this.audio.addEventListener("playing",()=>this.updateState({isStalled:false})),this.audio.addEventListener("canplay",()=>this.updateState({isStalled:false})),this.audio.addEventListener("timeupdate",()=>this.updateState({currentTime:this.audio.currentTime})),this.audio.addEventListener("durationchange",()=>this.updateState({duration:this.audio.duration})),this.audio.addEventListener("volumechange",()=>this.updateState({volume:this.audio.volume,muted:this.audio.muted})),this.audio.addEventListener("ended",()=>this.updateState({isPlaying:false})),this.audio.addEventListener("error",()=>{let t=this.audio.error,r="Unknown audio error";if(t)switch(t.code){case t.MEDIA_ERR_ABORTED:r="Playback aborted";break;case t.MEDIA_ERR_NETWORK:r="Network error";break;case t.MEDIA_ERR_DECODE:r="Audio decoding failed";break;case t.MEDIA_ERR_SRC_NOT_SUPPORTED:r="Audio format not supported";break}this.updateState({isPlaying:false,error:r});});}updateState(t){this._state={...this._state,...t},this.notify();}notify(){this.listeners.forEach(t=>t(this._state));}subscribe(t){return this.listeners.add(t),()=>this.listeners.delete(t)}get state(){return this._state}setSource(t){this.audio.src=t,this.audio.load(),this.updateState({error:null,currentTime:0,duration:0});}async play(){try{await this.audio.play();}catch(t){let r=t instanceof Error?t.message:"Playback failed";throw this.updateState({isPlaying:false,error:r}),t}}pause(){this.audio.pause();}async togglePlay(){if(this._state.isPlaying)this.pause();else try{await this.play();}catch{}}seek(t){this.audio.currentTime=t;}setVolume(t){this.audio.volume=t;}setMuted(t){this.audio.muted=t;}dispose(){this.pause(),this.audio.src="",this.listeners.clear();}};var z=class{audioCtx=null;constructor(){}getContext(){return this.audioCtx||(this.audioCtx=new(window.AudioContext||window.webkitAudioContext)),this.audioCtx}async generatePeaks(t,r=512){try{let a;if(typeof t=="string"){let g=await fetch(t);if(!g.ok)throw new Error(`Failed to fetch audio: ${g.statusText}`);a=await g.arrayBuffer();}else a=await t.arrayBuffer();let o=(await this.getContext().decodeAudioData(a)).getChannelData(0),s=Math.floor(o.length/r),l=[];for(let g=0;g<r;g++){let y=0,m=g*s,c=m+s;for(let S=m;S<c;S++){let u=Math.abs(o[S]);u>y&&(y=u);}l.push(y);}let x=Math.max(...l);return l.map(g=>g/(x||1))}catch(a){throw console.error("PeakAnalyzer Error:",a),a}}dispose(){this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null);}};var T=class{player;analyzer;listeners=new Set;_state;_media=null;_objectUrl=null;constructor(){this.player=new M,this.analyzer=new z,this._state={...this.player.state,peaks:[],isAnalyzing:false,error:null},this.player.subscribe(t=>{this.updateState({...t});});}updateState(t){this._state={...this._state,...t},this.notify();}notify(){this.listeners.forEach(t=>t(this._state));}subscribe(t){return this.listeners.add(t),()=>this.listeners.delete(t)}getSnapshot(){return this._state}get state(){return this._state}reset(){this.revokeOldSource(),this._media=null,this.player.dispose(),this.analyzer.dispose(),this.player=new M,this.analyzer=new z,this.player.subscribe(t=>{this.updateState({...t});}),this.updateState({...this.player.state,peaks:[],isAnalyzing:false,error:null});}revokeOldSource(){this._objectUrl&&(URL.revokeObjectURL(this._objectUrl),this._objectUrl=null);}load(t,r){if(this._media===t){r&&r.length!==this._state.peaks.length&&this.updateState({peaks:r});return}this.revokeOldSource(),this._media=t;let a;typeof t=="string"?a=t:(this._objectUrl=URL.createObjectURL(t),a=this._objectUrl),this.player.setSource(a);let n=r&&r.length>0;this.updateState({peaks:r||[],isAnalyzing:false,error:null}),n||this.analyze();}async analyze(t=512){if(!this._media){this.updateState({error:"No media loaded to analyze"});return}this.updateState({isAnalyzing:true,error:null});try{let r=await this.analyzer.generatePeaks(this._media,t);this.updateState({peaks:r,isAnalyzing:!1});}catch(r){this.updateState({isAnalyzing:false,error:r instanceof Error?r.message:"Analysis failed"});}}async togglePlay(){return await this.player.togglePlay()}async play(){return await this.player.play()}pause(){this.player.pause();}seek(t){let{duration:r}=this._state;r&&this.player.seek(t*r);}setVolume(t){this.player.setVolume(t);}setMuted(t){this.player.setMuted(t);}dispose(){this.revokeOldSource(),this.player.dispose(),this.analyzer.dispose(),this.listeners.clear();}};function f(e,t){return useSyncExternalStore(r=>e.subscribe(r),()=>{let r=e.getSnapshot();return t?t(r):r})}var J=(e,t={})=>{let{peaks:r,controller:a,autoPlay:n}=t,i=useRef(false),o=useMemo(()=>a||new T,[a]),s=a||o,l=f(s);return useEffect(()=>(i.current=true,e&&(s.load(e,r),n&&s.play().catch(()=>{})),()=>{i.current=false;}),[s,e,r,n]),useEffect(()=>()=>{a||o.dispose();},[o,a]),{controller:s,state:l}};var O=memo(({artworkUrl:e,title:t,isLoading:r})=>jsxs("div",{className:"relative flex-shrink-0 w-full md:w-auto md:h-full aspect-square overflow-hidden rounded-[var(--wf-artwork-rounded,0.75rem)] shadow-lg group/artwork",children:[jsx("div",{className:`w-full h-full transition-all duration-700 ${r?"blur-md scale-110":""}`,children:e?jsx("img",{src:e,alt:t,className:"w-full h-full object-cover transition-transform duration-500 group-hover/artwork:scale-110"}):jsx("div",{className:"w-full h-full bg-gradient-to-br from-[var(--wf-placeholder-from,#fb923c)] to-[var(--wf-placeholder-to,#ec4899)] flex items-center justify-center",children:jsx("svg",{className:"w-16 h-16 text-white opacity-50",fill:"currentColor",viewBox:"0 0 24 24",children:jsx("path",{d:"M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"})})})}),r&&jsx("div",{className:"absolute inset-0 flex items-center justify-center bg-black/10 backdrop-blur-[1px]",children:jsx("div",{className:"w-8 h-8 border-4 border-white/30 border-t-white rounded-full animate-spin"})})]}));O.displayName="ArtworkOverlay";var Q=e=>{let[t,r]=useState(0);return useEffect(()=>{if(!e.current)return;let a=new ResizeObserver(n=>{for(let i of n)r(i.contentRect.width);});return a.observe(e.current),()=>a.disconnect()},[e]),t};var It=async(e,t=512)=>{let r=new z;try{return await r.generatePeaks(e,t)}finally{r.dispose();}},Ht=async e=>{let r=await(await fetch(e)).blob();return URL.createObjectURL(r)},Vt=e=>{e&&e.startsWith("blob:")&&URL.revokeObjectURL(e);};var B=e=>{if(isNaN(e))return "0:00";let t=Math.floor(e/60),r=Math.floor(e%60);return `${t}:${r.toString().padStart(2,"0")}`},j=(e,t)=>{if(e.length===0)return [];if(e.length===t)return e;let r=new Array(t),a=e.length/t;if(a>1)for(let n=0;n<t;n++){let i=0,o=Math.floor(n*a),s=Math.floor((n+1)*a);for(let l=o;l<s;l++)e[l]>i&&(i=e[l]);r[n]=i;}else for(let n=0;n<t;n++){let i=n*a,o=Math.floor(i),s=Math.min(o+1,e.length-1),l=i-o;r[n]=e[o]+(e[s]-e[o])*l;}return r},qt=e=>e.split(`
3
+ `).map(t=>{let r=t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;"),a={},n=0,i=(o,s)=>{let l=`__TOKEN_${n++}__`;return a[l]=`<span class="${s}">${o}</span>`,l};return r=r.replace(/("(?:[^"\\]|\\.)*")/g,o=>i(o,"text-[#ce9178]")),r=r.replace(/\b(\d+(\.\d+)?)\b/g,o=>i(o,"text-[#b5cea8]")),r=r.replace(/\b(WaveframePlayer)\b/g,o=>i(o,"text-[#4ec9b0]")),r=r.replace(/\b([a-z][a-zA-Z0-9]+)(?==)/g,o=>i(o,"text-[#9cdcfe]")),r=r.replace(/(&lt;|&gt;|\{|\}|\/|:|,)/g,'<span class="text-gray-500">$1</span>'),Object.entries(a).forEach(([o,s])=>{r=r.replace(o,s);}),r});var I=memo(({controller:e,peaks:t,waveColor:r="#e5e7eb",progressColor:a="#3b82f6",height:n=80,resolution:i="auto",barWidth:o=2,barGap:s=1,amplitude:l=1,powerScale:x=1})=>{let g=useRef(null),y=useRef(null),m=useRef(null),c=Q(m),S=f(e,d=>d.currentTime),u=f(e,d=>d.duration),A=f(e,d=>d.peaks),E=useMemo(()=>{let d=t||A;if(d.length===0)return [];let v=i==="auto"?Math.floor(c/(o+s)):i,p=j(d,Math.max(1,v));return (x!==1||l!==1)&&(p=p.map(h=>Math.pow(h,x)*l)),p},[t,A,c,i,o,s,l,x]);useEffect(()=>{let d=g.current,v=y.current;if(!d||!v)return;let p=d.getContext("2d"),h=v.getContext("2d");if(!p||!h)return;let C=window.devicePixelRatio||1,w=d.getBoundingClientRect(),H=w.width*C,V=w.height*C;[d,v].forEach(k=>{(k.width!==H||k.height!==V)&&(k.width=H,k.height=V);}),(()=>{if(E.length===0)return;let{width:k,height:L}=d;p.clearRect(0,0,k,L),h.clearRect(0,0,k,L);let rt=E.length,F=k/rt,R=typeof i=="number"?F*.7:o*C,et=typeof i=="number"?F*.3:s*C;p.lineCap="round",p.lineWidth=R,h.lineCap="round",h.lineWidth=R;let q=L/2;E.forEach((K,at)=>{if(K<=0)return;let N=at*(R+et)+R/2,G=Math.max(R,K*L*.8),X=q-G/2,Y=q+G/2;p.beginPath(),p.strokeStyle=r,p.moveTo(N,X),p.lineTo(N,Y),p.stroke(),h.beginPath(),h.strokeStyle=a,h.moveTo(N,X),h.lineTo(N,Y),h.stroke();});})();},[E,r,a,i,o,s,n]);let W=d=>{if(m.current&&u){let v=m.current.getBoundingClientRect(),p=d.clientX-v.left,h=Math.max(0,Math.min(1,p/v.width));e.seek(h);}},U=u?S/u*100:0;return jsxs("div",{ref:m,className:"relative w-full cursor-pointer overflow-hidden",style:{height:`${n}px`,"--wf-wave-color":r,"--wf-progress-color":a},onClick:W,children:[jsx("canvas",{ref:g,className:"absolute inset-0 w-full h-full"}),jsx("div",{className:"absolute inset-0 h-full overflow-hidden transition-[width] duration-100 ease-linear pointer-events-none",style:{width:`${U}%`},children:jsx("canvas",{ref:y,className:"absolute h-full",style:{width:`${c}px`}})})]})});I.displayName="Waveform";var bt=memo(({media:e,peaks:t,artwork:r,title:a,artist:n,waveColor:i,progressColor:o,height:s=80,className:l="",style:x,resolution:g="auto",barWidth:y=2,barGap:m=1,theme:c,controller:S})=>{let{controller:u}=J(e,{peaks:t,controller:S}),A=f(u,w=>w.isPlaying),E=f(u,w=>w.isAnalyzing),W=f(u,w=>w.currentTime),U=f(u,w=>w.duration),[d,v]=useState(typeof r=="string"?r:void 0);useEffect(()=>{if(r instanceof Blob){let w=URL.createObjectURL(r);return v(w),()=>URL.revokeObjectURL(w)}else v(r);},[r]);let p=useMemo(()=>i||(c?c.bg==="#ffffff"?"#e5e7eb":"#374151":"#e5e7eb"),[i,c]),h=o||c?.primary||"#3b82f6",C=useMemo(()=>({...{"--wf-bg-color":c?.bg||"white","--wf-border-color":c?.border||"#f3f4f6","--wf-title-color":c?.text||"#111827","--wf-artist-color":c?.text||"#6b7280","--wf-time-color":c?.text||"#9ca3af","--wf-play-btn-bg":c?.primary||"#3b82f6","--wf-placeholder-from":c?.primary||"#fb923c","--wf-placeholder-to":c?.bg||"#ec4899"},...x}),[c,x]);return jsxs("div",{className:`group relative flex flex-col md:flex-row items-stretch gap-6 p-6 bg-[var(--wf-bg-color,white)] border border-[var(--wf-border-color,#f3f4f6)] rounded-[var(--wf-rounded,1rem)] shadow-xl hover:shadow-2xl transition-all duration-300 overflow-hidden ${l}`,style:C,children:[jsx("div",{className:"flex-shrink-0",children:jsx(O,{artworkUrl:d,title:a,isLoading:E})}),jsxs("div",{className:"flex-1 w-full flex flex-col min-w-0",children:[jsxs("div",{className:"flex items-center gap-4 mb-6",children:[jsx("button",{onClick:()=>u.togglePlay(),className:"w-12 h-12 md:w-14 md:h-14 flex-shrink-0 flex items-center justify-center rounded-full bg-[var(--wf-play-btn-bg,#3b82f6)] text-white shadow-[0_4px_12px_rgba(0,0,0,0.15)] hover:shadow-[0_6px_16px_rgba(0,0,0,0.2)] transition-all hover:scale-105 active:scale-95 cursor-pointer border-none outline-none group/play",children:A?jsx("svg",{className:"w-6 h-6 md:w-7 md:h-7",fill:"currentColor",viewBox:"0 0 24 24",children:jsx("path",{d:"M6 19h4V5H6v14zm8-14v14h4V5h-4z"})}):jsx("svg",{className:"w-6 h-6 md:w-7 md:h-7 ml-1",fill:"currentColor",viewBox:"0 0 24 24",children:jsx("path",{d:"M8 5v14l11-7z"})})}),jsxs("div",{className:"flex-1 flex flex-col min-w-0",children:[jsxs("div",{className:"flex items-center justify-between gap-4",children:[n&&jsx("p",{className:"text-[10px] md:text-xs font-bold uppercase text-[var(--wf-artist-color,#6b7280)] opacity-60 tracking-[0.1em] line-clamp-1",children:n}),jsxs("div",{className:"text-[10px] font-mono text-[var(--wf-time-color,#9ca3af)] tabular-nums flex-shrink-0",children:[B(W)," / ",B(U)]})]}),a&&jsx("h3",{className:"text-lg md:text-xl font-black text-[var(--wf-title-color,#111827)] tracking-tight line-clamp-1 mt-0.5 leading-tight",children:a})]})]}),jsx("div",{className:"mt-auto",children:jsx(I,{controller:u,peaks:t,waveColor:p,progressColor:h,height:s,resolution:g,barWidth:y,barGap:m})})]})]})});bt.displayName="WaveframePlayer";var gr=(e,t)=>useMemo(()=>j(e,t),[e,t]);export{z as PeakAnalyzer,M as PlayerCore,I as Waveform,T as WaveframeController,bt as WaveframePlayer,B as formatTime,It as generatePeaks,qt as highlightCode,Ht as loadAudioToMemory,j as resamplePeaks,Vt as revokeAudioMemory,gr as useResampledPeaks,Q as useResizeObserver,J as useWaveframe,f as useWaveframeStore};//# sourceMappingURL=index.js.map
4
4
  //# sourceMappingURL=index.js.map