1const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/vendor-audio-D-wTggbx.js","assets/vendor-react-Bay9Y7Lb.js","assets/vendor-D0P4XgNj.js","assets/vendor-i18n-BC7ZMXWQ.js","assets/vendor-DqKRdbjz.css"])))=>i.map(i=>d[i]); 2import { r as reactExports, j as jsxRuntimeExports } from "./vendor-react-Bay9Y7Lb.js"; 3import { _ as __vitePreload } from "./vendor-react-three-BCww3P1x.js"; 4import "./index-w5lXls15.js"; 5import { i as instance } from "./vendor-i18n-BC7ZMXWQ.js"; 6import { aX as ArrowLeft, dh as SlidersVertical, b4 as FolderOpen, bT as Mic, bg as Music, d1 as Download, c1 as Share2, dE as Undo2, dF as Redo2, dn as Scissors, c2 as Copy, dG as ClipboardPaste, b5 as Trash2, ey as Crop, bX as VolumeX, ez as CirclePlus, eA as ChartNoAxesColumn, cg as Activity, d6 as ShieldCheck, c5 as SkipBack, b_ as Pause, bV as Play, di as Square, c6 as SkipForward, c0 as Repeat, dt as ZoomIn, ds as ZoomOut, d0 as Maximize2, c$ as Minimize2, bU as Volume2, aW as X, bi as Sparkles, b7 as Check, b3 as FileHeadphone, aY as TriangleAlert, a_ as CircleCheck, b1 as CircleAlert, ec as CloudUpload } from "./vendor-D0P4XgNj.js"; 7class AudioBufferUtils { 8 /** 9 * Clones an existing AudioBuffer. 10 */ 11 static cloneBuffer(ctx, buffer) { 12 const cloned = ctx.createBuffer( 13 buffer.numberOfChannels, 14 buffer.length, 15 buffer.sampleRate 16 ); 17 for (let c = 0; c < buffer.numberOfChannels; c++) { 18 cloned.copyToChannel(buffer.getChannelData(c), c); 19 } 20 return cloned; 21 } 22 /** 23 * Creates an AudioBuffer from channel data arrays. 24 */ 25 static createFromChannels(ctx, channels, sampleRate) { 26 const numChannels = channels.length; 27 const length = channels[0].length; 28 const buffer = ctx.createBuffer(numChannels, length, sampleRate); 29 for (let c = 0; c < numChannels; c++) { 30 buffer.copyToChannel(channels[c], c); 31 } 32 return buffer; 33 } 34 /** 35 * Slices a sub-region from an AudioBuffer [startSec, endSec]. 36 */ 37 static sliceBuffer(ctx, buffer, startSec, endSec) { 38 const startSample = Math.max(0, Math.floor(startSec * buffer.sampleRate)); 39 const endSample = Math.min(buffer.length, Math.ceil(endSec * buffer.sampleRate)); 40 const length = Math.max(1, endSample - startSample); 41 const sliced = ctx.createBuffer(buffer.numberOfChannels, length, buffer.sampleRate); 42 for (let c = 0; c < buffer.numberOfChannels; c++) { 43 const src = buffer.getChannelData(c); 44 const dest = sliced.getChannelData(c); 45 dest.set(src.subarray(startSample, startSample + length)); 46 } 47 return sliced; 48 } 49 /** 50 * Cuts/deletes a sub-region from an AudioBuffer [startSec, endSec]. 51 */ 52 static deleteRegion(ctx, buffer, startSec, endSec) { 53 const startSample = Math.max(0, Math.floor(startSec * buffer.sampleRate)); 54 const endSample = Math.min(buffer.length, Math.ceil(endSec * buffer.sampleRate)); 55 const deleteLen = endSample - startSample; 56 const newLen = buffer.length - deleteLen; 57 if (newLen <= 0) { 58 return ctx.createBuffer(buffer.numberOfChannels, Math.floor(buffer.sampleRate * 0.1), buffer.sampleRate); 59 } 60 const newBuffer = ctx.createBuffer(buffer.numberOfChannels, newLen, buffer.sampleRate); 61 for (let c = 0; c < buffer.numberOfChannels; c++) { 62 const src = buffer.getChannelData(c); 63 const dest = newBuffer.getChannelData(c); 64 dest.set(src.subarray(0, startSample), 0); 65 dest.set(src.subarray(endSample), startSample); 66 } 67 return newBuffer; 68 } 69 /** 70 * Inserts an AudioBuffer at a specific timestamp in seconds. 71 */ 72 static insertBuffer(ctx, target, toInsert, atSec) { 73 const insertSample = Math.max(0, Math.min(target.length, Math.floor(atSec * target.sampleRate))); 74 const newLen = target.length + toInsert.length; 75 const numChannels = Math.max(target.numberOfChannels, toInsert.numberOfChannels); 76 const newBuffer = ctx.createBuffer(numChannels, newLen, target.sampleRate); 77 for (let c = 0; c < numChannels; c++) { 78 const targetChan = c < target.numberOfChannels ? target.getChannelData(c) : target.getChannelData(0); 79 const insertChan = c < toInsert.numberOfChannels ? toInsert.getChannelData(c) : toInsert.getChannelData(0); 80 const dest = newBuffer.getChannelData(c); 81 dest.set(targetChan.subarray(0, insertSample), 0); 82 dest.set(insertChan, insertSample); 83 dest.set(targetChan.subarray(insertSample), insertSample + toInsert.length); 84 } 85 return newBuffer; 86 } 87 /** 88 * Replaces a region in target AudioBuffer with another AudioBuffer (Paste over selection). 89 */ 90 static replaceRegion(ctx, target, toInsert, startSec, endSec) { 91 const startSample = Math.max(0, Math.floor(startSec * target.sampleRate)); 92 const endSample = Math.min(target.length, Math.ceil(endSec * target.sampleRate)); 93 const newLen = target.length - (endSample - startSample) + toInsert.length; 94 const numChannels = Math.max(target.numberOfChannels, toInsert.numberOfChannels); 95 const newBuffer = ctx.createBuffer(numChannels, newLen, target.sampleRate); 96 for (let c = 0; c < numChannels; c++) { 97 const targetChan = c < target.numberOfChannels ? target.getChannelData(c) : target.getChannelData(0); 98 const insertChan = c < toInsert.numberOfChannels ? toInsert.getChannelData(c) : toInsert.getChannelData(0); 99 const dest = newBuffer.getChannelData(c); 100 dest.set(targetChan.subarray(0, startSample), 0); 101 dest.set(insertChan, startSample); 102 dest.set(targetChan.subarray(endSample), startSample + toInsert.length); 103 } 104 return newBuffer; 105 } 106 /** 107 * Silences a region in an AudioBuffer without changing overall duration. 108 */ 109 static silenceRegion(ctx, buffer, startSec, endSec) { 110 const cloned = this.cloneBuffer(ctx, buffer); 111 const startSample = Math.max(0, Math.floor(startSec * buffer.sampleRate)); 112 const endSample = Math.min(buffer.length, Math.ceil(endSec * buffer.sampleRate)); 113 for (let c = 0; c < cloned.numberOfChannels; c++) { 114 const data = cloned.getChannelData(c); 115 data.fill(0, startSample, endSample); 116 } 117 return cloned; 118 } 119 /** 120 * Inserts silence of specified duration at a specific point in seconds. 121 */ 122 static insertSilence(ctx, buffer, atSec, durationSec) {
123 const silenceSamples = Math.floor(durationSec * buffer.sampleRate); 124 const silenceBuffer = ctx.createBuffer(buffer.numberOfChannels, silenceSamples, buffer.sampleRate); 125 return this.insertBuffer(ctx, buffer, silenceBuffer, atSec); 126 } 127 /** 128 * Reverses audio samples in the selected region (or full buffer). 129 */ 130 static reverseAudio(ctx, buffer, startSec, endSec) { 131 const cloned = this.cloneBuffer(ctx, buffer); 132 const start = startSec !== void 0 ? Math.max(0, Math.floor(startSec * buffer.sampleRate)) : 0; 133 const end = endSec !== void 0 ? Math.min(buffer.length, Math.ceil(endSec * buffer.sampleRate)) : buffer.length; 134 for (let c = 0; c < cloned.numberOfChannels; c++) { 135 const data = cloned.getChannelData(c); 136 let left = start; 137 let right = end - 1; 138 while (left < right) { 139 const temp = data[left]; 140 data[left] = data[right]; 141 data[right] = temp; 142 left++; 143 right--; 144 } 145 } 146 return cloned; 147 } 148 /** 149 * Inverts phase (180 degrees) of the selected region. 150 */ 151 static invertPhase(ctx, buffer, startSec, endSec) { 152 const cloned = this.cloneBuffer(ctx, buffer); 153 const start = startSec !== void 0 ? Math.max(0, Math.floor(startSec * buffer.sampleRate)) : 0; 154 const end = endSec !== void 0 ? Math.min(buffer.length, Math.ceil(endSec * buffer.sampleRate)) : buffer.length; 155 for (let c = 0; c < cloned.numberOfChannels; c++) { 156 const data = cloned.getChannelData(c); 157 for (let i = start; i < end; i++) { 158 data[i] = -data[i]; 159 } 160 } 161 return cloned; 162 } 163 /** 164 * Removes DC offset (centers waveform around 0V). 165 */ 166 static removeDCOffset(ctx, buffer, startSec, endSec) { 167 const cloned = this.cloneBuffer(ctx, buffer); 168 const start = startSec !== void 0 ? Math.max(0, Math.floor(startSec * buffer.sampleRate)) : 0; 169 const end = endSec !== void 0 ? Math.min(buffer.length, Math.ceil(endSec * buffer.sampleRate)) : buffer.length; 170 const len = end - start; 171 if (len <= 0) return cloned; 172 for (let c = 0; c < cloned.numberOfChannels; c++) { 173 const data = cloned.getChannelData(c); 174 let sum = 0; 175 for (let i = start; i < end; i++) { 176 sum += data[i]; 177 } 178 const mean = sum / len; 179 for (let i = start; i < end; i++) { 180 data[i] -= mean; 181 } 182 } 183 return cloned; 184 } 185 /** 186 * Swaps Left and Right audio channels (stereo only). 187 */ 188 static swapChannels(ctx, buffer) { 189 if (buffer.numberOfChannels < 2) return buffer; 190 const cloned = ctx.createBuffer(2, buffer.length, buffer.sampleRate); 191 cloned.copyToChannel(buffer.getChannelData(1), 0); 192 cloned.copyToChannel(buffer.getChannelData(0), 1); 193 return cloned; 194 } 195 /** 196 * Mixes down stereo to mono. 197 */ 198 static stereoToMono(ctx, buffer) { 199 if (buffer.numberOfChannels === 1) return buffer; 200 const mono = ctx.createBuffer(1, buffer.length, buffer.sampleRate); 201 const dest = mono.getChannelData(0); 202 const l = buffer.getChannelData(0); 203 const r = buffer.getChannelData(1); 204 for (let i = 0; i < buffer.length; i++) { 205 dest[i] = (l[i] + r[i]) * 0.5; 206 } 207 return mono; 208 } 209 /** 210 * Duplicates mono to stereo. 211 */ 212 static monoToStereo(ctx, buffer) { 213 if (buffer.numberOfChannels >= 2) return buffer; 214 const stereo = ctx.createBuffer(2, buffer.length, buffer.sampleRate); 215 const src = buffer.getChannelData(0); 216 stereo.copyToChannel(src, 0); 217 stereo.copyToChannel(src, 1); 218 return stereo; 219 } 220 /** 221 * Analyzes comprehensive statistics of an AudioBuffer. 222 */ 223 static computeStats(buffer) { 224 const l = buffer.getChannelData(0); 225 const r = buffer.numberOfChannels > 1 ? buffer.getChannelData(1) : l; 226 const len = buffer.length; 227 let peakL = 0; 228 let peakR = 0; 229 let sumSqL = 0; 230 let sumSqR = 0; 231 let sumL = 0; 232 let sumR = 0; 233 let hasClipping = false; 234 for (let i = 0; i < len; i++) { 235 const valL = l[i]; 236 const valR = r[i]; 237 const absL = Math.abs(valL); 238 const absR = Math.abs(valR); 239 if (absL > peakL) peakL = absL; 240 if (absR > peakR) peakR = absR; 241 if (absL >= 0.999 || absR >= 0.999) hasClipping = true; 242 sumSqL += valL * valL; 243 sumSqR += valR * valR; 244 sumL += valL; 245 sumR += valR; 246 } 247 const rmsL = Math.sqrt(sumSqL / Math.max(1, len)); 248 const rmsR = Math.sqrt(sumSqR / Math.max(1, len)); 249 const toDb = (linear) => linear > 1e-5 ? 20 * Math.log10(linear) : -100; 250 const peakDbL = toDb(peakL); 251 const peakDbR = toDb(peakR); 252 const rmsDbL = toDb(rmsL); 253 const rmsDbR = toDb(rmsR); 254 const maxPeakDb = Math.max(peakDbL, peakDbR); 255 const minRmsDb = Math.min(rmsDbL, rmsDbR); 256 return { 257 duration: buffer.duration, 258 sampleRate: buffer.sampleRate, 259 numberOfChannels: buffer.numberOfChannels, 260 length: buffer.length, 261 peakDbL: Math.round(peakDbL * 10) / 10, 262 peakDbR: Math.round(peakDbR * 10) / 10, 263 rmsDbL: Math.round(rmsDbL * 10) / 10, 264 rmsDbR: Math.round(rmsDbR * 10) / 10, 265 dcOffsetL: Math.round(sumL / Math.max(1, len) * 1e4) / 1e4, 266 dcOffsetR: Math.round(sumR / Math.max(1, len) * 1e4) / 1e4, 267 dynamicRangeDb: Math.round(Math.max(0, maxPeakDb - minRmsDb) * 10) / 10, 268 hasClipping 269 }; 270 } 271} 272class AudioPlaybackEngine {
273 constructor() { 274 this.ctx = null; 275 this.currentSource = null; 276 this.masterGain = null; 277 this.analyserL = null; 278 this.analyserR = null; 279 this.splitter = null; 280 this.buffer = null; 281 this.isPlaying = false; 282 this.startTime = 0; 283 this.pauseOffset = 0; 284 this.loopStart = 0; 285 this.loopEnd = 0; 286 this.isLooping = false; 287 this.animFrameId = null; 288 this.onTimeUpdate = null; 289 this.onEnded = null; 290 this.onStateChange = null; 291 } 292 getContext() { 293 if (!this.ctx || this.ctx.state === "closed") { 294 const AudioCtx = window.AudioContext || window.webkitAudioContext; 295 this.ctx = new AudioCtx(); 296 } 297 if (this.ctx.state === "suspended") { 298 this.ctx.resume(); 299 } 300 return this.ctx; 301 } 302 setupNodes() { 303 const ctx = this.getContext(); 304 if (!this.masterGain) { 305 this.masterGain = ctx.createGain(); 306 this.masterGain.connect(ctx.destination); 307 this.splitter = ctx.createChannelSplitter(2); 308 this.analyserL = ctx.createAnalyser(); 309 this.analyserR = ctx.createAnalyser(); 310 this.analyserL.fftSize = 1024; 311 this.analyserR.fftSize = 1024; 312 this.analyserL.smoothingTimeConstant = 0.8; 313 this.analyserR.smoothingTimeConstant = 0.8; 314 this.masterGain.connect(this.splitter); 315 this.splitter.connect(this.analyserL, 0); 316 this.splitter.connect(this.analyserR, 1); 317 } 318 } 319 setBuffer(buffer) { 320 this.stop(); 321 this.buffer = buffer; 322 this.pauseOffset = 0; 323 } 324 getBuffer() { 325 return this.buffer; 326 } 327 setVolume(val) { 328 this.setupNodes(); 329 if (this.masterGain) { 330 this.masterGain.gain.setValueAtTime(Math.max(0, Math.min(2, val)), this.getContext().currentTime); 331 } 332 } 333 play(offsetSec, loopRange) { 334 if (!this.buffer) return; 335 const ctx = this.getContext(); 336 this.setupNodes(); 337 if (this.isPlaying) { 338 this.stopSourceOnly(); 339 } 340 const startPos = offsetSec !== void 0 ? offsetSec : this.pauseOffset; 341 this.pauseOffset = Math.max(0, Math.min(this.buffer.duration, startPos)); 342 this.currentSource = ctx.createBufferSource(); 343 this.currentSource.buffer = this.buffer; 344 if (loopRange && loopRange.end > loopRange.start) { 345 this.isLooping = true; 346 this.loopStart = loopRange.start; 347 this.loopEnd = loopRange.end; 348 this.currentSource.loop = true; 349 this.currentSource.loopStart = this.loopStart; 350 this.currentSource.loopEnd = this.loopEnd; 351 } else { 352 this.isLooping = false; 353 this.currentSource.loop = false; 354 } 355 this.currentSource.connect(this.masterGain); 356 const source = this.currentSource; 357 source.onended = () => { 358 if (this.currentSource === source && this.isPlaying && !this.isLooping) { 359 this.isPlaying = false; 360 this.pauseOffset = 0; 361 this.cancelTicker(); 362 if (this.onTimeUpdate) this.onTimeUpdate(0); 363 if (this.onStateChange) this.onStateChange(false); 364 if (this.onEnded) this.onEnded(); 365 } 366 }; 367 const actualStart = this.isLooping && (this.pauseOffset < this.loopStart || this.pauseOffset >= this.loopEnd) ? this.loopStart : this.pauseOffset; 368 this.startTime = ctx.currentTime - actualStart; 369 source.start(0, actualStart); 370 this.isPlaying = true; 371 if (this.onStateChange) this.onStateChange(true); 372 this.startTicker(); 373 } 374 pause() { 375 var _a; 376 if (!this.isPlaying) return; 377 const ctx = this.getContext(); 378 const elapsed = ctx.currentTime - this.startTime; 379 if (this.isLooping && this.loopEnd > this.loopStart) { 380 const loopLen = this.loopEnd - this.loopStart; 381 this.pauseOffset = this.loopStart + (elapsed - this.loopStart) % loopLen; 382 } else { 383 this.pauseOffset = Math.max(0, Math.min(((_a = this.buffer) == null ? void 0 : _a.duration) || 0, elapsed)); 384 } 385 this.stopSourceOnly(); 386 this.isPlaying = false; 387 this.cancelTicker(); 388 if (this.onStateChange) this.onStateChange(false); 389 } 390 stop() { 391 this.stopSourceOnly(); 392 this.pauseOffset = 0; 393 this.isPlaying = false; 394 this.cancelTicker(); 395 if (this.onTimeUpdate) this.onTimeUpdate(0); 396 if (this.onStateChange) this.onStateChange(false); 397 } 398 seek(targetSec) { 399 var _a; 400 const clamped = Math.max(0, Math.min(((_a = this.buffer) == null ? void 0 : _a.duration) || 0, targetSec)); 401 this.pauseOffset = clamped; 402 if (this.isPlaying) { 403 this.play(clamped, this.isLooping ? { start: this.loopStart, end: this.loopEnd } : void 0); 404 } else { 405 if (this.onTimeUpdate) this.onTimeUpdate(clamped); 406 } 407 } 408 getCurrentTime() { 409 var _a; 410 if (!this.isPlaying || !this.ctx) return this.pauseOffset;
411 const elapsed = this.ctx.currentTime - this.startTime; 412 if (this.isLooping && this.loopEnd > this.loopStart) { 413 const loopLen = this.loopEnd - this.loopStart; 414 return this.loopStart + (elapsed - this.loopStart) % loopLen; 415 } 416 return Math.min(((_a = this.buffer) == null ? void 0 : _a.duration) || 0, elapsed); 417 } 418 getIsPlaying() { 419 return this.isPlaying; 420 } 421 getAnalysers() { 422 return { left: this.analyserL, right: this.analyserR }; 423 } 424 stopSourceOnly() { 425 if (this.currentSource) { 426 try { 427 this.currentSource.onended = null; 428 this.currentSource.stop(0); 429 this.currentSource.disconnect(); 430 } catch { 431 } 432 this.currentSource = null; 433 } 434 } 435 startTicker() { 436 this.cancelTicker(); 437 const tick = () => { 438 if (this.isPlaying && this.onTimeUpdate) { 439 this.onTimeUpdate(this.getCurrentTime()); 440 } 441 if (this.isPlaying) { 442 this.animFrameId = requestAnimationFrame(tick); 443 } 444 }; 445 this.animFrameId = requestAnimationFrame(tick); 446 } 447 cancelTicker() { 448 if (this.animFrameId !== null) { 449 cancelAnimationFrame(this.animFrameId); 450 this.animFrameId = null; 451 } 452 } 453 destroy() { 454 this.stop(); 455 if (this.ctx && this.ctx.state !== "closed") { 456 try { 457 this.ctx.close(); 458 } catch { 459 } 460 } 461 this.ctx = null; 462 } 463} 464class WavEncoder { 465 static encode(buffer, bitDepth = 16) { 466 const numChannels = buffer.numberOfChannels; 467 const sampleRate = buffer.sampleRate; 468 const length = buffer.length; 469 const bytesPerSample = bitDepth / 8; 470 const blockAlign = numChannels * bytesPerSample; 471 const byteRate = sampleRate * blockAlign; 472 const dataSize = length * blockAlign; 473 const headerSize = 44; 474 const totalSize = headerSize + dataSize; 475 const arrayBuffer = new ArrayBuffer(totalSize); 476 const view = new DataView(arrayBuffer); 477 this.writeString(view, 0, "RIFF"); 478 view.setUint32(4, 36 + dataSize, true); 479 this.writeString(view, 8, "WAVE"); 480 this.writeString(view, 12, "fmt "); 481 view.setUint32(16, 16, true); 482 const audioFormat = bitDepth === 32 ? 3 : 1; 483 view.setUint16(20, audioFormat, true); 484 view.setUint16(22, numChannels, true); 485 view.setUint32(24, sampleRate, true); 486 view.setUint32(28, byteRate, true); 487 view.setUint16(32, blockAlign, true); 488 view.setUint16(34, bitDepth, true); 489 this.writeString(view, 36, "data"); 490 view.setUint32(40, dataSize, true); 491 const channels = []; 492 for (let c = 0; c < numChannels; c++) { 493 channels.push(buffer.getChannelData(c)); 494 } 495 let offset = 44; 496 if (bitDepth === 16) { 497 for (let i = 0; i < length; i++) { 498 for (let c = 0; c < numChannels; c++) { 499 const sample = Math.max(-1, Math.min(1, channels[c][i])); 500 const intSample = sample < 0 ? sample * 32768 : sample * 32767; 501 view.setInt16(offset, Math.floor(intSample), true); 502 offset += 2; 503 } 504 } 505 } else if (bitDepth === 24) { 506 for (let i = 0; i < length; i++) { 507 for (let c = 0; c < numChannels; c++) { 508 const sample = Math.max(-1, Math.min(1, channels[c][i])); 509 const intSample = Math.floor(sample < 0 ? sample * 8388608 : sample * 8388607); 510 view.setUint8(offset, intSample & 255); 511 view.setUint8(offset + 1, intSample >> 8 & 255); 512 view.setUint8(offset + 2, intSample >> 16 & 255); 513 offset += 3; 514 } 515 } 516 } else if (bitDepth === 32) { 517 for (let i = 0; i < length; i++) { 518 for (let c = 0; c < numChannels; c++) { 519 view.setFloat32(offset, channels[c][i], true); 520 offset += 4; 521 } 522 } 523 } 524 return new Blob([view], { type: "audio/wav" }); 525 } 526 static writeString(view, offset, string) { 527 for (let i = 0; i < string.length; i++) { 528 view.setUint8(offset + i, string.charCodeAt(i)); 529 } 530 } 531} 532class Mp3Encoder { 533 static async encode(buffer, bitrate = 192, onProgress) { 534 const lamejs = await __vitePreload(() => import("./vendor-audio-D-wTggbx.js").then((n) => n.i), true ? __vite__mapDeps([0,1,2,3,4]) : void 0); 535 const numChannels = buffer.numberOfChannels; 536 const sampleRate = buffer.sampleRate; 537 const mp3encoder = new lamejs.Mp3Encoder(numChannels, sampleRate, bitrate); 538 const mp3Data = []; 539 const left = buffer.getChannelData(0); 540 const right = numChannels > 1 ? buffer.getChannelData(1) : left; 541 const sampleBlockSize = 1152; 542 const totalSamples = buffer.length; 543 for (let i = 0; i < totalSamples; i += sampleBlockSize) { 544 const leftChunk = left.subarray(i, i + sampleBlockSize); 545 const rightChunk = right.subarray(i, i + sampleBlockSize); 546 const leftInt = new Int16Array(leftChunk.length); 547 const rightInt = new Int16Array(rightChunk.length); 548 for (let j = 0; j < leftChunk.length; j++) { 549 const sL = Math.max(-1, Math.min(1, leftChunk[j])); 550 const sR = Math.max(-1, Math.min(1, rightChunk[j])); 551 leftInt[j] = sL < 0 ? sL * 32768 : sL * 32767; 552 rightInt[j] = sR < 0 ? sR * 32768 : sR * 32767; 553 } 554 let mp3buf; 555 if (numChannels === 1) { 556 mp3buf = mp3encoder.encodeBuffer(leftInt); 557 } else { 558 mp3buf = mp3encoder.encodeBuffer(leftInt, rightInt); 559 } 560 if (mp3buf.length > 0) { 561 mp3Data.push(new Uint8Array(mp3buf)); 562 } 563 if (onProgress && i % (sampleBlockSize * 10) === 0) { 564 onProgress(Math.min(99, Math.round(i / totalSamples * 100))); 565 } 566 } 567 const flushBuf = mp3encoder.flush(); 568 if (flushBuf.length > 0) { 569 mp3Data.push(new Uint8Array(flushBuf)); 570 } 571 if (onProgress) onProgress(100); 572 return new Blob(mp3Data, { type: "audio/mp3" }); 573 } 574} 575class DemoAudio { 576 static async generateDemoTrack(sampleRate = 44100) { 577 const duration = 10; 578 const offlineCtx = new OfflineAudioContext(2, Math.floor(sampleRate * duration), sampleRate); 579 const bpm = 120; 580 const beatSec = 60 / bpm; 581 const totalBeats = 20; 582 const chords = [ 583 [220, 261.63, 329.63], 584 // Am (A3, C4, E4) 585 [174.61, 220, 261.63], 586 // F (F3, A3, C4) 587 [261.63, 329.63, 392], 588 // C (C4, E4, G4) 589 [196, 246.94, 293.66] 590 // G (G3, B3, D4) 591 ]; 592 const bassNotes = [110, 87.31, 130.81, 98]; 593 const master = offlineCtx.createGain(); 594 master.gain.value = 0.8; 595 master.connect(offlineCtx.destination); 596 for (let b = 0; b < totalBeats; b++) { 597 const chordIdx = Math.floor(b / 2) % chords.length; 598 const freq = bassNotes[chordIdx]; 599 const time = b * beatSec; 600 const osc = offlineCtx.createOscillator(); 601 const gain = offlineCtx.createGain(); 602 osc.type = "sawtooth"; 603 osc.frequency.setValueAtTime(freq, time); 604 gain.gain.setValueAtTime(0.25, time); 605 gain.gain.exponentialRampToValueAtTime(1e-3, time + beatSec * 0.85); 606 const filter = offlineCtx.createBiquadFilter(); 607 filter.type = "lowpass"; 608 filter.frequency.setValueAtTime(450, time); 609 osc.connect(filter); 610 filter.connect(gain); 611 gain.connect(master); 612 osc.start(time); 613 osc.stop(time + beatSec * 0.9); 614 } 615 for (let c = 0; c < Math.floor(totalBeats / 2); c++) { 616 const chord = chords[c % chords.length]; 617 const time = c * 2 * beatSec; 618 chord.forEach((freq, i) => { 619 const osc = offlineCtx.createOscillator(); 620 const gain = offlineCtx.createGain(); 621 const panner = offlineCtx.createStereoPanner(); 622 osc.type = "triangle"; 623 osc.frequency.setValueAtTime(freq, time); 624 panner.pan.value = (i - 1) * 0.4; 625 gain.gain.setValueAtTime(1e-3, time); 626 gain.gain.linearRampToValueAtTime(0.12, time + 0.1); 627 gain.gain.setValueAtTime(0.12, time + beatSec * 1.8); 628 gain.gain.linearRampToValueAtTime(1e-3, time + beatSec * 2); 629 osc.connect(panner); 630 panner.connect(gain); 631 gain.connect(master); 632 osc.start(time); 633 osc.stop(time + beatSec * 2); 634 }); 635 } 636 const arpScale = [440, 523.25, 659.25, 783.99, 880]; 637 for (let s = 0; s < totalBeats * 4; s++) { 638 const time = s * (beatSec / 4); 639 const noteFreq = arpScale[s * 3 % arpScale.length]; 640 const osc = offlineCtx.createOscillator(); 641 const gain = offlineCtx.createGain(); 642 const panner = offlineCtx.createStereoPanner(); 643 osc.type = "sine"; 644 osc.frequency.setValueAtTime(noteFreq, time); 645 panner.pan.value = Math.sin(s * 0.5) * 0.6; 646 gain.gain.setValueAtTime(0.08, time); 647 gain.gain.exponentialRampToValueAtTime(1e-4, time + 0.18); 648 osc.connect(panner); 649 panner.connect(gain); 650 gain.connect(master); 651 osc.start(time); 652 osc.stop(time + 0.2); 653 } 654 for (let b = 0; b < totalBeats; b++) { 655 const time = b * beatSec; 656 if (b % 2 === 0) {
657 const kickOsc = offlineCtx.createOscillator(); 658 const kickGain = offlineCtx.createGain(); 659 kickOsc.frequency.setValueAtTime(150, time); 660 kickOsc.frequency.exponentialRampToValueAtTime(30, time + 0.12); 661 kickGain.gain.setValueAtTime(0.4, time); 662 kickGain.gain.exponentialRampToValueAtTime(1e-3, time + 0.18); 663 kickOsc.connect(kickGain); 664 kickGain.connect(master); 665 kickOsc.start(time); 666 kickOsc.stop(time + 0.2); 667 } else { 668 const snareNoise = offlineCtx.createBufferSource(); 669 const noiseLen = Math.floor(sampleRate * 0.15); 670 const noiseBuffer = offlineCtx.createBuffer(1, noiseLen, sampleRate); 671 const noiseData = noiseBuffer.getChannelData(0); 672 for (let i = 0; i < noiseLen; i++) { 673 noiseData[i] = Math.random() * 2 - 1; 674 } 675 snareNoise.buffer = noiseBuffer; 676 const snareFilter = offlineCtx.createBiquadFilter(); 677 snareFilter.type = "highpass"; 678 snareFilter.frequency.value = 1e3; 679 const snareGain = offlineCtx.createGain(); 680 snareGain.gain.setValueAtTime(0.2, time); 681 snareGain.gain.exponentialRampToValueAtTime(1e-3, time + 0.15); 682 snareNoise.connect(snareFilter); 683 snareFilter.connect(snareGain); 684 snareGain.connect(master); 685 snareNoise.start(time); 686 snareNoise.stop(time + 0.16); 687 } 688 } 689 return await offlineCtx.startRendering(); 690 } 691} 692const AudioEditorContext = reactExports.createContext(null); 693const MAX_HISTORY = 15; 694const AudioEditorProvider = ({ children }) => { 695 const engineRef = reactExports.useRef(new AudioPlaybackEngine()); 696 const engine = engineRef.current; 697 const [audioBuffer, setAudioBuffer] = reactExports.useState(null); 698 const [fileName, setFileName] = reactExports.useState("Untitled Audio"); 699 const [selection, setSelection] = reactExports.useState(null); 700 const [cursor, setCursor] = reactExports.useState(0); 701 const [zoom, setZoom] = reactExports.useState({ start: 0, end: 1 }); 702 const [verticalZoom, setVerticalZoom] = reactExports.useState(1); 703 const [isPlaying, setIsPlaying] = reactExports.useState(false); 704 const [isLooping, setIsLooping] = reactExports.useState(false); 705 const [playbackPosition, setPlaybackPosition] = reactExports.useState(0); 706 const [masterVolume, setMasterVolume] = reactExports.useState(1); 707 const [activeModal, setActiveModal] = reactExports.useState(null); 708 const [activeEffect, setActiveEffect] = reactExports.useState(null); 709 const [stats, setStats] = reactExports.useState(null); 710 const [isProcessing, setIsProcessing] = reactExports.useState(false); 711 const [processingMessage, setProcessingMessage] = reactExports.useState(""); 712 const [history, setHistory] = reactExports.useState([]); 713 const [historyIndex, setHistoryIndex] = reactExports.useState(-1); 714 const clipboardRef = reactExports.useRef(null); 715 reactExports.useEffect(() => { 716 engine.onTimeUpdate = (sec) => { 717 setPlaybackPosition(sec); 718 setCursor(sec); 719 }; 720 engine.onStateChange = (playing) => { 721 setIsPlaying(playing); 722 }; 723 engine.onEnded = () => { 724 setIsPlaying(false); 725 setPlaybackPosition(0); 726 }; 727 return () => { 728 engine.destroy(); 729 }; 730 }, [engine]); 731 reactExports.useEffect(() => { 732 if (audioBuffer) { 733 setStats(AudioBufferUtils.computeStats(audioBuffer)); 734 setZoom((prev) => { 735 if (prev.end <= prev.start || prev.end > audioBuffer.duration) { 736 return { start: 0, end: audioBuffer.duration }; 737 } 738 return prev; 739 }); 740 } else { 741 setStats(null); 742 } 743 }, [audioBuffer]); 744 const commitBuffer = reactExports.useCallback((newBuffer, resetSelection = false) => { 745 engine.setBuffer(newBuffer); 746 setAudioBuffer(newBuffer); 747 setHistory((prev) => { 748 const upToCurrent = prev.slice(0, historyIndex + 1); 749 const updated = [...upToCurrent, newBuffer]; 750 if (updated.length > MAX_HISTORY) { 751 updated.shift(); 752 } 753 return updated; 754 }); 755 setHistoryIndex((prev) => Math.min(prev + 1, MAX_HISTORY - 1)); 756 if (resetSelection) { 757 setSelection(null); 758 } 759 }, [engine, historyIndex]); 760 const loadBuffer = reactExports.useCallback((buffer, name) => { 761 engine.setBuffer(buffer); 762 setAudioBuffer(buffer); 763 setFileName(name); 764 setHistory([buffer]); 765 setHistoryIndex(0); 766 setSelection(null); 767 setCursor(0); 768 setZoom({ start: 0, end: buffer.duration }); 769 }, [engine]); 770 const loadAudioFile = reactExports.useCallback(async (file) => { 771 setIsProcessing(true); 772 setProcessingMessage("Decoding audio file in browser..."); 773 try { 774 const arrayBuf = await file.arrayBuffer(); 775 const ctx = engine.getContext(); 776 const decoded = await ctx.decodeAudioData(arrayBuf); 777 loadBuffer(decoded, file.name); 778 } catch (err) { 779 console.error("Failed to decode audio file:", err); 780 alert("Unable to decode this audio file. Please try another standard format."); 781 } finally { 782 setIsProcessing(false); 783 setProcessingMessage(""); 784 } 785 }, [engine, loadBuffer]); 786 const loadDemo = reactExports.useCallback(async () => { 787 setIsProcessing(true); 788 setProcessingMessage("Synthesizing demo track in browser..."); 789 try { 790 const demo = await DemoAudio.generateDemoTrack(); 791 loadBuffer(demo, "Hamie Synthesizer Demo.wav"); 792 } catch (err) { 793 console.error("Failed to generate demo track:", err); 794 } finally { 795 setIsProcessing(false); 796 setProcessingMessage(""); 797 } 798 }, [loadBuffer]); 799 const play = reactExports.useCallback(() => { 800 if (!audioBuffer) return; 801 const loopRange = isLooping && selection && selection.end > selection.start ? selection : void 0; 802 engine.play(cursor, loopRange); 803 }, [audioBuffer, cursor, engine, isLooping, selection]); 804 const pause = reactExports.useCallback(() => { 805 engine.pause(); 806 }, [engine]); 807 const stop = reactExports.useCallback(() => { 808 engine.stop(); 809 setCursor(selection ? selection.start : 0); 810 }, [engine, selection]); 811 const toggleLoop = reactExports.useCallback(() => { 812 setIsLooping((prev) => { 813 const next = !prev; 814 if (isPlaying) { 815 const loopRange = next && selection && selection.end > selection.start ? selection : void 0; 816 engine.play(engine.getCurrentTime(), loopRange); 817 } 818 return next; 819 }); 820 }, [engine, isPlaying, selection]); 821 const seek = reactExports.useCallback((sec) => { 822 setCursor(sec); 823 engine.seek(sec); 824 }, [engine]); 825 const zoomIn = reactExports.useCallback(() => { 826 if (!audioBuffer) return; 827 setZoom((z) => { 828 const center = cursor >= z.start && cursor <= z.end ? cursor : (z.start + z.end) / 2; 829 const span = (z.end - z.start) * 0.7; 830 const start = Math.max(0, center - span / 2); 831 const end = Math.min(audioBuffer.duration, start + span); 832 return { start, end }; 833 }); 834 }, [audioBuffer, cursor]); 835 const zoomOut = reactExports.useCallback(() => { 836 if (!audioBuffer) return; 837 setZoom((z) => { 838 const center = (z.start + z.end) / 2; 839 const span = (z.end - z.start) * 1.4; 840 const start = Math.max(0, center - span / 2); 841 const end = Math.min(audioBuffer.duration, start + span); 842 return { start, end }; 843 }); 844 }, [audioBuffer]); 845 const zoomFit = reactExports.useCallback(() => { 846 if (!audioBuffer) return; 847 setZoom({ start: 0, end: audioBuffer.duration }); 848 }, [audioBuffer]); 849 const zoomToSelection = reactExports.useCallback(() => { 850 if (!audioBuffer || !selection || selection.end <= selection.start) return; 851 const padding = (selection.end - selection.start) * 0.05; 852 const start = Math.max(0, selection.start - padding); 853 const end = Math.min(audioBuffer.duration, selection.end + padding); 854 setZoom({ start, end }); 855 }, [audioBuffer, selection]); 856 const handleSetMasterVolume = reactExports.useCallback((vol) => { 857 setMasterVolume(vol); 858 engine.setVolume(vol); 859 }, [engine]); 860 const undo = reactExports.useCallback(() => { 861 if (historyIndex > 0) { 862 const nextIdx = historyIndex - 1; 863 const buf = history[nextIdx]; 864 setHistoryIndex(nextIdx); 865 setAudioBuffer(buf); 866 engine.setBuffer(buf); 867 } 868 }, [engine, history, historyIndex]); 869 const redo = reactExports.useCallback(() => { 870 if (historyIndex < history.length - 1) { 871 const nextIdx = historyIndex + 1; 872 const buf = history[nextIdx]; 873 setHistoryIndex(nextIdx); 874 setAudioBuffer(buf); 875 engine.setBuffer(buf); 876 } 877 }, [engine, history, historyIndex]); 878 const cut = reactExports.useCallback(() => { 879 if (!audioBuffer || !selection || selection.end <= selection.start) return; 880 const ctx = engine.getContext(); 881 const sliced = AudioBufferUtils.sliceBuffer(ctx, audioBuffer, selection.start, selection.end); 882 clipboardRef.current = sliced; 883 const deleted = AudioBufferUtils.deleteRegion(ctx, audioBuffer, selection.start, selection.end); 884 commitBuffer(deleted); 885 setCursor(selection.start); 886 setSelection(null); 887 }, [audioBuffer, commitBuffer, engine, selection]); 888 const copy = reactExports.useCallback(() => { 889 if (!audioBuffer || !selection || selection.end <= selection.start) return; 890 const ctx = engine.getContext(); 891 clipboardRef.current = AudioBufferUtils.sliceBuffer(ctx, audioBuffer, selection.start, selection.end); 892 }, [audioBuffer, engine, selection]); 893 const paste = reactExports.useCallback(() => { 894 if (!audioBuffer || !clipboardRef.current) return; 895 const ctx = engine.getContext(); 896 let result; 897 if (selection && selection.end > selection.start) { 898 result = AudioBufferUtils.replaceRegion(ctx, audioBuffer, clipboardRef.current, selection.start, selection.end); 899 setCursor(selection.start + clipboardRef.current.duration); 900 } else { 901 result = AudioBufferUtils.insertBuffer(ctx, audioBuffer, clipboardRef.current, cursor); 902 setCursor(cursor + clipboardRef.current.duration); 903 } 904 commitBuffer(result); 905 setSelection(null); 906 }, [audioBuffer, commitBuffer, cursor, engine, selection]); 907 const deleteSelection = reactExports.useCallback(() => { 908 if (!audioBuffer || !selection || selection.end <= selection.start) return; 909 const ctx = engine.getContext(); 910 const deleted = AudioBufferUtils.deleteRegion(ctx, audioBuffer, selection.start, selection.end); 911 commitBuffer(deleted); 912 setCursor(selection.start); 913 setSelection(null); 914 }, [audioBuffer, commitBuffer, engine, selection]); 915 const cropSelection = reactExports.useCallback(() => { 916 if (!audioBuffer || !selection || selection.end <= selection.start) return; 917 const ctx = engine.getContext(); 918 const cropped = AudioBufferUtils.sliceBuffer(ctx, audioBuffer, selection.start, selection.end); 919 commitBuffer(cropped); 920 setCursor(0); 921 setSelection(null); 922 setZoom({ start: 0, end: cropped.duration }); 923 }, [audioBuffer, commitBuffer, engine, selection]); 924 const silenceSelection = reactExports.useCallback(() => { 925 if (!audioBuffer || !selection || selection.end <= selection.start) return; 926 const ctx = engine.getContext(); 927 const silenced = AudioBufferUtils.silenceRegion(ctx, audioBuffer, selection.start, selection.end); 928 commitBuffer(silenced); 929 }, [audioBuffer, commitBuffer, engine, selection]); 930 const insertSilence = reactExports.useCallback((seconds) => { 931 if (!audioBuffer || seconds <= 0) return; 932 const ctx = engine.getContext(); 933 const result = AudioBufferUtils.insertSilence(ctx, audioBuffer, cursor, seconds); 934 commitBuffer(result); 935 setCursor(cursor + seconds); 936 }, [audioBuffer, commitBuffer, cursor, engine]); 937 const selectAll = reactExports.useCallback(() => { 938 if (!audioBuffer) return; 939 setSelection({ start: 0, end: audioBuffer.duration }); 940 }, [audioBuffer]); 941 const swapChannels = reactExports.useCallback(() => { 942 if (!audioBuffer) return; 943 const ctx = engine.getContext(); 944 const res = AudioBufferUtils.swapChannels(ctx, audioBuffer); 945 commitBuffer(res); 946 }, [audioBuffer, commitBuffer, engine]); 947 const stereoToMono = reactExports.useCallback(() => { 948 if (!audioBuffer) return; 949 const ctx = engine.getContext(); 950 const res = AudioBufferUtils.stereoToMono(ctx, audioBuffer); 951 commitBuffer(res); 952 }, [audioBuffer, commitBuffer, engine]); 953 const monoToStereo = reactExports.useCallback(() => { 954 if (!audioBuffer) return; 955 const ctx = engine.getContext(); 956 const res = AudioBufferUtils.monoToStereo(ctx, audioBuffer); 957 commitBuffer(res); 958 }, [audioBuffer, commitBuffer, engine]); 959 const invertPhase = reactExports.useCallback(() => { 960 if (!audioBuffer) return; 961 const ctx = engine.getContext(); 962 const startSec = selection ? selection.start : void 0; 963 const endSec = selection ? selection.end : void 0; 964 const res = AudioBufferUtils.invertPhase(ctx, audioBuffer, startSec, endSec); 965 commitBuffer(res); 966 }, [audioBuffer, commitBuffer, engine, selection]); 967 const removeDc = reactExports.useCallback(() => { 968 if (!audioBuffer) return; 969 const ctx = engine.getContext(); 970 const startSec = selection ? selection.start : void 0; 971 const endSec = selection ? selection.end : void 0; 972 const res = AudioBufferUtils.removeDCOffset(ctx, audioBuffer, startSec, endSec); 973 commitBuffer(res); 974 }, [audioBuffer, commitBuffer, engine, selection]); 975 const applyDSP = reactExports.useCallback(async (processor) => { 976 if (!audioBuffer) return; 977 setIsProcessing(true); 978 setProcessingMessage("Applying DSP effect..."); 979 try { 980 const processed = await processor(audioBuffer); 981 commitBuffer(processed); 982 } catch (err) { 983 console.error("Failed to apply DSP effect:", err); 984 alert("Effect processing encountered an error."); 985 } finally { 986 setIsProcessing(false); 987 setProcessingMessage("");
988 setActiveModal(null); 989 } 990 }, [audioBuffer, commitBuffer]); 991 const exportAudio = reactExports.useCallback(async (format, bitDepth, bitrate, range) => { 992 if (!audioBuffer) return; 993 setIsProcessing(true); 994 setProcessingMessage("Rendering and encoding audio..."); 995 try { 996 const ctx = engine.getContext(); 997 let targetBuffer = audioBuffer; 998 if (range === "selection" && selection && selection.end > selection.start) { 999 targetBuffer = AudioBufferUtils.sliceBuffer(ctx, audioBuffer, selection.start, selection.end); 1000 } 1001 let blob; 1002 let ext; 1003 if (format === "wav") { 1004 blob = WavEncoder.encode(targetBuffer, bitDepth); 1005 ext = "wav"; 1006 } else { 1007 blob = await Mp3Encoder.encode(targetBuffer, bitrate, (percent) => { 1008 setProcessingMessage(`Encoding MP3: ${percent}%`); 1009 }); 1010 ext = "mp3"; 1011 } 1012 const url = URL.createObjectURL(blob); 1013 const a = document.createElement("a"); 1014 const baseName = fileName.replace(/\.[^/.]+$/, ""); 1015 const rangeSuffix = range === "selection" ? "_selection" : ""; 1016 a.href = url; 1017 a.download = `${baseName}${rangeSuffix}.${ext}`; 1018 document.body.appendChild(a); 1019 a.click(); 1020 document.body.removeChild(a); 1021 URL.revokeObjectURL(url); 1022 } catch (err) { 1023 console.error("Failed to export audio:", err); 1024 alert("Audio export failed."); 1025 } finally { 1026 setIsProcessing(false); 1027 setProcessingMessage(""); 1028 setActiveModal(null); 1029 } 1030 }, [audioBuffer, engine, fileName, selection]); 1031 const value = { 1032 audioBuffer, 1033 fileName, 1034 selection, 1035 cursor, 1036 zoom, 1037 verticalZoom, 1038 isPlaying, 1039 isLooping, 1040 playbackPosition, 1041 masterVolume, 1042 activeModal, 1043 activeEffect, 1044 canUndo: historyIndex > 0, 1045 canRedo: historyIndex < history.length - 1, 1046 stats, 1047 engine, 1048 isProcessing, 1049 processingMessage, 1050 setCursor, 1051 setSelection, 1052 setZoom, 1053 setVerticalZoom, 1054 setMasterVolume: handleSetMasterVolume, 1055 setActiveModal, 1056 setActiveEffect, 1057 loadAudioFile, 1058 loadBuffer, 1059 loadDemo, 1060 play, 1061 pause, 1062 stop, 1063 toggleLoop, 1064 seek, 1065 zoomIn, 1066 zoomOut, 1067 zoomFit, 1068 zoomToSelection, 1069 undo, 1070 redo, 1071 cut, 1072 copy, 1073 paste, 1074 deleteSelection, 1075 cropSelection, 1076 silenceSelection, 1077 insertSilence, 1078 selectAll, 1079 swapChannels, 1080 stereoToMono, 1081 monoToStereo, 1082 invertPhase, 1083 removeDc, 1084 applyDSP, 1085 exportAudio 1086 }; 1087 return /* @__PURE__ */ jsxRuntimeExports.jsx(AudioEditorContext.Provider, { value, children }); 1088}; 1089const useAudioEditor = () => { 1090 const ctx = reactExports.useContext(AudioEditorContext); 1091 if (!ctx) throw new Error("useAudioEditor must be used within AudioEditorProvider"); 1092 return ctx; 1093}; 1094const en = { 1095 app: { 1096 title: "Hamie AudioLab", 1097 subtitle: "Professional In-Browser Audio Waveform Editor", 1098 zeroServerNotice: "100% Client-Side. Zero server resources used. Total privacy.", 1099 loadPrompt: "Drop an audio file here, or click to open", 1100 supportedFormats: "Supports WAV, MP3, OGG, FLAC, M4A, AAC, Opus, WebM" 1101 }, 1102 menu: { 1103 file: "File", 1104 edit: "Edit", 1105 effects: "Effects", 1106 analyze: "Analyze", 1107 view: "View" 1108 }, 1109 file: { 1110 open: "Open Audio File", 1111 record: "Record Microphone", 1112 demo: "Load Demo Track", 1113 exportWav: "Export as WAV", 1114 exportMp3: "Export as MP3", 1115 exportSelection: "Export Selection", 1116 sendToDigitizer: "Send to Audio Transcriber", 1117 sendToConverter: "Send to MIDI Converter" 1118 }, 1119 edit: { 1120 undo: "Undo", 1121 redo: "Redo", 1122 cut: "Cut", 1123 copy: "Copy", 1124 paste: "Paste", 1125 delete: "Delete", 1126 crop: "Trim / Crop to Selection", 1127 silence: "Silence Selection", 1128 insertSilence: "Insert Silence...", 1129 selectAll: "Select All", 1130 swapChannels: "Swap Channels (L/R)", 1131 stereoToMono: "Stereo to Mono Mix", 1132 monoToStereo: "Mono to Stereo", 1133 invertPhase: "Invert Phase (180°)", 1134 removeDc: "Remove DC Offset" 1135 }, 1136 view: { 1137 zoomIn: "Zoom In", 1138 zoomOut: "Zoom Out", 1139 zoomFit: "Zoom to Fit", 1140 zoomSelection: "Zoom to Selection" 1141 }, 1142 effects: { 1143 title: "Audio Effects", 1144 normalize: "Normalize", 1145 amplify: "Amplify / Gain", 1146 fadeIn: "Fade In", 1147 fadeOut: "Fade Out", 1148 eq: "Parametric Equalizer", 1149 compressor: "Compressor & Limiter", 1150 reverb: "Studio Reverb", 1151 delay: "Stereo Delay / Echo", 1152 vocalRemover: "Vocal Remover (Center Canceller)", 1153 pitchTempo: "Pitch & Tempo Stretch", 1154 reverse: "Reverse Audio", 1155 saturation: "Tube / Tape Saturation", 1156 preview: "Audition Preview", 1157 stopPreview: "Stop Audition", 1158 apply: "Apply Effect", 1159 cancel: "Cancel" 1160 }, 1161 params: { 1162 targetDb: "Target Peak (dBFS)", 1163 gainDb: "Gain (dB)", 1164 curve: "Fade Curve", 1165 linear: "Linear", 1166 logarithmic: "Logarithmic", 1167 exponential: "Exponential", 1168 sCurve: "S-Curve", 1169 threshold: "Threshold (dB)", 1170 ratio: "Ratio", 1171 knee: "Knee (dB)", 1172 attack: "Attack (s)", 1173 release: "Release (s)", 1174 makeupGain: "Makeup Gain (dB)", 1175 roomSize: "Room Size", 1176 damping: "Damping", 1177 wet: "Wet Mix", 1178 dry: "Dry Mix", 1179 delayTime: "Delay Time (s)", 1180 feedback: "Feedback", 1181 pingPong: "Ping-Pong Stereo", 1182 crossoverFreq: "Bass Bypass Frequency (Hz)", 1183 pitchSemitones: "Pitch Shift (Semitones)",
1184 tempoMultiplier: "Playback Speed / Tempo (x)", 1185 drive: "Drive / Saturation Amount" 1186 }, 1187 analyze: { 1188 stats: "Audio Statistics", 1189 spectrum: "FFT Spectrum Analyzer", 1190 duration: "Duration", 1191 sampleRate: "Sample Rate", 1192 channels: "Channels", 1193 samples: "Total Samples", 1194 peakL: "Peak Left (dBFS)", 1195 peakR: "Peak Right (dBFS)", 1196 rmsL: "RMS Left (dBFS)", 1197 rmsR: "RMS Right (dBFS)", 1198 dynamicRange: "Dynamic Range", 1199 dcOffsetL: "DC Offset L", 1200 dcOffsetR: "DC Offset R", 1201 clippingDetected: "Clipping was detected!", 1202 noClipping: "Clean signal. No clipping detected." 1203 }, 1204 transport: { 1205 play: "Play (Space)", 1206 pause: "Pause (Space)", 1207 stop: "Stop", 1208 loop: "Loop Selection (Shift+Space)", 1209 record: "Record Mic", 1210 masterVolume: "Master Volume", 1211 selectionStart: "Start", 1212 selectionEnd: "End", 1213 selectionLength: "Length", 1214 cursor: "Cursor" 1215 }, 1216 exportModal: { 1217 title: "Export Audio File", 1218 format: "Output Format", 1219 bitDepth: "Bit Depth", 1220 bitrate: "MP3 Bitrate", 1221 range: "Export Range", 1222 fullTrack: "Entire Audio Track", 1223 selectionOnly: "Selected Region Only", 1224 fileName: "File Name", 1225 exportBtn: "Export & Download", 1226 processing: "Encoding audio in browser..." 1227 }, 1228 recordModal: { 1229 title: "Record from Microphone", 1230 prompt: "Click Start to record audio directly in your browser.", 1231 start: "Start Recording", 1232 stop: "Stop Recording", 1233 insert: "Insert into Track", 1234 replace: "Replace Entire Track" 1235 }, 1236 silenceModal: { 1237 title: "Insert Silence", 1238 prompt: "Enter the duration of silence to insert at current cursor:", 1239 seconds: "Seconds", 1240 insert: "Insert Silence" 1241 } 1242}; 1243const ja = { 1244 app: { 1245 title: "Hamie AudioLab", 1246 subtitle: "ãã©ã¦ã¶å°ç¨ ãããã§ãã·ã§ãã«æ³¢å½¢ç·¨éã¨ãã£ã¿", 1247 zeroServerNotice: "100% ã¯ã©ã¤ã¢ã³ãå¦çããµã¼ãã¼è² è·ã¼ãï¼å®å ¨ãã©ã¤ãã·ã¼ä¿è·ã", 1248 loadPrompt: "é³å£°ãã¡ã¤ã«ããã©ãã°ï¼ãããããã¾ãã¯ã¯ãªãã¯ãã¦éã", 1249 supportedFormats: "対å¿å½¢å¼: WAV, MP3, OGG, FLAC, M4A, AAC, Opus, WebM" 1250 }, 1251 menu: { 1252 file: "ãã¡ã¤ã«", 1253 edit: "ç·¨é", 1254 effects: "ã¨ãã§ã¯ã", 1255 analyze: "è§£æ", 1256 view: "表示" 1257 }, 1258 file: { 1259 open: "ãã¡ã¤ã«ãéã", 1260 record: "ãã¤ã¯é²é³", 1261 demo: "ãã¢æ²ãèªã¿è¾¼ã", 1262 exportWav: "WAVå½¢å¼ã§æ¸ãåºã", 1263 exportMp3: "MP3å½¢å¼ã§æ¸ãåºã", 1264 exportSelection: "鏿ç¯å²ãæ¸ãåºã", 1265 sendToDigitizer: "é³å£°æ¡è (Transcriber) ã¸éã", 1266 sendToConverter: "MIDIã³ã³ãã¼ã¿ã¼ã¸éã" 1267 }, 1268 edit: { 1269 undo: "å ã«æ»ã", 1270 redo: "ããç´ã", 1271 cut: "åãåã", 1272 copy: "ã³ãã¼", 1273 paste: "è²¼ãä»ã", 1274 delete: "åé¤", 1275 crop: "鏿ç¯å²ã®ã¿ããªãã³ã°", 1276 silence: "鏿ç¯å²ãç¡é³å", 1277 insertSilence: "ç¡é³ã®æ¿å ¥...", 1278 selectAll: "ãã¹ã¦é¸æ", 1279 swapChannels: "å·¦å³ãã£ã³ãã«å ¥æ¿", 1280 stereoToMono: "ã¹ãã¬ãªããã¢ãã©ã«å¤æ", 1281 monoToStereo: "ã¢ãã©ã«ããã¹ãã¬ãªå", 1282 invertPhase: "ä½ç¸ãå転 (180°)", 1283 removeDc: "DCãªãã»ãããé¤å»" 1284 }, 1285 view: { 1286 zoomIn: "æ¡å¤§", 1287 zoomOut: "縮å°", 1288 zoomFit: "å ¨ä½ã表示", 1289 zoomSelection: "鏿ç¯å²ã«ãºã¼ã " 1290 }, 1291 effects: { 1292 title: "ãªã¼ãã£ãªã¨ãã§ã¯ã", 1293 normalize: "ãã¼ãã©ã¤ãº (é³éåä¸å)", 1294 amplify: "å¢å¹ / ã²ã¤ã³èª¿æ´", 1295 fadeIn: "ãã§ã¼ãã¤ã³", 1296 fadeOut: "ãã§ã¼ãã¢ã¦ã", 1297 eq: "ãã©ã¡ããªãã¯EQ", 1298 compressor: "ã³ã³ãã¬ããµã¼ï¼ãªããã¿ã¼", 1299 reverb: "ã¹ã¿ã¸ãªã»ãªãã¼ã", 1300 delay: "ã¹ãã¬ãªã»ãã£ã¬ã¤ / ã¨ã³ã¼", 1301 vocalRemover: "ãã¼ã«ã«ä½æ¸ (ã»ã³ã¿ã¼é¤å»)", 1302 pitchTempo: "ãããï¼ãã³ã夿´", 1303 reverse: "éåç (ãªãã¼ã¹)", 1304 saturation: "ç空管 / ãã¼ããµãã¥ã¬ã¼ã·ã§ã³", 1305 preview: "ãã¬ãã¥ã¼è©¦è´", 1306 stopPreview: "試è´åæ¢", 1307 apply: "ã¨ãã§ã¯ãé©ç¨", 1308 cancel: "ãã£ã³ã»ã«" 1309 }, 1310 params: { 1311 targetDb: "ç®æ¨ãã¼ã¯ (dBFS)", 1312 gainDb: "ã²ã¤ã³ (dB)", 1313 curve: "ãã§ã¼ãæ²ç·", 1314 linear: "ãªãã¢", 1315 logarithmic: "対æ°", 1316 exponential: "ææ°", 1317 sCurve: "Såã«ã¼ã", 1318 threshold: "ãããå¤ (dB)", 1319 ratio: "å§ç¸®æ¯ (Ratio)", 1320 knee: "ãã¼å¹ (dB)", 1321 attack: "ã¢ã¿ãã¯æé (ç§)", 1322 release: "ãªãªã¼ã¹æé (ç§)", 1323 makeupGain: "ã¡ã¤ã¯ã¢ããã²ã¤ã³ (dB)", 1324 roomSize: "é¨å±ã®åºã (Room Size)", 1325 damping: "é«åæ¸è¡° (Damping)", 1326 wet: "ã¨ãã§ã¯ãé³ (Wet)", 1327 dry: "åé³ (Dry)", 1328 delayTime: "é å»¶æé (ç§)", 1329 feedback: "ãã£ã¼ãããã¯",
1330 pingPong: "ãã³ãã³ã»ã¹ãã¬ãª", 1331 crossoverFreq: "ä½é³ãã¤ãã¹å¨æ³¢æ° (Hz)", 1332 pitchSemitones: "ãããã·ãã (åé³)", 1333 tempoMultiplier: "åçé度 / ãã³ã (å)", 1334 drive: "ãµãã¥ã¬ã¼ã·ã§ã³å¼·åº¦" 1335 }, 1336 analyze: { 1337 stats: "ãªã¼ãã£ãªçµ±è¨æ å ±", 1338 spectrum: "FFT卿³¢æ°ã¢ãã©ã¤ã¶ã¼", 1339 duration: "åçæé", 1340 sampleRate: "ãµã³ããªã³ã°ã¬ã¼ã", 1341 channels: "ãã£ã³ãã«æ°", 1342 samples: "ç·ãµã³ãã«æ°", 1343 peakL: "ãã¼ã¯ L (dBFS)", 1344 peakR: "ãã¼ã¯ R (dBFS)", 1345 rmsL: "RMSé³é L (dBFS)", 1346 rmsR: "RMSé³é R (dBFS)", 1347 dynamicRange: "ãã¤ãããã¯ã¬ã³ã¸", 1348 dcOffsetL: "DCãªãã»ãã L", 1349 dcOffsetR: "DCãªãã»ãã R", 1350 clippingDetected: "ã¯ãªããã³ã° (é³å²ã) ãæ¤åºãã¾ããï¼", 1351 noClipping: "ã¯ãªããã³ã°ãªããã¯ãªã¼ã³ãªä¿¡å·ã§ãã" 1352 }, 1353 transport: { 1354 play: "åç (Space)", 1355 pause: "䏿忢 (Space)", 1356 stop: "忢", 1357 loop: "ã«ã¼ãåç (Shift+Space)", 1358 record: "ãã¤ã¯é²é³", 1359 masterVolume: "ãã¹ã¿ã¼é³é", 1360 selectionStart: "éå§ä½ç½®", 1361 selectionEnd: "çµäºä½ç½®", 1362 selectionLength: "鏿é·", 1363 cursor: "åçããã" 1364 }, 1365 exportModal: { 1366 title: "ãªã¼ãã£ãªãã¡ã¤ã«ã®æ¸ãåºã", 1367 format: "ä¿åå½¢å¼", 1368 bitDepth: "ãããæ·±åº¦", 1369 bitrate: "MP3 ãããã¬ã¼ã", 1370 range: "æ¸ãåºãç¯å²", 1371 fullTrack: "ãã©ãã¯å ¨ä½", 1372 selectionOnly: "鏿ç¯å²ã®ã¿", 1373 fileName: "ãã¡ã¤ã«å", 1374 exportBtn: "æ¸ãåºãã¦ãã¦ã³ãã¼ã", 1375 processing: "ãã©ã¦ã¶å ã§ã¨ã³ã³ã¼ãå¦çä¸..." 1376 }, 1377 recordModal: { 1378 title: "ãã¤ã¯ããé²é³", 1379 prompt: "éå§ãã¿ã³ãæ¼ãã¦ããã©ã¦ã¶ä¸ã§ç´æ¥é²é³ãã¾ãã", 1380 start: "é²é³éå§", 1381 stop: "é²é³åæ¢", 1382 insert: "ãã©ãã¯ã«æ¿å ¥", 1383 replace: "ãã©ãã¯å ¨ä½ãç½®æ" 1384 }, 1385 silenceModal: { 1386 title: "ç¡é³ã®æ¿å ¥", 1387 prompt: "ç¾å¨ã®ã«ã¼ã½ã«ä½ç½®ã«æ¿å ¥ããç¡é³ã®é·ããæå®ãã¦ãã ãã:", 1388 seconds: "ç§", 1389 insert: "ç¡é³ãæ¿å ¥" 1390 } 1391}; 1392const zhCN = { 1393 app: { 1394 title: "Hamie AudioLab", 1395 subtitle: "ä¸ä¸æµè§å¨é³é¢æ³¢å½¢ç¼è¾å¨", 1396 zeroServerNotice: "100% 纯æ¬å°å®¢æ·ç«¯å¤çã䏿¶èæå¡å¨èµæºï¼ä¿æ¤éç§ã", 1397 loadPrompt: "ææ¾é³é¢æä»¶è³æ¤å¤ï¼æç¹å»æå¼", 1398 supportedFormats: "æ¯æ WAV, MP3, OGG, FLAC, M4A, AAC, Opus, WebM" 1399 }, 1400 menu: { 1401 file: "æä»¶", 1402 edit: "ç¼è¾", 1403 effects: "ææ", 1404 analyze: "åæ", 1405 view: "è§å¾" 1406 }, 1407 file: { 1408 open: "æå¼é³é¢æä»¶", 1409 record: "å½å¶éº¦å é£", 1410 demo: "å è½½ç¤ºä¾æ²ç®", 1411 exportWav: "导åºä¸º WAV", 1412 exportMp3: "导åºä¸º MP3", 1413 exportSelection: "导åºéåº", 1414 sendToDigitizer: "åéå°é³é¢è½¬è°±å¨", 1415 sendToConverter: "åéå° MIDI 转æ¢å¨" 1416 }, 1417 edit: { 1418 undo: "æ¤é", 1419 redo: "éå", 1420 cut: "åªå", 1421 copy: "å¤å¶", 1422 paste: "ç²è´´", 1423 delete: "å é¤", 1424 crop: "è£åè³éåº", 1425 silence: "éé³éåº", 1426 insertSilence: "æå ¥éé³...", 1427 selectAll: "å ¨é", 1428 swapChannels: "交æ¢å·¦å³å£°é", 1429 stereoToMono: "ç«ä½å£°è½¬å声é", 1430 monoToStereo: "å声é转ç«ä½å£°", 1431 invertPhase: "å转ç¸ä½ (180°)", 1432 removeDc: "æ¶é¤ç´æµåç§»" 1433 }, 1434 view: { 1435 zoomIn: "æ¾å¤§", 1436 zoomOut: "缩å°", 1437 zoomFit: "éåºçªå£", 1438 zoomSelection: "缩æ¾è³éåº" 1439 }, 1440 effects: { 1441 title: "é³é¢ææå¨", 1442 normalize: "æ åå (Normalize)", 1443 amplify: "æ¾å¤§ / å¢ç", 1444 fadeIn: "æ·¡å ¥", 1445 fadeOut: "æ·¡åº", 1446 eq: "åæ°ååè¡¡å¨ (EQ)", 1447 compressor: "å缩å¨ä¸éå¹ å¨", 1448 reverb: "å½é³å®¤æ··å", 1449 delay: "ç«ä½å£°å»¶è¿ / å声", 1450 vocalRemover: "人声æ¶é¤ (ä¸ç½®å£°éæ¶é¤)", 1451 pitchTempo: "åéä¸åè°", 1452 reverse: "åæ¾ (Reverse)", 1453 saturation: "çµå管 / ç£å¸¦é¥±å", 1454 preview: "è¯å¬é¢è§", 1455 stopPreview: "忢è¯å¬", 1456 apply: "åºç¨ææ", 1457 cancel: "åæ¶" 1458 }, 1459 params: { 1460 targetDb: "ç®æ å³°å¼ (dBFS)",
1461 gainDb: "å¢ç (dB)", 1462 curve: "æ·¡åæ²çº¿", 1463 linear: "线æ§", 1464 logarithmic: "对æ°", 1465 exponential: "ææ°", 1466 sCurve: "Såæ²çº¿", 1467 threshold: "éå¼ (dB)", 1468 ratio: "åç¼©æ¯ (Ratio)", 1469 knee: "æç¹ (dB)", 1470 attack: "å¯å¨æ¶é´ (ç§)", 1471 release: "éæ¾æ¶é´ (ç§)", 1472 makeupGain: "è¡¥å¿å¢ç (dB)", 1473 roomSize: "空é´å¤§å°", 1474 damping: "é«é¢é»å°¼", 1475 wet: "æ¹¿ä¿¡å· (Wet)", 1476 dry: "å¹²ä¿¡å· (Dry)", 1477 delayTime: "å»¶è¿æ¶é´ (ç§)", 1478 feedback: "åé¦ç³»æ°", 1479 pingPong: "ä¹ä¹ç«ä½å£°", 1480 crossoverFreq: "ä½é¢ä¿çé¢ç (Hz)", 1481 pitchSemitones: "é³è°åé (åé³)", 1482 tempoMultiplier: "ææ¾é度 / èæ (åç)", 1483 drive: "饱å驱å¨é" 1484 }, 1485 analyze: { 1486 stats: "é³é¢ç»è®¡æ°æ®", 1487 spectrum: "FFT é¢è°±åæä»ª", 1488 duration: "æ¶é¿", 1489 sampleRate: "éæ ·ç", 1490 channels: "声鿰", 1491 samples: "æ»éæ ·ç¹æ°", 1492 peakL: "左声éå³°å¼ (dBFS)", 1493 peakR: "å³å£°éå³°å¼ (dBFS)", 1494 rmsL: "左声é RMS (dBFS)", 1495 rmsR: "å³å£°é RMS (dBFS)", 1496 dynamicRange: "卿èå´", 1497 dcOffsetL: "左声éç´æµåç§»", 1498 dcOffsetR: "å³å£°éç´æµåç§»", 1499 clippingDetected: "æ£æµå°åæ³¢è¿è½½ï¼", 1500 noClipping: "ä¿¡å·æ£å¸¸ï¼æªåçåæ³¢ã" 1501 }, 1502 transport: { 1503 play: "ææ¾ (ç©ºæ ¼)", 1504 pause: "æå (ç©ºæ ¼)", 1505 stop: "忢", 1506 loop: "循ç¯éåº (Shift+ç©ºæ ¼)", 1507 record: "å½é³", 1508 masterVolume: "æ»é³é", 1509 selectionStart: "èµ·å§", 1510 selectionEnd: "ç»æ", 1511 selectionLength: "éåºé¿åº¦", 1512 cursor: "å æ ä½ç½®" 1513 }, 1514 exportModal: { 1515 title: "导åºé³é¢æä»¶", 1516 format: "è¾åºæ ¼å¼", 1517 bitDepth: "éæ ·æ·±åº¦", 1518 bitrate: "MP3 æ¯ç¹ç", 1519 range: "导åºèå´", 1520 fullTrack: "æ´é¦é³è½¨", 1521 selectionOnly: "ä» å½åéåº", 1522 fileName: "æä»¶å", 1523 exportBtn: "导åºå¹¶ä¸è½½", 1524 processing: "æ£å¨æµè§å¨ç«¯ç¼ç å¤ç..." 1525 }, 1526 recordModal: { 1527 title: "麦å é£å½é³", 1528 prompt: "ç¹å»å¼å§å¨æµè§å¨ä¸ç´æ¥å½å¶é³é¢ã", 1529 start: "å¼å§å½å¶", 1530 stop: "忢å½å¶", 1531 insert: "æå ¥å½åé³è½¨", 1532 replace: "æ¿æ¢æ´é¦é³è½¨" 1533 }, 1534 silenceModal: { 1535 title: "æå ¥éé³", 1536 prompt: "å¨å½åå æ å¤æå ¥é鳿¶é¿ï¼", 1537 seconds: "ç§", 1538 insert: "确认æå ¥" 1539 } 1540}; 1541const zhTW = { 1542 ...zhCN, 1543 app: { 1544 ...zhCN.app, 1545 subtitle: "å°æ¥ç覽å¨é³è¨æ³¢å½¢ç·¨è¼¯å¨", 1546 zeroServerNotice: "100% ç´æ¬æ©å®¢æ¶ç«¯èçã䏿¶è伺æå¨è³æºï¼ä¿éé±ç§ã", 1547 loadPrompt: "ææ¾é³è¨æªæ¡è³æ¤èï¼æé»æéå" 1548 }, 1549 menu: { 1550 file: "æªæ¡", 1551 edit: "編輯", 1552 effects: "ææ", 1553 analyze: "åæ", 1554 view: "檢è¦" 1555 }, 1556 file: { 1557 ...zhCN.file, 1558 open: "éåé³è¨æªæ¡", 1559 record: "é製麥å 風", 1560 demo: "è¼å ¥ç¯ä¾æ²ç®", 1561 exportWav: "å¯åºçº WAV", 1562 exportMp3: "å¯åºçº MP3", 1563 exportSelection: "å¯åºé¸å", 1564 sendToDigitizer: "å³éè³é³è¨è½èå¨", 1565 sendToConverter: "å³éè³ MIDI è½æå¨" 1566 }, 1567 edit: { 1568 ...zhCN.edit, 1569 undo: "復å", 1570 redo: "éå", 1571 cut: "åªä¸", 1572 copy: "è¤è£½", 1573 paste: "è²¼ä¸", 1574 crop: "è£åè³é¸å", 1575 silence: "éé³é¸å", 1576 insertSilence: "æå ¥éé³...", 1577 selectAll: "å ¨é¸" 1578 }, 1579 exportModal: { 1580 ...zhCN.exportModal, 1581 title: "å¯åºé³è¨æªæ¡", 1582 exportBtn: "å¯åºä¸¦ä¸è¼" 1583 } 1584}; 1585const es = { 1586 app: { 1587 title: "Hamie AudioLab", 1588 subtitle: "Editor Profesional de Ondas de Audio en el Navegador", 1589 zeroServerNotice: "100% del lado del cliente. Cero recursos del servidor. Máxima privacidad.", 1590 loadPrompt: "Arrastra un archivo de audio aquÃ, o haz clic para abrir", 1591 supportedFormats: "Compatible con WAV, MP3, OGG, FLAC, M4A, AAC, Opus, WebM" 1592 }, 1593 menu: { 1594 file: "Archivo", 1595 edit: "Editar", 1596 effects: "Efectos", 1597 analyze: "Analizar", 1598 view: "Ver" 1599 }, 1600 file: { 1601 open: "Abrir Archivo de Audio", 1602 record: "Grabar Micrófono", 1603 demo: "Cargar Pista de Demostración", 1604 exportWav: "Exportar como WAV", 1605 exportMp3: "Exportar como MP3", 1606 exportSelection: "Exportar Selección", 1607 sendToDigitizer: "Enviar a Transcriptor de Audio",
1608 sendToConverter: "Enviar a Convertidor MIDI" 1609 }, 1610 edit: { 1611 undo: "Deshacer", 1612 redo: "Rehacer", 1613 cut: "Cortar", 1614 copy: "Copiar", 1615 paste: "Pegar", 1616 delete: "Eliminar", 1617 crop: "Recortar a Selección", 1618 silence: "Silenciar Selección", 1619 insertSilence: "Insertar Silencio...", 1620 selectAll: "Seleccionar Todo", 1621 swapChannels: "Intercambiar Canales (I/D)", 1622 stereoToMono: "Mezclar Estéreo a Mono", 1623 monoToStereo: "Mono a Estéreo", 1624 invertPhase: "Invertir Fase (180°)", 1625 removeDc: "Eliminar Desplazamiento DC" 1626 }, 1627 view: { 1628 zoomIn: "Acercar", 1629 zoomOut: "Alejar", 1630 zoomFit: "Ajustar a la Ventana", 1631 zoomSelection: "Zoom a Selección" 1632 }, 1633 effects: { 1634 title: "Efectos de Audio", 1635 normalize: "Normalizar", 1636 amplify: "Amplificar / Ganancia", 1637 fadeIn: "Aparecer (Fade In)", 1638 fadeOut: "Desvanecer (Fade Out)", 1639 eq: "Ecualizador Paramétrico", 1640 compressor: "Compresor y Limitador", 1641 reverb: "Reverberación de Estudio", 1642 delay: "Retardo Estéreo / Eco", 1643 vocalRemover: "Eliminador de Voz (Centro)", 1644 pitchTempo: "Tono y Estiramiento Temporal", 1645 reverse: "Invertir Audio (Reversa)", 1646 saturation: "Saturación Valvular", 1647 preview: "Audición Previa", 1648 stopPreview: "Detener Previa", 1649 apply: "Aplicar Efecto", 1650 cancel: "Cancelar" 1651 }, 1652 params: { 1653 targetDb: "Pico Objetivo (dBFS)", 1654 gainDb: "Ganancia (dB)", 1655 curve: "Curva de Fundido", 1656 linear: "Lineal", 1657 logarithmic: "LogarÃtmica", 1658 exponential: "Exponencial", 1659 sCurve: "Curva en S", 1660 threshold: "Umbral (dB)", 1661 ratio: "Proporción (Ratio)", 1662 knee: "Codo (dB)", 1663 attack: "Ataque (s)", 1664 release: "Liberación (s)", 1665 makeupGain: "Ganancia de Compensación (dB)", 1666 roomSize: "Tamaño de Sala", 1667 damping: "Amortiguación", 1668 wet: "Mezcla Señal Procesada (Wet)", 1669 dry: "Mezcla Señal Directa (Dry)", 1670 delayTime: "Tiempo de Retardo (s)", 1671 feedback: "Retroalimentación", 1672 pingPong: "Ping-Pong Estéreo", 1673 crossoverFreq: "Cruce de Graves (Hz)", 1674 pitchSemitones: "Desplazamiento de Tono (Semitonos)", 1675 tempoMultiplier: "Velocidad / Tempo (x)", 1676 drive: "Cantidad de Saturación" 1677 }, 1678 analyze: { 1679 stats: "EstadÃsticas de Audio", 1680 spectrum: "Analizador de Espectro FFT", 1681 duration: "Duración", 1682 sampleRate: "Frecuencia de Muestreo", 1683 channels: "Canales", 1684 samples: "Muestras Totales", 1685 peakL: "Pico Izquierdo (dBFS)", 1686 peakR: "Pico Derecho (dBFS)", 1687 rmsL: "RMS Izquierdo (dBFS)", 1688 rmsR: "RMS Derecho (dBFS)", 1689 dynamicRange: "Rango Dinámico", 1690 dcOffsetL: "Desplazamiento DC Izq.", 1691 dcOffsetR: "Desplazamiento DC Der.", 1692 clippingDetected: "¡Saturación (Clipping) detectada!", 1693 noClipping: "Señal limpia sin saturación." 1694 }, 1695 transport: { 1696 play: "Reproducir (Espacio)", 1697 pause: "Pausar (Espacio)", 1698 stop: "Detener", 1699 loop: "Bucle en Selección (Shift+Espacio)", 1700 record: "Grabar Micrófono", 1701 masterVolume: "Volumen Maestro", 1702 selectionStart: "Inicio", 1703 selectionEnd: "Fin", 1704 selectionLength: "Longitud", 1705 cursor: "Cursor" 1706 }, 1707 exportModal: { 1708 title: "Exportar Archivo de Audio", 1709 format: "Formato de Salida", 1710 bitDepth: "Profundidad de Bits", 1711 bitrate: "Tasa de Bits MP3", 1712 range: "Rango de Exportación", 1713 fullTrack: "Pista Completa", 1714 selectionOnly: "Solo Región Seleccionada", 1715 fileName: "Nombre de Archivo", 1716 exportBtn: "Exportar y Descargar", 1717 processing: "Codificando en el navegador..." 1718 }, 1719 recordModal: { 1720 title: "Grabar desde Micrófono", 1721 prompt: "Haz clic en Iniciar para grabar audio directamente.", 1722 start: "Iniciar Grabación", 1723 stop: "Detener Grabación", 1724 insert: "Insertar en la Pista", 1725 replace: "Reemplazar Pista Completa" 1726 }, 1727 silenceModal: { 1728 title: "Insertar Silencio", 1729 prompt: "Ingresa la duración del silencio a insertar en la posición actual:", 1730 seconds: "Segundos", 1731 insert: "Insertar Silencio" 1732 } 1733}; 1734const fr = { 1735 ...en, 1736 app: { 1737 ...en.app, 1738 title: "Hamie AudioLab", 1739 subtitle: "Ãditeur de Formes dâOnde Audio Professionnel sur Navigateur", 1740 zeroServerNotice: "100% côté client. Zéro ressource serveur requise. Confidentialité totale.", 1741 loadPrompt: "Déposez un fichier audio ici, ou cliquez pour ouvrir" 1742 }, 1743 menu: { 1744 file: "Fichier", 1745 edit: "Ãdition", 1746 effects: "Effets", 1747 analyze: "Analyser", 1748 view: "Affichage" 1749 }, 1750 file: { 1751 ...en.file, 1752 open: "Ouvrir un Fichier Audio", 1753 record: "Enregistrer au Microphone", 1754 demo: "Charger le Morceau Démo", 1755 exportWav: "Exporter en WAV", 1756 exportMp3: "Exporter en MP3", 1757 exportSelection: "Exporter la Sélection" 1758 }, 1759 edit: { 1760 ...en.edit, 1761 undo: "Annuler", 1762 redo: "Rétablir", 1763 cut: "Couper", 1764 copy: "Copier", 1765 paste: "Coller", 1766 delete: "Supprimer", 1767 crop: "Rogner selon la Sélection",
1768 silence: "Rendre la Sélection Silencieuse", 1769 insertSilence: "Insérer du Silence...", 1770 selectAll: "Tout Sélectionner" 1771 } 1772}; 1773const de = { 1774 ...en, 1775 app: { 1776 ...en.app, 1777 title: "Hamie AudioLab", 1778 subtitle: "Professioneller Audio-Wellenform-Editor im Browser", 1779 zeroServerNotice: "100% clientseitig. Keine Serverressourcen genutzt. Vollständige Privatsphäre.", 1780 loadPrompt: "Audiodatei hier ablegen oder zum Ãffnen klicken" 1781 }, 1782 menu: { 1783 file: "Datei", 1784 edit: "Bearbeiten", 1785 effects: "Effekte", 1786 analyze: "Analysieren", 1787 view: "Ansicht" 1788 }, 1789 file: { 1790 ...en.file, 1791 open: "Audiodatei öffnen", 1792 record: "Mikrofon aufnehmen", 1793 demo: "Demo-Track laden", 1794 exportWav: "Als WAV exportieren", 1795 exportMp3: "Als MP3 exportieren", 1796 exportSelection: "Auswahl exportieren" 1797 }, 1798 edit: { 1799 ...en.edit, 1800 undo: "Rückgängig", 1801 redo: "Wiederholen", 1802 cut: "Ausschneiden", 1803 copy: "Kopieren", 1804 paste: "Einfügen", 1805 delete: "Löschen", 1806 crop: "Auf Auswahl zuschneiden", 1807 silence: "Auswahl stummschalten", 1808 insertSilence: "Stille einfügen...", 1809 selectAll: "Alles auswählen" 1810 } 1811}; 1812const it = { 1813 ...en, 1814 app: { 1815 ...en.app, 1816 title: "Hamie AudioLab", 1817 subtitle: "Editor Professionale di Forme dâOnda Audio nel Browser", 1818 zeroServerNotice: "100% lato client. Zero risorse server utilizzate. Massima riservatezza." 1819 }, 1820 menu: { 1821 file: "File", 1822 edit: "Modifica", 1823 effects: "Effetti", 1824 analyze: "Analisi", 1825 view: "Visualizza" 1826 } 1827}; 1828const pt = { 1829 ...en, 1830 app: { 1831 ...en.app, 1832 title: "Hamie AudioLab", 1833 subtitle: "Editor Profissional de Formas de Onda no Navegador", 1834 zeroServerNotice: "100% no cliente. Zero recursos do servidor. Privacidade total." 1835 }, 1836 menu: { 1837 file: "Arquivo", 1838 edit: "Editar", 1839 effects: "Efeitos", 1840 analyze: "Análise", 1841 view: "Exibir" 1842 } 1843}; 1844const ru = { 1845 ...en, 1846 app: { 1847 ...en.app, 1848 title: "Hamie AudioLab", 1849 subtitle: "ÐÑоÑеÑÑионалÑнÑй аÑдиоÑедакÑÐ¾Ñ ÑоÑÐ¼Ñ Ð²Ð¾Ð»Ð½Ñ Ð² бÑаÑзеÑе", 1850 zeroServerNotice: "100% на ÑÑоÑоне клиенÑа. ÐÐ¸ÐºÐ°ÐºÐ¸Ñ ÑеÑÑÑÑов ÑеÑвеÑа. ÐÐ¾Ð»Ð½Ð°Ñ ÐºÐ¾Ð½ÑиденÑиалÑноÑÑÑ." 1851 }, 1852 menu: { 1853 file: "Файл", 1854 edit: "ÐÑавка", 1855 effects: "ÐÑÑекÑÑ", 1856 analyze: "Ðнализ", 1857 view: "Ðид" 1858 } 1859}; 1860const ko = { 1861 ...en, 1862 app: { 1863 ...en.app, 1864 title: "Hamie AudioLab", 1865 subtitle: "ë¸ë¼ì°ì ì ì© ì 문ê°ì© ì¤ëì¤ íí í¸ì§ê¸°", 1866 zeroServerNotice: "100% í´ë¼ì´ì¸í¸ ì²ë¦¬. ìë² ë¦¬ìì¤ ì¬ì©ë ì ë¡ ë° ê°ì¸ì ë³´ ìë²½ ë³´í¸." 1867 }, 1868 menu: { 1869 file: "íì¼", 1870 edit: "í¸ì§", 1871 effects: "í¨ê³¼", 1872 analyze: "ë¶ì", 1873 view: "보기" 1874 } 1875}; 1876const ar = { 1877 ...en, 1878 app: { 1879 ...en.app, 1880 title: "Hamie AudioLab", 1881 subtitle: "Ù ØØ±Ø± Ø´Ù٠اÙÙ ÙØ¬Ø© Ø§ÙØµÙØªÙØ© Ø§ÙØ§ØØªØ±Ø§Ù٠داخ٠اÙÙ ØªØµÙØ", 1882 zeroServerNotice: "Ù Ø¹Ø§ÙØ¬Ø© Ø¨ÙØ³Ø¨Ø© 100Ùª عÙÙ Ø¬ÙØ§Ø²Ù. ÙØ§ تستÙÙÙ Ù ÙØ§Ø±Ø¯ Ø§ÙØ®Ø§Ø¯Ù . Ø®ØµÙØµÙØ© تا٠ة." 1883 }, 1884 menu: { 1885 file: "Ù ÙÙ", 1886 edit: "تعدÙÙ", 1887 effects: "ØªØ£Ø«ÙØ±Ø§Øª", 1888 analyze: "تØÙÙÙ", 1889 view: "عرض" 1890 } 1891}; 1892const id = { 1893 ...en, 1894 app: { 1895 ...en.app, 1896 title: "Hamie AudioLab", 1897 subtitle: "Editor Bentuk Gelombang Audio Profesional di Browser", 1898 zeroServerNotice: "100% di browser. Tanpa beban server. Privasi terjamin." 1899 }, 1900 menu: { 1901 file: "Berkas", 1902 edit: "Edit", 1903 effects: "Efek", 1904 analyze: "Analisis", 1905 view: "Tampilan" 1906 } 1907}; 1908const dictionaries = { 1909 en, 1910 ja, 1911 "zh-CN": zhCN, 1912 "zh-TW": zhTW, 1913 zh: zhCN, 1914 es, 1915 fr, 1916 de, 1917 it, 1918 pt, 1919 ru, 1920 ko, 1921 ar, 1922 id 1923}; 1924function useAudioEditorTranslation() { 1925 const [lang, setLang] = reactExports.useState(() => { 1926 const current = instance.language || "en"; 1927 if (current.startsWith("zh-TW") || current.startsWith("zh-Hant")) return "zh-TW"; 1928 if (current.startsWith("zh")) return "zh-CN"; 1929 const base = current.split("-")[0]; 1930 return dictionaries[base] ? base : "en"; 1931 }); 1932 reactExports.useEffect(() => { 1933 const handleLangChange = (newLang) => { 1934 let code = "en"; 1935 if (newLang.startsWith("zh-TW") || newLang.startsWith("zh-Hant")) code = "zh-TW"; 1936 else if (newLang.startsWith("zh")) code = "zh-CN"; 1937 else { 1938 const base = newLang.split("-")[0]; 1939 if (dictionaries[base]) code = base; 1940 } 1941 setLang(code); 1942 }; 1943 instance.on("languageChanged", handleLangChange); 1944 return () => { 1945 instance.off("languageChanged", handleLangChange); 1946 }; 1947 }, []); 1948 const t = reactExports.useCallback((path) => {
1949 const keys = path.split("."); 1950 let current = dictionaries[lang] || dictionaries.en; 1951 for (const key of keys) { 1952 if (current && typeof current === "object" && key in current) { 1953 current = current[key]; 1954 } else { 1955 let fallback = dictionaries.en; 1956 for (const fKey of keys) { 1957 if (fallback && typeof fallback === "object" && fKey in fallback) { 1958 fallback = fallback[fKey]; 1959 } else { 1960 return path; 1961 } 1962 } 1963 return fallback; 1964 } 1965 } 1966 return typeof current === "string" ? current : path; 1967 }, [lang]); 1968 return { t, lang }; 1969} 1970const EditorMenuBar = ({ onNavigate }) => { 1971 const { 1972 audioBuffer, 1973 fileName, 1974 selection, 1975 canUndo, 1976 canRedo, 1977 undo, 1978 redo, 1979 cut, 1980 copy, 1981 paste, 1982 deleteSelection, 1983 cropSelection, 1984 silenceSelection, 1985 selectAll, 1986 swapChannels, 1987 stereoToMono, 1988 monoToStereo, 1989 invertPhase, 1990 removeDc, 1991 zoomIn, 1992 zoomOut, 1993 zoomFit, 1994 zoomToSelection, 1995 loadAudioFile, 1996 loadDemo, 1997 setActiveModal, 1998 setActiveEffect, 1999 applyDSP 2000 } = useAudioEditor(); 2001 const { t } = useAudioEditorTranslation(); 2002 const [openMenu, setOpenMenu] = reactExports.useState(null); 2003 const fileInputRef = reactExports.useRef(null); 2004 const menuBarRef = reactExports.useRef(null); 2005 reactExports.useEffect(() => { 2006 const handleClickOutside = (e) => { 2007 if (menuBarRef.current && !menuBarRef.current.contains(e.target)) { 2008 setOpenMenu(null); 2009 } 2010 }; 2011 document.addEventListener("mousedown", handleClickOutside); 2012 return () => document.removeEventListener("mousedown", handleClickOutside); 2013 }, []); 2014 const handleOpenClick = () => { 2015 var _a; 2016 (_a = fileInputRef.current) == null ? void 0 : _a.click(); 2017 setOpenMenu(null); 2018 }; 2019 const handleFileChange = (e) => { 2020 var _a; 2021 const file = (_a = e.target.files) == null ? void 0 : _a[0]; 2022 if (file) { 2023 loadAudioFile(file); 2024 } 2025 }; 2026 const openEffect = (effectKey) => { 2027 setActiveEffect(effectKey); 2028 setActiveModal("effects"); 2029 setOpenMenu(null); 2030 }; 2031 const hasSelection = !!(selection && selection.end > selection.start); 2032 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 2033 "div", 2034 { 2035 ref: menuBarRef, 2036 className: "flex-none h-12 bg-zinc-900 border-b border-zinc-800 px-3 flex items-center justify-between select-none z-30 relative", 2037 children: [
2038 /* @__PURE__ */ jsxRuntimeExports.jsx( 2039 "input", 2040 { 2041 type: "file", 2042 ref: fileInputRef, 2043 onChange: handleFileChange, 2044 accept: "audio/*,.wav,.mp3,.ogg,.flac,.m4a,.aac,.opus,.webm", 2045 className: "hidden" 2046 } 2047 ), 2048 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 2049 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2050 "button", 2051 { 2052 onClick: () => onNavigate ? onNavigate("lab") : window.history.back(), 2053 className: "flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-zinc-800/80 hover:bg-zinc-700 text-zinc-300 text-xs font-medium transition-colors mr-2", 2054 title: "Return to Hamie Lab", 2055 children: [ 2056 /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeft, { className: "w-3.5 h-3.5" }), 2057 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: "Lab" }) 2058 ] 2059 } 2060 ),
2061 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 mr-3 pr-3 border-r border-zinc-800", children: [ 2062 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-6 h-6 rounded-md bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center shadow-sm", children: /* @__PURE__ */ jsxRuntimeExports.jsx(SlidersVertical, { className: "w-3.5 h-3.5 text-white" }) }), 2063 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-sm text-white tracking-tight hidden md:inline", children: "AudioLab" }) 2064 ] }), 2065 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [ 2066 /* @__PURE__ */ jsxRuntimeExports.jsx( 2067 "button", 2068 { 2069 onClick: () => setOpenMenu(openMenu === "file" ? null : "file"), 2070 className: `px-2.5 py-1 rounded text-xs font-medium transition-colors ${openMenu === "file" ? "bg-zinc-800 text-white" : "text-zinc-300 hover:bg-zinc-800/60"}`, 2071 children: t("menu.file") 2072 } 2073 ), 2074 openMenu === "file" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute left-0 mt-1 w-64 bg-zinc-900 border border-zinc-800 rounded-xl shadow-2xl py-1 text-xs text-zinc-200 z-50", children: [
2075 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2076 "button", 2077 { 2078 onClick: handleOpenClick, 2079 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 flex items-center gap-2.5", 2080 children: [ 2081 /* @__PURE__ */ jsxRuntimeExports.jsx(FolderOpen, { className: "w-4 h-4 text-indigo-400" }), 2082 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1", children: t("file.open") }), 2083 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 font-mono", children: "Ctrl+O" }) 2084 ] 2085 } 2086 ), 2087 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2088 "button", 2089 { 2090 onClick: () => { 2091 setActiveModal("record"); 2092 setOpenMenu(null); 2093 }, 2094 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 flex items-center gap-2.5", 2095 children: [
2096 /* @__PURE__ */ jsxRuntimeExports.jsx(Mic, { className: "w-4 h-4 text-red-400" }), 2097 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("file.record") }) 2098 ] 2099 } 2100 ), 2101 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2102 "button", 2103 { 2104 onClick: () => { 2105 loadDemo(); 2106 setOpenMenu(null); 2107 }, 2108 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 flex items-center gap-2.5", 2109 children: [ 2110 /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { className: "w-4 h-4 text-emerald-400" }), 2111 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("file.demo") }) 2112 ] 2113 } 2114 ), 2115 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-px bg-zinc-800 my-1" }), 2116 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2117 "button", 2118 { 2119 onClick: () => {
2120 setActiveModal("export"); 2121 setOpenMenu(null); 2122 }, 2123 disabled: !audioBuffer, 2124 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none flex items-center gap-2.5", 2125 children: [ 2126 /* @__PURE__ */ jsxRuntimeExports.jsx(Download, { className: "w-4 h-4 text-indigo-400" }), 2127 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 2128 t("file.exportWav"), 2129 " / MP3" 2130 ] }) 2131 ] 2132 } 2133 ), 2134 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-px bg-zinc-800 my-1" }), 2135 onNavigate && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 2136 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2137 "button", 2138 { 2139 onClick: () => { 2140 onNavigate("client-transcriber"); 2141 setOpenMenu(null); 2142 }, 2143 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 flex items-center gap-2.5 text-zinc-400 hover:text-zinc-200", 2144 children: [
2145 /* @__PURE__ */ jsxRuntimeExports.jsx(Share2, { className: "w-4 h-4 text-purple-400" }), 2146 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("file.sendToDigitizer") }) 2147 ] 2148 } 2149 ), 2150 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2151 "button", 2152 { 2153 onClick: () => { 2154 onNavigate("converter"); 2155 setOpenMenu(null); 2156 }, 2157 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 flex items-center gap-2.5 text-zinc-400 hover:text-zinc-200", 2158 children: [ 2159 /* @__PURE__ */ jsxRuntimeExports.jsx(Share2, { className: "w-4 h-4 text-emerald-400" }), 2160 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("file.sendToConverter") }) 2161 ] 2162 } 2163 ) 2164 ] }) 2165 ] }) 2166 ] }),
2167 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [ 2168 /* @__PURE__ */ jsxRuntimeExports.jsx( 2169 "button", 2170 { 2171 onClick: () => setOpenMenu(openMenu === "edit" ? null : "edit"), 2172 className: `px-2.5 py-1 rounded text-xs font-medium transition-colors ${openMenu === "edit" ? "bg-zinc-800 text-white" : "text-zinc-300 hover:bg-zinc-800/60"}`, 2173 children: t("menu.edit") 2174 } 2175 ), 2176 openMenu === "edit" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute left-0 mt-1 w-64 bg-zinc-900 border border-zinc-800 rounded-xl shadow-2xl py-1 text-xs text-zinc-200 z-50", children: [ 2177 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2178 "button", 2179 { 2180 onClick: () => { 2181 undo(); 2182 setOpenMenu(null); 2183 }, 2184 disabled: !canUndo, 2185 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none flex items-center gap-2.5", 2186 children: [
2187 /* @__PURE__ */ jsxRuntimeExports.jsx(Undo2, { className: "w-4 h-4 text-zinc-400" }), 2188 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1", children: t("edit.undo") }), 2189 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 font-mono", children: "Ctrl+Z" }) 2190 ] 2191 } 2192 ), 2193 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2194 "button", 2195 { 2196 onClick: () => { 2197 redo(); 2198 setOpenMenu(null); 2199 }, 2200 disabled: !canRedo, 2201 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none flex items-center gap-2.5", 2202 children: [ 2203 /* @__PURE__ */ jsxRuntimeExports.jsx(Redo2, { className: "w-4 h-4 text-zinc-400" }), 2204 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1", children: t("edit.redo") }),
2205 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 font-mono", children: "Ctrl+Y" }) 2206 ] 2207 } 2208 ), 2209 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-px bg-zinc-800 my-1" }), 2210 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2211 "button", 2212 { 2213 onClick: () => { 2214 cut(); 2215 setOpenMenu(null); 2216 }, 2217 disabled: !hasSelection, 2218 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none flex items-center gap-2.5", 2219 children: [ 2220 /* @__PURE__ */ jsxRuntimeExports.jsx(Scissors, { className: "w-4 h-4 text-indigo-400" }), 2221 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1", children: t("edit.cut") }), 2222 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 font-mono", children: "Ctrl+X" }) 2223 ] 2224 } 2225 ),
2226 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2227 "button", 2228 { 2229 onClick: () => { 2230 copy(); 2231 setOpenMenu(null); 2232 }, 2233 disabled: !hasSelection, 2234 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none flex items-center gap-2.5", 2235 children: [ 2236 /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { className: "w-4 h-4 text-indigo-400" }), 2237 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1", children: t("edit.copy") }), 2238 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 font-mono", children: "Ctrl+C" }) 2239 ] 2240 } 2241 ), 2242 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2243 "button", 2244 { 2245 onClick: () => { 2246 paste(); 2247 setOpenMenu(null); 2248 }, 2249 disabled: !audioBuffer, 2250 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none flex items-center gap-2.5", 2251 children: [
2252 /* @__PURE__ */ jsxRuntimeExports.jsx(ClipboardPaste, { className: "w-4 h-4 text-indigo-400" }), 2253 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1", children: t("edit.paste") }), 2254 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 font-mono", children: "Ctrl+V" }) 2255 ] 2256 } 2257 ), 2258 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2259 "button", 2260 { 2261 onClick: () => { 2262 deleteSelection(); 2263 setOpenMenu(null); 2264 }, 2265 disabled: !hasSelection, 2266 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none flex items-center gap-2.5", 2267 children: [ 2268 /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "w-4 h-4 text-red-400" }), 2269 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1", children: t("edit.delete") }),
2270 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 font-mono", children: "Del" }) 2271 ] 2272 } 2273 ), 2274 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2275 "button", 2276 { 2277 onClick: () => { 2278 cropSelection(); 2279 setOpenMenu(null); 2280 }, 2281 disabled: !hasSelection, 2282 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none flex items-center gap-2.5", 2283 children: [ 2284 /* @__PURE__ */ jsxRuntimeExports.jsx(Crop, { className: "w-4 h-4 text-emerald-400" }), 2285 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1", children: t("edit.crop") }), 2286 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 font-mono", children: "Ctrl+T" }) 2287 ] 2288 } 2289 ),
2290 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-px bg-zinc-800 my-1" }), 2291 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2292 "button", 2293 { 2294 onClick: () => { 2295 silenceSelection(); 2296 setOpenMenu(null); 2297 }, 2298 disabled: !hasSelection, 2299 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none flex items-center gap-2.5", 2300 children: [ 2301 /* @__PURE__ */ jsxRuntimeExports.jsx(VolumeX, { className: "w-4 h-4 text-zinc-400" }), 2302 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("edit.silence") }) 2303 ] 2304 } 2305 ), 2306 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2307 "button", 2308 { 2309 onClick: () => { 2310 setActiveModal("silence"); 2311 setOpenMenu(null); 2312 }, 2313 disabled: !audioBuffer, 2314 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none flex items-center gap-2.5", 2315 children: [
2316 /* @__PURE__ */ jsxRuntimeExports.jsx(CirclePlus, { className: "w-4 h-4 text-zinc-400" }), 2317 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("edit.insertSilence") }) 2318 ] 2319 } 2320 ), 2321 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2322 "button", 2323 { 2324 onClick: () => { 2325 selectAll(); 2326 setOpenMenu(null); 2327 }, 2328 disabled: !audioBuffer, 2329 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none flex items-center gap-2.5", 2330 children: [ 2331 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1", children: t("edit.selectAll") }), 2332 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 font-mono", children: "Ctrl+A" }) 2333 ] 2334 } 2335 ),
2336 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-px bg-zinc-800 my-1" }), 2337 /* @__PURE__ */ jsxRuntimeExports.jsx( 2338 "button", 2339 { 2340 onClick: () => { 2341 swapChannels(); 2342 setOpenMenu(null); 2343 }, 2344 disabled: !audioBuffer || audioBuffer.numberOfChannels < 2, 2345 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none", 2346 children: t("edit.swapChannels") 2347 } 2348 ), 2349 /* @__PURE__ */ jsxRuntimeExports.jsx( 2350 "button", 2351 { 2352 onClick: () => { 2353 stereoToMono(); 2354 setOpenMenu(null); 2355 }, 2356 disabled: !audioBuffer || audioBuffer.numberOfChannels < 2, 2357 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none", 2358 children: t("edit.stereoToMono") 2359 } 2360 ),
2361 /* @__PURE__ */ jsxRuntimeExports.jsx( 2362 "button", 2363 { 2364 onClick: () => { 2365 monoToStereo(); 2366 setOpenMenu(null); 2367 }, 2368 disabled: !audioBuffer || audioBuffer.numberOfChannels >= 2, 2369 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none", 2370 children: t("edit.monoToStereo") 2371 } 2372 ), 2373 /* @__PURE__ */ jsxRuntimeExports.jsx( 2374 "button", 2375 { 2376 onClick: () => { 2377 invertPhase(); 2378 setOpenMenu(null); 2379 }, 2380 disabled: !audioBuffer, 2381 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none", 2382 children: t("edit.invertPhase") 2383 } 2384 ), 2385 /* @__PURE__ */ jsxRuntimeExports.jsx( 2386 "button", 2387 { 2388 onClick: () => { 2389 removeDc(); 2390 setOpenMenu(null); 2391 }, 2392 disabled: !audioBuffer, 2393 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none", 2394 children: t("edit.removeDc") 2395 } 2396 ) 2397 ] }) 2398 ] }),
2399 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [ 2400 /* @__PURE__ */ jsxRuntimeExports.jsx( 2401 "button", 2402 { 2403 onClick: () => setOpenMenu(openMenu === "effects" ? null : "effects"), 2404 className: `px-2.5 py-1 rounded text-xs font-medium transition-colors ${openMenu === "effects" ? "bg-zinc-800 text-white" : "text-zinc-300 hover:bg-zinc-800/60"}`, 2405 children: t("menu.effects") 2406 } 2407 ), 2408 openMenu === "effects" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute left-0 mt-1 w-64 bg-zinc-900 border border-zinc-800 rounded-xl shadow-2xl py-1 text-xs text-zinc-200 z-50", children: [ 2409 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2410 "button", 2411 { 2412 onClick: () => openEffect("normalize"), 2413 disabled: !audioBuffer, 2414 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none", 2415 children: [ 2416 t("effects.normalize"), 2417 "..." 2418 ] 2419 } 2420 ),
2421 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2422 "button", 2423 { 2424 onClick: () => openEffect("amplify"), 2425 disabled: !audioBuffer, 2426 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none", 2427 children: [ 2428 t("effects.amplify"), 2429 "..." 2430 ] 2431 } 2432 ), 2433 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2434 "button", 2435 { 2436 onClick: () => openEffect("fadeIn"), 2437 disabled: !audioBuffer || !hasSelection, 2438 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none", 2439 children: [ 2440 t("effects.fadeIn"), 2441 "..." 2442 ] 2443 } 2444 ), 2445 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2446 "button", 2447 { 2448 onClick: () => openEffect("fadeOut"), 2449 disabled: !audioBuffer || !hasSelection, 2450 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none", 2451 children: [ 2452 t("effects.fadeOut"), 2453 "..." 2454 ] 2455 } 2456 ),
2457 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-px bg-zinc-800 my-1" }), 2458 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2459 "button", 2460 { 2461 onClick: () => openEffect("eq"), 2462 disabled: !audioBuffer, 2463 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none", 2464 children: [ 2465 t("effects.eq"), 2466 "..." 2467 ] 2468 } 2469 ), 2470 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2471 "button", 2472 { 2473 onClick: () => openEffect("compressor"), 2474 disabled: !audioBuffer, 2475 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none", 2476 children: [ 2477 t("effects.compressor"), 2478 "..." 2479 ] 2480 } 2481 ),
2482 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2483 "button", 2484 { 2485 onClick: () => openEffect("reverb"), 2486 disabled: !audioBuffer, 2487 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none", 2488 children: [ 2489 t("effects.reverb"), 2490 "..." 2491 ] 2492 } 2493 ), 2494 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2495 "button", 2496 { 2497 onClick: () => openEffect("delay"), 2498 disabled: !audioBuffer, 2499 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none", 2500 children: [ 2501 t("effects.delay"), 2502 "..." 2503 ] 2504 } 2505 ), 2506 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2507 "button", 2508 { 2509 onClick: () => openEffect("vocalRemover"), 2510 disabled: !audioBuffer || audioBuffer.numberOfChannels < 2, 2511 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none", 2512 children: [ 2513 t("effects.vocalRemover"), 2514 "..." 2515 ] 2516 } 2517 ),
2518 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2519 "button", 2520 { 2521 onClick: () => openEffect("pitchTempo"), 2522 disabled: !audioBuffer, 2523 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none", 2524 children: [ 2525 t("effects.pitchTempo"), 2526 "..." 2527 ] 2528 } 2529 ), 2530 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-px bg-zinc-800 my-1" }), 2531 /* @__PURE__ */ jsxRuntimeExports.jsx( 2532 "button", 2533 { 2534 onClick: () => openEffect("reverse"), 2535 disabled: !audioBuffer, 2536 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none", 2537 children: t("effects.reverse") 2538 } 2539 ), 2540 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2541 "button", 2542 { 2543 onClick: () => openEffect("saturation"), 2544 disabled: !audioBuffer, 2545 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none", 2546 children: [ 2547 t("effects.saturation"), 2548 "..." 2549 ] 2550 } 2551 ) 2552 ] }) 2553 ] }),
2554 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [ 2555 /* @__PURE__ */ jsxRuntimeExports.jsx( 2556 "button", 2557 { 2558 onClick: () => setOpenMenu(openMenu === "analyze" ? null : "analyze"), 2559 className: `px-2.5 py-1 rounded text-xs font-medium transition-colors ${openMenu === "analyze" ? "bg-zinc-800 text-white" : "text-zinc-300 hover:bg-zinc-800/60"}`, 2560 children: t("menu.analyze") 2561 } 2562 ), 2563 openMenu === "analyze" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute left-0 mt-1 w-56 bg-zinc-900 border border-zinc-800 rounded-xl shadow-2xl py-1 text-xs text-zinc-200 z-50", children: [ 2564 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2565 "button", 2566 { 2567 onClick: () => { 2568 setActiveModal("stats"); 2569 setOpenMenu(null); 2570 }, 2571 disabled: !audioBuffer, 2572 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none flex items-center gap-2.5", 2573 children: [
2574 /* @__PURE__ */ jsxRuntimeExports.jsx(ChartNoAxesColumn, { className: "w-4 h-4 text-indigo-400" }), 2575 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 2576 t("analyze.stats"), 2577 "..." 2578 ] }) 2579 ] 2580 } 2581 ), 2582 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2583 "button", 2584 { 2585 onClick: () => { 2586 setActiveModal("spectrum"); 2587 setOpenMenu(null); 2588 }, 2589 disabled: !audioBuffer, 2590 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none flex items-center gap-2.5", 2591 children: [ 2592 /* @__PURE__ */ jsxRuntimeExports.jsx(Activity, { className: "w-4 h-4 text-emerald-400" }), 2593 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 2594 t("analyze.spectrum"), 2595 "..." 2596 ] }) 2597 ] 2598 } 2599 ) 2600 ] }) 2601 ] }),
2602 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [ 2603 /* @__PURE__ */ jsxRuntimeExports.jsx( 2604 "button", 2605 { 2606 onClick: () => setOpenMenu(openMenu === "view" ? null : "view"), 2607 className: `px-2.5 py-1 rounded text-xs font-medium transition-colors ${openMenu === "view" ? "bg-zinc-800 text-white" : "text-zinc-300 hover:bg-zinc-800/60"}`, 2608 children: t("menu.view") 2609 } 2610 ), 2611 openMenu === "view" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute left-0 mt-1 w-52 bg-zinc-900 border border-zinc-800 rounded-xl shadow-2xl py-1 text-xs text-zinc-200 z-50", children: [ 2612 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2613 "button", 2614 { 2615 onClick: () => { 2616 zoomIn(); 2617 setOpenMenu(null); 2618 }, 2619 disabled: !audioBuffer, 2620 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 flex items-center justify-between", 2621 children: [
2622 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("view.zoomIn") }), 2623 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 font-mono", children: "+" }) 2624 ] 2625 } 2626 ), 2627 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2628 "button", 2629 { 2630 onClick: () => { 2631 zoomOut(); 2632 setOpenMenu(null); 2633 }, 2634 disabled: !audioBuffer, 2635 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30 flex items-center justify-between", 2636 children: [ 2637 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("view.zoomOut") }), 2638 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 font-mono", children: "-" }) 2639 ] 2640 } 2641 ), 2642 /* @__PURE__ */ jsxRuntimeExports.jsx( 2643 "button", 2644 { 2645 onClick: () => { 2646 zoomFit(); 2647 setOpenMenu(null); 2648 }, 2649 disabled: !audioBuffer, 2650 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30", 2651 children: t("view.zoomFit") 2652 } 2653 ),
2654 /* @__PURE__ */ jsxRuntimeExports.jsx( 2655 "button", 2656 { 2657 onClick: () => { 2658 zoomToSelection(); 2659 setOpenMenu(null); 2660 }, 2661 disabled: !hasSelection, 2662 className: "w-full px-3 py-2 text-left hover:bg-zinc-800 disabled:opacity-30", 2663 children: t("view.zoomSelection") 2664 } 2665 ) 2666 ] }) 2667 ] }) 2668 ] }), 2669 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 2670 audioBuffer && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "hidden lg:flex items-center gap-2 bg-zinc-950 px-2.5 py-1 rounded-md border border-zinc-800 text-xs text-zinc-400 font-mono", children: [ 2671 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-200 font-semibold truncate max-w-[180px]", children: fileName }), 2672 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-600", children: "|" }),
2673 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 2674 audioBuffer.sampleRate, 2675 " Hz" 2676 ] }), 2677 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-600", children: "|" }), 2678 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: audioBuffer.numberOfChannels === 1 ? "Mono" : "Stereo" }) 2679 ] }), 2680 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2681 "div", 2682 { 2683 className: "flex items-center gap-1.5 px-2 py-1 bg-emerald-500/10 border border-emerald-500/20 rounded-md text-[11px] text-emerald-400 font-medium", 2684 title: "All processing is done entirely inside your browser. No files are uploaded to any server.", 2685 children: [ 2686 /* @__PURE__ */ jsxRuntimeExports.jsx(ShieldCheck, { className: "w-3.5 h-3.5 text-emerald-400" }), 2687 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: "100% Local / Free" }) 2688 ] 2689 } 2690 ) 2691 ] }) 2692 ] 2693 } 2694 ); 2695}; 2696const RULER_HEIGHT = 28; 2697const MINIMAP_HEIGHT = 44; 2698const LEFT_SCALE_WIDTH = 48; 2699const WaveformCanvas = () => { 2700 const { 2701 audioBuffer, 2702 selection, 2703 cursor, 2704 zoom, 2705 verticalZoom, 2706 isPlaying, 2707 playbackPosition, 2708 setCursor, 2709 setSelection, 2710 setZoom, 2711 seek 2712 } = useAudioEditor(); 2713 const containerRef = reactExports.useRef(null); 2714 const mainCanvasRef = reactExports.useRef(null); 2715 const minimapCanvasRef = reactExports.useRef(null); 2716 const [isSelecting, setIsSelecting] = reactExports.useState(false); 2717 const [dragStartSec, setDragStartSec] = reactExports.useState(null); 2718 const [activeHandle, setActiveHandle] = reactExports.useState(null); 2719 const [isDraggingMinimap, setIsDraggingMinimap] = reactExports.useState(false); 2720 const resizeCanvases = reactExports.useCallback(() => { 2721 const container = containerRef.current; 2722 if (!container) return; 2723 const rect = container.getBoundingClientRect(); 2724 const dpr = window.devicePixelRatio || 1; 2725 if (mainCanvasRef.current) { 2726 const width = rect.width; 2727 const height = Math.max(260, rect.height - MINIMAP_HEIGHT); 2728 mainCanvasRef.current.width = width * dpr; 2729 mainCanvasRef.current.height = height * dpr; 2730 mainCanvasRef.current.style.width = `${width}px`; 2731 mainCanvasRef.current.style.height = `${height}px`; 2732 } 2733 if (minimapCanvasRef.current) { 2734 minimapCanvasRef.current.width = rect.width * dpr; 2735 minimapCanvasRef.current.height = MINIMAP_HEIGHT * dpr; 2736 minimapCanvasRef.current.style.width = `${rect.width}px`; 2737 minimapCanvasRef.current.style.height = `${MINIMAP_HEIGHT}px`; 2738 } 2739 }, []); 2740 reactExports.useEffect(() => { 2741 resizeCanvases(); 2742 window.addEventListener("resize", resizeCanvases); 2743 return () => window.removeEventListener("resize", resizeCanvases); 2744 }, [resizeCanvases]); 2745 reactExports.useEffect(() => { 2746 const canvas = mainCanvasRef.current; 2747 if (!canvas) return; 2748 const ctx = canvas.getContext("2d"); 2749 if (!ctx) return; 2750 const dpr = window.devicePixelRatio || 1; 2751 const width = canvas.width / dpr; 2752 const height = canvas.height / dpr; 2753 ctx.save(); 2754 ctx.scale(dpr, dpr); 2755 ctx.clearRect(0, 0, width, height); 2756 ctx.fillStyle = "#09090b"; 2757 ctx.fillRect(0, 0, width, height); 2758 const waveWidth = width - LEFT_SCALE_WIDTH; 2759 const waveHeight = height - RULER_HEIGHT; 2760 if (!audioBuffer) { 2761 ctx.fillStyle = "#71717a"; 2762 ctx.font = "14px Inter, sans-serif"; 2763 ctx.textAlign = "center"; 2764 ctx.fillText("Drop an audio file here or open from File menu", width / 2, height / 2); 2765 ctx.restore(); 2766 return; 2767 } 2768 const duration = audioBuffer.duration; 2769 const zoomStart = Math.max(0, zoom.start); 2770 const zoomEnd = Math.min(duration, zoom.end > zoomStart ? zoom.end : duration); 2771 const zoomSpan = Math.max(1e-3, zoomEnd - zoomStart); 2772 const numChannels = audioBuffer.numberOfChannels; 2773 const channelHeight = waveHeight / numChannels; 2774 const timeToX = (timeSec) => { 2775 const ratio = (timeSec - zoomStart) / zoomSpan; 2776 return LEFT_SCALE_WIDTH + ratio * waveWidth; 2777 }; 2778 ctx.fillStyle = "#18181b"; 2779 ctx.fillRect(0, 0, width, RULER_HEIGHT); 2780 ctx.strokeStyle = "#27272a"; 2781 ctx.lineWidth = 1; 2782 ctx.beginPath(); 2783 ctx.moveTo(0, RULER_HEIGHT); 2784 ctx.lineTo(width, RULER_HEIGHT); 2785 ctx.stroke(); 2786 const idealTickCount = Math.max(4, Math.floor(waveWidth / 90)); 2787 const rawStep = zoomSpan / idealTickCount; 2788 const niceSteps = [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300]; 2789 const step = niceSteps.find((s) => s >= rawStep) || rawStep;
2790 const firstTickTime = Math.ceil(zoomStart / step) * step; 2791 ctx.fillStyle = "#a1a1aa"; 2792 ctx.font = "10px monospace"; 2793 ctx.textAlign = "center"; 2794 for (let t = firstTickTime; t <= zoomEnd; t += step) { 2795 const x = timeToX(t); 2796 if (x < LEFT_SCALE_WIDTH || x > width) continue; 2797 ctx.strokeStyle = "#3f3f46"; 2798 ctx.beginPath(); 2799 ctx.moveTo(x, RULER_HEIGHT - 6); 2800 ctx.lineTo(x, RULER_HEIGHT); 2801 ctx.stroke(); 2802 const mins = Math.floor(t / 60); 2803 const secs = (t % 60).toFixed(step < 1 ? 2 : 0); 2804 const label = mins > 0 ? `${mins}:${secs.padStart(step < 1 ? 5 : 2, "0")}` : `${secs}s`; 2805 ctx.fillText(label, x, RULER_HEIGHT - 10); 2806 } 2807 ctx.fillStyle = "#121214"; 2808 ctx.fillRect(0, RULER_HEIGHT, LEFT_SCALE_WIDTH, waveHeight); 2809 ctx.strokeStyle = "#27272a"; 2810 ctx.beginPath(); 2811 ctx.moveTo(LEFT_SCALE_WIDTH, RULER_HEIGHT); 2812 ctx.lineTo(LEFT_SCALE_WIDTH, height); 2813 ctx.stroke(); 2814 for (let c = 0; c < numChannels; c++) { 2815 const yOffset = RULER_HEIGHT + c * channelHeight; 2816 const centerY = yOffset + channelHeight / 2; 2817 ctx.fillStyle = "#71717a"; 2818 ctx.font = "bold 9px sans-serif"; 2819 ctx.textAlign = "left"; 2820 ctx.fillText(numChannels === 1 ? "MONO" : c === 0 ? "L" : "R", 6, yOffset + 14); 2821 ctx.font = "9px monospace"; 2822 ctx.textAlign = "right"; 2823 ctx.fillStyle = "#52525b"; 2824 ctx.fillText("+1.0", LEFT_SCALE_WIDTH - 6, yOffset + 12); 2825 ctx.fillText(" 0.0", LEFT_SCALE_WIDTH - 6, centerY + 3); 2826 ctx.fillText("-1.0", LEFT_SCALE_WIDTH - 6, yOffset + channelHeight - 4); 2827 ctx.strokeStyle = "#27272a"; 2828 ctx.lineWidth = 1; 2829 ctx.beginPath(); 2830 ctx.moveTo(LEFT_SCALE_WIDTH, centerY); 2831 ctx.lineTo(width, centerY); 2832 ctx.stroke(); 2833 } 2834 const sampleRate = audioBuffer.sampleRate; 2835 const startSample = Math.floor(zoomStart * sampleRate); 2836 const endSample = Math.ceil(zoomEnd * sampleRate); 2837 const totalSamplesInView = Math.max(1, endSample - startSample); 2838 const samplesPerPixel = Math.max(1, Math.floor(totalSamplesInView / waveWidth)); 2839 for (let c = 0; c < numChannels; c++) { 2840 const channelData = audioBuffer.getChannelData(c); 2841 const yOffset = RULER_HEIGHT + c * channelHeight; 2842 const centerY = yOffset + channelHeight / 2; 2843 const maxAmplitude = channelHeight / 2 * 0.95 * verticalZoom; 2844 ctx.fillStyle = c === 0 ? "#06b6d4" : "#10b981"; 2845 ctx.strokeStyle = c === 0 ? "#22d3ee" : "#34d399"; 2846 ctx.lineWidth = 1; 2847 for (let px = 0; px < waveWidth; px++) { 2848 const sampleIdx = startSample + px * samplesPerPixel; 2849 if (sampleIdx >= channelData.length) break; 2850 let minVal = 0; 2851 let maxVal = 0; 2852 for (let s = 0; s < samplesPerPixel && sampleIdx + s < channelData.length; s++) { 2853 const val = channelData[sampleIdx + s]; 2854 if (val < minVal) minVal = val; 2855 if (val > maxVal) maxVal = val; 2856 } 2857 const topY = centerY - maxVal * maxAmplitude; 2858 const bottomY = centerY - minVal * maxAmplitude; 2859 const barHeight = Math.max(1, bottomY - topY); 2860 ctx.fillRect(LEFT_SCALE_WIDTH + px, topY, 1, barHeight); 2861 } 2862 } 2863 if (selection && selection.end > selection.start) { 2864 const selStartX = Math.max(LEFT_SCALE_WIDTH, timeToX(selection.start)); 2865 const selEndX = Math.min(width, timeToX(selection.end)); 2866 if (selEndX > selStartX) { 2867 ctx.fillStyle = "rgba(99, 102, 241, 0.22)"; 2868 ctx.fillRect(selStartX, RULER_HEIGHT, selEndX - selStartX, waveHeight); 2869 ctx.strokeStyle = "#6366f1"; 2870 ctx.lineWidth = 1.5; 2871 ctx.beginPath(); 2872 ctx.moveTo(selStartX, RULER_HEIGHT); 2873 ctx.lineTo(selStartX, height); 2874 ctx.moveTo(selEndX, RULER_HEIGHT); 2875 ctx.lineTo(selEndX, height); 2876 ctx.stroke(); 2877 ctx.fillStyle = "#818cf8"; 2878 ctx.fillRect(selStartX - 3, RULER_HEIGHT, 6, 12); 2879 ctx.fillRect(selEndX - 3, RULER_HEIGHT, 6, 12); 2880 } 2881 } 2882 const currentPlayhead = isPlaying ? playbackPosition : cursor; 2883 if (currentPlayhead >= zoomStart && currentPlayhead <= zoomEnd) { 2884 const playheadX = timeToX(currentPlayhead); 2885 ctx.strokeStyle = isPlaying ? "#f59e0b" : "#38bdf8"; 2886 ctx.lineWidth = 2; 2887 ctx.beginPath(); 2888 ctx.moveTo(playheadX, 0); 2889 ctx.lineTo(playheadX, height); 2890 ctx.stroke(); 2891 ctx.fillStyle = isPlaying ? "#f59e0b" : "#38bdf8"; 2892 ctx.beginPath(); 2893 ctx.moveTo(playheadX - 6, 0); 2894 ctx.lineTo(playheadX + 6, 0); 2895 ctx.lineTo(playheadX, 10); 2896 ctx.closePath(); 2897 ctx.fill(); 2898 }
2899 ctx.restore(); 2900 }, [audioBuffer, cursor, isPlaying, playbackPosition, selection, verticalZoom, zoom]); 2901 reactExports.useEffect(() => { 2902 const canvas = minimapCanvasRef.current; 2903 if (!canvas || !audioBuffer) return; 2904 const ctx = canvas.getContext("2d"); 2905 if (!ctx) return; 2906 const dpr = window.devicePixelRatio || 1; 2907 const width = canvas.width / dpr; 2908 const height = canvas.height / dpr; 2909 ctx.save(); 2910 ctx.scale(dpr, dpr); 2911 ctx.clearRect(0, 0, width, height); 2912 ctx.fillStyle = "#18181b"; 2913 ctx.fillRect(0, 0, width, height); 2914 const duration = audioBuffer.duration; 2915 const channelData = audioBuffer.getChannelData(0); 2916 const totalSamples = channelData.length; 2917 const samplesPerPixel = Math.max(1, Math.floor(totalSamples / width)); 2918 const centerY = height / 2; 2919 const maxAmplitude = height / 2 * 0.9; 2920 ctx.fillStyle = "#3f3f46"; 2921 for (let px = 0; px < width; px++) { 2922 const sampleIdx = px * samplesPerPixel; 2923 let maxVal = 0; 2924 for (let s = 0; s < samplesPerPixel && sampleIdx + s < totalSamples; s++) { 2925 const abs = Math.abs(channelData[sampleIdx + s]); 2926 if (abs > maxVal) maxVal = abs; 2927 } 2928 const barH = Math.max(1, maxVal * maxAmplitude); 2929 ctx.fillRect(px, centerY - barH, 1, barH * 2); 2930 } 2931 const viewStartX = zoom.start / duration * width; 2932 const viewEndX = zoom.end / duration * width; 2933 const viewWidth = Math.max(8, viewEndX - viewStartX); 2934 ctx.fillStyle = "rgba(99, 102, 241, 0.25)"; 2935 ctx.fillRect(viewStartX, 0, viewWidth, height); 2936 ctx.strokeStyle = "#6366f1"; 2937 ctx.lineWidth = 1.5; 2938 ctx.strokeRect(viewStartX, 0, viewWidth, height); 2939 ctx.restore(); 2940 }, [audioBuffer, zoom]); 2941 const handleMouseDown = (e) => { 2942 if (!audioBuffer) return; 2943 const canvas = mainCanvasRef.current; 2944 if (!canvas) return; 2945 const rect = canvas.getBoundingClientRect(); 2946 const x = e.clientX - rect.left; 2947 e.clientY - rect.top; 2948 if (x < LEFT_SCALE_WIDTH) return; 2949 const duration = audioBuffer.duration; 2950 const zoomStart = Math.max(0, zoom.start); 2951 const zoomEnd = Math.min(duration, zoom.end); 2952 const zoomSpan = zoomEnd - zoomStart; 2953 const waveWidth = rect.width - LEFT_SCALE_WIDTH; 2954 const clickTime = Math.max(0, Math.min(duration, zoomStart + (x - LEFT_SCALE_WIDTH) / waveWidth * zoomSpan)); 2955 if (selection && selection.end > selection.start) { 2956 const startX = LEFT_SCALE_WIDTH + (selection.start - zoomStart) / zoomSpan * waveWidth; 2957 const endX = LEFT_SCALE_WIDTH + (selection.end - zoomStart) / zoomSpan * waveWidth; 2958 if (Math.abs(x - startX) <= 6) { 2959 setActiveHandle("start"); 2960 return; 2961 } 2962 if (Math.abs(x - endX) <= 6) { 2963 setActiveHandle("end"); 2964 return; 2965 } 2966 } 2967 setIsSelecting(true); 2968 setDragStartSec(clickTime); 2969 setSelection(null); 2970 seek(clickTime); 2971 }; 2972 const handleMouseMove = (e) => { 2973 if (!audioBuffer) return; 2974 const canvas = mainCanvasRef.current; 2975 if (!canvas) return; 2976 const rect = canvas.getBoundingClientRect(); 2977 const x = e.clientX - rect.left; 2978 const duration = audioBuffer.duration; 2979 const zoomStart = Math.max(0, zoom.start); 2980 const zoomEnd = Math.min(duration, zoom.end); 2981 const zoomSpan = zoomEnd - zoomStart; 2982 const waveWidth = rect.width - LEFT_SCALE_WIDTH; 2983 const currentTime = Math.max(0, Math.min(duration, zoomStart + (x - LEFT_SCALE_WIDTH) / waveWidth * zoomSpan)); 2984 if (activeHandle && selection) { 2985 if (activeHandle === "start") { 2986 const newStart = Math.min(currentTime, selection.end - 0.01); 2987 setSelection({ start: Math.max(0, newStart), end: selection.end }); 2988 } else { 2989 const newEnd = Math.max(currentTime, selection.start + 0.01); 2990 setSelection({ start: selection.start, end: Math.min(duration, newEnd) }); 2991 } 2992 return; 2993 } 2994 if (isSelecting && dragStartSec !== null) { 2995 const start = Math.min(dragStartSec, currentTime); 2996 const end = Math.max(dragStartSec, currentTime); 2997 if (end - start > 0.01) { 2998 setSelection({ start, end }); 2999 } else { 3000 setSelection(null); 3001 } 3002 } 3003 }; 3004 const handleMouseUp = () => { 3005 setIsSelecting(false); 3006 setDragStartSec(null); 3007 setActiveHandle(null); 3008 }; 3009 const handleWheel = (e) => { 3010 if (!audioBuffer) return; 3011 e.preventDefault(); 3012 const canvas = mainCanvasRef.current; 3013 if (!canvas) return; 3014 const rect = canvas.getBoundingClientRect(); 3015 const x = e.clientX - rect.left; 3016 const duration = audioBuffer.duration; 3017 const zoomStart = Math.max(0, zoom.start); 3018 const zoomEnd = Math.min(duration, zoom.end); 3019 const zoomSpan = zoomEnd - zoomStart; 3020 const waveWidth = rect.width - LEFT_SCALE_WIDTH; 3021 const mouseTime = zoomStart + (x - LEFT_SCALE_WIDTH) / waveWidth * zoomSpan; 3022 const zoomFactor = e.deltaY < 0 ? 0.8 : 1.25; 3023 const newSpan = Math.max(0.05, Math.min(duration, zoomSpan * zoomFactor)); 3024 const mouseRatio = (x - LEFT_SCALE_WIDTH) / waveWidth; 3025 const newStart = Math.max(0, mouseTime - mouseRatio * newSpan); 3026 const newEnd = Math.min(duration, newStart + newSpan); 3027 setZoom({ start: newStart, end: newEnd }); 3028 }; 3029 const handleMinimapMouseDown = (e) => { 3030 if (!audioBuffer) return; 3031 const canvas = minimapCanvasRef.current; 3032 if (!canvas) return; 3033 const rect = canvas.getBoundingClientRect(); 3034 const x = e.clientX - rect.left; 3035 const duration = audioBuffer.duration; 3036 const clickRatio = x / rect.width; 3037 const clickTime = clickRatio * duration; 3038 const currentSpan = zoom.end - zoom.start; 3039 const newStart = Math.max(0, Math.min(duration - currentSpan, clickTime - currentSpan / 2)); 3040 const newEnd = Math.min(duration, newStart + currentSpan); 3041 setZoom({ start: newStart, end: newEnd }); 3042 setIsDraggingMinimap(true); 3043 }; 3044 const handleMinimapMouseMove = (e) => { 3045 if (!isDraggingMinimap || !audioBuffer) return; 3046 const canvas = minimapCanvasRef.current; 3047 if (!canvas) return; 3048 const rect = canvas.getBoundingClientRect(); 3049 const x = e.clientX - rect.left; 3050 const duration = audioBuffer.duration; 3051 const clickRatio = Math.max(0, Math.min(1, x / rect.width)); 3052 const clickTime = clickRatio * duration; 3053 const currentSpan = zoom.end - zoom.start; 3054 const newStart = Math.max(0, Math.min(duration - currentSpan, clickTime - currentSpan / 2)); 3055 const newEnd = Math.min(duration, newStart + currentSpan); 3056 setZoom({ start: newStart, end: newEnd }); 3057 }; 3058 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 3059 "div", 3060 { 3061 ref: containerRef, 3062 className: "flex-1 flex flex-col bg-zinc-950 select-none overflow-hidden relative border-b border-zinc-800", 3063 onMouseUp: handleMouseUp, 3064 onMouseLeave: handleMouseUp, 3065 children: [
3066 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-none h-11 border-b border-zinc-800/80 bg-zinc-900/60 relative", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 3067 "canvas", 3068 { 3069 ref: minimapCanvasRef, 3070 className: "w-full h-full cursor-pointer", 3071 onMouseDown: handleMinimapMouseDown, 3072 onMouseMove: handleMinimapMouseMove, 3073 onMouseUp: () => setIsDraggingMinimap(false) 3074 } 3075 ) }), 3076 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 w-full h-full relative cursor-crosshair", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 3077 "canvas", 3078 { 3079 ref: mainCanvasRef, 3080 className: "w-full h-full block", 3081 onMouseDown: handleMouseDown, 3082 onMouseMove: handleMouseMove, 3083 onWheel: handleWheel 3084 } 3085 ) }) 3086 ] 3087 } 3088 ); 3089}; 3090const TransportBar = () => { 3091 const { 3092 audioBuffer, 3093 isPlaying, 3094 isLooping, 3095 cursor, 3096 selection, 3097 masterVolume, 3098 verticalZoom, 3099 play, 3100 pause, 3101 stop, 3102 toggleLoop, 3103 seek, 3104 zoomIn, 3105 zoomOut, 3106 zoomFit, 3107 zoomToSelection, 3108 setMasterVolume, 3109 setVerticalZoom, 3110 setActiveModal 3111 } = useAudioEditor(); 3112 const { t } = useAudioEditorTranslation(); 3113 const formatTime = (seconds) => { 3114 const mins = Math.floor(seconds / 60); 3115 const secs = Math.floor(seconds % 60); 3116 const ms = Math.floor(seconds % 1 * 1e3); 3117 return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}.${ms.toString().padStart(3, "0")}`; 3118 }; 3119 const selLength = selection && selection.end > selection.start ? selection.end - selection.start : 0; 3120 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-none h-16 bg-zinc-900 border-t border-zinc-800 px-4 flex items-center justify-between gap-4 select-none text-zinc-300", children: [ 3121 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5", children: [
3122 /* @__PURE__ */ jsxRuntimeExports.jsx( 3123 "button", 3124 { 3125 onClick: () => seek(0), 3126 disabled: !audioBuffer, 3127 className: "p-2 rounded-lg hover:bg-zinc-800 active:bg-zinc-700 disabled:opacity-30 disabled:pointer-events-none transition-colors", 3128 title: "Rewind to Start (Home)", 3129 children: /* @__PURE__ */ jsxRuntimeExports.jsx(SkipBack, { className: "w-5 h-5 text-zinc-300" }) 3130 } 3131 ), 3132 isPlaying ? /* @__PURE__ */ jsxRuntimeExports.jsx( 3133 "button", 3134 { 3135 onClick: pause, 3136 className: "p-2.5 bg-amber-500 hover:bg-amber-400 text-zinc-950 font-bold rounded-xl shadow-lg transition-transform active:scale-95", 3137 title: t("transport.pause"), 3138 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Pause, { className: "w-5 h-5 fill-current" }) 3139 } 3140 ) : /* @__PURE__ */ jsxRuntimeExports.jsx( 3141 "button", 3142 { 3143 onClick: play, 3144 disabled: !audioBuffer, 3145 className: "p-2.5 bg-indigo-500 hover:bg-indigo-400 text-white font-bold rounded-xl shadow-lg shadow-indigo-500/20 disabled:opacity-30 disabled:pointer-events-none transition-transform active:scale-95", 3146 title: t("transport.play"), 3147 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { className: "w-5 h-5 fill-current" }) 3148 } 3149 ),
3150 /* @__PURE__ */ jsxRuntimeExports.jsx( 3151 "button", 3152 { 3153 onClick: stop, 3154 disabled: !audioBuffer, 3155 className: "p-2 rounded-lg hover:bg-zinc-800 active:bg-zinc-700 disabled:opacity-30 disabled:pointer-events-none transition-colors", 3156 title: t("transport.stop"), 3157 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Square, { className: "w-5 h-5 text-zinc-300" }) 3158 } 3159 ), 3160 /* @__PURE__ */ jsxRuntimeExports.jsx( 3161 "button", 3162 { 3163 onClick: () => seek((audioBuffer == null ? void 0 : audioBuffer.duration) || 0), 3164 disabled: !audioBuffer, 3165 className: "p-2 rounded-lg hover:bg-zinc-800 active:bg-zinc-700 disabled:opacity-30 disabled:pointer-events-none transition-colors", 3166 title: "Forward to End (End)", 3167 children: /* @__PURE__ */ jsxRuntimeExports.jsx(SkipForward, { className: "w-5 h-5 text-zinc-300" }) 3168 } 3169 ), 3170 /* @__PURE__ */ jsxRuntimeExports.jsx( 3171 "button", 3172 { 3173 onClick: toggleLoop, 3174 disabled: !audioBuffer, 3175 className: `p-2 rounded-lg transition-colors ${isLooping ? "bg-indigo-500/20 text-indigo-400 border border-indigo-500/40" : "hover:bg-zinc-800 text-zinc-400"} disabled:opacity-30 disabled:pointer-events-none`, 3176 title: t("transport.loop"), 3177 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Repeat, { className: "w-4 h-4" }) 3178 } 3179 ),
3180 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3181 "button", 3182 { 3183 onClick: () => setActiveModal("record"), 3184 className: "p-2 rounded-lg hover:bg-red-500/10 text-red-400 hover:text-red-300 transition-colors ml-2 flex items-center gap-1.5", 3185 title: t("transport.record"), 3186 children: [ 3187 /* @__PURE__ */ jsxRuntimeExports.jsx(Mic, { className: "w-4 h-4 text-red-500 fill-red-500/30" }), 3188 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-semibold uppercase tracking-wider hidden md:inline", children: "Rec" }) 3189 ] 3190 } 3191 ) 3192 ] }), 3193 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4 bg-zinc-950/80 px-4 py-1.5 rounded-xl border border-zinc-800 font-mono text-xs shadow-inner", children: [ 3194 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center", children: [ 3195 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 uppercase", children: t("transport.cursor") }),
3196 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-indigo-400 font-bold", children: formatTime(cursor) }) 3197 ] }), 3198 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-6 w-px bg-zinc-800" }), 3199 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center", children: [ 3200 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 uppercase", children: t("transport.selectionStart") }), 3201 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-300", children: selection ? formatTime(selection.start) : "--:--.---" }) 3202 ] }), 3203 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center", children: [ 3204 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 uppercase", children: t("transport.selectionEnd") }), 3205 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-300", children: selection ? formatTime(selection.end) : "--:--.---" }) 3206 ] }),
3207 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center", children: [ 3208 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 uppercase", children: t("transport.selectionLength") }), 3209 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-amber-400 font-bold", children: selection ? formatTime(selLength) : "--:--.---" }) 3210 ] }) 3211 ] }), 3212 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 3213 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 bg-zinc-950/60 p-1 rounded-lg border border-zinc-800", children: [ 3214 /* @__PURE__ */ jsxRuntimeExports.jsx( 3215 "button", 3216 { 3217 onClick: zoomIn, 3218 disabled: !audioBuffer, 3219 className: "p-1.5 rounded hover:bg-zinc-800 text-zinc-400 hover:text-white disabled:opacity-30", 3220 title: t("view.zoomIn"), 3221 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomIn, { className: "w-4 h-4" }) 3222 } 3223 ),
3224 /* @__PURE__ */ jsxRuntimeExports.jsx( 3225 "button", 3226 { 3227 onClick: zoomOut, 3228 disabled: !audioBuffer, 3229 className: "p-1.5 rounded hover:bg-zinc-800 text-zinc-400 hover:text-white disabled:opacity-30", 3230 title: t("view.zoomOut"), 3231 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomOut, { className: "w-4 h-4" }) 3232 } 3233 ), 3234 /* @__PURE__ */ jsxRuntimeExports.jsx( 3235 "button", 3236 { 3237 onClick: zoomFit, 3238 disabled: !audioBuffer, 3239 className: "p-1.5 rounded hover:bg-zinc-800 text-zinc-400 hover:text-white disabled:opacity-30", 3240 title: t("view.zoomFit"), 3241 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Maximize2, { className: "w-4 h-4" }) 3242 } 3243 ), 3244 /* @__PURE__ */ jsxRuntimeExports.jsx( 3245 "button", 3246 { 3247 onClick: zoomToSelection, 3248 disabled: !audioBuffer || !selection, 3249 className: "p-1.5 rounded hover:bg-zinc-800 text-zinc-400 hover:text-white disabled:opacity-30", 3250 title: t("view.zoomSelection"), 3251 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Minimize2, { className: "w-4 h-4" }) 3252 } 3253 ) 3254 ] }),
3255 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "hidden lg:flex items-center gap-1.5 text-xs text-zinc-400", children: [ 3256 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px]", children: "Amp" }), 3257 /* @__PURE__ */ jsxRuntimeExports.jsx( 3258 "input", 3259 { 3260 type: "range", 3261 min: "0.5", 3262 max: "3.0", 3263 step: "0.1", 3264 value: verticalZoom, 3265 onChange: (e) => setVerticalZoom(parseFloat(e.target.value)), 3266 className: "w-16 accent-indigo-500 h-1 bg-zinc-800 rounded-lg cursor-pointer", 3267 title: "Vertical Amplitude Scale" 3268 } 3269 ) 3270 ] }), 3271 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 3272 /* @__PURE__ */ jsxRuntimeExports.jsx( 3273 "button", 3274 { 3275 onClick: () => setMasterVolume(masterVolume > 0 ? 0 : 1), 3276 className: "text-zinc-400 hover:text-white", 3277 children: masterVolume === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx(VolumeX, { className: "w-4 h-4 text-red-400" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Volume2, { className: "w-4 h-4" }) 3278 } 3279 ),
3280 /* @__PURE__ */ jsxRuntimeExports.jsx( 3281 "input", 3282 { 3283 type: "range", 3284 min: "0", 3285 max: "1.5", 3286 step: "0.05", 3287 value: masterVolume, 3288 onChange: (e) => setMasterVolume(parseFloat(e.target.value)), 3289 className: "w-20 accent-indigo-500 h-1.5 bg-zinc-800 rounded-lg cursor-pointer", 3290 title: t("transport.masterVolume") 3291 } 3292 ) 3293 ] }) 3294 ] }) 3295 ] }); 3296}; 3297const VUMeter = () => { 3298 const { engine, isPlaying } = useAudioEditor(); 3299 const canvasRef = reactExports.useRef(null); 3300 const animRef = reactExports.useRef(null); 3301 const peakHoldLRef = reactExports.useRef(-100); 3302 const peakHoldRRef = reactExports.useRef(-100); 3303 const clipLRef = reactExports.useRef(false); 3304 const clipRRef = reactExports.useRef(false); 3305 reactExports.useEffect(() => { 3306 const canvas = canvasRef.current; 3307 if (!canvas) return; 3308 const ctx = canvas.getContext("2d"); 3309 if (!ctx) return; 3310 const dataL = new Float32Array(512); 3311 const dataR = new Float32Array(512); 3312 const render = () => { 3313 const { left, right } = engine.getAnalysers(); 3314 let peakL = 0; 3315 let peakR = 0; 3316 if (left && isPlaying) { 3317 left.getFloatTimeDomainData(dataL); 3318 for (let i = 0; i < dataL.length; i++) { 3319 const abs = Math.abs(dataL[i]); 3320 if (abs > peakL) peakL = abs; 3321 } 3322 } 3323 if (right && isPlaying) { 3324 right.getFloatTimeDomainData(dataR); 3325 for (let i = 0; i < dataR.length; i++) { 3326 const abs = Math.abs(dataR[i]); 3327 if (abs > peakR) peakR = abs; 3328 } 3329 } else { 3330 peakR = peakL; 3331 } 3332 const toDb = (val) => val > 1e-5 ? 20 * Math.log10(val) : -100; 3333 const dbL = Math.max(-60, toDb(peakL)); 3334 const dbR = Math.max(-60, toDb(peakR)); 3335 peakHoldLRef.current = Math.max(dbL, peakHoldLRef.current - 0.5); 3336 peakHoldRRef.current = Math.max(dbR, peakHoldRRef.current - 0.5); 3337 if (dbL >= -0.1) clipLRef.current = true; 3338 if (dbR >= -0.1) clipRRef.current = true; 3339 const width = canvas.width; 3340 const height = canvas.height; 3341 ctx.clearRect(0, 0, width, height); 3342 const barHeight = 8; 3343 const barSpacing = 4; 3344 const drawBar = (y, dbVal, holdVal, hasClipped) => { 3345 const barWidth = width - 20; 3346 const norm = Math.max(0, Math.min(1, (dbVal + 60) / 60)); 3347 const fillW = norm * barWidth; 3348 ctx.fillStyle = "#18181b"; 3349 ctx.fillRect(0, y, barWidth, barHeight); 3350 const grad = ctx.createLinearGradient(0, y, barWidth, y); 3351 grad.addColorStop(0, "#10b981"); 3352 grad.addColorStop(0.7, "#10b981"); 3353 grad.addColorStop(0.85, "#f59e0b"); 3354 grad.addColorStop(1, "#ef4444"); 3355 ctx.fillStyle = grad; 3356 ctx.fillRect(0, y, fillW, barHeight); 3357 const holdNorm = Math.max(0, Math.min(1, (holdVal + 60) / 60)); 3358 const holdX = holdNorm * barWidth; 3359 ctx.fillStyle = "#ffffff"; 3360 ctx.fillRect(holdX - 1, y, 2, barHeight); 3361 ctx.fillStyle = hasClipped ? "#ef4444" : "#27272a"; 3362 ctx.fillRect(width - 14, y, 12, barHeight); 3363 }; 3364 drawBar(4, dbL, peakHoldLRef.current, clipLRef.current); 3365 drawBar(4 + barHeight + barSpacing, dbR, peakHoldRRef.current, clipRRef.current); 3366 animRef.current = requestAnimationFrame(render); 3367 }; 3368 animRef.current = requestAnimationFrame(render); 3369 return () => { 3370 if (animRef.current !== null) { 3371 cancelAnimationFrame(animRef.current); 3372 } 3373 }; 3374 }, [engine, isPlaying]); 3375 const resetClip = () => { 3376 clipLRef.current = false; 3377 clipRRef.current = false; 3378 }; 3379 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 3380 "div", 3381 { 3382 onClick: resetClip, 3383 className: "flex items-center gap-2 bg-zinc-950 px-3 py-1.5 rounded-xl border border-zinc-800 cursor-pointer select-none", 3384 title: "Stereo VU Meter (Click to reset Clip LED)", 3385 children: [
3386 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col text-[9px] font-mono text-zinc-500 font-bold leading-3", children: [ 3387 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "L" }), 3388 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "mt-1", children: "R" }) 3389 ] }), 3390 /* @__PURE__ */ jsxRuntimeExports.jsx("canvas", { ref: canvasRef, width: 130, height: 28, className: "block" }) 3391 ] 3392 } 3393 ); 3394}; 3395const SpectrumAnalyzer = () => { 3396 const { engine, isPlaying, setActiveModal } = useAudioEditor(); 3397 const { t } = useAudioEditorTranslation(); 3398 const canvasRef = reactExports.useRef(null); 3399 const animRef = reactExports.useRef(null); 3400 reactExports.useEffect(() => { 3401 const canvas = canvasRef.current; 3402 if (!canvas) return; 3403 const ctx = canvas.getContext("2d"); 3404 if (!ctx) return; 3405 const freqData = new Uint8Array(256); 3406 const render = () => { 3407 const { left } = engine.getAnalysers(); 3408 if (left && isPlaying) { 3409 left.getByteFrequencyData(freqData); 3410 } else { 3411 for (let i = 0; i < freqData.length; i++) { 3412 freqData[i] = Math.max(0, freqData[i] - 4); 3413 } 3414 } 3415 const width = canvas.width; 3416 const height = canvas.height; 3417 ctx.clearRect(0, 0, width, height); 3418 ctx.fillStyle = "#09090b"; 3419 ctx.fillRect(0, 0, width, height); 3420 ctx.strokeStyle = "#27272a"; 3421 ctx.lineWidth = 1; 3422 for (let y = 0; y < height; y += 40) { 3423 ctx.beginPath(); 3424 ctx.moveTo(0, y); 3425 ctx.lineTo(width, y); 3426 ctx.stroke(); 3427 } 3428 const numBars = 64; 3429 const barWidth = width / numBars; 3430 for (let b = 0; b < numBars; b++) { 3431 const binIndex = Math.min( 3432 freqData.length - 1, 3433 Math.floor(Math.pow(b / numBars, 1.8) * freqData.length) 3434 ); 3435 const val = freqData[binIndex] / 255; 3436 const barH = val * (height - 20); 3437 const x = b * barWidth; 3438 const y = height - barH; 3439 const grad = ctx.createLinearGradient(x, y, x, height); 3440 grad.addColorStop(0, "#38bdf8"); 3441 grad.addColorStop(0.5, "#6366f1"); 3442 grad.addColorStop(1, "#a855f7"); 3443 ctx.fillStyle = grad; 3444 ctx.fillRect(x + 1, y, barWidth - 2, barH); 3445 } 3446 ctx.fillStyle = "#71717a"; 3447 ctx.font = "10px monospace"; 3448 ctx.textAlign = "center"; 3449 const markers = [ 3450 { label: "50Hz", x: width * 0.1 }, 3451 { label: "200Hz", x: width * 0.28 }, 3452 { label: "1kHz", x: width * 0.52 }, 3453 { label: "5kHz", x: width * 0.78 }, 3454 { label: "16kHz", x: width * 0.94 } 3455 ]; 3456 for (const m of markers) { 3457 ctx.fillText(m.label, m.x, height - 4); 3458 } 3459 animRef.current = requestAnimationFrame(render); 3460 }; 3461 animRef.current = requestAnimationFrame(render); 3462 return () => { 3463 if (animRef.current !== null) { 3464 cancelAnimationFrame(animRef.current); 3465 } 3466 }; 3467 }, [engine, isPlaying]); 3468 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900 border border-zinc-800 rounded-2xl w-full max-w-2xl shadow-2xl overflow-hidden flex flex-col", children: [ 3469 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between p-4 border-b border-zinc-800", children: [
3470 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-white text-base flex items-center gap-2", children: [ 3471 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-3 h-3 rounded-full bg-emerald-500 animate-pulse" }), 3472 t("analyze.spectrum") 3473 ] }), 3474 /* @__PURE__ */ jsxRuntimeExports.jsx( 3475 "button", 3476 { 3477 onClick: () => setActiveModal(null), 3478 className: "p-1 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800 transition-colors", 3479 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { className: "w-5 h-5" }) 3480 } 3481 ) 3482 ] }), 3483 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-6 flex flex-col items-center", children: [ 3484 /* @__PURE__ */ jsxRuntimeExports.jsx("canvas", { ref: canvasRef, width: 600, height: 260, className: "w-full h-auto rounded-xl border border-zinc-800" }), 3485 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500 text-xs mt-3", children: "Real-time FFT Frequency Analysis (20 Hz - 20 kHz Logarithmic). Play audio to view live response." }) 3486 ] }) 3487 ] }) }); 3488}; 3489class DSPFilters { 3490 /** 3491 * Applies peak normalization to a target dBFS. 3492 */ 3493 static normalize(ctx, buffer, targetDb = -0.1, startSec, endSec, removeDC = true) { 3494 let target = buffer; 3495 if (removeDC) { 3496 target = AudioBufferUtils.removeDCOffset(ctx, buffer, startSec, endSec); 3497 } 3498 const cloned = AudioBufferUtils.cloneBuffer(ctx, target); 3499 const start = startSec !== void 0 ? Math.max(0, Math.floor(startSec * buffer.sampleRate)) : 0; 3500 const end = endSec !== void 0 ? Math.min(buffer.length, Math.ceil(endSec * buffer.sampleRate)) : buffer.length; 3501 let peak = 0; 3502 for (let c = 0; c < cloned.numberOfChannels; c++) { 3503 const data = cloned.getChannelData(c); 3504 for (let i = start; i < end; i++) { 3505 const abs = Math.abs(data[i]); 3506 if (abs > peak) peak = abs; 3507 } 3508 } 3509 if (peak <= 1e-6) return cloned; 3510 const targetLinear = Math.pow(10, targetDb / 20); 3511 const multiplier = targetLinear / peak; 3512 for (let c = 0; c < cloned.numberOfChannels; c++) { 3513 const data = cloned.getChannelData(c); 3514 for (let i = start; i < end; i++) { 3515 data[i] *= multiplier; 3516 } 3517 } 3518 return cloned; 3519 } 3520 /** 3521 * Amplifies (gain boost/cut in dB). 3522 */ 3523 static amplify(ctx, buffer, gainDb, startSec, endSec) { 3524 const cloned = AudioBufferUtils.cloneBuffer(ctx, buffer); 3525 const start = startSec !== void 0 ? Math.max(0, Math.floor(startSec * buffer.sampleRate)) : 0; 3526 const end = endSec !== void 0 ? Math.min(buffer.length, Math.ceil(endSec * buffer.sampleRate)) : buffer.length; 3527 const multiplier = Math.pow(10, gainDb / 20); 3528 for (let c = 0; c < cloned.numberOfChannels; c++) { 3529 const data = cloned.getChannelData(c); 3530 for (let i = start; i < end; i++) { 3531 data[i] *= multiplier; 3532 } 3533 } 3534 return cloned; 3535 } 3536 /** 3537 * Applies Fade In. 3538 */ 3539 static fadeIn(ctx, buffer, startSec, endSec, curve = "linear") { 3540 const cloned = AudioBufferUtils.cloneBuffer(ctx, buffer); 3541 const start = Math.max(0, Math.floor(startSec * buffer.sampleRate)); 3542 const end = Math.min(buffer.length, Math.ceil(endSec * buffer.sampleRate)); 3543 const len = Math.max(1, end - start); 3544 for (let c = 0; c < cloned.numberOfChannels; c++) { 3545 const data = cloned.getChannelData(c); 3546 for (let i = 0; i < len; i++) { 3547 const t = i / len; 3548 let gain = t; 3549 if (curve === "exponential") gain = Math.pow(t, 2); 3550 else if (curve === "logarithmic") gain = Math.sqrt(t); 3551 else if (curve === "sCurve") gain = 0.5 * (1 - Math.cos(Math.PI * t)); 3552 data[start + i] *= gain; 3553 } 3554 } 3555 return cloned; 3556 } 3557 /** 3558 * Applies Fade Out. 3559 */ 3560 static fadeOut(ctx, buffer, startSec, endSec, curve = "linear") { 3561 const cloned = AudioBufferUtils.cloneBuffer(ctx, buffer); 3562 const start = Math.max(0, Math.floor(startSec * buffer.sampleRate)); 3563 const end = Math.min(buffer.length, Math.ceil(endSec * buffer.sampleRate)); 3564 const len = Math.max(1, end - start); 3565 for (let c = 0; c < cloned.numberOfChannels; c++) { 3566 const data = cloned.getChannelData(c); 3567 for (let i = 0; i < len; i++) { 3568 const t = 1 - i / len; 3569 let gain = t; 3570 if (curve === "exponential") gain = Math.pow(t, 2); 3571 else if (curve === "logarithmic") gain = Math.sqrt(t);
3572 else if (curve === "sCurve") gain = 0.5 * (1 + Math.cos(Math.PI * (1 - t))); 3573 data[start + i] *= gain; 3574 } 3575 } 3576 return cloned; 3577 } 3578 /** 3579 * Parametric Multi-band Equalizer using OfflineAudioContext. 3580 */ 3581 static async applyEQ(buffer, bands, startSec, endSec) { 3582 const isFullTrack = startSec === void 0 && endSec === void 0; 3583 const targetBuffer = isFullTrack ? buffer : AudioBufferUtils.sliceBuffer(new OfflineAudioContext(1, 1, buffer.sampleRate), buffer, startSec, endSec); 3584 const offlineCtx = new OfflineAudioContext( 3585 targetBuffer.numberOfChannels, 3586 targetBuffer.length, 3587 targetBuffer.sampleRate 3588 ); 3589 const source = offlineCtx.createBufferSource(); 3590 source.buffer = targetBuffer; 3591 let lastNode = source; 3592 for (const band of bands) { 3593 if (Math.abs(band.gain) < 0.01 && band.type === "peaking") continue; 3594 const filter = offlineCtx.createBiquadFilter(); 3595 filter.type = band.type; 3596 filter.frequency.value = band.frequency; 3597 filter.gain.value = band.gain; 3598 filter.Q.value = band.q; 3599 lastNode.connect(filter); 3600 lastNode = filter; 3601 } 3602 lastNode.connect(offlineCtx.destination); 3603 source.start(0); 3604 const rendered = await offlineCtx.startRendering(); 3605 if (isFullTrack) return rendered; 3606 const baseCtx = new OfflineAudioContext(1, 1, buffer.sampleRate); 3607 return AudioBufferUtils.replaceRegion(baseCtx, buffer, rendered, startSec, endSec); 3608 } 3609 /** 3610 * Dynamics Compressor using OfflineAudioContext. 3611 */ 3612 static async applyCompressor(buffer, params, startSec, endSec) { 3613 const isFullTrack = startSec === void 0 && endSec === void 0; 3614 const targetBuffer = isFullTrack ? buffer : AudioBufferUtils.sliceBuffer(new OfflineAudioContext(1, 1, buffer.sampleRate), buffer, startSec, endSec); 3615 const offlineCtx = new OfflineAudioContext( 3616 targetBuffer.numberOfChannels, 3617 targetBuffer.length, 3618 targetBuffer.sampleRate 3619 ); 3620 const source = offlineCtx.createBufferSource(); 3621 source.buffer = targetBuffer; 3622 const compressor = offlineCtx.createDynamicsCompressor(); 3623 compressor.threshold.value = params.threshold; 3624 compressor.knee.value = params.knee; 3625 compressor.ratio.value = params.ratio; 3626 compressor.attack.value = params.attack; 3627 compressor.release.value = params.release; 3628 const makeupGain = offlineCtx.createGain(); 3629 makeupGain.gain.value = Math.pow(10, params.makeupGainDb / 20); 3630 source.connect(compressor); 3631 compressor.connect(makeupGain); 3632 makeupGain.connect(offlineCtx.destination); 3633 source.start(0); 3634 const rendered = await offlineCtx.startRendering(); 3635 if (isFullTrack) return rendered; 3636 const baseCtx = new OfflineAudioContext(1, 1, buffer.sampleRate); 3637 return AudioBufferUtils.replaceRegion(baseCtx, buffer, rendered, startSec, endSec); 3638 } 3639 /** 3640 * Algorithmic Reverb with room size and damping. 3641 */ 3642 static async applyReverb(buffer, params, startSec, endSec) { 3643 const isFullTrack = startSec === void 0 && endSec === void 0; 3644 const targetBuffer = isFullTrack ? buffer : AudioBufferUtils.sliceBuffer(new OfflineAudioContext(1, 1, buffer.sampleRate), buffer, startSec, endSec); 3645 const impulseLen = Math.floor(targetBuffer.sampleRate * (1 + params.roomSize * 2.5)); 3646 const impulseBuffer = new OfflineAudioContext(2, impulseLen, targetBuffer.sampleRate).createBuffer( 3647 2, 3648 impulseLen, 3649 targetBuffer.sampleRate 3650 ); 3651 const decay = 2 + (1 - params.damping) * 3; 3652 for (let c = 0; c < 2; c++) { 3653 const channel = impulseBuffer.getChannelData(c); 3654 for (let i = 0; i < impulseLen; i++) { 3655 const t = i / impulseLen; 3656 channel[i] = (Math.random() * 2 - 1) * Math.pow(1 - t, decay); 3657 } 3658 } 3659 const offlineCtx = new OfflineAudioContext( 3660 targetBuffer.numberOfChannels, 3661 targetBuffer.length, 3662 targetBuffer.sampleRate 3663 ); 3664 const source = offlineCtx.createBufferSource(); 3665 source.buffer = targetBuffer; 3666 const convolver = offlineCtx.createConvolver(); 3667 convolver.buffer = impulseBuffer;
3668 const dryGain = offlineCtx.createGain(); 3669 dryGain.gain.value = params.dry; 3670 const wetGain = offlineCtx.createGain(); 3671 wetGain.gain.value = params.wet; 3672 source.connect(dryGain); 3673 dryGain.connect(offlineCtx.destination); 3674 source.connect(convolver); 3675 convolver.connect(wetGain); 3676 wetGain.connect(offlineCtx.destination); 3677 source.start(0); 3678 const rendered = await offlineCtx.startRendering(); 3679 if (isFullTrack) return rendered; 3680 const baseCtx = new OfflineAudioContext(1, 1, buffer.sampleRate); 3681 return AudioBufferUtils.replaceRegion(baseCtx, buffer, rendered, startSec, endSec); 3682 } 3683 /** 3684 * Stereo Delay / Echo with Feedback. 3685 */ 3686 static applyDelay(ctx, buffer, params, startSec, endSec) { 3687 const cloned = AudioBufferUtils.cloneBuffer(ctx, buffer); 3688 const start = startSec !== void 0 ? Math.max(0, Math.floor(startSec * buffer.sampleRate)) : 0; 3689 const end = endSec !== void 0 ? Math.min(buffer.length, Math.ceil(endSec * buffer.sampleRate)) : buffer.length; 3690 const delaySamples = Math.max(1, Math.floor(params.delayTimeSec * buffer.sampleRate)); 3691 const numChannels = cloned.numberOfChannels; 3692 for (let c = 0; c < numChannels; c++) { 3693 const src = buffer.getChannelData(c); 3694 const dest = cloned.getChannelData(c); 3695 const otherChan = numChannels > 1 && params.pingPong ? cloned.getChannelData(1 - c) : dest; 3696 for (let i = start; i < end; i++) { 3697 const dryVal = src[i] * params.dry; 3698 let echoVal = 0; 3699 if (i - delaySamples >= start) { 3700 echoVal = otherChan[i - delaySamples] * params.feedback; 3701 } 3702 dest[i] = dryVal + echoVal * params.wet; 3703 } 3704 } 3705 return cloned; 3706 } 3707 /** 3708 * Vocal Remover (Center-Channel Extractor / Phase Cancellation) 3709 * with bass preservation below crossover frequency (e.g. 150 Hz). 3710 */ 3711 static async applyVocalRemover(buffer, crossoverFreq = 180, startSec, endSec) { 3712 if (buffer.numberOfChannels < 2) return buffer; 3713 const isFullTrack = startSec === void 0 && endSec === void 0; 3714 const targetBuffer = isFullTrack ? buffer : AudioBufferUtils.sliceBuffer(new OfflineAudioContext(1, 1, buffer.sampleRate), buffer, startSec, endSec); 3715 const offlineCtx = new OfflineAudioContext(2, targetBuffer.length, targetBuffer.sampleRate); 3716 const source = offlineCtx.createBufferSource(); 3717 source.buffer = targetBuffer; 3718 const lowpass = offlineCtx.createBiquadFilter(); 3719 lowpass.type = "lowpass"; 3720 lowpass.frequency.value = crossoverFreq; 3721 source.connect(lowpass); 3722 lowpass.connect(offlineCtx.destination); 3723 source.start(0); 3724 const bassBuffer = await offlineCtx.startRendering(); 3725 const resultBuffer = offlineCtx.createBuffer(2, targetBuffer.length, targetBuffer.sampleRate); 3726 const srcL = targetBuffer.getChannelData(0); 3727 const srcR = targetBuffer.getChannelData(1); 3728 const bassL = bassBuffer.getChannelData(0); 3729 const bassR = bassBuffer.getChannelData(1); 3730 const destL = resultBuffer.getChannelData(0); 3731 const destR = resultBuffer.getChannelData(1); 3732 for (let i = 0; i < targetBuffer.length; i++) { 3733 const diff = (srcL[i] - srcR[i]) * 0.7071; 3734 const bassMono = (bassL[i] + bassR[i]) * 0.5; 3735 destL[i] = diff + bassMono; 3736 destR[i] = -diff + bassMono; 3737 } 3738 if (isFullTrack) return resultBuffer; 3739 const baseCtx = new OfflineAudioContext(1, 1, buffer.sampleRate); 3740 return AudioBufferUtils.replaceRegion(baseCtx, buffer, resultBuffer, startSec, endSec); 3741 } 3742 /** 3743 * Soft-clipping Tube / Tape Saturation. 3744 */ 3745 static applySaturation(ctx, buffer, drive = 2, startSec, endSec) { 3746 const cloned = AudioBufferUtils.cloneBuffer(ctx, buffer); 3747 const start = startSec !== void 0 ? Math.max(0, Math.floor(startSec * buffer.sampleRate)) : 0; 3748 const end = endSec !== void 0 ? Math.min(buffer.length, Math.ceil(endSec * buffer.sampleRate)) : buffer.length; 3749 for (let c = 0; c < cloned.numberOfChannels; c++) { 3750 const data = cloned.getChannelData(c); 3751 for (let i = start; i < end; i++) { 3752 const x = data[i] * drive; 3753 data[i] = Math.tanh(x); 3754 } 3755 } 3756 return cloned; 3757 } 3758 /** 3759 * Pitch Shift & Tempo Time-Stretch using soundtouchjs. 3760 */
3761 static async applyPitchAndTempo(buffer, pitchSemitones, tempoMultiplier, startSec, endSec) { 3762 const isFullTrack = startSec === void 0 && endSec === void 0; 3763 const targetBuffer = isFullTrack ? buffer : AudioBufferUtils.sliceBuffer(new OfflineAudioContext(1, 1, buffer.sampleRate), buffer, startSec, endSec); 3764 try { 3765 const { SoundTouch, SimpleFilter } = await __vitePreload(async () => { 3766 const { SoundTouch: SoundTouch2, SimpleFilter: SimpleFilter2 } = await import("./vendor-audio-D-wTggbx.js").then((n) => n.s); 3767 return { SoundTouch: SoundTouch2, SimpleFilter: SimpleFilter2 }; 3768 }, true ? __vite__mapDeps([0,1,2,3,4]) : void 0); 3769 const st = new SoundTouch(targetBuffer.sampleRate); 3770 st.pitchSemitones = pitchSemitones; 3771 st.tempo = tempoMultiplier; 3772 const numChannels = targetBuffer.numberOfChannels; 3773 const l = targetBuffer.getChannelData(0); 3774 const r = numChannels > 1 ? targetBuffer.getChannelData(1) : l; 3775 const source = { 3776 extract: (target, numFrames, position) => { 3777 let framesRead = 0; 3778 for (let i = 0; i < numFrames; i++) { 3779 const idx = position + i; 3780 if (idx < targetBuffer.length) { 3781 target[i * 2] = l[idx]; 3782 target[i * 2 + 1] = r[idx]; 3783 framesRead++; 3784 } else { 3785 target[i * 2] = 0; 3786 target[i * 2 + 1] = 0; 3787 } 3788 } 3789 return framesRead; 3790 } 3791 }; 3792 const filter = new SimpleFilter(source, st); 3793 const outFrames = []; 3794 const tempOut = new Float32Array(4096 * 2); 3795 let read = 0; 3796 do { 3797 read = filter.extract(tempOut, 4096); 3798 for (let i = 0; i < read * 2; i++) { 3799 outFrames.push(tempOut[i]); 3800 } 3801 } while (read > 0); 3802 const totalFrames = Math.floor(outFrames.length / 2); 3803 if (totalFrames <= 0) return targetBuffer; 3804 const offlineCtx = new OfflineAudioContext(numChannels, totalFrames, targetBuffer.sampleRate); 3805 const result = offlineCtx.createBuffer(numChannels, totalFrames, targetBuffer.sampleRate); 3806 const resL = result.getChannelData(0); 3807 const resR = numChannels > 1 ? result.getChannelData(1) : null; 3808 for (let i = 0; i < totalFrames; i++) { 3809 resL[i] = outFrames[i * 2]; 3810 if (resR) resR[i] = outFrames[i * 2 + 1]; 3811 } 3812 if (isFullTrack) return result; 3813 const baseCtx = new OfflineAudioContext(1, 1, buffer.sampleRate); 3814 return AudioBufferUtils.replaceRegion(baseCtx, buffer, result, startSec, endSec); 3815 } catch (e) { 3816 console.error("Failed to run pitch & tempo shifting:", e); 3817 return buffer; 3818 } 3819 } 3820} 3821const EffectsModal = () => { 3822 const { 3823 audioBuffer, 3824 selection, 3825 activeEffect, 3826 setActiveModal, 3827 setActiveEffect, 3828 applyDSP, 3829 engine 3830 } = useAudioEditor(); 3831 const { t } = useAudioEditorTranslation(); 3832 const previewSourceRef = reactExports.useRef(null); 3833 const [isPreviewing, setIsPreviewing] = reactExports.useState(false); 3834 const [targetDb, setTargetDb] = reactExports.useState(-0.1); 3835 const [gainDb, setGainDb] = reactExports.useState(3); 3836 const [fadeCurve, setFadeCurve] = reactExports.useState("linear"); 3837 const [eqLowGain, setEqLowGain] = reactExports.useState(0); 3838 const [eqMidGain, setEqMidGain] = reactExports.useState(0); 3839 const [eqHighGain, setEqHighGain] = reactExports.useState(0); 3840 const [compThreshold, setCompThreshold] = reactExports.useState(-18); 3841 const [compRatio, setCompRatio] = reactExports.useState(4); 3842 const [compKnee, setCompKnee] = reactExports.useState(6); 3843 const [compAttack, setCompAttack] = reactExports.useState(0.02); 3844 const [compRelease, setCompRelease] = reactExports.useState(0.25); 3845 const [compMakeup, setCompMakeup] = reactExports.useState(3); 3846 const [revRoomSize, setRevRoomSize] = reactExports.useState(0.6); 3847 const [revDamping, setRevDamping] = reactExports.useState(0.5); 3848 const [revWet, setRevWet] = reactExports.useState(0.35); 3849 const [delayTime, setDelayTime] = reactExports.useState(0.3); 3850 const [delayFeedback, setDelayFeedback] = reactExports.useState(0.4); 3851 const [delayPingPong, setDelayPingPong] = reactExports.useState(true); 3852 const [delayWet, setDelayWet] = reactExports.useState(0.35); 3853 const [crossoverFreq, setCrossoverFreq] = reactExports.useState(180); 3854 const [pitchSemitones, setPitchSemitones] = reactExports.useState(0); 3855 const [tempoMultiplier, setTempoMultiplier] = reactExports.useState(1); 3856 const [satDrive, setSatDrive] = reactExports.useState(2.5); 3857 reactExports.useEffect(() => { 3858 return () => { 3859 stopPreview(); 3860 }; 3861 }, []); 3862 const stopPreview = () => { 3863 if (previewSourceRef.current) {
3864 try { 3865 previewSourceRef.current.stop(); 3866 previewSourceRef.current.disconnect(); 3867 } catch { 3868 } 3869 previewSourceRef.current = null; 3870 } 3871 setIsPreviewing(false); 3872 }; 3873 const handleAuditionPreview = async () => { 3874 if (!audioBuffer) return; 3875 if (isPreviewing) { 3876 stopPreview(); 3877 return; 3878 } 3879 const ctx = engine.getContext(); 3880 const previewStart = selection ? selection.start : 0; 3881 const previewEnd = selection ? Math.min(selection.end, previewStart + 5) : Math.min(audioBuffer.duration, 5); 3882 const sampleSlice = AudioBufferUtils.sliceBuffer(ctx, audioBuffer, previewStart, previewEnd); 3883 let processed = sampleSlice; 3884 if (activeEffect === "normalize") { 3885 processed = DSPFilters.normalize(ctx, sampleSlice, targetDb); 3886 } else if (activeEffect === "amplify") { 3887 processed = DSPFilters.amplify(ctx, sampleSlice, gainDb); 3888 } else if (activeEffect === "fadeIn") { 3889 processed = DSPFilters.fadeIn(ctx, sampleSlice, 0, sampleSlice.duration, fadeCurve); 3890 } else if (activeEffect === "fadeOut") { 3891 processed = DSPFilters.fadeOut(ctx, sampleSlice, 0, sampleSlice.duration, fadeCurve); 3892 } else if (activeEffect === "eq") { 3893 processed = await DSPFilters.applyEQ(sampleSlice, [ 3894 { type: "lowshelf", frequency: 200, gain: eqLowGain, q: 0.7 }, 3895 { type: "peaking", frequency: 1500, gain: eqMidGain, q: 1 }, 3896 { type: "highshelf", frequency: 6e3, gain: eqHighGain, q: 0.7 } 3897 ]); 3898 } else if (activeEffect === "compressor") { 3899 processed = await DSPFilters.applyCompressor(sampleSlice, { 3900 threshold: compThreshold, 3901 knee: compKnee, 3902 ratio: compRatio, 3903 attack: compAttack, 3904 release: compRelease, 3905 makeupGainDb: compMakeup 3906 }); 3907 } else if (activeEffect === "reverb") { 3908 processed = await DSPFilters.applyReverb(sampleSlice, { 3909 roomSize: revRoomSize, 3910 damping: revDamping, 3911 wet: revWet, 3912 dry: 1 - revWet * 0.5 3913 }); 3914 } else if (activeEffect === "delay") { 3915 processed = DSPFilters.applyDelay(ctx, sampleSlice, { 3916 delayTimeSec: delayTime, 3917 feedback: delayFeedback, 3918 wet: delayWet, 3919 dry: 1, 3920 pingPong: delayPingPong 3921 }); 3922 } else if (activeEffect === "vocalRemover") { 3923 processed = await DSPFilters.applyVocalRemover(sampleSlice, crossoverFreq); 3924 } else if (activeEffect === "pitchTempo") { 3925 processed = await DSPFilters.applyPitchAndTempo(sampleSlice, pitchSemitones, tempoMultiplier); 3926 } else if (activeEffect === "saturation") { 3927 processed = DSPFilters.applySaturation(ctx, sampleSlice, satDrive); 3928 } 3929 const source = ctx.createBufferSource(); 3930 source.buffer = processed; 3931 source.loop = true; 3932 source.connect(ctx.destination); 3933 source.start(0); 3934 previewSourceRef.current = source; 3935 setIsPreviewing(true); 3936 }; 3937 const handleApply = () => { 3938 stopPreview(); 3939 const start = selection ? selection.start : void 0; 3940 const end = selection ? selection.end : void 0; 3941 applyDSP(async (buffer) => { 3942 const ctx = engine.getContext(); 3943 switch (activeEffect) { 3944 case "normalize": 3945 return DSPFilters.normalize(ctx, buffer, targetDb, start, end); 3946 case "amplify": 3947 return DSPFilters.amplify(ctx, buffer, gainDb, start, end); 3948 case "fadeIn": 3949 return DSPFilters.fadeIn(ctx, buffer, start || 0, end || buffer.duration, fadeCurve); 3950 case "fadeOut": 3951 return DSPFilters.fadeOut(ctx, buffer, start || 0, end || buffer.duration, fadeCurve); 3952 case "eq": 3953 return DSPFilters.applyEQ( 3954 buffer, 3955 [ 3956 { type: "lowshelf", frequency: 200, gain: eqLowGain, q: 0.7 }, 3957 { type: "peaking", frequency: 1500, gain: eqMidGain, q: 1 }, 3958 { type: "highshelf", frequency: 6e3, gain: eqHighGain, q: 0.7 } 3959 ], 3960 start, 3961 end 3962 ); 3963 case "compressor": 3964 return DSPFilters.applyCompressor( 3965 buffer, 3966 { 3967 threshold: compThreshold, 3968 knee: compKnee, 3969 ratio: compRatio, 3970 attack: compAttack, 3971 release: compRelease, 3972 makeupGainDb: compMakeup 3973 }, 3974 start, 3975 end 3976 ); 3977 case "reverb": 3978 return DSPFilters.applyReverb( 3979 buffer, 3980 { 3981 roomSize: revRoomSize, 3982 damping: revDamping,
3983 wet: revWet, 3984 dry: 1 - revWet * 0.5 3985 }, 3986 start, 3987 end 3988 ); 3989 case "delay": 3990 return DSPFilters.applyDelay( 3991 ctx, 3992 buffer, 3993 { 3994 delayTimeSec: delayTime, 3995 feedback: delayFeedback, 3996 wet: delayWet, 3997 dry: 1, 3998 pingPong: delayPingPong 3999 }, 4000 start, 4001 end 4002 ); 4003 case "vocalRemover": 4004 return DSPFilters.applyVocalRemover(buffer, crossoverFreq, start, end); 4005 case "pitchTempo": 4006 return DSPFilters.applyPitchAndTempo(buffer, pitchSemitones, tempoMultiplier, start, end); 4007 case "reverse": 4008 return AudioBufferUtils.reverseAudio(ctx, buffer, start, end); 4009 case "saturation": 4010 return DSPFilters.applySaturation(ctx, buffer, satDrive, start, end); 4011 default: 4012 return buffer; 4013 } 4014 }); 4015 }; 4016 const handleClose = () => { 4017 stopPreview(); 4018 setActiveModal(null); 4019 setActiveEffect(null); 4020 }; 4021 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-50 bg-black/75 backdrop-blur-sm flex items-center justify-center p-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900 border border-zinc-800 rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden flex flex-col", children: [ 4022 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between p-4 border-b border-zinc-800", children: [
4023 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-white text-base flex items-center gap-2 capitalize", children: [ 4024 /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { className: "w-4 h-4 text-indigo-400" }), 4025 t(`effects.${activeEffect}`) || activeEffect 4026 ] }), 4027 /* @__PURE__ */ jsxRuntimeExports.jsx( 4028 "button", 4029 { 4030 onClick: handleClose, 4031 className: "p-1 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800 transition-colors", 4032 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { className: "w-5 h-5" }) 4033 } 4034 ) 4035 ] }), 4036 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-6 space-y-5 text-sm text-zinc-300", children: [ 4037 activeEffect === "normalize" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [ 4038 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between", children: [ 4039 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("params.targetDb") }),
4040 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-indigo-400 font-bold", children: [ 4041 targetDb, 4042 " dBFS" 4043 ] }) 4044 ] }), 4045 /* @__PURE__ */ jsxRuntimeExports.jsx( 4046 "input", 4047 { 4048 type: "range", 4049 min: "-12", 4050 max: "0", 4051 step: "0.1", 4052 value: targetDb, 4053 onChange: (e) => setTargetDb(parseFloat(e.target.value)), 4054 className: "w-full accent-indigo-500 h-2 bg-zinc-800 rounded-lg cursor-pointer" 4055 } 4056 ), 4057 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex gap-2", children: [-0.1, -1, -3].map((val) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 4058 "button", 4059 { 4060 onClick: () => setTargetDb(val), 4061 className: "px-3 py-1 bg-zinc-800 hover:bg-zinc-700 text-xs rounded-lg text-zinc-300 transition-colors", 4062 children: [ 4063 val, 4064 " dBFS" 4065 ] 4066 }, 4067 val 4068 )) }) 4069 ] }), 4070 activeEffect === "amplify" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
4071 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between", children: [ 4072 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("params.gainDb") }), 4073 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-indigo-400 font-bold", children: [ 4074 gainDb > 0 ? `+${gainDb}` : gainDb, 4075 " dB" 4076 ] }) 4077 ] }), 4078 /* @__PURE__ */ jsxRuntimeExports.jsx( 4079 "input", 4080 { 4081 type: "range", 4082 min: "-24", 4083 max: "24", 4084 step: "0.5", 4085 value: gainDb, 4086 onChange: (e) => setGainDb(parseFloat(e.target.value)), 4087 className: "w-full accent-indigo-500 h-2 bg-zinc-800 rounded-lg cursor-pointer" 4088 } 4089 ), 4090 gainDb > 6 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-amber-400 text-xs", children: "Warning: High gain may cause digital clipping/distortion." }) 4091 ] }), 4092 (activeEffect === "fadeIn" || activeEffect === "fadeOut") && /* @__P
4092URE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [ 4093 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs uppercase text-zinc-400 font-semibold", children: t("params.curve") }), 4094 /* @__PURE__ */ jsxRuntimeExports.jsxs( 4095 "select", 4096 { 4097 value: fadeCurve, 4098 onChange: (e) => setFadeCurve(e.target.value), 4099 className: "w-full p-2.5 rounded-xl border border-zinc-700 bg-zinc-800 text-zinc-200", 4100 children: [ 4101 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "linear", className: "bg-zinc-800 text-zinc-200", children: t("params.linear") }), 4102 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "sCurve", className: "bg-zinc-800 text-zinc-200", children: t("params.sCurve") }), 4103 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "logarithmic", className: "bg-zinc-800 text-zinc-200", children: t("params.logarithmic") }), 4104 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "exponential", className: "bg-zinc-800 text-zinc-200", children: t("params.exponential") }) 4105 ] 4106 } 4107 ) 4108 ] }), 4109 activeEffect === "eq" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
4110 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4111 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-xs mb-1", children: [ 4112 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Bass (200 Hz Shelf)" }), 4113 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-indigo-400", children: [ 4114 eqLowGain > 0 ? `+${eqLowGain}` : eqLowGain, 4115 " dB" 4116 ] }) 4117 ] }), 4118 /* @__PURE__ */ jsxRuntimeExports.jsx( 4119 "input", 4120 { 4121 type: "range", 4122 min: "-15", 4123 max: "15", 4124 step: "0.5", 4125 value: eqLowGain, 4126 onChange: (e) => setEqLowGain(parseFloat(e.target.value)), 4127 className: "w-full accent-indigo-500 h-2 bg-zinc-800 rounded-lg cursor-pointer" 4128 } 4129 ) 4130 ] }), 4131 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4132 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-xs mb-1", children: [
4133 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Mid (1.5 kHz Bell)" }), 4134 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-indigo-400", children: [ 4135 eqMidGain > 0 ? `+${eqMidGain}` : eqMidGain, 4136 " dB" 4137 ] }) 4138 ] }), 4139 /* @__PURE__ */ jsxRuntimeExports.jsx( 4140 "input", 4141 { 4142 type: "range", 4143 min: "-15", 4144 max: "15", 4145 step: "0.5", 4146 value: eqMidGain, 4147 onChange: (e) => setEqMidGain(parseFloat(e.target.value)), 4148 className: "w-full accent-indigo-500 h-2 bg-zinc-800 rounded-lg cursor-pointer" 4149 } 4150 ) 4151 ] }), 4152 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4153 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-xs mb-1", children: [ 4154 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Treble (6 kHz Shelf)" }),
4155 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-indigo-400", children: [ 4156 eqHighGain > 0 ? `+${eqHighGain}` : eqHighGain, 4157 " dB" 4158 ] }) 4159 ] }), 4160 /* @__PURE__ */ jsxRuntimeExports.jsx( 4161 "input", 4162 { 4163 type: "range", 4164 min: "-15", 4165 max: "15", 4166 step: "0.5", 4167 value: eqHighGain, 4168 onChange: (e) => setEqHighGain(parseFloat(e.target.value)), 4169 className: "w-full accent-indigo-500 h-2 bg-zinc-800 rounded-lg cursor-pointer" 4170 } 4171 ) 4172 ] }) 4173 ] }), 4174 activeEffect === "compressor" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3", children: [ 4175 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4176 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-xs mb-1", children: [ 4177 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("params.threshold") }),
4178 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-indigo-400", children: [ 4179 compThreshold, 4180 " dB" 4181 ] }) 4182 ] }), 4183 /* @__PURE__ */ jsxRuntimeExports.jsx( 4184 "input", 4185 { 4186 type: "range", 4187 min: "-50", 4188 max: "0", 4189 step: "1", 4190 value: compThreshold, 4191 onChange: (e) => setCompThreshold(parseFloat(e.target.value)), 4192 className: "w-full accent-indigo-500 h-1.5 bg-zinc-800 rounded cursor-pointer" 4193 } 4194 ) 4195 ] }), 4196 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4197 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-xs mb-1", children: [ 4198 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("params.ratio") }), 4199 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-indigo-400", children: [ 4200 compRatio, 4201 ":1" 4202 ] }) 4203 ] }),
4204 /* @__PURE__ */ jsxRuntimeExports.jsx( 4205 "input", 4206 { 4207 type: "range", 4208 min: "1", 4209 max: "20", 4210 step: "0.5", 4211 value: compRatio, 4212 onChange: (e) => setCompRatio(parseFloat(e.target.value)), 4213 className: "w-full accent-indigo-500 h-1.5 bg-zinc-800 rounded cursor-pointer" 4214 } 4215 ) 4216 ] }), 4217 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4218 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-xs mb-1", children: [ 4219 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("params.makeupGain") }), 4220 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-indigo-400", children: [ 4221 "+", 4222 compMakeup, 4223 " dB" 4224 ] }) 4225 ] }), 4226 /* @__PURE__ */ jsxRuntimeExports.jsx( 4227 "input", 4228 { 4229 type: "range", 4230 min: "0", 4231 max: "18", 4232 step: "0.5", 4233 value: compMakeup, 4234 onChange: (e) => setCompMakeup(parseFloat(e.target.value)), 4235 className: "w-full accent-indigo-500 h-1.5 bg-zinc-800 rounded cursor-pointer" 4236 } 4237 ) 4238 ] }) 4239 ] }), 4240 activeEffect === "reverb" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
4241 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4242 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-xs mb-1", children: [ 4243 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("params.roomSize") }), 4244 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-indigo-400", children: [ 4245 Math.round(revRoomSize * 100), 4246 "%" 4247 ] }) 4248 ] }), 4249 /* @__PURE__ */ jsxRuntimeExports.jsx( 4250 "input", 4251 { 4252 type: "range", 4253 min: "0.1", 4254 max: "1.0", 4255 step: "0.05", 4256 value: revRoomSize, 4257 onChange: (e) => setRevRoomSize(parseFloat(e.target.value)), 4258 className: "w-full accent-indigo-500 h-2 bg-zinc-800 rounded-lg cursor-pointer" 4259 } 4260 ) 4261 ] }), 4262 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4263 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-xs mb-1", children: [
4264 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("params.wet") }), 4265 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-indigo-400", children: [ 4266 Math.round(revWet * 100), 4267 "%" 4268 ] }) 4269 ] }), 4270 /* @__PURE__ */ jsxRuntimeExports.jsx( 4271 "input", 4272 { 4273 type: "range", 4274 min: "0.05", 4275 max: "0.8", 4276 step: "0.05", 4277 value: revWet, 4278 onChange: (e) => setRevWet(parseFloat(e.target.value)), 4279 className: "w-full accent-indigo-500 h-2 bg-zinc-800 rounded-lg cursor-pointer" 4280 } 4281 ) 4282 ] }) 4283 ] }), 4284 activeEffect === "delay" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [ 4285 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4286 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-xs mb-1", children: [
4287 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("params.delayTime") }), 4288 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-indigo-400", children: [ 4289 delayTime, 4290 "s" 4291 ] }) 4292 ] }), 4293 /* @__PURE__ */ jsxRuntimeExports.jsx( 4294 "input", 4295 { 4296 type: "range", 4297 min: "0.05", 4298 max: "1.0", 4299 step: "0.05", 4300 value: delayTime, 4301 onChange: (e) => setDelayTime(parseFloat(e.target.value)), 4302 className: "w-full accent-indigo-500 h-2 bg-zinc-800 rounded-lg cursor-pointer" 4303 } 4304 ) 4305 ] }), 4306 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4307 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-xs mb-1", children: [ 4308 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("params.feedback") }), 4309 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-indigo-400", children: [ 4310 Math.round(delayFeedback * 100), 4311 "%" 4312 ] }) 4313 ] }),
4314 /* @__PURE__ */ jsxRuntimeExports.jsx( 4315 "input", 4316 { 4317 type: "range", 4318 min: "0", 4319 max: "0.85", 4320 step: "0.05", 4321 value: delayFeedback, 4322 onChange: (e) => setDelayFeedback(parseFloat(e.target.value)), 4323 className: "w-full accent-indigo-500 h-2 bg-zinc-800 rounded-lg cursor-pointer" 4324 } 4325 ) 4326 ] }), 4327 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 4328 /* @__PURE__ */ jsxRuntimeExports.jsx( 4329 "input", 4330 { 4331 type: "checkbox", 4332 id: "pingpong", 4333 checked: delayPingPong, 4334 onChange: (e) => setDelayPingPong(e.target.checked), 4335 className: "rounded text-indigo-500 focus:ring-0 cursor-pointer" 4336 } 4337 ), 4338 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { htmlFor: "pingpong", className: "text-xs cursor-pointer select-none", children: t("params.pingPong") }) 4339 ] }) 4340 ] }), 4341 activeEffect === "vocalRemover" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
4342 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-400", children: "Isolates center panned vocals using mid-side phase cancellation. Bass and kick drums are preserved below the crossover frequency." }), 4343 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4344 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-xs mb-1", children: [ 4345 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("params.crossoverFreq") }), 4346 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-indigo-400", children: [ 4347 crossoverFreq, 4348 " Hz" 4349 ] }) 4350 ] }), 4351 /* @__PURE__ */ jsxRuntimeExports.jsx( 4352 "input", 4353 { 4354 type: "range", 4355 min: "80", 4356 max: "320", 4357 step: "10", 4358 value: crossoverFreq, 4359 onChange: (e) => setCrossoverFreq(parseFloat(e.target.value)), 4360 className: "w-full accent-indigo-500 h-2 bg-zinc-800 rounded-lg cursor-pointer" 4361 } 4362 ) 4363 ] }) 4364 ] }), 4365 activeEffect === "pitchTempo" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
4366 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4367 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-xs mb-1", children: [ 4368 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("params.pitchSemitones") }), 4369 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-indigo-400", children: pitchSemitones > 0 ? `+${pitchSemitones}` : pitchSemitones }) 4370 ] }), 4371 /* @__PURE__ */ jsxRuntimeExports.jsx( 4372 "input", 4373 { 4374 type: "range", 4375 min: "-12", 4376 max: "12", 4377 step: "1", 4378 value: pitchSemitones, 4379 onChange: (e) => setPitchSemitones(parseInt(e.target.value, 10)), 4380 className: "w-full accent-indigo-500 h-2 bg-zinc-800 rounded-lg cursor-pointer" 4381 } 4382 ) 4383 ] }), 4384 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4385 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-xs mb-1", children: [
4386 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("params.tempoMultiplier") }), 4387 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-indigo-400", children: [ 4388 tempoMultiplier, 4389 "x" 4390 ] }) 4391 ] }), 4392 /* @__PURE__ */ jsxRuntimeExports.jsx( 4393 "input", 4394 { 4395 type: "range", 4396 min: "0.5", 4397 max: "2.0", 4398 step: "0.05", 4399 value: tempoMultiplier, 4400 onChange: (e) => setTempoMultiplier(parseFloat(e.target.value)), 4401 className: "w-full accent-indigo-500 h-2 bg-zinc-800 rounded-lg cursor-pointer" 4402 } 4403 ) 4404 ] }) 4405 ] }), 4406 activeEffect === "saturation" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [ 4407 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between", children: [ 4408 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("params.drive") }),
4409 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-indigo-400 font-bold", children: satDrive }) 4410 ] }), 4411 /* @__PURE__ */ jsxRuntimeExports.jsx( 4412 "input", 4413 { 4414 type: "range", 4415 min: "1.0", 4416 max: "8.0", 4417 step: "0.2", 4418 value: satDrive, 4419 onChange: (e) => setSatDrive(parseFloat(e.target.value)), 4420 className: "w-full accent-indigo-500 h-2 bg-zinc-800 rounded-lg cursor-pointer" 4421 } 4422 ) 4423 ] }) 4424 ] }), 4425 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between p-4 border-t border-zinc-800 bg-zinc-950/50", children: [ 4426 /* @__PURE__ */ jsxRuntimeExports.jsxs( 4427 "button", 4428 { 4429 onClick: handleAuditionPreview, 4430 className: `px-4 py-2 rounded-xl border text-xs font-semibold flex items-center gap-2 transition-colors ${isPreviewing ? "bg-amber-500 text-zinc-950 border-amber-400 shadow-md" : "bg-zinc-800 hover:bg-zinc-700 text-zinc-200 border-zinc-700"}`, 4431 children: [
4432 isPreviewing ? /* @__PURE__ */ jsxRuntimeExports.jsx(Square, { className: "w-3.5 h-3.5 fill-current" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { className: "w-3.5 h-3.5" }), 4433 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: isPreviewing ? t("effects.stopPreview") : t("effects.preview") }) 4434 ] 4435 } 4436 ), 4437 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 4438 /* @__PURE__ */ jsxRuntimeExports.jsx( 4439 "button", 4440 { 4441 onClick: handleClose, 4442 className: "px-4 py-2 rounded-xl text-xs font-medium text-zinc-400 hover:text-zinc-200 transition-colors", 4443 children: t("effects.cancel") 4444 } 4445 ), 4446 /* @__PURE__ */ jsxRuntimeExports.jsxs( 4447 "button", 4448 { 4449 onClick: handleApply, 4450 className: "px-5 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold rounded-xl shadow-lg shadow-indigo-600/20 flex items-center gap-1.5 transition-transform active:scale-95", 4451 children: [
4452 /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "w-4 h-4" }), 4453 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("effects.apply") }) 4454 ] 4455 } 4456 ) 4457 ] }) 4458 ] }) 4459 ] }) }); 4460}; 4461const ExportModal = () => { 4462 const { fileName, selection, exportAudio, setActiveModal, isProcessing, processingMessage } = useAudioEditor(); 4463 const { t } = useAudioEditorTranslation(); 4464 const [format, setFormat] = reactExports.useState("wav"); 4465 const [bitDepth, setBitDepth] = reactExports.useState(16); 4466 const [bitrate, setBitrate] = reactExports.useState(192); 4467 const [range, setRange] = reactExports.useState("full"); 4468 const [customName, setCustomName] = reactExports.useState(fileName.replace(/\.[^/.]+$/, "")); 4469 const hasSelection = !!(selection && selection.end > selection.start); 4470 const handleExport = () => { 4471 exportAudio(format, bitDepth, bitrate, range); 4472 }; 4473 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-50 bg-black/75 backdrop-blur-sm flex items-center justify-center p-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900 border border-zinc-800 rounded-2xl w-full max-w-md
4473shadow-2xl overflow-hidden flex flex-col", children: [ 4474 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between p-4 border-b border-zinc-800", children: [ 4475 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-white text-base flex items-center gap-2", children: [ 4476 /* @__PURE__ */ jsxRuntimeExports.jsx(FileHeadphone, { className: "w-5 h-5 text-indigo-400" }), 4477 t("exportModal.title") 4478 ] }), 4479 /* @__PURE__ */ jsxRuntimeExports.jsx( 4480 "button", 4481 { 4482 onClick: () => setActiveModal(null), 4483 className: "p-1 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800 transition-colors", 4484 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { className: "w-5 h-5" }) 4485 } 4486 ) 4487 ] }), 4488 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-6 space-y-4 text-sm text-zinc-300", children: [ 4489 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4490 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs uppercase text-zinc-400 font-semibold mb-1", children: t("exportModal.fileName") }),
4491 /* @__PURE__ */ jsxRuntimeExports.jsx( 4492 "input", 4493 { 4494 type: "text", 4495 value: customName, 4496 onChange: (e) => setCustomName(e.target.value), 4497 className: "w-full p-2.5 rounded-xl border border-zinc-700 bg-zinc-800 text-zinc-100 font-mono text-xs focus:border-indigo-500 focus:outline-none" 4498 } 4499 ) 4500 ] }), 4501 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4502 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs uppercase text-zinc-400 font-semibold mb-1", children: t("exportModal.format") }), 4503 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-2", children: [ 4504 /* @__PURE__ */ jsxRuntimeExports.jsx( 4505 "button", 4506 { 4507 type: "button", 4508 onClick: () => setFormat("wav"), 4509 className: `py-2 px-3 rounded-xl border text-xs font-semibold transition-colors ${format === "wav" ? "bg-indigo-600 text-white border-indigo-500" : "bg-zinc-800 text-zinc-400 border-zinc-700 hover:bg-zinc-700"}`, 4510 children: "WAV (Lossless)" 4511 } 4512 ),
4513 /* @__PURE__ */ jsxRuntimeExports.jsx( 4514 "button", 4515 { 4516 type: "button", 4517 onClick: () => setFormat("mp3"), 4518 className: `py-2 px-3 rounded-xl border text-xs font-semibold transition-colors ${format === "mp3" ? "bg-indigo-600 text-white border-indigo-500" : "bg-zinc-800 text-zinc-400 border-zinc-700 hover:bg-zinc-700"}`, 4519 children: "MP3 (Compressed)" 4520 } 4521 ) 4522 ] }) 4523 ] }), 4524 format === "wav" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4525 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs uppercase text-zinc-400 font-semibold mb-1", children: t("exportModal.bitDepth") }), 4526 /* @__PURE__ */ jsxRuntimeExports.jsxs( 4527 "select", 4528 { 4529 value: bitDepth, 4530 onChange: (e) => setBitDepth(parseInt(e.target.value, 10)), 4531 className: "w-full p-2.5 rounded-xl border border-zinc-700 bg-zinc-800 text-zinc-200", 4532 children: [
4533 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 16, className: "bg-zinc-800 text-zinc-200", children: "16-bit PCM (CD Quality)" }), 4534 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 24, className: "bg-zinc-800 text-zinc-200", children: "24-bit PCM (Studio Quality)" }), 4535 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 32, className: "bg-zinc-800 text-zinc-200", children: "32-bit Float (Mastering Quality)" }) 4536 ] 4537 } 4538 ) 4539 ] }), 4540 format === "mp3" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4541 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs uppercase text-zinc-400 font-semibold mb-1", children: t("exportModal.bitrate") }), 4542 /* @__PURE__ */ jsxRuntimeExports.jsxs( 4543 "select", 4544 { 4545 value: bitrate, 4546 onChange: (e) => setBitrate(parseInt(e.target.value, 10)), 4547 className: "w-full p-2.5 rounded-xl border border-zinc-700 bg-zinc-800 text-zinc-200", 4548 children: [
4549 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 128, className: "bg-zinc-800 text-zinc-200", children: "128 kbps (Standard)" }), 4550 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 192, className: "bg-zinc-800 text-zinc-200", children: "192 kbps (High Quality)" }), 4551 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 320, className: "bg-zinc-800 text-zinc-200", children: "320 kbps (Maximum Quality)" }) 4552 ] 4553 } 4554 ) 4555 ] }), 4556 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4557 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs uppercase text-zinc-400 font-semibold mb-1", children: t("exportModal.range") }), 4558 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [ 4559 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [ 4560 /* @__PURE__ */ jsxRuntimeExports.jsx( 4561 "input", 4562 { 4563 type: "radio", 4564 name: "exportRange", 4565 value: "full", 4566 checked: range === "full", 4567 onChange: () => setRange("full"), 4568 className: "text-indigo-600 focus:ring-0" 4569 } 4570 ),
4571 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("exportModal.fullTrack") }) 4572 ] }), 4573 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: `flex items-center gap-2 ${hasSelection ? "cursor-pointer" : "opacity-40"}`, children: [ 4574 /* @__PURE__ */ jsxRuntimeExports.jsx( 4575 "input", 4576 { 4577 type: "radio", 4578 name: "exportRange", 4579 value: "selection", 4580 disabled: !hasSelection, 4581 checked: range === "selection", 4582 onChange: () => setRange("selection"), 4583 className: "text-indigo-600 focus:ring-0" 4584 } 4585 ), 4586 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("exportModal.selectionOnly") }) 4587 ] }) 4588 ] }) 4589 ] }), 4590 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-3 bg-zinc-950/80 rounded-xl border border-zinc-800/80 text-xs text-zinc-400", children: "Rendered 100% locally via browser Web Audio & Web Workers. No file data leaves your device." }) 4591 ] }), 4592 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-end gap-2 p-4 border-t border-zinc-800 bg-zinc-950/50", children: [
4593 /* @__PURE__ */ jsxRuntimeExports.jsx( 4594 "button", 4595 { 4596 onClick: () => setActiveModal(null), 4597 className: "px-4 py-2 rounded-xl text-xs font-medium text-zinc-400 hover:text-zinc-200 transition-colors", 4598 children: t("effects.cancel") 4599 } 4600 ), 4601 /* @__PURE__ */ jsxRuntimeExports.jsxs( 4602 "button", 4603 { 4604 onClick: handleExport, 4605 disabled: isProcessing, 4606 className: "px-5 py-2 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white text-xs font-bold rounded-xl shadow-lg shadow-indigo-600/20 flex items-center gap-2 transition-transform active:scale-95", 4607 children: [ 4608 /* @__PURE__ */ jsxRuntimeExports.jsx(Download, { className: "w-4 h-4" }), 4609 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: isProcessing ? processingMessage || t("exportModal.processing") : t("exportModal.exportBtn") }) 4610 ] 4611 } 4612 ) 4613 ] }) 4614 ] }) }); 4615}; 4616const AudioStatsModal = () => { 4617 const { stats, setActiveModal } = useAudioEditor(); 4618 const { t } = useAudioEditorTranslation(); 4619 if (!stats) return null; 4620 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-50 bg-black/75 backdrop-blur-sm flex items-center justify-center p-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900 border border-zinc-800 rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden flex flex-col", children: [ 4621 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between p-4 border-b border-zinc-800", children: [
4622 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-white text-base flex items-center gap-2", children: [ 4623 /* @__PURE__ */ jsxRuntimeExports.jsx(ChartNoAxesColumn, { className: "w-5 h-5 text-indigo-400" }), 4624 t("analyze.stats") 4625 ] }), 4626 /* @__PURE__ */ jsxRuntimeExports.jsx( 4627 "button", 4628 { 4629 onClick: () => setActiveModal(null), 4630 className: "p-1 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800 transition-colors", 4631 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { className: "w-5 h-5" }) 4632 } 4633 ) 4634 ] }), 4635 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-6 space-y-4 text-xs text-zinc-300", children: [ 4636 stats.hasClipping ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2.5 p-3 rounded-xl bg-red-500/10 border border-red-500/30 text-red-400 font-semibold", children: [ 4637 /* @__PURE__ */ jsxRuntimeExports.jsx(TriangleAlert, { className: "w-4 h-4 text-red-400 flex-shrink-0" }),
4638 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("analyze.clippingDetected") }) 4639 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2.5 p-3 rounded-xl bg-emerald-500/10 border border-emerald-500/30 text-emerald-400 font-semibold", children: [ 4640 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleCheck, { className: "w-4 h-4 text-emerald-400 flex-shrink-0" }), 4641 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("analyze.noClipping") }) 4642 ] }), 4643 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-3 font-mono", children: [ 4644 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-950 p-3 rounded-xl border border-zinc-800", children: [ 4645 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 text-[10px] uppercase block mb-0.5", children: t("analyze.duration") }), 4646 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-100 font-bold text-sm", children: [ 4647 stats.duration.toFixed(3), 4648 "s" 4649 ] }) 4650 ] }),
4651 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-950 p-3 rounded-xl border border-zinc-800", children: [ 4652 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 text-[10px] uppercase block mb-0.5", children: t("analyze.sampleRate") }), 4653 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-100 font-bold text-sm", children: [ 4654 stats.sampleRate, 4655 " Hz" 4656 ] }) 4657 ] }), 4658 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-950 p-3 rounded-xl border border-zinc-800", children: [ 4659 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 text-[10px] uppercase block mb-0.5", children: t("analyze.channels") }), 4660 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-100 font-bold text-sm", children: stats.numberOfChannels === 1 ? "Mono (1)" : "Stereo (2)" }) 4661 ] }), 4662 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-950 p-3 rounded-xl border border-zinc-800", children: [
4663 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 text-[10px] uppercase block mb-0.5", children: t("analyze.samples") }), 4664 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-100 font-bold text-sm", children: stats.length.toLocaleString() }) 4665 ] }), 4666 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-950 p-3 rounded-xl border border-zinc-800", children: [ 4667 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 text-[10px] uppercase block mb-0.5", children: t("analyze.peakL") }), 4668 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: `font-bold text-sm ${stats.peakDbL >= -0.1 ? "text-red-400" : "text-indigo-400"}`, children: [ 4669 stats.peakDbL, 4670 " dBFS" 4671 ] }) 4672 ] }), 4673 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-950 p-3 rounded-xl border border-zinc-800", children: [ 4674 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 text-[10px] uppercase block mb-0.5", children: t("analyze.peakR") }),
4675 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: `font-bold text-sm ${stats.peakDbR >= -0.1 ? "text-red-400" : "text-indigo-400"}`, children: [ 4676 stats.peakDbR, 4677 " dBFS" 4678 ] }) 4679 ] }), 4680 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-950 p-3 rounded-xl border border-zinc-800", children: [ 4681 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 text-[10px] uppercase block mb-0.5", children: t("analyze.rmsL") }), 4682 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-200 font-bold text-sm", children: [ 4683 stats.rmsDbL, 4684 " dBFS" 4685 ] }) 4686 ] }), 4687 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-950 p-3 rounded-xl border border-zinc-800", children: [ 4688 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 text-[10px] uppercase block mb-0.5", children: t("analyze.rmsR") }), 4689 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-200 font-bold text-sm", children: [ 4690 stats.rmsDbR, 4691 " dBFS" 4692 ] }) 4693 ] }),
4694 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-950 p-3 rounded-xl border border-zinc-800", children: [ 4695 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 text-[10px] uppercase block mb-0.5", children: t("analyze.dynamicRange") }), 4696 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-emerald-400 font-bold text-sm", children: [ 4697 stats.dynamicRangeDb, 4698 " dB" 4699 ] }) 4700 ] }), 4701 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-950 p-3 rounded-xl border border-zinc-800", children: [ 4702 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 text-[10px] uppercase block mb-0.5", children: t("analyze.dcOffsetL") }), 4703 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-300 font-bold text-sm", children: stats.dcOffsetL }) 4704 ] }) 4705 ] }) 4706 ] }), 4707 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end p-4 border-t border-zinc-800 bg-zinc-950/50", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 4708 "button", 4709 { 4710 onClick: () => setActiveModal(null), 4711 className: "px-5 py-2 bg-zinc-800 hover:bg-zinc-700 text-white text-xs font-semibold rounded-xl transition-colors", 4712 children: "Close" 4713 } 4714 ) }) 4715 ] }) }); 4716}; 4717const InsertSilenceModal = () => { 4718 const { insertSilence, setActiveModal } = useAudioEditor(); 4719 const { t } = useAudioEditorTranslation(); 4720 const [seconds, setSeconds] = reactExports.useState(1); 4721 const handleInsert = () => { 4722 if (seconds > 0) { 4723 insertSilence(seconds); 4724 setActiveModal(null); 4725 } 4726 }; 4727 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-50 bg-black/75 backdrop-blur-sm flex items-center justify-center p-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900 border border-zinc-800 rounded-2xl w-full max-w-sm shadow-2xl overflow-hidden flex flex-col", children: [ 4728 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between p-4 border-b border-zinc-800", children: [
4729 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-white text-base flex items-center gap-2", children: [ 4730 /* @__PURE__ */ jsxRuntimeExports.jsx(VolumeX, { className: "w-5 h-5 text-indigo-400" }), 4731 t("silenceModal.title") 4732 ] }), 4733 /* @__PURE__ */ jsxRuntimeExports.jsx( 4734 "button", 4735 { 4736 onClick: () => setActiveModal(null), 4737 className: "p-1 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800 transition-colors", 4738 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { className: "w-5 h-5" }) 4739 } 4740 ) 4741 ] }), 4742 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-6 space-y-4 text-xs text-zinc-300", children: [ 4743 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: t("silenceModal.prompt") }), 4744 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 4745 /* @__PURE__ */ jsxRuntimeExports.jsx( 4746 "input", 4747 { 4748 type: "number", 4749 min: "0.1", 4750 max: "60", 4751 step: "0.1", 4752 value: seconds, 4753 onChange: (e) => setSeconds(Math.max(0.1, parseFloat(e.target.value) || 0.1)), 4754 className: "flex-1 p-2.5 rounded-xl border border-zinc-700 bg-zinc-800 text-zinc-100 font-mono text-sm focus:border-indigo-500 focus:outline-none" 4755 } 4756 ),
4757 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-400", children: t("silenceModal.seconds") }) 4758 ] }), 4759 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex gap-2", children: [0.5, 1, 2, 5].map((s) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 4760 "button", 4761 { 4762 onClick: () => setSeconds(s), 4763 className: "px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg transition-colors text-xs", 4764 children: [ 4765 "+", 4766 s, 4767 "s" 4768 ] 4769 }, 4770 s 4771 )) }) 4772 ] }), 4773 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2 p-4 border-t border-zinc-800 bg-zinc-950/50", children: [ 4774 /* @__PURE__ */ jsxRuntimeExports.jsx( 4775 "button", 4776 { 4777 onClick: () => setActiveModal(null), 4778 className: "px-4 py-2 rounded-xl text-xs font-medium text-zinc-400 hover:text-zinc-200 transition-colors", 4779 children: t("effects.cancel") 4780 } 4781 ),
4782 /* @__PURE__ */ jsxRuntimeExports.jsx( 4783 "button", 4784 { 4785 onClick: handleInsert, 4786 className: "px-5 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold rounded-xl shadow-lg shadow-indigo-600/20 transition-transform active:scale-95", 4787 children: t("silenceModal.insert") 4788 } 4789 ) 4790 ] }) 4791 ] }) }); 4792}; 4793class MicRecorder { 4794 constructor() { 4795 this.stream = null; 4796 this.mediaRecorder = null; 4797 this.audioChunks = []; 4798 this.audioCtx = null; 4799 this.analyser = null; 4800 this.sourceNode = null; 4801 this.animId = null; 4802 this.isRecording = false; 4803 this.onVolumeUpdate = null; 4804 } 4805 async start() { 4806 this.audioChunks = []; 4807 const stream = await navigator.mediaDevices.getUserMedia({ 4808 audio: { 4809 echoCancellation: false, 4810 noiseSuppression: false, 4811 autoGainControl: false 4812 } 4813 }); 4814 this.stream = stream; 4815 this.mediaRecorder = new MediaRecorder(stream); 4816 this.mediaRecorder.ondataavailable = (e) => { 4817 if (e.data.size > 0) { 4818 this.audioChunks.push(e.data); 4819 } 4820 }; 4821 const AudioCtx = window.AudioContext || window.webkitAudioContext; 4822 this.audioCtx = new AudioCtx(); 4823 this.sourceNode = this.audioCtx.createMediaStreamSource(stream); 4824 this.analyser = this.audioCtx.createAnalyser(); 4825 this.analyser.fftSize = 512; 4826 this.sourceNode.connect(this.analyser); 4827 this.isRecording = true; 4828 this.mediaRecorder.start(100); 4829 this.startLevelLoop(); 4830 } 4831 startLevelLoop() { 4832 var _a; 4833 const data = new Float32Array(((_a = this.analyser) == null ? void 0 : _a.fftSize) || 512); 4834 const tick = () => { 4835 if (!this.isRecording || !this.analyser) return; 4836 this.analyser.getFloatTimeDomainData(data); 4837 let peak = 0; 4838 for (let i = 0; i < data.length; i++) { 4839 const abs = Math.abs(data[i]); 4840 if (abs > peak) peak = abs; 4841 } 4842 const db = peak > 1e-5 ? 20 * Math.log10(peak) : -100; 4843 if (this.onVolumeUpdate) { 4844 this.onVolumeUpdate(Math.round(db * 10) / 10); 4845 } 4846 this.animId = requestAnimationFrame(tick); 4847 }; 4848 this.animId = requestAnimationFrame(tick); 4849 } 4850 async stop() { 4851 this.isRecording = false; 4852 if (this.animId !== null) { 4853 cancelAnimationFrame(this.animId); 4854 this.animId = null; 4855 } 4856 return new Promise((resolve, reject) => { 4857 if (!this.mediaRecorder) { 4858 reject(new Error("Recorder not initialized")); 4859 return; 4860 } 4861 this.mediaRecorder.onstop = async () => { 4862 var _a; 4863 try { 4864 const blob = new Blob(this.audioChunks, { type: ((_a = this.mediaRecorder) == null ? void 0 : _a.mimeType) || "audio/webm" }); 4865 const arrayBuffer = await blob.arrayBuffer(); 4866 const decodeCtx = new (window.AudioContext || window.webkitAudioContext)(); 4867 const decoded = await decodeCtx.decodeAudioData(arrayBuffer); 4868 await decodeCtx.close(); 4869 this.cleanup(); 4870 resolve(decoded); 4871 } catch (err) { 4872 this.cleanup(); 4873 reject(err); 4874 } 4875 }; 4876 this.mediaRecorder.stop(); 4877 }); 4878 } 4879 cleanup() { 4880 if (this.stream) { 4881 this.stream.getTracks().forEach((t) => t.stop()); 4882 this.stream = null; 4883 } 4884 if (this.audioCtx && this.audioCtx.state !== "closed") { 4885 try { 4886 this.audioCtx.close(); 4887 } catch { 4888 } 4889 this.audioCtx = null; 4890 } 4891 } 4892} 4893const RecordModal = () => { 4894 const { audioBuffer, cursor, loadBuffer, applyDSP, setActiveModal, engine } = useAudioEditor(); 4895 const { t } = useAudioEditorTranslation(); 4896 const recorderRef = reactExports.useRef(null); 4897 const [isRecording, setIsRecording] = reactExports.useState(false); 4898 const [recordSec, setRecordSec] = reactExports.useState(0); 4899 const [recordedBuffer, setRecordedBuffer] = reactExports.useState(null); 4900 const [volumeLevel, setVolumeLevel] = reactExports.useState(-100); 4901 const [errorMsg, setErrorMsg] = reactExports.useState(null); 4902 const timerRef = reactExports.useRef(null); 4903 reactExports.useEffect(() => { 4904 return () => { 4905 if (timerRef.current) clearInterval(timerRef.current); 4906 }; 4907 }, []); 4908 const handleStartRecording = async () => { 4909 setErrorMsg(null); 4910 try { 4911 const recorder = new MicRecorder(); 4912 recorderRef.current = recorder; 4913 recorder.onVolumeUpdate = (db) => { 4914 setVolumeLevel(db); 4915 }; 4916 await recorder.start(); 4917 setIsRecording(true); 4918 setRecordSec(0); 4919 setRecordedBuffer(null); 4920 const startTime = Date.now(); 4921 timerRef.current = setInterval(() => { 4922 setRecordSec((Date.now() - startTime) / 1e3); 4923 }, 100); 4924 } catch (err) { 4925 console.error("Microphone access denied:", err); 4926 setErrorMsg("Microphone access denied or unavailable. Please check browser permissions."); 4927 } 4928 }; 4929 const handleStopRecording = async () => { 4930 if (!recorderRef.current || !isRecording) return; 4931 if (timerRef.current) clearInterval(timerRef.current); 4932 try { 4933 const buffer = await recorderRef.current.stop(); 4934 setRecordedBuffer(buffer); 4935 setIsRecording(false); 4936 } catch (err) { 4937 console.error("Recording failed:", err); 4938 setErrorMsg("Failed to process recorded audio."); 4939 setIsRecording(false); 4940 } 4941 }; 4942 const handleReplace = () => { 4943 if (!recordedBuffer) return; 4944 loadBuffer(recordedBuffer, `Microphone Recording ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}.wav`); 4945 setActiveModal(null); 4946 }; 4947 const handleInsert = () => { 4948 if (!recordedBuffer) return; 4949 if (!audioBuffer) { 4950 handleReplace(); 4951 return; 4952 } 4953 const ctx = engine.getContext(); 4954 applyDSP((buf) => { 4955 return AudioBufferUtils.insertBuffer(ctx, buf, recordedBuffer, cursor); 4956 }); 4957 setActiveModal(null); 4958 }; 4959 const formatTimer = (sec) => { 4960 const mins = Math.floor(sec / 60); 4961 const s = Math.floor(sec % 60); 4962 const ms = Math.floor(sec % 1 * 10); 4963 return `${mins.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}.${ms}`; 4964 }; 4965 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-50 bg-black/75 backdrop-blur-sm flex items-center justify-center p-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900 border border-zinc-800 rounded-2xl w-full max-w-md
4965shadow-2xl overflow-hidden flex flex-col", children: [ 4966 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between p-4 border-b border-zinc-800", children: [ 4967 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-white text-base flex items-center gap-2", children: [ 4968 /* @__PURE__ */ jsxRuntimeExports.jsx(Mic, { className: "w-5 h-5 text-red-500" }), 4969 t("recordModal.title") 4970 ] }), 4971 /* @__PURE__ */ jsxRuntimeExports.jsx( 4972 "button", 4973 { 4974 onClick: () => { 4975 if (isRecording) handleStopRecording(); 4976 setActiveModal(null); 4977 }, 4978 className: "p-1 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800 transition-colors", 4979 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { className: "w-5 h-5" }) 4980 } 4981 ) 4982 ] }), 4983 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-6 space-y-5 text-center", children: [ 4984 errorMsg ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-3 bg-red-500/10 border border-red-500/30 rounded-xl text-red-400 text-xs flex items-center gap-2 text-left", children: [
4985 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { className: "w-4 h-4 flex-shrink-0" }), 4986 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: errorMsg }) 4987 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-400 text-xs", children: t("recordModal.prompt") }), 4988 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-3xl font-black tracking-wider text-white", children: formatTimer(recordSec) }), 4989 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full bg-zinc-950 h-3 rounded-full overflow-hidden border border-zinc-800 p-0.5", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 4990 "div", 4991 { 4992 className: `h-full rounded-full transition-all duration-75 ${volumeLevel > -3 ? "bg-red-500" : volumeLevel > -12 ? "bg-amber-500" : "bg-emerald-500"}`, 4993 style: { width: `${Math.max(0, Math.min(100, (volumeLevel + 60) / 60 * 100))}%` } 4994 } 4995 ) }), 4996 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-center", children: isRecording ? /* @__P
4996URE__ */ jsxRuntimeExports.jsxs( 4997 "button", 4998 { 4999 onClick: handleStopRecording, 5000 className: "px-6 py-3 bg-red-600 hover:bg-red-500 text-white font-bold rounded-2xl shadow-xl shadow-red-600/30 flex items-center gap-2 transition-transform active:scale-95 animate-pulse", 5001 children: [ 5002 /* @__PURE__ */ jsxRuntimeExports.jsx(Square, { className: "w-5 h-5 fill-current" }), 5003 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("recordModal.stop") }) 5004 ] 5005 } 5006 ) : /* @__PURE__ */ jsxRuntimeExports.jsxs( 5007 "button", 5008 { 5009 onClick: handleStartRecording, 5010 className: "px-6 py-3 bg-indigo-600 hover:bg-indigo-500 text-white font-bold rounded-2xl shadow-xl shadow-indigo-600/30 flex items-center gap-2 transition-transform active:scale-95", 5011 children: [ 5012 /* @__PURE__ */ jsxRuntimeExports.jsx(Mic, { className: "w-5 h-5" }), 5013 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("recordModal.start") }) 5014 ] 5015 } 5016 ) }), 5017 recordedBuffer && !isRecording && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "pt-3 border-t border-zinc-800 flex flex-col gap-2", children: [
5018 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs text-zinc-400", children: [ 5019 "Recorded ", 5020 recordedBuffer.duration.toFixed(2), 5021 "s. What would you like to do?" 5022 ] }), 5023 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 justify-center", children: [ 5024 audioBuffer && /* @__PURE__ */ jsxRuntimeExports.jsx( 5025 "button", 5026 { 5027 onClick: handleInsert, 5028 className: "px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-zinc-200 text-xs font-semibold rounded-xl transition-colors", 5029 children: t("recordModal.insert") 5030 } 5031 ), 5032 /* @__PURE__ */ jsxRuntimeExports.jsx( 5033 "button", 5034 { 5035 onClick: handleReplace, 5036 className: "px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-semibold rounded-xl transition-colors", 5037 children: t("recordModal.replace") 5038 } 5039 ) 5040 ] }) 5041 ] }) 5042 ] }) 5043 ] }) }); 5044}; 5045const AudioEditorWorkspace = ({ onNavigate }) => { 5046 const { 5047 audioBuffer, 5048 selection, 5049 canUndo, 5050 canRedo, 5051 isPlaying, 5052 activeModal, 5053 isProcessing, 5054 processingMessage, 5055 play, 5056 pause, 5057 stop, 5058 seek, 5059 undo, 5060 redo, 5061 cut, 5062 copy, 5063 paste, 5064 deleteSelection, 5065 cropSelection, 5066 selectAll, 5067 setSelection, 5068 loadAudioFile, 5069 loadDemo, 5070 setActiveModal, 5071 setActiveEffect 5072 } = useAudioEditor(); 5073 const { t } = useAudioEditorTranslation(); 5074 const [isDragOver, setIsDragOver] = reactExports.useState(false); 5075 reactExports.useEffect(() => { 5076 const handleKeyDown = (e) => { 5077 var _a; 5078 if (["INPUT", "TEXTAREA", "SELECT"].includes((_a = e.target) == null ? void 0 : _a.tagName)) { 5079 return; 5080 } 5081 if (e.code === "Space") { 5082 e.preventDefault(); 5083 if (isPlaying) { 5084 pause(); 5085 } else { 5086 play(); 5087 } 5088 } else if (e.code === "Home") { 5089 e.preventDefault(); 5090 seek(0); 5091 } else if (e.code === "End") { 5092 e.preventDefault(); 5093 if (audioBuffer) seek(audioBuffer.duration); 5094 } else if (e.key === "Delete" || e.key === "Backspace") { 5095 if (selection && selection.end > selection.start) { 5096 e.preventDefault(); 5097 deleteSelection(); 5098 } 5099 } else if (e.ctrlKey || e.metaKey) { 5100 if (e.key.toLowerCase() === "z") { 5101 e.preventDefault(); 5102 if (e.shiftKey) { 5103 redo(); 5104 } else { 5105 undo(); 5106 } 5107 } else if (e.key.toLowerCase() === "y") { 5108 e.preventDefault(); 5109 redo(); 5110 } else if (e.key.toLowerCase() === "x") { 5111 e.preventDefault(); 5112 cut(); 5113 } else if (e.key.toLowerCase() === "c") { 5114 e.preventDefault(); 5115 copy(); 5116 } else if (e.key.toLowerCase() === "v") { 5117 e.preventDefault(); 5118 paste(); 5119 } else if (e.key.toLowerCase() === "a") { 5120 e.preventDefault(); 5121 selectAll(); 5122 } else if (e.key.toLowerCase() === "t") { 5123 e.preventDefault(); 5124 cropSelection(); 5125 } 5126 } else if (e.key === "Escape") { 5127 if (activeModal) {
5128 setActiveModal(null); 5129 } else { 5130 setSelection(null); 5131 } 5132 } 5133 }; 5134 window.addEventListener("keydown", handleKeyDown); 5135 return () => window.removeEventListener("keydown", handleKeyDown); 5136 }, [ 5137 activeModal, 5138 audioBuffer, 5139 copy, 5140 cropSelection, 5141 cut, 5142 deleteSelection, 5143 isPlaying, 5144 paste, 5145 pause, 5146 play, 5147 redo, 5148 seek, 5149 selectAll, 5150 selection, 5151 setActiveModal, 5152 setSelection, 5153 undo 5154 ]); 5155 const handleDragOver = (e) => { 5156 e.preventDefault(); 5157 setIsDragOver(true); 5158 }; 5159 const handleDragLeave = (e) => { 5160 e.preventDefault(); 5161 setIsDragOver(false); 5162 }; 5163 const handleDrop = (e) => { 5164 var _a; 5165 e.preventDefault(); 5166 setIsDragOver(false); 5167 const file = (_a = e.dataTransfer.files) == null ? void 0 : _a[0]; 5168 if (file) { 5169 loadAudioFile(file); 5170 } 5171 }; 5172 const hasSelection = !!(selection && selection.end > selection.start); 5173 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 5174 "div", 5175 { 5176 className: "flex flex-col h-screen w-full bg-zinc-950 text-white overflow-hidden relative select-none", 5177 onDragOver: handleDragOver, 5178 onDragLeave: handleDragLeave, 5179 onDrop: handleDrop, 5180 children: [
5181 /* @__PURE__ */ jsxRuntimeExports.jsx(EditorMenuBar, { onNavigate }), 5182 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-none h-11 bg-zinc-900/90 border-b border-zinc-800 px-3 flex items-center justify-between gap-2 text-zinc-300", children: [ 5183 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 5184 /* @__PURE__ */ jsxRuntimeExports.jsx( 5185 "button", 5186 { 5187 onClick: undo, 5188 disabled: !canUndo, 5189 className: "p-1.5 rounded hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none text-zinc-400 hover:text-zinc-100 transition-colors", 5190 title: "Undo (Ctrl+Z)", 5191 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Undo2, { className: "w-4 h-4" }) 5192 } 5193 ), 5194 /* @__PURE__ */ jsxRuntimeExports.jsx( 5195 "button", 5196 { 5197 onClick: redo, 5198 disabled: !canRedo, 5199 className: "p-1.5 rounded hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none text-zinc-400 hover:text-zinc-100 transition-colors", 5200 title: "Redo (Ctrl+Y)", 5201 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Redo2, { className: "w-4 h-4" }) 5202 } 5203 ),
5204 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-5 w-px bg-zinc-800 mx-1" }), 5205 /* @__PURE__ */ jsxRuntimeExports.jsx( 5206 "button", 5207 { 5208 onClick: cut, 5209 disabled: !hasSelection, 5210 className: "p-1.5 rounded hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none text-zinc-400 hover:text-zinc-100 transition-colors", 5211 title: "Cut Selection (Ctrl+X)", 5212 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Scissors, { className: "w-4 h-4" }) 5213 } 5214 ), 5215 /* @__PURE__ */ jsxRuntimeExports.jsx( 5216 "button", 5217 { 5218 onClick: copy, 5219 disabled: !hasSelection, 5220 className: "p-1.5 rounded hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none text-zinc-400 hover:text-zinc-100 transition-colors", 5221 title: "Copy Selection (Ctrl+C)", 5222 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { className: "w-4 h-4" }) 5223 } 5224 ),
5225 /* @__PURE__ */ jsxRuntimeExports.jsx( 5226 "button", 5227 { 5228 onClick: paste, 5229 disabled: !audioBuffer, 5230 className: "p-1.5 rounded hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none text-zinc-400 hover:text-zinc-100 transition-colors", 5231 title: "Paste (Ctrl+V)", 5232 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ClipboardPaste, { className: "w-4 h-4" }) 5233 } 5234 ), 5235 /* @__PURE__ */ jsxRuntimeExports.jsx( 5236 "button", 5237 { 5238 onClick: deleteSelection, 5239 disabled: !hasSelection, 5240 className: "p-1.5 rounded hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none text-zinc-400 hover:text-red-400 transition-colors", 5241 title: "Delete Selection (Del)", 5242 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "w-4 h-4" }) 5243 } 5244 ), 5245 /* @__P
5245URE__ */ jsxRuntimeExports.jsx( 5246 "button", 5247 { 5248 onClick: cropSelection, 5249 disabled: !hasSelection, 5250 className: "p-1.5 rounded hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none text-zinc-400 hover:text-emerald-400 transition-colors", 5251 title: "Crop to Selection (Ctrl+T)", 5252 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Crop, { className: "w-4 h-4" }) 5253 } 5254 ), 5255 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-5 w-px bg-zinc-800 mx-1" }), 5256 /* @__PURE__ */ jsxRuntimeExports.jsx( 5257 "button", 5258 { 5259 onClick: () => { 5260 setActiveEffect("normalize"); 5261 setActiveModal("effects"); 5262 }, 5263 disabled: !audioBuffer, 5264 className: "px-2.5 py-1 rounded bg-zinc-800 hover:bg-zinc-700 disabled:opacity-30 disabled:pointer-events-none text-xs font-medium text-zinc-300 transition-colors", 5265 children: "Normalize" 5266 } 5267 ),
5268 /* @__PURE__ */ jsxRuntimeExports.jsx( 5269 "button", 5270 { 5271 onClick: () => { 5272 setActiveEffect("eq"); 5273 setActiveModal("effects"); 5274 }, 5275 disabled: !audioBuffer, 5276 className: "px-2.5 py-1 rounded bg-zinc-800 hover:bg-zinc-700 disabled:opacity-30 disabled:pointer-events-none text-xs font-medium text-zinc-300 transition-colors hidden sm:inline-block", 5277 children: "EQ" 5278 } 5279 ), 5280 /* @__PURE__ */ jsxRuntimeExports.jsx( 5281 "button", 5282 { 5283 onClick: () => { 5284 setActiveEffect("compressor"); 5285 setActiveModal("effects"); 5286 }, 5287 disabled: !audioBuffer, 5288 className: "px-2.5 py-1 rounded bg-zinc-800 hover:bg-zinc-700 disabled:opacity-30 disabled:pointer-events-none text-xs font-medium text-zinc-300 transition-colors hidden md:inline-block", 5289 children: "Compressor" 5290 } 5291 ),
5292 /* @__PURE__ */ jsxRuntimeExports.jsx( 5293 "button", 5294 { 5295 onClick: () => { 5296 setActiveEffect("vocalRemover"); 5297 setActiveModal("effects"); 5298 }, 5299 disabled: !audioBuffer || audioBuffer.numberOfChannels < 2, 5300 className: "px-2.5 py-1 rounded bg-zinc-800 hover:bg-zinc-700 disabled:opacity-30 disabled:pointer-events-none text-xs font-medium text-zinc-300 transition-colors hidden lg:inline-block", 5301 children: "Vocal Remover" 5302 } 5303 ) 5304 ] }), 5305 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 5306 /* @__PURE__ */ jsxRuntimeExports.jsxs( 5307 "button", 5308 { 5309 onClick: () => setActiveModal("spectrum"), 5310 disabled: !audioBuffer, 5311 className: "p-1.5 rounded hover:bg-zinc-800 disabled:opacity-30 text-zinc-400 hover:text-emerald-400 transition-colors flex items-center gap-1 text-xs", 5312 title: "Open FFT Spectrum Analyzer", 5313 children: [
5314 /* @__PURE__ */ jsxRuntimeExports.jsx(Activity, { className: "w-4 h-4 text-emerald-400" }), 5315 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden xl:inline", children: "Spectrum" }) 5316 ] 5317 } 5318 ), 5319 /* @__PURE__ */ jsxRuntimeExports.jsx(VUMeter, {}) 5320 ] }) 5321 ] }), 5322 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 flex flex-col relative overflow-hidden", children: audioBuffer ? /* @__PURE__ */ jsxRuntimeExports.jsx(WaveformCanvas, {}) : ( 5323 /* Empty State / Welcome Dropzone */ 5324 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col items-center justify-center p-8 bg-zinc-950 text-center select-none", children: [ 5325 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-20 h-20 rounded-3xl bg-indigo-500/10 border border-indigo-500/20 flex items-center justify-center mb-6 shadow-2xl", children: /* @__PURE__ */ jsxRuntimeExports.jsx(FileHeadphone, { className: "w-10 h-10 text-indigo-400 animate-pulse" }) }),
5326 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-2xl font-bold text-white mb-2", children: t("app.title") }), 5327 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-400 max-w-md text-sm mb-6", children: t("app.subtitle") }), 5328 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center justify-center gap-3 mb-6", children: [ 5329 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "px-5 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white font-semibold text-xs rounded-xl shadow-lg shadow-indigo-600/25 cursor-pointer flex items-center gap-2 transition-transform active:scale-95", children: [ 5330 /* @__PURE__ */ jsxRuntimeExports.jsx(FolderOpen, { className: "w-4 h-4" }), 5331 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("file.open") }), 5332 /* @__PURE__ */ jsxRuntimeExports.jsx( 5333 "input", 5334 { 5335 type: "file", 5336 accept: "audio/*,.wav,.mp3,.ogg,.flac,.m4a,.aac,.opus,.webm", 5337 onChange: (e) => { 5338 var _a; 5339 const f = (_a = e.target.files) == null ? void 0 : _a[0]; 5340 if (f) loadAudioFile(f); 5341 }, 5342 className: "hidden" 5343 } 5344 ) 5345 ] }),
5346 /* @__PURE__ */ jsxRuntimeExports.jsxs( 5347 "button", 5348 { 5349 onClick: loadDemo, 5350 className: "px-5 py-2.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-200 font-semibold text-xs rounded-xl border border-zinc-700 flex items-center gap-2 transition-transform active:scale-95", 5351 children: [ 5352 /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { className: "w-4 h-4 text-emerald-400" }), 5353 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("file.demo") }) 5354 ] 5355 } 5356 ), 5357 /* @__PURE__ */ jsxRuntimeExports.jsxs( 5358 "button", 5359 { 5360 onClick: () => setActiveModal("record"), 5361 className: "px-5 py-2.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-200 font-semibold text-xs rounded-xl border border-zinc-700 flex items-center gap-2 transition-transform active:scale-95", 5362 children: [ 5363 /* @__P
5363URE__ */ jsxRuntimeExports.jsx(Activity, { className: "w-4 h-4 text-red-400" }), 5364 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("file.record") }) 5365 ] 5366 } 5367 ) 5368 ] }), 5369 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500 text-xs", children: t("app.supportedFormats") }), 5370 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-600 text-[11px] mt-1", children: t("app.zeroServerNotice") }) 5371 ] }) 5372 ) }), 5373 /* @__PURE__ */ jsxRuntimeExports.jsx(TransportBar, {}), 5374 isDragOver && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute inset-0 z-50 bg-indigo-950/80 backdrop-blur-md flex flex-col items-center justify-center border-4 border-dashed border-indigo-400 p-8", children: [ 5375 /* @__PURE__ */ jsxRuntimeExports.jsx(CloudUpload, { className: "w-16 h-16 text-indigo-400 mb-4 animate-bounce" }), 5376 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-xl font-bold text-white mb-1", children: "Drop audio file to open" }),
5377 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-indigo-200 text-xs", children: t("app.supportedFormats") }) 5378 ] }), 5379 isProcessing && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 z-50 bg-black/60 backdrop-blur-xs flex flex-col items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-6 bg-zinc-900 border border-zinc-800 rounded-2xl shadow-2xl flex flex-col items-center gap-3", children: [ 5380 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-8 h-8 border-3 border-indigo-500 border-t-transparent rounded-full animate-spin" }), 5381 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-medium text-zinc-200", children: processingMessage || "Processing in browser..." }) 5382 ] }) }), 5383 activeModal === "effects" && /* @__PURE__ */ jsxRuntimeExports.jsx(EffectsModal, {}), 5384 activeModal === "export" && /* @__PURE__ */ jsxRuntimeExports.jsx(ExportModal, {}), 5385 activeModal === "stats" && /* @__PURE__ */ jsxRuntimeExports.jsx(AudioStatsModal, {}), 5386 activeModal === "silence" && /* @__PURE__ */ jsxRuntimeExports.jsx(InsertSilenceModal, {}), 5387 activeModal === "record" && /* @__PURE__ */ jsxRuntimeExports.jsx(RecordModal, {}), 5388 activeModal === "spectrum" && /* @__PURE__ */ jsxRuntimeExports.jsx(SpectrumAnalyzer, {}) 5389 ] 5390 } 5391 ); 5392}; 5393function AudioEditorPage({ onNavigate }) { 5394 return /* @__PURE__ */ jsxRuntimeExports.jsx(AudioEditorProvider, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(AudioEditorWorkspace, { onNavigate }) }); 5395} 5396export { 5397 AudioEditorPage as default 5398};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.