@arraypress/waveform-player 1.0.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.
@@ -0,0 +1,42 @@
1
+ (()=>{function T(t){let e={};return t.dataset.url&&(e.url=t.dataset.url),t.dataset.height&&(e.height=parseInt(t.dataset.height)),t.dataset.samples&&(e.samples=parseInt(t.dataset.samples)),t.dataset.waveformStyle&&(e.waveformStyle=t.dataset.waveformStyle),t.dataset.barWidth&&(e.barWidth=parseInt(t.dataset.barWidth)),t.dataset.barSpacing&&(e.barSpacing=parseInt(t.dataset.barSpacing)),t.dataset.colorPreset&&(e.colorPreset=t.dataset.colorPreset),t.dataset.waveformColor&&(e.waveformColor=t.dataset.waveformColor),t.dataset.progressColor&&(e.progressColor=t.dataset.progressColor),t.dataset.buttonColor&&(e.buttonColor=t.dataset.buttonColor),t.dataset.buttonHoverColor&&(e.buttonHoverColor=t.dataset.buttonHoverColor),t.dataset.textColor&&(e.textColor=t.dataset.textColor),t.dataset.textSecondaryColor&&(e.textSecondaryColor=t.dataset.textSecondaryColor),t.dataset.backgroundColor&&(e.backgroundColor=t.dataset.backgroundColor),t.dataset.borderColor&&(e.borderColor=t.dataset.borderColor),t.dataset.color&&(e.waveformColor=t.dataset.color),t.dataset.theme&&(e.colorPreset=t.dataset.theme),t.dataset.autoplay&&(e.autoplay=t.dataset.autoplay==="true"),t.dataset.showTime&&(e.showTime=t.dataset.showTime==="true"),t.dataset.showHoverTime&&(e.showHoverTime=t.dataset.showHoverTime==="true"),t.dataset.showBpm&&(e.showBPM=t.dataset.showBpm==="true"),t.dataset.singlePlay&&(e.singlePlay=t.dataset.singlePlay==="true"),t.dataset.playOnSeek&&(e.playOnSeek=t.dataset.playOnSeek==="true"),t.dataset.title&&(e.title=t.dataset.title),t.dataset.subtitle&&(e.subtitle=t.dataset.subtitle),t.dataset.waveform&&(e.waveform=t.dataset.waveform),e}function P(t){if(!t||isNaN(t))return"0:00";let e=Math.floor(t/60),o=Math.floor(t%60);return`${e}:${o.toString().padStart(2,"0")}`}function k(t){let e=t||Math.random().toString();return btoa(e.substring(0,10)).replace(/[^a-zA-Z0-9]/g,"")}function W(t){if(!t)return"Audio";let e=t.split("/");return e[e.length-1].split(".")[0].replace(/[-_]/g," ").replace(/\b\w/g,s=>s.toUpperCase())}function B(...t){let e={};for(let o of t)for(let r in o)o[r]!==null&&o[r]!==void 0&&(e[r]=o[r]);return e}function L(t,e){let o;return function(...s){let n=()=>{clearTimeout(o),t(...s)};clearTimeout(o),o=setTimeout(n,e)}}function S(t,e){if(t.length===e)return t;if(t.length===0||e===0)return[];let o=[];if(e>t.length){let r=(t.length-1)/(e-1);for(let s=0;s<e;s++){let n=s*r,a=Math.floor(n),i=Math.ceil(n),d=n-a;if(i>=t.length)o.push(t[t.length-1]);else if(a===i)o.push(t[a]);else{let l=t[a]*(1-d)+t[i]*d;o.push(l)}}}else{let r=t.length/e;for(let s=0;s<e;s++){let n=Math.floor(s*r),a=Math.floor((s+1)*r),i=0,d=0;for(let l=n;l<=a&&l<t.length;l++)t[l]>i&&(i=t[l]),d++;if(d===0){let l=Math.min(Math.round(s*r),t.length-1);i=t[l]}o.push(i)}}return o}function I(t,e,o,r,s){let n=window.devicePixelRatio||1,a=s.barWidth*n,i=s.barSpacing*n,d=Math.floor(e.width/(a+i)),l=S(o,d),h=e.height,f=r*e.width;t.clearRect(0,0,e.width,e.height);for(let m=0;m<l.length;m++){let p=m*(a+i);if(p+a>e.width)break;let c=l[m]*h*.9,g=h-c;t.fillStyle=s.color,t.fillRect(p,g,a,c)}t.save(),t.beginPath(),t.rect(0,0,f,h),t.clip();for(let m=0;m<l.length;m++){let p=m*(a+i);if(p>f)break;let c=l[m]*h*.9,g=h-c;t.fillStyle=s.progressColor,t.fillRect(p,g,a,c)}t.restore()}function x(t,e,o,r,s){let n=window.devicePixelRatio||1,a=s.barWidth*n,i=s.barSpacing*n,d=Math.floor(e.width/(a+i)),l=S(o,d),h=e.height,f=h/2,m=r*e.width;t.clearRect(0,0,e.width,e.height);for(let p=0;p<l.length;p++){let c=p*(a+i);if(c+a>e.width)break;let g=l[p]*h*.45;t.fillStyle=s.color,t.fillRect(c,f-g,a,g),t.fillRect(c,f,a,g)}t.save(),t.beginPath(),t.rect(0,0,m,h),t.clip();for(let p=0;p<l.length;p++){let c=p*(a+i);if(c>m)break;let g=l[p]*h*.45;t.fillStyle=s.progressColor,t.fillRect(c,f-g,a,g),t.fillRect(c,f,a,g)}t.restore()}function H(t,e,o,r,s){let n=e.width,a=e.height,i=a/2,d=a*.35;t.clearRect(0,0,n,a);let l=(h,f,m=1,p=!1)=>{p&&(t.shadowBlur=12,t.shadowColor=h),t.strokeStyle=h,t.lineWidth=f,t.lineCap="round",t.lineJoin="round",t.beginPath(),t.moveTo(0,i);let c=[],g=Math.floor(o.length*m);for(let u=0;u<g;u++){let v=u/(o.length-1)*n,C=o[u],y=Math.sin(u*.1)*C,w=i+y*d;c.push({x:v,y:w})}for(let u=0;u<c.length-1;u++){let v=c[u].x+(c[u+1].x-c[u].x)*.5,C=c[u].y,y=c[u+1].x-(c[u+1].x-c[u].x)*.5,w=c[u+1].y;t.bezierCurveTo(v,C,y,w,c[u+1].x,c[u+1].y)}t.stroke(),p&&(t.shadowBlur=0)};t.strokeStyle="rgba(255, 255, 255, 0.03)",t.lineWidth=.5,t.beginPath(),t.moveTo(0,i),t.lineTo(n,i),t.stroke();for(let h=0;h<=10;h++){let f=n/10*h;t.beginPath(),t.moveTo(f,0),t.lineTo(f,a),t.stroke()}l(s.color,2,1,!1),r>0&&l(s.progressColor,3,r,!0)}function q(t,e,o,r,s){let n=window.devicePixelRatio||1,a=(s.barWidth||3)*n,i=(s.barSpacing||1)*n,d=Math.floor(e.width/(a+i)),l=S(o,d),h=e.height,f=4*n,m=2*n,p=r*e.width,c=h/2;t.clearRect(0,0,e.width,e.height);for(let g=0;g<l.length;g++){let u=g*(a+i);if(u+a>e.width)break;let v=l[g]*h*.9,C=Math.floor(v/(f+m));t.fillStyle=u<p?s.progressColor:s.color;for(let y=0;y<C;y++){let w=y*(f+m);t.fillRect(u,c-w-f,a,f),y>0&&t.fillRect(u,c+w,a,f)}}}function U(t,e,o,r,s){let n=window.devicePixelRatio||1,a=(s.barWidth||2)*n,i=(s.barSpacing||3)*n,d=Math.floor(e.width/(a+i)),l=S(o,d),h=e.height,f=Math.max(1.5*n,a/2),m=r*e.width,p=h/2;t.clearRect(0,0,e.width,e.height);for(let c=0;c<l.length;c++){let g=c*(a+i)+a/2;if(g>e.width)break;let u=l[c]*h*.9;t.fillStyle=g<m?s.progressColor:s.color,t.beginPath(),t.arc(g,p-u/2,f,0,Math.PI*2),t.fill(),t.beginPath(),t.arc(g,p+u/2,f,0,Math.PI*2),t.fill()}}function F(t,e,o,r,s){let n=e.width,a=e.height,i=a/2,d=4,l=d/2;if(t.clearRect(0,0,n,a),t.fillStyle=s.color||"rgba(255, 255, 255, 0.2)",t.beginPath(),t.moveTo(l,i-d/2),t.lineTo(n-l,i-d/2),t.arc(n-l,i,d/2,-Math.PI/2,Math.PI/2),t.lineTo(l,i+d/2),t.arc(l,i,d/2,Math.PI/2,-Math.PI/2),t.closePath(),t.fill(),r>0){let h=Math.max(l*2,r*n);t.shadowBlur=8,t.shadowColor=s.progressColor,t.fillStyle=s.progressColor||"rgba(255, 255, 255, 0.9)",t.beginPath(),t.moveTo(l,i-d/2),t.lineTo(h-l,i-d/2),t.arc(h-l,i,d/2,-Math.PI/2,Math.PI/2),t.lineTo(l,i+d/2),t.arc(l,i,d/2,Math.PI/2,-Math.PI/2),t.closePath(),t.fill(),t.shadowBlur=0;let f=8,m=h;t.shadowBlur=4,t.shadowColor="rgba(0, 0, 0, 0.3)",t.shadowOffsetY=2,t.fillStyle="#ffffff",t.beginPath(),t.arc(m,i,f,0,Math.PI*2),t.fill(),t.shadowBlur=0,t.shadowOffsetY=0,t.fillStyle=s.progressColor||"rgba(255, 255, 255, 0.9)",t.beginPath(),t.arc(m,i,f*.4,0,Math.PI*2),t.fill()}}var $={bars:I,mirror:x,line:H,blocks:q,dots:U,seekbar:F};function z(t,e,o,r,s){($[s.waveformStyle]||I)(t,e,o,r,s)}function R(t){try{let e=t.getChannelData(0),o=t.sampleRate,r=Y(e,o);if(r.length<2)return 120;let s=[];for(let d=1;d<r.length;d++)s.push((r[d]-r[d-1])/o);let n={};s.forEach(d=>{let l=60/d,h=Math.round(l/3)*3;h>60&&h<200&&(n[h]=(n[h]||0)+1)});let a=0,i=120;for(let[d,l]of Object.entries(n))l>a&&(a=l,i=parseInt(d));return i<70&&n[i*2]?i*=2:i>160&&n[Math.round(i/2)]&&(i=Math.round(i/2)),i-1}catch(e){return console.warn("BPM detection failed:",e),null}}function Y(t,e){let s=[],n=0;for(let a=0;a<t.length-2048;a+=1024){let i=0;for(let h=a;h<a+2048;h++)i+=t[h]*t[h];i=i/2048;let d=i-n,l=n*1.8+.01;if(d>l&&i>.01){let h=s[s.length-1]||0,f=e*.15;a-h>f&&s.push(a)}n=i*.8+n*.2}return s}function N(t,e=200){let o=t.length/e,r=~~(o/10)||1,s=t.numberOfChannels,n=[];for(let i=0;i<s;i++){let d=t.getChannelData(i);for(let l=0;l<e;l++){let h=~~(l*o),f=~~(h+o),m=0,p=0;for(let g=h;g<f;g+=r){let u=d[g];u>p&&(p=u),u<m&&(m=u)}let c=Math.max(Math.abs(p),Math.abs(m));(i===0||c>n[l])&&(n[l]=c)}}let a=Math.max(...n);return a>0?n.map(i=>i/a):n}async function M(t,e=200,o=!1){let r=await fetch(t);if(!r.ok)throw new Error(`HTTP error! status: ${r.status}`);let s=await r.arrayBuffer(),n=window.AudioContext||window.webkitAudioContext,a=new n;try{let i=await a.decodeAudioData(s),l={peaks:N(i,e)};return o&&(l.bpm=R(i)),l}finally{await a.close()}}function A(t=200){let e=[];for(let o=0;o<t;o++){let r=Math.random()*.5+.3,s=Math.sin(o/t*Math.PI*4)*.2;e.push(Math.max(.1,Math.min(1,r+s)))}return e}var D={url:"",height:60,samples:200,waveformStyle:"mirror",barWidth:2,barSpacing:0,colorPreset:"dark",waveformColor:null,progressColor:null,buttonColor:null,buttonHoverColor:null,textColor:null,textSecondaryColor:null,backgroundColor:null,borderColor:null,autoplay:!1,showTime:!0,showHoverTime:!1,showBPM:!1,singlePlay:!0,playOnSeek:!0,title:null,subtitle:null,playIcon:'<svg viewBox="0 0 24 24" width="16" height="16"><path d="M8 5v14l11-7z"/></svg>',pauseIcon:'<svg viewBox="0 0 24 24" width="16" height="16"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg>',onLoad:null,onPlay:null,onPause:null,onEnd:null,onError:null,onTimeUpdate:null},O={bars:{barWidth:3,barSpacing:1},mirror:{barWidth:2,barSpacing:0},line:{barWidth:2,barSpacing:0},blocks:{barWidth:4,barSpacing:2},dots:{barWidth:3,barSpacing:3},seekbar:{barWidth:1,barSpacing:0}};var b=class t{static instances=new Map;static currentlyPlaying=null;constructor(e,o={}){if(this.container=typeof e=="string"?document.querySelector(e):e,!this.container)throw new Error("WaveformPlayer: Container element not found");let r=T(this.container);this.options=B(D,r,o);let s=O[this.options.waveformStyle];s&&(r.barWidth===void 0&&o.barWidth===void 0&&(this.options.barWidth=s.barWidth),r.barSpacing===void 0&&o.barSpacing===void 0&&(this.options.barSpacing=s.barSpacing)),this.options.waveformColor=this.options.waveformColor||"rgba(255, 255, 255, 0.3)",this.options.progressColor=this.options.progressColor||"rgba(255, 255, 255, 0.9)",this.options.buttonColor=this.options.buttonColor||"rgba(255, 255, 255, 0.9)",this.options.textColor=this.options.textColor||"#ffffff",this.options.textSecondaryColor=this.options.textSecondaryColor||"rgba(255, 255, 255, 0.6)",this.audio=null,this.canvas=null,this.ctx=null,this.waveformData=[],this.progress=0,this.isPlaying=!1,this.isLoading=!1,this.hasError=!1,this.updateTimer=null,this.resizeObserver=null,this.id=this.container.id||k(this.options.url),t.instances.set(this.id,this),this.init()}init(){this.createDOM(),this.createAudio(),this.bindEvents(),this.setupResizeObserver(),requestAnimationFrame(()=>{this.resizeCanvas(),this.options.url&&this.load(this.options.url).then(()=>{this.options.autoplay&&this.play()}).catch(e=>{console.error("Failed to load audio:",e)})})}createDOM(){this.container.innerHTML="",this.container.className="waveform-player",this.container.innerHTML=`
2
+ <div class="waveform-player-inner">
3
+ <div class="waveform-body">
4
+ <div class="waveform-track">
5
+ <button class="waveform-btn" aria-label="Play/Pause" style="
6
+ border-color: ${this.options.buttonColor};
7
+ color: ${this.options.buttonColor};
8
+ ">
9
+ <span class="waveform-icon-play">${this.options.playIcon}</span>
10
+ <span class="waveform-icon-pause" style="display:none;">${this.options.pauseIcon}</span>
11
+ </button>
12
+
13
+ <div class="waveform-container">
14
+ <canvas></canvas>
15
+ <div class="waveform-loading" style="display:none;"></div>
16
+ <div class="waveform-error" style="display:none;">
17
+ <span class="waveform-error-text">Unable to load audio</span>
18
+ </div>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="waveform-info">
23
+ <div class="waveform-text">
24
+ <span class="waveform-title" style="color: ${this.options.textColor};"></span>
25
+ ${this.options.subtitle?`<span class="waveform-subtitle" style="color: ${this.options.textSecondaryColor};">${this.options.subtitle}</span>`:""}
26
+ </div>
27
+ <div style="display: flex; align-items: center; gap: 1rem;">
28
+ ${this.options.showBPM?`
29
+ <span class="waveform-bpm" style="color: ${this.options.textSecondaryColor}; display: none;">
30
+ <span class="bpm-value">--</span> BPM
31
+ </span>
32
+ `:""}
33
+ ${this.options.showTime?`
34
+ <span class="waveform-time" style="color: ${this.options.textSecondaryColor};">
35
+ <span class="time-current">0:00</span> / <span class="time-total">0:00</span>
36
+ </span>
37
+ `:""}
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ `,this.playBtn=this.container.querySelector(".waveform-btn"),this.canvas=this.container.querySelector("canvas"),this.ctx=this.canvas.getContext("2d"),this.titleEl=this.container.querySelector(".waveform-title"),this.subtitleEl=this.container.querySelector(".waveform-subtitle"),this.currentTimeEl=this.container.querySelector(".time-current"),this.totalTimeEl=this.container.querySelector(".time-total"),this.bpmEl=this.container.querySelector(".waveform-bpm"),this.bpmValueEl=this.container.querySelector(".bpm-value"),this.loadingEl=this.container.querySelector(".waveform-loading"),this.errorEl=this.container.querySelector(".waveform-error"),this.resizeCanvas()}createAudio(){this.audio=new Audio,this.audio.preload="metadata",this.audio.crossOrigin="anonymous"}bindEvents(){this.playBtn.addEventListener("click",()=>this.togglePlay()),this.audio.addEventListener("loadstart",()=>this.setLoading(!0)),this.audio.addEventListener("loadedmetadata",()=>this.onMetadataLoaded()),this.audio.addEventListener("canplay",()=>this.setLoading(!1)),this.audio.addEventListener("play",()=>this.onPlay()),this.audio.addEventListener("pause",()=>this.onPause()),this.audio.addEventListener("ended",()=>this.onEnded()),this.audio.addEventListener("error",e=>this.onError(e)),this.canvas.addEventListener("click",e=>this.handleCanvasClick(e)),window.addEventListener("resize",L(()=>this.resizeCanvas(),100))}setupResizeObserver(){"ResizeObserver"in window&&(this.resizeObserver=new ResizeObserver(()=>{this.resizeCanvas()}),this.canvas?.parentElement&&this.resizeObserver.observe(this.canvas.parentElement))}async load(e){try{this.setLoading(!0),this.progress=0,this.hasError=!1,this.audio.src=e,await new Promise((r,s)=>{let n=()=>{this.audio.removeEventListener("loadedmetadata",n),this.audio.removeEventListener("error",a),r()},a=i=>{this.audio.removeEventListener("loadedmetadata",n),this.audio.removeEventListener("error",a),s(i)};this.audio.addEventListener("loadedmetadata",n),this.audio.addEventListener("error",a)});let o=this.options.title||W(e);if(this.titleEl&&(this.titleEl.textContent=o),this.options.waveform)this.setWaveformData(this.options.waveform);else try{let r=await M(e,this.options.samples,this.options.showBPM);this.waveformData=r.peaks,r.bpm&&(this.detectedBPM=r.bpm,this.updateBPMDisplay())}catch(r){console.warn("Using placeholder waveform:",r),this.waveformData=A(this.options.samples)}this.drawWaveform(),this.options.onLoad&&this.options.onLoad(this)}catch(o){console.error("Failed to load audio:",o),this.onError(o)}finally{this.setLoading(!1)}}setWaveformData(e){if(typeof e=="string")try{let o=JSON.parse(e);this.waveformData=Array.isArray(o)?o:[]}catch{this.waveformData=e.split(",").map(Number)}else this.waveformData=Array.isArray(e)?e:[];this.drawWaveform()}drawWaveform(){!this.ctx||this.waveformData.length===0||z(this.ctx,this.canvas,this.waveformData,this.progress,{...this.options,waveformStyle:this.options.waveformStyle||"bars",color:this.options.waveformColor,progressColor:this.options.progressColor})}resizeCanvas(){let e=window.devicePixelRatio||1,o=this.canvas.getBoundingClientRect();this.canvas.width=o.width*e,this.canvas.height=this.options.height*e,this.canvas.style.height=this.options.height+"px",this.canvas.parentElement.style.height=this.options.height+"px",this.drawWaveform()}handleCanvasClick(e){if(!this.audio.duration)return;let o=this.canvas.getBoundingClientRect(),r=e.clientX-o.left,s=Math.max(0,Math.min(1,r/o.width));this.seekToPercent(s)}setLoading(e){this.isLoading=e,this.loadingEl&&(this.loadingEl.style.display=e?"block":"none")}onMetadataLoaded(){this.totalTimeEl&&(this.totalTimeEl.textContent=P(this.audio.duration))}onPlay(){this.isPlaying=!0,this.playBtn.classList.add("playing");let e=this.playBtn.querySelector(".waveform-icon-play"),o=this.playBtn.querySelector(".waveform-icon-pause");e&&(e.style.display="none"),o&&(o.style.display="flex"),this.startSmoothUpdate(),this.options.onPlay&&this.options.onPlay(this)}onPause(){this.isPlaying=!1,this.playBtn.classList.remove("playing");let e=this.playBtn.querySelector(".waveform-icon-play"),o=this.playBtn.querySelector(".waveform-icon-pause");e&&(e.style.display="flex"),o&&(o.style.display="none"),this.stopSmoothUpdate(),this.options.onPause&&this.options.onPause(this)}onEnded(){this.progress=0,this.audio.currentTime=0,this.drawWaveform(),this.currentTimeEl&&(this.currentTimeEl.textContent="0:00"),this.onPause(),this.options.onEnd&&this.options.onEnd(this)}onError(e){console.error("Audio error:",e),this.hasError=!0,this.setLoading(!1),this.errorEl&&(this.errorEl.style.display="flex"),this.canvas&&(this.canvas.style.opacity="0.2"),this.playBtn&&(this.playBtn.disabled=!0),this.options.onError&&this.options.onError(e,this)}startSmoothUpdate(){this.stopSmoothUpdate();let e=()=>{this.isPlaying&&this.audio.duration&&(this.updateProgress(),this.updateTimer=requestAnimationFrame(e))};this.updateTimer=requestAnimationFrame(e)}stopSmoothUpdate(){this.updateTimer&&(cancelAnimationFrame(this.updateTimer),this.updateTimer=null)}updateProgress(){if(!this.audio.duration)return;let e=this.audio.currentTime/this.audio.duration;Math.abs(e-this.progress)>.001&&(this.progress=e,this.drawWaveform()),this.currentTimeEl&&(this.currentTimeEl.textContent=P(this.audio.currentTime)),this.options.onTimeUpdate&&this.options.onTimeUpdate(this.audio.currentTime,this.audio.duration,this)}updateBPMDisplay(){this.bpmEl&&this.bpmValueEl&&this.detectedBPM&&(this.bpmValueEl.textContent=Math.round(this.detectedBPM),this.bpmEl.style.display="inline-flex")}play(){this.options.singlePlay&&t.currentlyPlaying&&t.currentlyPlaying!==this&&t.currentlyPlaying.pause(),t.currentlyPlaying=this,this.audio.play()}pause(){t.currentlyPlaying===this&&(t.currentlyPlaying=null),this.audio.pause()}togglePlay(){this.isPlaying?this.pause():this.play()}seekToPercent(e){this.audio&&this.audio.duration&&(this.audio.currentTime=this.audio.duration*Math.max(0,Math.min(1,e)),this.updateProgress())}setVolume(e){this.audio&&(this.audio.volume=Math.max(0,Math.min(1,e)))}destroy(){this.pause(),this.stopSmoothUpdate(),this.resizeObserver&&this.resizeObserver.disconnect(),t.instances.delete(this.id),this.audio&&(this.audio.src=""),this.container.innerHTML=""}static getInstance(e){if(typeof e=="string"){let o=this.instances.get(e);if(o)return o;let r=document.getElementById(e);if(r)return Array.from(this.instances.values()).find(s=>s.container===r)}if(e instanceof HTMLElement)return Array.from(this.instances.values()).find(o=>o.container===e)}static getAllInstances(){return Array.from(this.instances.values())}static destroyAll(){this.instances.forEach(e=>e.destroy()),this.instances.clear()}static async generateWaveformData(e,o=200){try{return(await M(e,o)).peaks}catch(r){throw console.error("Failed to generate waveform:",r),r}}};function E(){if(typeof document>"u")return;document.querySelectorAll("[data-waveform-player]").forEach(e=>{if(e.dataset.waveformInitialized!=="true")try{new b(e),e.dataset.waveformInitialized="true"}catch(o){console.error("Failed to initialize WaveformPlayer:",o,e)}})}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",E):E());b.init=E;typeof window<"u"&&(window.WaveformPlayer=b);var st=b;})();
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@arraypress/waveform-player",
3
+ "version": "1.0.0",
4
+ "description": "Lightweight, customizable audio player with waveform visualization",
5
+ "main": "dist/waveform-player.js",
6
+ "module": "dist/waveform-player.esm.js",
7
+ "unpkg": "dist/waveform-player.min.js",
8
+ "files": [
9
+ "dist/",
10
+ "src/",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "build": "npm run build:iife && npm run build:esm && npm run build:min",
16
+ "build:iife": "esbuild src/index.js --bundle --format=iife --outfile=dist/waveform-player.js",
17
+ "build:min": "esbuild src/index.js --bundle --format=iife --outfile=dist/waveform-player.min.js --minify",
18
+ "build:esm": "esbuild src/index.js --bundle --format=esm --outfile=dist/waveform-player.esm.js --minify",
19
+ "dev": "esbuild src/index.js --bundle --format=iife --outfile=dist/waveform-player.js --watch",
20
+ "size": "npm run build:min && gzip -c dist/waveform-player.min.js | wc -c",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "keywords": [
24
+ "audio",
25
+ "player",
26
+ "waveform",
27
+ "visualization",
28
+ "music",
29
+ "sound",
30
+ "html5",
31
+ "web-audio"
32
+ ],
33
+ "author": "ArrayPress",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/arraypress/waveform-player.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/arraypress/waveform-player/issues"
41
+ },
42
+ "homepage": "https://github.com/arraypress/waveform-player#readme",
43
+ "devDependencies": {
44
+ "esbuild": "^0.25.0"
45
+ }
46
+ }
package/src/audio.js ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @module audio
3
+ * @description Audio processing for WaveformPlayer
4
+ */
5
+
6
+ import { detectBPM } from './bpm.js';
7
+
8
+ /**
9
+ * Extract peaks from audio buffer for waveform visualization
10
+ * @param {AudioBuffer} buffer - Audio buffer
11
+ * @param {number} samples - Number of samples to extract
12
+ * @returns {number[]} Array of peak values (0-1)
13
+ */
14
+ export function extractPeaks(buffer, samples = 200) {
15
+ const sampleSize = buffer.length / samples;
16
+ const sampleStep = ~~(sampleSize / 10) || 1;
17
+ const channels = buffer.numberOfChannels;
18
+ const peaks = [];
19
+
20
+ for (let c = 0; c < channels; c++) {
21
+ const chan = buffer.getChannelData(c);
22
+
23
+ for (let i = 0; i < samples; i++) {
24
+ const start = ~~(i * sampleSize);
25
+ const end = ~~(start + sampleSize);
26
+
27
+ let min = 0;
28
+ let max = 0;
29
+
30
+ for (let j = start; j < end; j += sampleStep) {
31
+ const value = chan[j];
32
+ if (value > max) max = value;
33
+ if (value < min) min = value;
34
+ }
35
+
36
+ const peak = Math.max(Math.abs(max), Math.abs(min));
37
+
38
+ if (c === 0 || peak > peaks[i]) {
39
+ peaks[i] = peak;
40
+ }
41
+ }
42
+ }
43
+
44
+ // Normalize peaks
45
+ const maxPeak = Math.max(...peaks);
46
+ return maxPeak > 0 ? peaks.map(peak => peak / maxPeak) : peaks;
47
+ }
48
+
49
+ /**
50
+ * Generate waveform data from audio URL
51
+ * @param {string} url - Audio URL
52
+ * @param {number} samples - Number of samples
53
+ * @param {boolean} [includeBPM=false] - Whether to detect BPM
54
+ * @returns {Promise<{peaks: number[], bpm?: number}>} Waveform data
55
+ */
56
+ export async function generateWaveform(url, samples = 200, includeBPM = false) {
57
+ const response = await fetch(url);
58
+ if (!response.ok) {
59
+ throw new Error(`HTTP error! status: ${response.status}`);
60
+ }
61
+
62
+ const arrayBuffer = await response.arrayBuffer();
63
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
64
+ const audioContext = new AudioContextClass();
65
+
66
+ try {
67
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
68
+ const peaks = extractPeaks(audioBuffer, samples);
69
+
70
+ const result = { peaks };
71
+ if (includeBPM) {
72
+ result.bpm = detectBPM(audioBuffer);
73
+ }
74
+
75
+ return result;
76
+ } finally {
77
+ await audioContext.close();
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Generate placeholder waveform data
83
+ * @param {number} samples - Number of samples
84
+ * @returns {number[]} Random waveform data
85
+ */
86
+ export function generatePlaceholderWaveform(samples = 200) {
87
+ const data = [];
88
+ for (let i = 0; i < samples; i++) {
89
+ const base = Math.random() * 0.5 + 0.3;
90
+ const variation = Math.sin(i / samples * Math.PI * 4) * 0.2;
91
+ data.push(Math.max(0.1, Math.min(1, base + variation)));
92
+ }
93
+ return data;
94
+ }
package/src/bpm.js ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @module bpm
3
+ * @description BPM detection for audio analysis
4
+ */
5
+
6
+ /**
7
+ * Detect BPM from audio buffer
8
+ * @param {AudioBuffer} buffer - Audio buffer to analyze
9
+ * @returns {number|null} Detected BPM or null
10
+ */
11
+ export function detectBPM(buffer) {
12
+ try {
13
+ const channelData = buffer.getChannelData(0);
14
+ const sampleRate = buffer.sampleRate;
15
+ const onsets = detectOnsets(channelData, sampleRate);
16
+
17
+ if (onsets.length < 2) return 120;
18
+
19
+ // Calculate intervals
20
+ const intervals = [];
21
+ for (let i = 1; i < onsets.length; i++) {
22
+ intervals.push((onsets[i] - onsets[i - 1]) / sampleRate);
23
+ }
24
+
25
+ // Convert to tempos and group
26
+ const tempoGroups = {};
27
+ intervals.forEach(interval => {
28
+ const tempo = 60 / interval;
29
+ const bucket = Math.round(tempo / 3) * 3;
30
+ if (bucket > 60 && bucket < 200) {
31
+ tempoGroups[bucket] = (tempoGroups[bucket] || 0) + 1;
32
+ }
33
+ });
34
+
35
+ // Find most common
36
+ let maxCount = 0;
37
+ let detectedBPM = 120;
38
+ for (const [tempo, count] of Object.entries(tempoGroups)) {
39
+ if (count > maxCount) {
40
+ maxCount = count;
41
+ detectedBPM = parseInt(tempo);
42
+ }
43
+ }
44
+
45
+ // Handle tempo ambiguity
46
+ if (detectedBPM < 70 && tempoGroups[detectedBPM * 2]) {
47
+ detectedBPM *= 2;
48
+ } else if (detectedBPM > 160 && tempoGroups[Math.round(detectedBPM / 2)]) {
49
+ detectedBPM = Math.round(detectedBPM / 2);
50
+ }
51
+
52
+ return detectedBPM - 1; // Calibration offset
53
+ } catch (e) {
54
+ console.warn('BPM detection failed:', e);
55
+ return null;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Detect onsets (transients/beats) in audio
61
+ * @private
62
+ */
63
+ function detectOnsets(channelData, sampleRate) {
64
+ const windowSize = 2048;
65
+ const hopSize = windowSize / 2;
66
+ const onsets = [];
67
+ let previousEnergy = 0;
68
+
69
+ for (let i = 0; i < channelData.length - windowSize; i += hopSize) {
70
+ let energy = 0;
71
+ for (let j = i; j < i + windowSize; j++) {
72
+ energy += channelData[j] * channelData[j];
73
+ }
74
+ energy = energy / windowSize;
75
+
76
+ const energyDiff = energy - previousEnergy;
77
+ const threshold = previousEnergy * 1.8 + 0.01;
78
+
79
+ if (energyDiff > threshold && energy > 0.01) {
80
+ const lastOnset = onsets[onsets.length - 1] || 0;
81
+ const minDistance = sampleRate * 0.15;
82
+
83
+ if (i - lastOnset > minDistance) {
84
+ onsets.push(i);
85
+ }
86
+ }
87
+
88
+ previousEnergy = energy * 0.8 + previousEnergy * 0.2;
89
+ }
90
+
91
+ return onsets;
92
+ }