waveframe 0.3.1 → 0.4.1

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,32 +196,37 @@ 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;
215
217
  private _state;
216
218
  private _media;
217
219
  private _objectUrl;
220
+ private _isDisposed;
218
221
  /**
219
- * Creates a new instance of the WaveframeEngine.
222
+ * Creates a new instance of the WaveframeController.
220
223
  * Initializes internal PlayerCore and PeakAnalyzer.
221
224
  */
222
225
  constructor();
226
+ /**
227
+ * Returns whether the controller has been disposed and is no longer usable.
228
+ */
229
+ get isDisposed(): boolean;
223
230
  /**
224
231
  * Internal method to update the state and notify all subscribers.
225
232
  */
@@ -239,6 +246,14 @@ declare class WaveframeEngine {
239
246
  * Useful for `useSyncExternalStore`.
240
247
  */
241
248
  getSnapshot(): EngineState;
249
+ /**
250
+ * Returns the current engine state.
251
+ */
252
+ get state(): EngineState;
253
+ /**
254
+ * Resets the audio player and analyzer, clearing state and current media.
255
+ */
256
+ reset(): void;
242
257
  /**
243
258
  * Revokes any existing Object URLs to prevent memory leaks.
244
259
  */
@@ -263,11 +278,11 @@ declare class WaveframeEngine {
263
278
  /**
264
279
  * Toggles playback between playing and paused.
265
280
  */
266
- togglePlay(): void;
281
+ togglePlay(): Promise<void>;
267
282
  /**
268
283
  * Starts audio playback.
269
284
  */
270
- play(): void;
285
+ play(): Promise<void>;
271
286
  /**
272
287
  * Pauses audio playback.
273
288
  */
@@ -288,7 +303,7 @@ declare class WaveframeEngine {
288
303
  */
289
304
  setMuted(muted: boolean): void;
290
305
  /**
291
- * Disposes of the engine, pausing playback and clearing all listeners and resources.
306
+ * Disposes of the controller, pausing playback and clearing all listeners and resources.
292
307
  */
293
308
  dispose(): void;
294
309
  }
@@ -361,10 +376,10 @@ interface WaveframePlayerProps {
361
376
  */
362
377
  theme?: WaveframeTheme;
363
378
  /**
364
- * Optional WaveframeEngine instance for external control.
365
- * If provided, the player will sync with this engine instead of creating its own.
379
+ * Optional WaveframeController instance for external control.
380
+ * If provided, the player will sync with this controller instead of creating its own.
366
381
  */
367
- engine?: WaveframeEngine;
382
+ controller?: WaveframeController;
368
383
  }
369
384
  /**
370
385
  * The standard "all-in-one" Waveframe player component.
@@ -375,17 +390,37 @@ interface WaveframePlayerProps {
375
390
  declare const WaveframePlayer: React$1.FC<WaveframePlayerProps>;
376
391
 
377
392
  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;
393
+ /** The WaveframeController instance managing audio state */
394
+ controller: WaveframeController;
395
+ /** Optional pre-computed peaks. If omitted, peaks from the controller are used. */
396
+ peaks?: number[];
397
+ /** Color of the background waveform bars */
398
+ waveColor?: string;
399
+ /** Color of the progress (played) waveform bars */
400
+ progressColor?: string;
401
+ /** Total height of the waveform in pixels */
402
+ height?: number;
403
+ /**
404
+ * Resolution of the waveform.
405
+ * 'auto' matches the container width (1 bar per pixel).
406
+ * A number sets a fixed number of bars.
407
+ */
385
408
  resolution?: number | 'auto';
409
+ /** Width of each bar in pixels (if resolution is 'auto') */
386
410
  barWidth?: number;
411
+ /** Gap between bars in pixels (if resolution is 'auto') */
387
412
  barGap?: number;
413
+ /** Overall amplitude multiplier (default 1.0) */
414
+ amplitude?: number;
415
+ /** Non-linear power scale to increase detail (e.g. 0.7-0.9 reduces 'sausage' look). Default 1.0 (linear). */
416
+ powerScale?: number;
388
417
  }
418
+ /**
419
+ * A "smart" waveform component that visualizes audio progress and allows seeking.
420
+ *
421
+ * It subscribes directly to the provided WaveframeController for high-frequency
422
+ * progress updates, ensuring the parent component remains immune to re-renders.
423
+ */
389
424
  declare const Waveform: React$1.FC<WaveformProps>;
390
425
 
391
426
  /**
@@ -436,77 +471,61 @@ declare class PeakAnalyzer {
436
471
  interface UseWaveframeOptions {
437
472
  /** Optional pre-computed peaks to skip automatic analysis */
438
473
  peaks?: number[];
439
- /** Optional external engine instance for shared playback across components */
440
- engine?: WaveframeEngine;
474
+ /** Optional external controller instance for shared playback across components */
475
+ controller?: WaveframeController;
476
+ /** Whether to automatically start playback when media is loaded */
477
+ autoPlay?: boolean;
441
478
  }
