PageSourceSearch

https://www.hamienet.com/assets/AudioEditorPage-Doje0cBS.js

js hamienet.com collected 2026-10-02 05:00:19 UTC 227,625 bytes, 5,398 lines download raw bytes

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.