1489 lines
61 KiB
HTML
Executable File
1489 lines
61 KiB
HTML
Executable File
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>U-Howler.js Demo</title>
|
|
<style>
|
|
:root {
|
|
--space: 8px;
|
|
--radius: 5px;
|
|
|
|
--gray: #6c757d;
|
|
--gray-bg: #f5f5f5;
|
|
--blue: #007acc;
|
|
--green: #28a745;
|
|
--white: white;
|
|
--dark: #333;
|
|
|
|
--max-width: 1400px;
|
|
--sidebar: 400px;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
max-width: var(--max-width);
|
|
margin: 0 auto;
|
|
padding: var(--space);
|
|
background: var(--gray-bg);
|
|
}
|
|
|
|
.container {
|
|
background: var(--white);
|
|
padding: calc(var(--space) * 1.5);
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
.main-layout {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: calc(var(--space) * 1.5);
|
|
}
|
|
|
|
.api-column {
|
|
background: var(--gray-bg);
|
|
padding: var(--space);
|
|
border-radius: var(--radius);
|
|
border-left: 4px solid var(--green);
|
|
}
|
|
|
|
h1 {
|
|
color: var(--dark);
|
|
text-align: center;
|
|
margin-bottom: calc(var(--space) * 1.5);
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
h2 {
|
|
color: var(--gray);
|
|
border-bottom: 2px solid #e0e0e0;
|
|
padding-bottom: var(--space);
|
|
margin-top: calc(var(--space) * 1.5);
|
|
}
|
|
|
|
.demo-section {
|
|
margin: var(--space) 0;
|
|
padding: var(--space);
|
|
background: var(--gray-bg);
|
|
border-radius: var(--radius);
|
|
border-left: 4px solid var(--blue);
|
|
}
|
|
|
|
.controls, .slider-group {
|
|
display: flex;
|
|
gap: var(--space);
|
|
margin: 15px 0;
|
|
}
|
|
|
|
.controls { flex-wrap: wrap; }
|
|
.slider-group { align-items: center; }
|
|
.feature-grid { display: grid; gap: var(--space); margin: var(--space) 0; }
|
|
|
|
button {
|
|
background: var(--blue);
|
|
color: var(--white);
|
|
border: none;
|
|
padding: var(--space) var(--space);
|
|
border-radius: var(--radius);
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
button:hover { filter: brightness(0.9); }
|
|
button:disabled { background: #ccc; cursor: not-allowed; }
|
|
input[type="range"] { flex: 1; max-width: 200px; }
|
|
|
|
.status, .code-example {
|
|
font-family: monospace;
|
|
padding: var(--space);
|
|
border-radius: var(--radius);
|
|
margin: var(--space) 0;
|
|
}
|
|
|
|
.status {
|
|
background: var(--dark);
|
|
color: #0f0;
|
|
white-space: pre-wrap;
|
|
max-height: 150px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.code-example {
|
|
background: #2d3748;
|
|
color: #e2e8f0;
|
|
padding: 15px;
|
|
overflow-x: auto;
|
|
margin: var(--space) 0;
|
|
}
|
|
|
|
.highlight { color: #68d391; }
|
|
.keyword { color: #fbb6ce; }
|
|
.string { color: #fbd38d; }
|
|
|
|
.audio-visual {
|
|
height: 100px;
|
|
background: linear-gradient(45deg, #1e3c72, #2a5298);
|
|
border-radius: var(--radius);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--white);
|
|
font-weight: bold;
|
|
margin: 15px 0;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.waveform {
|
|
position: absolute;
|
|
inset: 0;
|
|
opacity: 0.3;
|
|
background: repeating-linear-gradient(90deg, transparent 0 10px, rgba(255,255,255,0.2) 10px 20px);
|
|
animation: wave 2s linear infinite;
|
|
}
|
|
|
|
.eq-visualizer {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
align-items: end;
|
|
gap: 2px;
|
|
padding: 5px;
|
|
opacity: 1;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.eq-bar {
|
|
flex: 1;
|
|
background: linear-gradient(to top, #68d391, #48bb78, #38a169);
|
|
border-radius: 2px 2px 0 0;
|
|
min-height: 2px;
|
|
transition: height 0.05s ease;
|
|
width: 100%;
|
|
transform-origin: bottom;
|
|
}
|
|
|
|
@keyframes wave { to { transform: translateX(20px); } }
|
|
|
|
.api-section { margin: 15px 0; }
|
|
.api-section h3 {
|
|
color: var(--green);
|
|
margin: 0 0 var(--space) 0;
|
|
}
|
|
|
|
.api-method {
|
|
font-family: monospace;
|
|
margin: 3px 0;
|
|
color: var(--gray);
|
|
}
|
|
|
|
.api-method .method-name { color: var(--blue); font-weight: bold; }
|
|
.api-method .return-type { color: #6f42c1; }
|
|
.api-method .param { color: #e83e8c; }
|
|
|
|
.api-description {
|
|
margin: var(--space) 0;
|
|
}
|
|
|
|
.api-options {
|
|
color: var(--gray);
|
|
}
|
|
|
|
/* Spatial demo styles */
|
|
.spatial-container {
|
|
width: 100%;
|
|
height: 200px;
|
|
background: var(--dark);
|
|
border-radius: var(--radius);
|
|
position: relative;
|
|
cursor: crosshair;
|
|
margin: var(--space) 0;
|
|
}
|
|
|
|
.spatial-dot {
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
cursor: move;
|
|
}
|
|
|
|
.spatial-listener {
|
|
width: 32px;
|
|
height: 32px;
|
|
background: var(--green);
|
|
transform: translate(-8px, -8px);
|
|
left: 50%;
|
|
top: 50%;
|
|
}
|
|
|
|
.spatial-source {
|
|
width: 20px;
|
|
height: 20px;
|
|
transform: translate(-6px, -6px);
|
|
}
|
|
|
|
.spatial-source1 {
|
|
background: #fbd38d;
|
|
left: 25%;
|
|
top: 30%;
|
|
}
|
|
|
|
.spatial-source2 {
|
|
background: #fbb6ce;
|
|
left: 75%;
|
|
top: 70%;
|
|
}
|
|
|
|
.spatial-flash {
|
|
animation: ping-flash 0.3s ease-out;
|
|
}
|
|
|
|
@keyframes ping-flash {
|
|
0% {
|
|
transform: translate(-6px, -6px) scale(1);
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
transform: translate(-6px, -6px) scale(1.4);
|
|
opacity: 0.8;
|
|
box-shadow: 0 0 15px currentColor;
|
|
}
|
|
100% {
|
|
transform: translate(-6px, -6px) scale(1);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.help-text {
|
|
color: var(--gray);
|
|
margin: var(--space) 0;
|
|
}
|
|
|
|
.code-comment {
|
|
color: #68d391;
|
|
}
|
|
|
|
/* Sprite demo button styles */
|
|
.feature-grid button {
|
|
padding: calc(var(--space) * 1.5) var(--space);
|
|
font-size: 14px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.feature-grid button:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 8px rgba(0, 122, 204, 0.3);
|
|
}
|
|
|
|
.feature-grid button:active:not(:disabled) {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
@media (max-width: 1200px) {
|
|
.main-layout { grid-template-columns: 1fr; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body onload="createBasicSound();">
|
|
<div class="container">
|
|
<h1>U-Howler.js Demo</h1>
|
|
|
|
<div class="main-layout">
|
|
<div class="demo-column">
|
|
<h2>Basic Controls</h2>
|
|
<div class="demo-section">
|
|
<div class="audio-visual">
|
|
<div class="waveform"></div>
|
|
<div class="eq-visualizer" id="eqVisualizer">
|
|
</div>
|
|
</div>
|
|
<div class="controls">
|
|
<button onclick="createBasicSound()">Create Sound</button>
|
|
<button onclick="playBasic()" id="playBasic" disabled>Play</button>
|
|
<button onclick="pauseBasic()" id="pauseBasic" disabled>Pause</button>
|
|
<button onclick="stopBasic()" id="stopBasic" disabled>Stop</button>
|
|
</div>
|
|
|
|
<div class="slider-group">
|
|
<label>Frequency:</label>
|
|
<input type="range" min="200" max="2000" step="50" value="440"
|
|
onchange="updateFrequency(this.value)" id="frequencySlider">
|
|
<span id="frequencyValue">440 Hz</span>
|
|
</div>
|
|
|
|
<div class="slider-group">
|
|
<label>Volume:</label>
|
|
<input type="range" min="0" max="1" step="0.1" value="1"
|
|
onchange="setBasicVolume(this.value)" id="volumeSlider" disabled>
|
|
<span id="volumeValue">1.0</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="demo-section">
|
|
<div class="controls">
|
|
<button onclick="demonstratePromises()">Demo Async/Await</button>
|
|
<button onclick="demonstrateTraditional()">Demo Traditional Callbacks</button>
|
|
</div>
|
|
|
|
<div class="status" id="promiseStatus"></div>
|
|
</div>
|
|
|
|
<div class="demo-section">
|
|
<h3>Global Controls</h3>
|
|
<div class="controls">
|
|
<button onclick="globalMute()">Global Mute/Unmute</button>
|
|
<button onclick="globalStop()">Stop All Sounds</button>
|
|
</div>
|
|
|
|
<div class="slider-group">
|
|
<label>Master Volume:</label>
|
|
<input type="range" min="0" max="1" step="0.1" value="1"
|
|
onchange="Howler.volume(this.value); updateMasterVol(this.value)">
|
|
<span id="masterVolValue">1.0</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="api-column">
|
|
<h2>API Overview</h2>
|
|
|
|
<div class="api-section">
|
|
<h3>Howler (Global)</h3>
|
|
<div class="api-method"><span class="method-name">volume</span>(<span class="param">vol?</span>) → <span class="return-type">Howler|number</span></div>
|
|
<div class="api-method"><span class="method-name">mute</span>(<span class="param">muted?</span>) → <span class="return-type">Howler</span></div>
|
|
<div class="api-method"><span class="method-name">stop</span>() → <span class="return-type">Howler</span></div>
|
|
<div class="api-method"><span class="method-name">unload</span>() → <span class="return-type">Howler</span></div>
|
|
<div class="api-method"><span class="method-name">codecs</span>(<span class="param">ext</span>) → <span class="return-type">boolean</span></div>
|
|
<div class="api-method"><span class="method-name">environment</span>(<span class="param">env</span>) → <span class="return-type">Howler</span></div>
|
|
</div>
|
|
|
|
<div class="api-section">
|
|
<h3>Howl Constructor</h3>
|
|
<div class="api-method"><span class="keyword">new</span> <span class="method-name">Howl</span>(<span class="param">options</span>)</div>
|
|
<div class="api-options">
|
|
Options: src, volume, rate, loop, mute, preload, autoplay, sprite, format, html5, pool, xhr, spatial, on*
|
|
</div>
|
|
</div>
|
|
|
|
<div class="api-section">
|
|
<h3>Core Methods</h3>
|
|
<div class="api-method"><span class="method-name">load</span>() → <span class="return-type">Promise<Howl></span></div>
|
|
<div class="api-method"><span class="method-name">play</span>(<span class="param">sprite?</span>) → <span class="return-type">number|Promise</span></div>
|
|
<div class="api-method"><span class="method-name">pause</span>(<span class="param">id?</span>) → <span class="return-type">Howl</span></div>
|
|
<div class="api-method"><span class="method-name">stop</span>(<span class="param">id?</span>) → <span class="return-type">Howl</span></div>
|
|
<div class="api-method"><span class="method-name">mute</span>(<span class="param">muted?, id?</span>) → <span class="return-type">Howl|boolean</span></div>
|
|
<div class="api-method"><span class="method-name">volume</span>(<span class="param">vol?, id?</span>) → <span class="return-type">Howl|number</span></div>
|
|
<div class="api-method"><span class="method-name">fade</span>(<span class="param">from, to, duration, id?</span>) → <span class="return-type">Howl</span></div>
|
|
<div class="api-method"><span class="method-name">rate</span>(<span class="param">rate?, id?</span>) → <span class="return-type">Howl|number</span></div>
|
|
<div class="api-method"><span class="method-name">seek</span>(<span class="param">seek?, id?</span>) → <span class="return-type">Howl|number</span></div>
|
|
</div>
|
|
|
|
<div class="api-section">
|
|
<h3>Examples</h3>
|
|
<pre class="code-example">
|
|
<span class="code-comment">// Promise support</span>
|
|
<span class="keyword">const</span> howl = <span class="keyword">new</span> <span class="highlight">Howl</span>({src: <span class="string">'audio.mp3'</span>});
|
|
<span class="keyword">await</span> howl.load();
|
|
<span class="keyword">const</span> id = howl.play();
|
|
|
|
<span class="code-comment">// Sprite usage</span>
|
|
<span class="keyword">const</span> soundFx = <span class="keyword">new</span> <span class="highlight">Howl</span>({
|
|
src: <span class="string">'sounds.mp3'</span>,
|
|
sprite: {
|
|
jump: [<span class="highlight">0</span>, <span class="highlight">500</span>],
|
|
shoot: [<span class="highlight">600</span>, <span class="highlight">300</span>]
|
|
}
|
|
});
|
|
soundFx.play(<span class="string">'jump'</span>);
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="main-layout">
|
|
<div class="demo-column">
|
|
<h2>Audio Sprites</h2>
|
|
<div class="demo-section">
|
|
<h3>Audio Sprites Demo</h3>
|
|
<div class="audio-visual" id="spriteVisual">
|
|
<div class="waveform"></div>
|
|
<div class="eq-visualizer" id="spriteEqVisualizer">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<button onclick="createSpriteSound()" id="createSpriteBtn">Create Sprite Collection</button>
|
|
<button onclick="playFullSprite()" id="playFullBtn" disabled>Play Full Collection</button>
|
|
</div>
|
|
|
|
<div class="feature-grid" id="spriteButtons" style="grid-template-columns: repeat(4, 1fr);">
|
|
<button onclick="playSpriteSound('jump')" disabled>🦘 Jump</button>
|
|
<button onclick="playSpriteSound('coin')" disabled>🪙 Coin</button>
|
|
<button onclick="playSpriteSound('laser')" disabled>⚡ Laser</button>
|
|
<button onclick="playSpriteSound('explosion')" disabled>💥 Boom</button>
|
|
<button onclick="playSpriteSound('click')" disabled>🖱️ Click</button>
|
|
<button onclick="playSpriteSound('power_up')" disabled>⭐ Power</button>
|
|
<button onclick="playSpriteSound('hurt')" disabled>😵 Hurt</button>
|
|
<button onclick="playSpriteSound('victory')" disabled>🏆 Victory</button>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="api-column">
|
|
<h2>Audio Sprites API</h2>
|
|
|
|
<div class="api-section">
|
|
<h3>Sprite Methods</h3>
|
|
<div class="api-method"><span class="method-name">play</span>(<span class="param">spriteName</span>) → <span class="return-type">number</span></div>
|
|
<div class="api-description">
|
|
Play a specific sprite by name from the sprite collection.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="api-section">
|
|
<h3>Sprite Configuration</h3>
|
|
<div class="api-description">
|
|
<strong>Sprite Format:</strong> { name: [start_ms, duration_ms] }<br>
|
|
• start_ms: Starting position in milliseconds<br>
|
|
• duration_ms: Length of the sprite in milliseconds<br>
|
|
• Sprites can be tightly packed or have gaps between them<br>
|
|
</div>
|
|
<pre class="code-example">
|
|
<span class="keyword">const</span> spriteMap = {
|
|
jump: [<span class="highlight">0</span>, <span class="highlight">500</span>], <span class="code-comment">// 0ms - 500ms</span>
|
|
coin: [<span class="highlight">500</span>, <span class="highlight">300</span>], <span class="code-comment">// 500ms - 800ms</span>
|
|
laser: [<span class="highlight">800</span>, <span class="highlight">400</span>], <span class="code-comment">// 800ms - 1200ms</span>
|
|
explosion: [<span class="highlight">1200</span>, <span class="highlight">800</span>] <span class="code-comment">// 1200ms - 2000ms</span>
|
|
};
|
|
|
|
<span class="keyword">const</span> gameAudio = <span class="keyword">new</span> <span class="highlight">Howl</span>({
|
|
src: <span class="string">'game-sounds.wav'</span>,
|
|
sprite: spriteMap
|
|
});
|
|
|
|
gameAudio.play(<span class="string">'jump'</span>);
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="main-layout">
|
|
<div class="demo-column">
|
|
<h2>Audio Effects</h2>
|
|
<div class="demo-section">
|
|
<h3>Filters</h3>
|
|
<div class="controls">
|
|
<button onclick="addLowpass()" id="lowpassBtn" disabled>Add Lowpass</button>
|
|
<button onclick="addHighpass()" id="highpassBtn" disabled>Add Highpass</button>
|
|
<button onclick="addBandpass()" id="bandpassBtn" disabled>Add Bandpass</button>
|
|
<button onclick="addDelay()" id="delayBtn" disabled>Add Delay</button>
|
|
<button onclick="addReverb()" id="reverbBtn" disabled>Add Reverb</button>
|
|
<button onclick="addDistortion()" id="distortionBtn" disabled>Add Distortion</button>
|
|
</div>
|
|
<div class="controls">
|
|
<button onclick="clearEffects()" id="clearEffectsBtn" disabled>Clear All Effects</button>
|
|
<button onclick="reverseEffects()" id="reverseEffectsBtn" disabled>Reverse Effect Order</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="feature-grid">
|
|
<div class="demo-section">
|
|
<h3>Rate Control</h3>
|
|
<div class="slider-group">
|
|
<label>Playback Rate:</label>
|
|
<input type="range" min="0.25" max="4" step="0.1" value="1"
|
|
onchange="setRate(this.value)" id="rateSlider" disabled>
|
|
<span id="rateValue">1.0x</span>
|
|
</div>
|
|
<button onclick="demoRateChange()" id="rateDemo" disabled>Demo Rate Changes</button>
|
|
</div>
|
|
|
|
<div class="demo-section">
|
|
<h3>Fade Effects</h3>
|
|
<div class="controls">
|
|
<button onclick="fadeIn()" id="fadeInBtn" disabled>Fade In</button>
|
|
<button onclick="fadeOut()" id="fadeOutBtn" disabled>Fade Out</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="api-column">
|
|
<h2>Effects API</h2>
|
|
|
|
<div class="api-section">
|
|
<h3>Effect Methods</h3>
|
|
<div class="api-method"><span class="method-name">addEffect</span>(<span class="param">type, options?</span>) → <span class="return-type">AudioNode</span></div>
|
|
<div class="api-method"><span class="method-name">effects</span> → <span class="return-type">Array<AudioNode></span></div>
|
|
<div class="api-method"><span class="method-name">updateEffectsChain</span>() → <span class="return-type">Howl</span></div>
|
|
<div class="api-method"><span class="method-name">enableEQ</span>(<span class="param">bands?, callback?, boost?</span>) → <span class="return-type">Howl</span></div>
|
|
<div class="api-method"><span class="method-name">disableEQ</span>() → <span class="return-type">Howl</span></div>
|
|
</div>
|
|
|
|
<div class="api-section">
|
|
<h3>Audio Effects</h3>
|
|
<div class="api-description">
|
|
<strong>addEffect(type, options)</strong> - Add audio effect to the sound<br>
|
|
• Returns the Web Audio node for direct manipulation<br>
|
|
• Types: 'lowpass', 'highpass', 'bandpass', 'notch', 'delay', 'reverb', 'gain', 'distortion'<br>
|
|
• Options vary by type (frequency, Q, delay, gain, etc.)<br><br>
|
|
<strong>effects</strong> - Getter for effects array (for direct manipulation)<br>
|
|
<strong>updateEffectsChain()</strong> - Rebuild audio chain after effects manipulation
|
|
</div>
|
|
<pre class="code-example">
|
|
<span class="code-comment">// Add effects and manipulate them</span>
|
|
<span class="keyword">const</span> filter = howl.addEffect(<span class="string">'lowpass'</span>, {
|
|
frequency: <span class="highlight">1000</span>, Q: <span class="highlight">1</span>
|
|
});
|
|
|
|
<span class="code-comment">// Direct effects array manipulation</span>
|
|
howl.effects.splice(<span class="highlight">1</span>, <span class="highlight">1</span>); <span class="code-comment">// Remove delay effect</span>
|
|
howl.effects.reverse(); <span class="code-comment">// Reorder effects</span>
|
|
howl.updateEffectsChain(); <span class="code-comment">// Apply changes</span>
|
|
</pre>
|
|
</div>
|
|
|
|
<div class="api-section">
|
|
<h3>Frequency Analysis</h3>
|
|
<div class="api-description">
|
|
<strong>enableEQ(bands, callback, boost)</strong> - Enable real-time frequency analysis<br>
|
|
• bands: Number of frequency bands (default: 16)<br>
|
|
• callback: Function called with frequency data array<br>
|
|
• boost: Gain multiplier for visualization (default: 4.0)<br><br>
|
|
<strong>disableEQ()</strong> - Disable frequency analysis<br>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="main-layout">
|
|
<div class="demo-column">
|
|
<h2>3D Spatial Audio</h2>
|
|
<div class="demo-section">
|
|
<div id="spatialDemo" class="spatial-container">
|
|
<div id="listener" class="spatial-dot spatial-listener"></div>
|
|
<div id="source1" class="spatial-dot spatial-source spatial-source1"></div>
|
|
<div id="source2" class="spatial-dot spatial-source spatial-source2"></div>
|
|
</div>
|
|
<div class="help-text">
|
|
🟢 Green is you!
|
|
Drag the dots to move them around the 3D space!
|
|
🏛️ Cathedral environment provides realistic reverb.
|
|
</div>
|
|
<div class="controls">
|
|
<button onclick="startSpatialDemo()" id="spatialStartBtn">Start Spatial Demo</button>
|
|
<button onclick="stopSpatialDemo()" id="spatialStopBtn" disabled>Stop Demo</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="api-column">
|
|
<h2>Spatial API</h2>
|
|
|
|
<div class="api-section">
|
|
<h3>3D Spatial Audio</h3>
|
|
<div class="api-description">
|
|
<strong>spatial</strong> - Object containing 3D audio properties<br>
|
|
• Properties: position{x,y,z}, orientation{x,y,z}, velocity{x,y,z}<br>
|
|
• distance{ref, max, rolloff, model}, cone{inner, outer, outerGain}<br>
|
|
• occlusion, obstruction, panningModel<br><br>
|
|
<strong>Howler.listener</strong> - Global listener (camera/player) object<br>
|
|
<strong>Howler.environment(env)</strong> - Set reverb environment<br>
|
|
• Accepts preset strings: 'room', 'hall', 'cathedral'<br>
|
|
• Or custom objects: { size, decay, dampening }<br>
|
|
• Pass null to disable environment reverb
|
|
</div>
|
|
</div>
|
|
|
|
<div class="api-section">
|
|
<h3>Environment Presets</h3>
|
|
<div class="api-description">
|
|
<strong>Built-in Reverb Environments:</strong> 'room', 'hall', 'cathedral', 'cave', 'underwater'<br><br>
|
|
|
|
<strong>Custom Environment Parameters:</strong><br>
|
|
• <strong>size</strong>: 'small' | 'medium' | 'large' - Room size<br>
|
|
• <strong>decay</strong>: 0.1-20.0 - Reverb tail length in seconds<br>
|
|
• <strong>dampening</strong>: 0.0-1.0 - High frequency absorption (0=bright, 1=muffled)
|
|
</div>
|
|
</div>
|
|
|
|
<div class="api-section">
|
|
<h3>Spatial Examples</h3>
|
|
<pre class="code-example">
|
|
<span class="keyword">const</span> spatial = <span class="keyword">new</span> <span class="highlight">Howl</span>({
|
|
src: <span class="string">'footsteps.mp3'</span>,
|
|
spatial: {
|
|
position: [<span class="highlight">10</span>, <span class="highlight">0</span>, <span class="highlight">-5</span>],
|
|
distance: { ref: <span class="highlight">2</span>, max: <span class="highlight">100</span> },
|
|
cone: { inner: <span class="highlight">45</span>, outer: <span class="highlight">90</span>, outerGain: <span class="highlight">0.2</span> }
|
|
}
|
|
});
|
|
|
|
Howler.listener.position = { x: <span class="highlight">0</span>, y: <span class="highlight">1.8</span>, z: <span class="highlight">0</span> };
|
|
Howler.listener.update();
|
|
|
|
spatial.spatial.position = { x: enemy.x, y: enemy.y, z: enemy.z };
|
|
spatial.spatial.update();
|
|
|
|
<span class="code-comment">// Built-in presets</span>
|
|
Howler.environment(<span class="string">'room'</span>); <span class="code-comment">// Small room reverb</span>
|
|
Howler.environment(<span class="string">'hall'</span>); <span class="code-comment">// Medium hall reverb</span>
|
|
Howler.environment(<span class="string">'cathedral'</span>); <span class="code-comment">// Large cathedral reverb</span>
|
|
|
|
<span class="code-comment">// Custom environment</span>
|
|
Howler.environment({
|
|
size: <span class="string">'medium'</span>,
|
|
decay: <span class="highlight">3.0</span>,
|
|
dampening: <span class="highlight">0.4</span>
|
|
});
|
|
|
|
Howler.environment(<span class="keyword">null</span>); <span class="code-comment">// Disable environment</span>
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="main-layout">
|
|
<div class="demo-column">
|
|
<h2>Library Info</h2>
|
|
<div class="demo-section">
|
|
<div class="status" id="libraryInfo">Loading library information...</div>
|
|
|
|
<h3>Supported Codecs:</h3>
|
|
<div id="codecSupport" class="status"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="api-column">
|
|
<h2>Events & Utilities</h2>
|
|
|
|
<div class="api-section">
|
|
<h3>Event Listeners</h3>
|
|
<div class="api-method"><span class="method-name">on</span>(<span class="param">event, fn, id?</span>) → <span class="return-type">Howl</span></div>
|
|
<div class="api-method"><span class="method-name">off</span>(<span class="param">event?, fn?, id?</span>) → <span class="return-type">Howl</span></div>
|
|
<div class="api-method"><span class="method-name">once</span>(<span class="param">event, fn, id?</span>) → <span class="return-type">Howl</span></div>
|
|
</div>
|
|
|
|
<div class="api-section">
|
|
<h3>Utility Methods</h3>
|
|
<div class="api-method"><span class="method-name">playing</span>(<span class="param">id?</span>) → <span class="return-type">boolean</span></div>
|
|
<div class="api-method"><span class="method-name">duration</span>(<span class="param">id?</span>) → <span class="return-type">number</span></div>
|
|
<div class="api-method"><span class="method-name">state</span>() → <span class="return-type">string</span></div>
|
|
<div class="api-method"><span class="method-name">unload</span>() → <span class="return-type">null</span></div>
|
|
</div>
|
|
|
|
<div class="api-section">
|
|
<h3>Events</h3>
|
|
<div class="api-method"><span class="method-name">load</span>() - Audio loaded successfully</div>
|
|
<div class="api-method"><span class="method-name">loaderror</span>(<span class="param">id, error</span>) - Failed to load audio</div>
|
|
<div class="api-method"><span class="method-name">play</span>(<span class="param">id</span>) - Sound started playing</div>
|
|
<div class="api-method"><span class="method-name">pause</span>(<span class="param">id</span>) - Sound paused</div>
|
|
<div class="api-method"><span class="method-name">stop</span>(<span class="param">id</span>) - Sound stopped</div>
|
|
<div class="api-method"><span class="method-name">end</span>(<span class="param">id</span>) - Sound finished playing</div>
|
|
<div class="api-method"><span class="method-name">fade</span>(<span class="param">id</span>) - Fade effect completed</div>
|
|
<div class="api-method"><span class="method-name">volume</span>(<span class="param">id</span>) - Volume changed</div>
|
|
<div class="api-method"><span class="method-name">rate</span>(<span class="param">id</span>) - Playback rate changed</div>
|
|
<div class="api-method"><span class="method-name">seek</span>(<span class="param">id</span>) - Playback position changed</div>
|
|
<div class="api-method"><span class="method-name">mute</span>(<span class="param">id</span>) - Mute state changed</div>
|
|
<div class="api-method"><span class="method-name">unlock</span>() - Audio context unlocked (user interaction)</div>
|
|
<div class="api-method"><span class="method-name">playerror</span>(<span class="param">id, error</span>) - Playback error occurred</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="u-howler.js"></script>
|
|
<script>
|
|
let basicSound = null;
|
|
let currentSoundId = null;
|
|
let currentFrequency = 440;
|
|
let spriteSound = null;
|
|
let spriteData = null;
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
updateLibraryInfo();
|
|
updateCodecSupport();
|
|
});
|
|
|
|
function updateLibraryInfo() {
|
|
const info = document.getElementById('libraryInfo');
|
|
info.textContent = `
|
|
Web Audio Context: ${Howler.usingWebAudio ? 'Supported' : 'Not Available'}
|
|
Audio Context State: ${Howler.ctx ? Howler.ctx.state : 'N/A'}
|
|
Master Volume: ${Howler.volume()}
|
|
Is Muted: ${Howler._muted}
|
|
Active Howls: ${Howler._howls.length}
|
|
`.trim();
|
|
}
|
|
|
|
function updateCodecSupport() {
|
|
const codecs = ['mp3', 'ogg', 'wav', 'aac', 'webm', 'flac'];
|
|
const support = codecs.map(codec =>
|
|
`${codec.toUpperCase()}: ${Howler.codecs(codec) ? '✅' : '❌'}`
|
|
).join('\n');
|
|
document.getElementById('codecSupport').textContent = support;
|
|
}
|
|
|
|
function log(message) {
|
|
const status = document.getElementById('promiseStatus');
|
|
status.textContent += new Date().toLocaleTimeString() + ': ' + message + '\n';
|
|
status.scrollTop = status.scrollHeight;
|
|
}
|
|
|
|
function createSoundBuffer(frequency, duration = 0.1, volume = 0.3, type = 'sine', spriteConfig = null) {
|
|
if (spriteConfig) {
|
|
return createMultiSpriteBuffer(spriteConfig);
|
|
}
|
|
|
|
const sampleRate = 44100;
|
|
const frameCount = sampleRate * duration;
|
|
const arrayBuffer = new ArrayBuffer(44 + frameCount * 2);
|
|
const view = new DataView(arrayBuffer);
|
|
|
|
const writeString = (offset, string) => {
|
|
for (let i = 0; i < string.length; i++) {
|
|
view.setUint8(offset + i, string.charCodeAt(i));
|
|
}
|
|
};
|
|
|
|
writeString(0, 'RIFF');
|
|
view.setUint32(4, 36 + frameCount * 2, true);
|
|
writeString(8, 'WAVE');
|
|
writeString(12, 'fmt ');
|
|
view.setUint32(16, 16, true);
|
|
view.setUint16(20, 1, true);
|
|
view.setUint16(22, 1, true);
|
|
view.setUint32(24, sampleRate, true);
|
|
view.setUint32(28, sampleRate * 2, true);
|
|
view.setUint16(32, 2, true);
|
|
view.setUint16(34, 16, true);
|
|
writeString(36, 'data');
|
|
view.setUint32(40, frameCount * 2, true);
|
|
|
|
let offset = 44;
|
|
for (let i = 0; i < frameCount; i++) {
|
|
const t = i / sampleRate;
|
|
const envelope = duration > 1 ? 1 : Math.exp(-t * 8);
|
|
|
|
let sample = 0;
|
|
switch (type) {
|
|
case 'sine':
|
|
sample = Math.sin(2 * Math.PI * frequency * t);
|
|
break;
|
|
case 'sweep':
|
|
const sweepFreq = frequency + (frequency * 2 * (t / duration));
|
|
sample = Math.sin(2 * Math.PI * sweepFreq * t);
|
|
break;
|
|
case 'noise':
|
|
sample = (Math.random() * 2 - 1) * Math.exp(-t * 5);
|
|
break;
|
|
case 'click':
|
|
sample = Math.exp(-t * 50) * Math.sin(2 * Math.PI * frequency * t);
|
|
break;
|
|
case 'chord':
|
|
sample = (Math.sin(2 * Math.PI * frequency * t) +
|
|
Math.sin(2 * Math.PI * frequency * 1.25 * t) +
|
|
Math.sin(2 * Math.PI * frequency * 1.5 * t)) / 3;
|
|
break;
|
|
}
|
|
|
|
sample = sample * envelope * volume;
|
|
view.setInt16(offset, Math.max(-32767, Math.min(32767, sample * 32767)), true);
|
|
offset += 2;
|
|
}
|
|
|
|
return arrayBuffer;
|
|
}
|
|
|
|
function createMultiSpriteBuffer(sprites) {
|
|
const sampleRate = 44100;
|
|
const totalDuration = Math.max(...sprites.map(s => s.start + s.duration)) + 0.2;
|
|
const frameCount = Math.floor(sampleRate * totalDuration);
|
|
const arrayBuffer = new ArrayBuffer(44 + frameCount * 2);
|
|
const view = new DataView(arrayBuffer);
|
|
|
|
const writeString = (offset, string) => {
|
|
for (let i = 0; i < string.length; i++) {
|
|
view.setUint8(offset + i, string.charCodeAt(i));
|
|
}
|
|
};
|
|
|
|
writeString(0, 'RIFF');
|
|
view.setUint32(4, 36 + frameCount * 2, true);
|
|
writeString(8, 'WAVE');
|
|
writeString(12, 'fmt ');
|
|
view.setUint32(16, 16, true);
|
|
view.setUint16(20, 1, true);
|
|
view.setUint16(22, 1, true);
|
|
view.setUint32(24, sampleRate, true);
|
|
view.setUint32(28, sampleRate * 2, true);
|
|
view.setUint16(32, 2, true);
|
|
view.setUint16(34, 16, true);
|
|
writeString(36, 'data');
|
|
view.setUint32(40, frameCount * 2, true);
|
|
|
|
let offset = 44;
|
|
|
|
for (let i = 0; i < frameCount; i++) {
|
|
const t = i / sampleRate;
|
|
let sample = 0;
|
|
|
|
for (const sprite of sprites) {
|
|
if (t >= sprite.start && t < sprite.start + sprite.duration) {
|
|
const relativeTime = t - sprite.start;
|
|
const progress = relativeTime / sprite.duration;
|
|
const envelope = Math.exp(-relativeTime * 3);
|
|
|
|
switch (sprite.type) {
|
|
case 'sweep':
|
|
const sweepFreq = sprite.freq * (1 + progress * 2);
|
|
sample = Math.sin(2 * Math.PI * sweepFreq * relativeTime) * envelope;
|
|
break;
|
|
case 'chord':
|
|
sample = (Math.sin(2 * Math.PI * sprite.freq * relativeTime) +
|
|
Math.sin(2 * Math.PI * sprite.freq * 1.25 * relativeTime) +
|
|
Math.sin(2 * Math.PI * sprite.freq * 1.5 * relativeTime)) / 3 * envelope;
|
|
break;
|
|
case 'noise':
|
|
sample = (Math.random() * 2 - 1) * envelope * 0.5;
|
|
break;
|
|
case 'click':
|
|
sample = Math.exp(-relativeTime * 50) * Math.sin(2 * Math.PI * sprite.freq * relativeTime);
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (offset + 2 <= arrayBuffer.byteLength) {
|
|
view.setInt16(offset, Math.max(-32767, Math.min(32767, sample * 16383)), true);
|
|
}
|
|
offset += 2;
|
|
}
|
|
|
|
return { buffer: arrayBuffer, sprites };
|
|
}
|
|
|
|
function updateFrequency(value) {
|
|
currentFrequency = parseInt(value);
|
|
updateDisplayValue('frequencyValue', currentFrequency, ' Hz');
|
|
|
|
if (basicSound) {
|
|
createBasicSound();
|
|
}
|
|
}
|
|
|
|
function createBasicSound() {
|
|
const wav = createSoundBuffer(currentFrequency, 8, 0.1);
|
|
|
|
if (basicSound) {
|
|
basicSound.unload();
|
|
}
|
|
|
|
basicSound = createHowlFromBuffer(wav, {
|
|
volume: 1,
|
|
onload: () => {
|
|
const basicControlIds = [
|
|
'playBasic', 'pauseBasic', 'stopBasic', 'volumeSlider', 'rateSlider',
|
|
'rateDemo', 'fadeInBtn', 'fadeOutBtn', 'lowpassBtn', 'highpassBtn',
|
|
'bandpassBtn', 'delayBtn', 'reverbBtn', 'distortionBtn',
|
|
'clearEffectsBtn', 'reverseEffectsBtn'
|
|
];
|
|
setElementsDisabled(basicControlIds, false);
|
|
|
|
basicSound.enableEQ(16, (data) => updateEQVisualizer('eqVisualizer', data));
|
|
},
|
|
onplay: (id) => {
|
|
log(`Sound playing (ID: ${id})`);
|
|
currentSoundId = id;
|
|
updateVisualizer(true);
|
|
},
|
|
onpause: () => {
|
|
log('Sound paused');
|
|
updateVisualizer(false);
|
|
},
|
|
onstop: () => {
|
|
log('Sound stopped');
|
|
updateVisualizer(false);
|
|
},
|
|
onend: () => {
|
|
log('Sound ended');
|
|
updateVisualizer(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateVisualizer(playing) {
|
|
const visual = document.querySelector('.audio-visual');
|
|
const waveform = visual.querySelector('.waveform');
|
|
const text = visual.querySelector('span');
|
|
|
|
if (playing) {
|
|
waveform.style.animationPlayState = 'running';
|
|
visual.style.background = 'linear-gradient(45deg, #000000, #112244)';
|
|
} else {
|
|
waveform.style.animationPlayState = 'paused';
|
|
visual.style.background = 'linear-gradient(45deg, #111, #124)';
|
|
}
|
|
}
|
|
|
|
function playBasic() {
|
|
if (basicSound) {
|
|
currentSoundId = basicSound.play();
|
|
}
|
|
}
|
|
|
|
function pauseBasic() {
|
|
if (basicSound && currentSoundId) {
|
|
basicSound.pause(currentSoundId);
|
|
}
|
|
}
|
|
|
|
function stopBasic() {
|
|
if (basicSound) {
|
|
basicSound.stop();
|
|
}
|
|
}
|
|
|
|
function setBasicVolume(value) {
|
|
if (basicSound) {
|
|
basicSound.volume(parseFloat(value));
|
|
updateDisplayValue('volumeValue', parseFloat(value).toFixed(1));
|
|
}
|
|
}
|
|
|
|
function setRate(value) {
|
|
if (basicSound) {
|
|
basicSound.rate(parseFloat(value));
|
|
updateDisplayValue('rateValue', parseFloat(value).toFixed(1), 'x');
|
|
}
|
|
}
|
|
|
|
function demoRateChange() {
|
|
if (!basicSound) return;
|
|
|
|
const rates = [0.25, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 6.0];
|
|
let index = 0;
|
|
|
|
const changeRate = () => {
|
|
if (index < rates.length) {
|
|
basicSound.rate(rates[index]);
|
|
document.getElementById('rateSlider').value = rates[index];
|
|
updateDisplayValue('rateValue', rates[index].toFixed(1), 'x');
|
|
log(`Rate changed to ${rates[index]}x`);
|
|
index++;
|
|
setTimeout(changeRate, 250);
|
|
}
|
|
};
|
|
|
|
if (!basicSound.playing()) {
|
|
basicSound.play();
|
|
}
|
|
changeRate();
|
|
}
|
|
|
|
function fadeIn() {
|
|
if (basicSound) {
|
|
basicSound.volume(0);
|
|
if (!basicSound.playing()) {
|
|
basicSound.play();
|
|
}
|
|
basicSound.fade(0, 1, 2000);
|
|
}
|
|
}
|
|
|
|
function fadeOut() {
|
|
if (basicSound && basicSound.playing()) {
|
|
basicSound.fade(1, 0, 2000);
|
|
}
|
|
}
|
|
|
|
async function demonstratePromises() {
|
|
try {
|
|
const wav = createSoundBuffer(880, 1, 0.1);
|
|
|
|
const howl = createHowlFromBuffer(wav, {
|
|
onload: () => {
|
|
howl.enableEQ(16, (data) => updateEQVisualizer('eqVisualizer', data));
|
|
}
|
|
});
|
|
|
|
await howl.load();
|
|
const soundId = howl.play();
|
|
|
|
setTimeout(() => {
|
|
howl.stop();
|
|
}, 2000);
|
|
|
|
} catch (error) {
|
|
log(`❌ Error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
function demonstrateTraditional() {
|
|
const wav = createSoundBuffer(660, 1, 0.1);
|
|
|
|
const howl = createHowlFromBuffer(wav, {
|
|
onload: () => {
|
|
howl.enableEQ(16, (data) => updateEQVisualizer('eqVisualizer', data));
|
|
const soundId = howl.play();
|
|
setTimeout(() => {
|
|
howl.stop();
|
|
}, 2000);
|
|
},
|
|
onloaderror: (error) => {
|
|
log(`❌ Traditional callback error: ${error}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
function globalMute() {
|
|
const isMuted = Howler._muted;
|
|
Howler.mute(!isMuted);
|
|
log(`Global ${!isMuted ? 'muted' : 'unmuted'}`);
|
|
updateLibraryInfo();
|
|
}
|
|
|
|
function globalStop() {
|
|
Howler.stop();
|
|
updateVisualizer(false);
|
|
}
|
|
|
|
function updateMasterVol(value) {
|
|
updateDisplayValue('masterVolValue', parseFloat(value).toFixed(1));
|
|
updateLibraryInfo();
|
|
}
|
|
|
|
function createSpriteSound() {
|
|
const spriteConfig = [
|
|
{ name: 'jump', start: 0, duration: 0.5, freq: 440, type: 'sweep' },
|
|
{ name: 'coin', start: 0.5, duration: 0.3, freq: 800, type: 'chord' },
|
|
{ name: 'laser', start: 0.8, duration: 0.4, freq: 1200, type: 'sweep' },
|
|
{ name: 'explosion', start: 1.2, duration: 0.8, freq: 200, type: 'noise' },
|
|
{ name: 'click', start: 2.0, duration: 0.1, freq: 2000, type: 'click' },
|
|
{ name: 'power_up', start: 2.1, duration: 0.6, freq: 500, type: 'chord' },
|
|
{ name: 'hurt', start: 2.7, duration: 0.3, freq: 150, type: 'noise' },
|
|
{ name: 'victory', start: 3.0, duration: 0.8, freq: 660, type: 'chord' }
|
|
];
|
|
|
|
const { buffer, sprites } = createSoundBuffer(null, null, null, null, spriteConfig);
|
|
|
|
if (spriteSound) {
|
|
spriteSound.unload();
|
|
}
|
|
|
|
const spriteMap = {};
|
|
sprites.forEach(sprite => {
|
|
spriteMap[sprite.name] = [sprite.start * 1000, sprite.duration * 1000];
|
|
});
|
|
|
|
spriteData = sprites;
|
|
|
|
spriteSound = createHowlFromBuffer(buffer, {
|
|
sprite: spriteMap,
|
|
volume: 0.7,
|
|
onload: () => {
|
|
setElementsDisabled(['playFullBtn'], false);
|
|
document.getElementById('spriteButtons').querySelectorAll('button').forEach(btn => {
|
|
btn.disabled = false;
|
|
});
|
|
|
|
spriteSound.enableEQ(16, (data) => updateEQVisualizer('spriteEqVisualizer', data));
|
|
updateSpriteVisual('ready');
|
|
},
|
|
onplay: (id) => {
|
|
updateSpriteVisual('playing');
|
|
},
|
|
onend: (id) => {
|
|
updateSpriteVisual('ready');
|
|
},
|
|
onstop: (id) => {
|
|
},
|
|
onpause: (id) => {
|
|
},
|
|
onerror: (id, error) => {
|
|
}
|
|
});
|
|
}
|
|
|
|
function playFullSprite() {
|
|
if (!spriteSound) return;
|
|
|
|
const sprites = ['jump', 'coin', 'laser', 'explosion', 'click', 'power_up', 'hurt', 'victory'];
|
|
let delay = 0;
|
|
|
|
sprites.forEach((spriteName, index) => {
|
|
setTimeout(() => {
|
|
if (spriteSound) {
|
|
spriteSound.play(spriteName);
|
|
}
|
|
}, delay);
|
|
|
|
const spriteInfo = spriteData.find(s => s.name === spriteName);
|
|
delay += (spriteInfo ? spriteInfo.duration * 1000 : 500) + 200;
|
|
});
|
|
}
|
|
|
|
function playSpriteSound(spriteName) {
|
|
if (!spriteSound) return;
|
|
|
|
const soundId = spriteSound.play(spriteName);
|
|
|
|
const button = document.getElementById(spriteName === 'power_up' ? 'powerUpBtn' : spriteName + 'Btn');
|
|
if (button) {
|
|
button.style.transform = 'scale(0.95)';
|
|
button.style.filter = 'brightness(1.2)';
|
|
setTimeout(() => {
|
|
button.style.transform = '';
|
|
button.style.filter = '';
|
|
}, 200);
|
|
}
|
|
}
|
|
|
|
function updateSpriteVisual(state) {
|
|
const visual = document.getElementById('spriteVisual');
|
|
const waveform = visual.querySelector('.waveform');
|
|
|
|
switch (state) {
|
|
case 'ready':
|
|
visual.style.background = 'linear-gradient(45deg, #1e3c72, #2a5298)';
|
|
waveform.style.animationPlayState = 'paused';
|
|
break;
|
|
case 'playing':
|
|
visual.style.background = 'linear-gradient(45deg, #2d1b69, #11998e)';
|
|
waveform.style.animationPlayState = 'running';
|
|
break;
|
|
}
|
|
}
|
|
|
|
function addLowpass() {
|
|
if (basicSound) {
|
|
const filter = basicSound.addEffect('lowpass', { frequency: 800, Q: 1 });
|
|
if (filter) {
|
|
log('Added lowpass filter at 800Hz');
|
|
}
|
|
}
|
|
}
|
|
|
|
function addHighpass() {
|
|
if (basicSound) {
|
|
const filter = basicSound.addEffect('highpass', { frequency: 400, Q: 1 });
|
|
if (filter) {
|
|
log('Added highpass filter at 400Hz');
|
|
}
|
|
}
|
|
}
|
|
|
|
function addBandpass() {
|
|
if (basicSound) {
|
|
const filter = basicSound.addEffect('bandpass', { frequency: 1000, Q: 2 });
|
|
if (filter) {
|
|
log('Added bandpass filter at 1000Hz');
|
|
}
|
|
}
|
|
}
|
|
|
|
function addDelay() {
|
|
if (basicSound) {
|
|
const delay = basicSound.addEffect('delay', { delay: 0.3, maxDelay: 1 });
|
|
if (delay) {
|
|
log('Added delay effect (300ms)');
|
|
}
|
|
}
|
|
}
|
|
|
|
function addReverb() {
|
|
if (basicSound) {
|
|
const reverb = basicSound.addEffect('reverb', { duration: 2, decay: 2 });
|
|
if (reverb) {
|
|
log('Added reverb effect');
|
|
}
|
|
}
|
|
}
|
|
|
|
function addDistortion() {
|
|
if (basicSound) {
|
|
const distortion = basicSound.addEffect('distortion', { amount: 20 });
|
|
if (distortion) {
|
|
log('Added distortion effect');
|
|
}
|
|
}
|
|
}
|
|
|
|
function clearEffects() {
|
|
if (basicSound) {
|
|
const count = basicSound.effects.length;
|
|
basicSound.effects.length = 0;
|
|
basicSound.updateEffectsChain();
|
|
log(`Cleared ${count} effects`);
|
|
}
|
|
}
|
|
|
|
function reverseEffects() {
|
|
if (basicSound) {
|
|
const count = basicSound.effects.length;
|
|
if (count > 1) {
|
|
basicSound.effects.reverse();
|
|
basicSound.updateEffectsChain();
|
|
log(`Reversed order of ${count} effects`);
|
|
} else {
|
|
log('Need at least 2 effects to reverse order');
|
|
}
|
|
}
|
|
}
|
|
|
|
function audioBufferToWav(buffer) {
|
|
const length = buffer.length;
|
|
const numberOfChannels = buffer.numberOfChannels;
|
|
const sampleRate = buffer.sampleRate;
|
|
const arrayBuffer = new ArrayBuffer(44 + length * 2);
|
|
const view = new DataView(arrayBuffer);
|
|
|
|
const writeString = (offset, string) => {
|
|
for (let i = 0; i < string.length; i++) {
|
|
view.setUint8(offset + i, string.charCodeAt(i));
|
|
}
|
|
};
|
|
|
|
writeString(0, 'RIFF');
|
|
view.setUint32(4, 36 + length * 2, true);
|
|
writeString(8, 'WAVE');
|
|
writeString(12, 'fmt ');
|
|
view.setUint32(16, 16, true);
|
|
view.setUint16(20, 1, true);
|
|
view.setUint16(22, numberOfChannels, true);
|
|
view.setUint32(24, sampleRate, true);
|
|
view.setUint32(28, sampleRate * 2, true);
|
|
view.setUint16(32, 2, true);
|
|
view.setUint16(34, 16, true);
|
|
writeString(36, 'data');
|
|
view.setUint32(40, length * 2, true);
|
|
|
|
const channelData = buffer.getChannelData(0);
|
|
let offset = 44;
|
|
for (let i = 0; i < length; i++) {
|
|
const sample = Math.max(-1, Math.min(1, channelData[i]));
|
|
view.setInt16(offset, sample * 0x7FFF, true);
|
|
offset += 2;
|
|
}
|
|
|
|
return arrayBuffer;
|
|
}
|
|
|
|
let eqVisualizers = new Map();
|
|
|
|
function setElementsDisabled(elementIds, disabled) {
|
|
elementIds.forEach(id => {
|
|
const element = document.getElementById(id);
|
|
if (element) element.disabled = disabled;
|
|
});
|
|
}
|
|
|
|
function createHowlFromBuffer(buffer, options = {}) {
|
|
const blob = new Blob([buffer], { type: 'audio/wav' });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
return new Howl({
|
|
src: [url],
|
|
format: ['wav'],
|
|
...options
|
|
});
|
|
}
|
|
|
|
function updateDisplayValue(elementId, value, suffix = '') {
|
|
const element = document.getElementById(elementId);
|
|
if (element) element.textContent = value + suffix;
|
|
}
|
|
|
|
function createEQVisualizer(containerId, barCount = 16) {
|
|
const visualizer = document.getElementById(containerId);
|
|
if (!visualizer) return null;
|
|
|
|
const bars = [];
|
|
|
|
for (let i = 0; i < barCount; i++) {
|
|
const bar = document.createElement('div');
|
|
bar.className = 'eq-bar';
|
|
bar.style.height = '2px';
|
|
visualizer.appendChild(bar);
|
|
bars.push(bar);
|
|
}
|
|
|
|
eqVisualizers.set(containerId, bars);
|
|
return bars;
|
|
}
|
|
|
|
function updateEQVisualizer(containerId, frequencyData) {
|
|
const bars = eqVisualizers.get(containerId);
|
|
if (!bars || !bars.length) return;
|
|
|
|
for (let i = 0; i < bars.length && i < frequencyData.length; i++) {
|
|
const normalizedValue = frequencyData[i];
|
|
const height = Math.max(5, normalizedValue * 85);
|
|
|
|
bars[i].setAttribute('style', `height: ${height}px !important; min-height: ${height}px !important;`);
|
|
|
|
bars[i].title = `Band ${i + 1}: ${(normalizedValue * 100).toFixed(1)}%`;
|
|
}
|
|
}
|
|
|
|
function initAllEQVisualizers() {
|
|
createEQVisualizer('eqVisualizer', 16);
|
|
createEQVisualizer('spriteEqVisualizer', 16);
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', initAllEQVisualizers);
|
|
|
|
setInterval(updateLibraryInfo, 2000);
|
|
|
|
let spatialSource1, spatialSource2;
|
|
let spatialIntervals = [];
|
|
let isDragging = null;
|
|
|
|
function startSpatialDemo() {
|
|
Howler.environment('cathedral');
|
|
|
|
const ping1Wav = createSoundBuffer(800, 0.1);
|
|
const ping2Wav = createSoundBuffer(400, 0.1);
|
|
|
|
spatialSource1 = createHowlFromBuffer(ping1Wav, {
|
|
spatial: {
|
|
position: [-5, 0, -4],
|
|
distance: { ref: 1, max: 20, rolloff: 2 }
|
|
}
|
|
});
|
|
|
|
spatialSource2 = createHowlFromBuffer(ping2Wav, {
|
|
spatial: {
|
|
position: [5, 0, 4],
|
|
distance: { ref: 1, max: 20, rolloff: 2 }
|
|
}
|
|
});
|
|
|
|
Howler.listener.position = { x: 0, y: 0, z: 0 };
|
|
Howler.listener.update();
|
|
|
|
spatialIntervals.push(setInterval(() => {
|
|
spatialSource1.play();
|
|
flashSpatialSource('source1');
|
|
}, 1500));
|
|
spatialIntervals.push(setInterval(() => {
|
|
spatialSource2.play();
|
|
flashSpatialSource('source2');
|
|
}, 1200));
|
|
|
|
setupSpatialDrag();
|
|
|
|
updateSpatialPositions();
|
|
|
|
setElementsDisabled(['spatialStartBtn'], true);
|
|
setElementsDisabled(['spatialStopBtn'], false);
|
|
}
|
|
|
|
function stopSpatialDemo() {
|
|
spatialIntervals.forEach(clearInterval);
|
|
spatialIntervals = [];
|
|
|
|
if (spatialSource1) {
|
|
spatialSource1.stop();
|
|
spatialSource1.unload();
|
|
spatialSource1 = null;
|
|
}
|
|
if (spatialSource2) {
|
|
spatialSource2.stop();
|
|
spatialSource2.unload();
|
|
spatialSource2 = null;
|
|
}
|
|
|
|
setElementsDisabled(['spatialStartBtn'], false);
|
|
setElementsDisabled(['spatialStopBtn'], true);
|
|
log('Spatial demo stopped');
|
|
}
|
|
|
|
function setupSpatialDrag() {
|
|
const demo = document.getElementById('spatialDemo');
|
|
const listener = document.getElementById('listener');
|
|
const source1 = document.getElementById('source1');
|
|
const source2 = document.getElementById('source2');
|
|
|
|
[listener, source1, source2].forEach(element => {
|
|
element.addEventListener('mousedown', (e) => {
|
|
isDragging = element;
|
|
e.preventDefault();
|
|
});
|
|
});
|
|
|
|
demo.addEventListener('mousemove', (e) => {
|
|
if (!isDragging) return;
|
|
|
|
const rect = demo.getBoundingClientRect();
|
|
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
|
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
|
|
|
isDragging.style.left = Math.max(0, Math.min(100, x)) + '%';
|
|
isDragging.style.top = Math.max(0, Math.min(100, y)) + '%';
|
|
|
|
updateSpatialPositions();
|
|
});
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
isDragging = null;
|
|
});
|
|
}
|
|
|
|
function updateSpatialPositions() {
|
|
const demo = document.getElementById('spatialDemo');
|
|
const listener = document.getElementById('listener');
|
|
const source1 = document.getElementById('source1');
|
|
const source2 = document.getElementById('source2');
|
|
|
|
const safeParsePosition = (element, property, fallback) => {
|
|
const value = parseFloat(element.style[property]);
|
|
return isNaN(value) ? fallback : value;
|
|
};
|
|
|
|
const listenerLeft = safeParsePosition(listener, 'left', 50);
|
|
const listenerTop = safeParsePosition(listener, 'top', 50);
|
|
const listenerX = (listenerLeft - 50) / 5;
|
|
const listenerZ = (listenerTop - 50) / 5;
|
|
|
|
const source1Left = safeParsePosition(source1, 'left', 25);
|
|
const source1Top = safeParsePosition(source1, 'top', 30);
|
|
const source1X = (source1Left - 50) / 5;
|
|
const source1Z = (source1Top - 50) / 5;
|
|
|
|
const source2Left = safeParsePosition(source2, 'left', 75);
|
|
const source2Top = safeParsePosition(source2, 'top', 70);
|
|
const source2X = (source2Left - 50) / 5;
|
|
const source2Z = (source2Top - 50) / 5;
|
|
|
|
Howler.listener.position = { x: listenerX, y: 0, z: listenerZ };
|
|
Howler.listener.update();
|
|
|
|
if (spatialSource1) {
|
|
spatialSource1.spatial.position = { x: source1X, y: 0, z: source1Z };
|
|
spatialSource1.spatial.update();
|
|
}
|
|
|
|
if (spatialSource2) {
|
|
spatialSource2.spatial.position = { x: source2X, y: 0, z: source2Z };
|
|
spatialSource2.spatial.update();
|
|
}
|
|
}
|
|
|
|
function flashSpatialSource(sourceId) {
|
|
const element = document.getElementById(sourceId);
|
|
if (element) {
|
|
element.classList.add('spatial-flash');
|
|
setTimeout(() => {
|
|
element.classList.remove('spatial-flash');
|
|
}, 300);
|
|
}
|
|
}
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|