442
479
  /**
443
- * A headless hook that provides full control over the Waveframe engine.
480
+ * A headless hook that manages the lifecycle of a WaveframeController.
444
481
  *
445
- * It manages the engine's lifecycle, loads the provided media, and returns
446
- * the current state along with playback controls.
482
+ * It returns a stable controller instance that can be passed to components
483
+ * like <Waveform /> or used for custom playback logic.
447
484
  *
448
485
  * @param media The audio source (URL string or Blob/File object).
449
- * @param options Additional configuration and an optional external engine.
486
+ * @param options Additional configuration and an optional external controller.
450
487
  *
451
488
  * @example
452
489
  * ```tsx
453
- * const { state, togglePlay, seek } = useWaveframe('https://example.com/audio.mp3');
490
+ * const { controller, state } = useWaveframe('https://example.com/audio.mp3');
454
491
  *
455
492
  * return (
456
493
  * <div>
457
- * <button onClick={togglePlay}>{state.isPlaying ? 'Pause' : 'Play'}</button>
458
- * <div onClick={(e) => seek(0.5)}>Seek to Middle</div>
494
+ * <Waveform controller={controller} />
495
+ * <button onClick={() => controller.togglePlay()}>
496
+ * {state.isPlaying ? 'Pause' : 'Play'}
497
+ * </button>
459
498
  * </div>
460
499
  * );
461
500
  * ```
462
501
  */
463
502
  declare const useWaveframe: (media: string | Blob | undefined, options?: UseWaveframeOptions) => {
464
- /** The current reactive state of the engine */
503
+ /** The stable WaveframeController instance */
504
+ controller: WaveframeController;
505
+ /** The reactive engine state */
465
506
  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
507
  };
483
508
 
484
509
  /**
485
- * A React hook that synchronizes a WaveframeEngine's state with a React component.
510
+ * A React hook that synchronizes a WaveframeController's state with a React component.
486
511
  *
487
- * It uses `useSyncExternalStore` for high-performance updates, ensuring that
488
- * the component only re-renders when the engine's state snapshot actually changes.
512
+ * It supports an optional selector function to subscribe to specific parts of the state,
513
+ * preventing unnecessary re-renders when unrelated state changes.
489
514
  *
490
- * @param engine The WaveframeEngine instance to subscribe to.
491
- * @returns The current EngineState (isPlaying, currentTime, peaks, etc.).
515
+ * @param controller The WaveframeController instance to subscribe to.
516
+ * @param selector An optional function to select a specific slice of the state.
517
+ * @returns The selected state or the full EngineState if no selector is provided.
492
518
  *
493
519
  * @example
494
520
  * ```tsx
495
- * const MyPlayer = ({ engine }: { engine: WaveframeEngine }) => {
496
- * const { isPlaying, currentTime, duration } = useWaveframeStore(engine);
521
+ * // Subscribe only to isPlaying
522
+ * const isPlaying = useWaveframeStore(controller, state => state.isPlaying);
497
523
  *
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
- * };
524
+ * // Subscribe to the full state
525
+ * const state = useWaveframeStore(controller);
507
526
  * ```
508
527
  */
509
- declare const useWaveframeStore: (engine: WaveframeEngine) => EngineState;
528
+ declare function useWaveframeStore<T = EngineState>(controller: WaveframeController, selector?: (state: EngineState) => T): T;
510
529
 
511
530
  declare const useResampledPeaks: (peaks: number[], targetCount: number) => number[];
512
531
 
@@ -559,4 +578,4 @@ declare const resamplePeaks: (peaks: number[], targetCount: number) => number[];
559
578
  */
560
579
  declare const highlightCode: (code: string) => string[];
561
580
 
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 };
581
+ 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,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 z=class{audioCtx=null;constructor(){}getContext(){return this.audioCtx||(this.audioCtx=new(window.AudioContext||window.webkitAudioContext)),this.audioCtx}async generatePeaks(t,e=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/e),l=[];for(let g=0;g<e;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;_isDisposed=false;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});});}get isDisposed(){return this._isDisposed}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._isDisposed=false,this.updateState({...this.player.state,peaks:[],isAnalyzing:false,error:null});}revokeOldSource(){this._objectUrl&&(URL.revokeObjectURL(this._objectUrl),this._objectUrl=null);}load(t,e){if(this._media===t){e&&e.length!==this._state.peaks.length&&this.updateState({peaks:e});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=e&&e.length>0;this.updateState({peaks:e||[],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 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"});}}async togglePlay(){return await this.player.togglePlay()}async play(){return await 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._isDisposed=true,this.revokeOldSource(),this.player.dispose(),this.analyzer.dispose(),this.listeners.clear();}};function f(r,t){return useSyncExternalStore(e=>r.subscribe(e),()=>{let e=r.getSnapshot();return t?t(e):e})}var J=(r,t={})=>{let{peaks:e,controller:a,autoPlay:n}=t,i=useRef(false),o=useMemo(()=>a||new T,[a]),s=a||o;useEffect(()=>{!a&&o.isDisposed&&o.reset();},[o,a]);let l=f(s);return useEffect(()=>(i.current=true,r&&(s.load(r,e),n&&s.play().catch(()=>{})),()=>{i.current=false;}),[s,r,e,n]),useEffect(()=>()=>{a||o.dispose();},[o,a]),{controller:s,state:l}};var B=memo(({artworkUrl:r,title:t,isLoading:e})=>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 ${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"})})]}));B.displayName="ArtworkOverlay";var Q=r=>{let[t,e]=useState(0);return useEffect(()=>{if(!r.current)return;let a=new ResizeObserver(n=>{for(let i of n)e(i.contentRect.width);});return a.observe(r.current),()=>a.disconnect()},[r]),t};var It=async(r,t=512)=>{let e=new z;try{return await e.generatePeaks(r,t)}finally{e.dispose();}},Ht=async r=>{let e=await(await fetch(r)).blob();return URL.createObjectURL(e)},Vt=r=>{r&&r.startsWith("blob:")&&URL.revokeObjectURL(r);};var D=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")}`},j=(r,t)=>{if(r.length===0)return [];if(r.length===t)return r;let e=new Array(t),a=r.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++)r[l]>i&&(i=r[l]);e[n]=i;}else for(let n=0;n<t;n++){let i=n*a,o=Math.floor(i),s=Math.min(o+1,r.length-1),l=i-o;e[n]=r[o]+(r[s]-r[o])*l;}return e},qt=r=>r.split(`
3
+ `).map(t=>{let e=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 e=e.replace(/("(?:[^"\\]|\\.)*")/g,o=>i(o,"text-[#ce9178]")),e=e.replace(/\b(\d+(\.\d+)?)\b/g,o=>i(o,"text-[#b5cea8]")),e=e.replace(/\b(WaveframePlayer)\b/g,o=>i(o,"text-[#4ec9b0]")),e=e.replace(/\b([a-z][a-zA-Z0-9]+)(?==)/g,o=>i(o,"text-[#9cdcfe]")),e=e.replace(/(&lt;|&gt;|\{|\}|\/|:|,)/g,'<span class="text-gray-500">$1</span>'),Object.entries(a).forEach(([o,s])=>{e=e.replace(o,s);}),e});var H=memo(({controller:r,peaks:t,waveColor:e="#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(r,d=>d.currentTime),u=f(r,d=>d.duration),A=f(r,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(),V=w.width*C,F=w.height*C;[d,v].forEach(k=>{(k.width!==V||k.height!==F)&&(k.width=V,k.height=F);}),(()=>{if(E.length===0)return;let{width:k,height:L}=d;p.clearRect(0,0,k,L),h.clearRect(0,0,k,L);let et=E.length,q=k/et,R=typeof i=="number"?q*.7:o*C,rt=typeof i=="number"?q*.3:s*C;p.lineCap="round",p.lineWidth=R,h.lineCap="round",h.lineWidth=R;let K=L/2;E.forEach((G,at)=>{if(G<=0)return;let N=at*(R+rt)+R/2,X=Math.max(R,G*L*.8),Y=K-X/2,Z=K+X/2;p.beginPath(),p.strokeStyle=e,p.moveTo(N,Y),p.lineTo(N,Z),p.stroke(),h.beginPath(),h.strokeStyle=a,h.moveTo(N,Y),h.lineTo(N,Z),h.stroke();});})();},[E,e,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));r.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":e,"--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`}})})]})});H.displayName="Waveform";var bt=memo(({media:r,peaks:t,artwork:e,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(r,{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 e=="string"?e:void 0);useEffect(()=>{if(e instanceof Blob){let w=URL.createObjectURL(e);return v(w),()=>URL.revokeObjectURL(w)}else v(e);},[e]);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(B,{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:[D(W)," / ",D(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(H,{controller:u,peaks:t,waveColor:p,progressColor:h,height:s,resolution:g,barWidth:y,barGap:m})})]})]})});bt.displayName="WaveframePlayer";var ge=(r,t)=>useMemo(()=>j(r,t),[r,t]);export{z as PeakAnalyzer,M as PlayerCore,H as Waveform,T as WaveframeController,bt as WaveframePlayer,D as formatTime,It as generatePeaks,qt as highlightCode,Ht as loadAudioToMemory,j as resamplePeaks,Vt as revokeAudioMemory,ge as useResampledPeaks,Q as useResizeObserver,J as useWaveframe,f as useWaveframeStore};//# sourceMappingURL=index.js.map
4
4
  //# sourceMappingURL=index.js.map