sirsan69 commited on
Commit
af77333
·
verified ·
1 Parent(s): a595fec

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +992 -1078
index.html CHANGED
@@ -1,1101 +1,1015 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>PitchLab - Advanced Audio Pitch Editor</title>
7
- <style>
8
- * {
9
- margin: 0;
10
- padding: 0;
11
- box-sizing: border-box;
12
- }
13
-
14
- :root {
15
- --bg-primary: #1a1a1a;
16
- --bg-secondary: #242424;
17
- --bg-tertiary: #2d2d2d;
18
- --text-primary: #ffffff;
19
- --text-secondary: #b0b0b0;
20
- --accent-blue: #4a9eff;
21
- --accent-purple: #8b5cf6;
22
- --accent-green: #10b981;
23
- --accent-orange: #f59e0b;
24
- --accent-red: #ef4444;
25
- --border-color: #3a3a3a;
26
- --hover-bg: #353535;
27
- }
28
-
29
- body {
30
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
31
- background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
32
- color: var(--text-primary);
33
- height: 100vh;
34
- overflow: hidden;
35
- display: flex;
36
- flex-direction: column;
37
- }
38
-
39
- /* Header */
40
- .header {
41
- background: var(--bg-secondary);
42
- border-bottom: 1px solid var(--border-color);
43
- padding: 12px 20px;
44
- display: flex;
45
- justify-content: space-between;
46
- align-items: center;
47
- flex-shrink: 0;
48
- }
49
-
50
- .logo {
51
- display: flex;
52
- align-items: center;
53
- gap: 12px;
54
- font-size: 20px;
55
- font-weight: 600;
56
- }
57
-
58
- .logo-icon {
59
- width: 32px;
60
- height: 32px;
61
- background: linear-gradient(135deg, var(--accent-purple), var(--accent-blue));
62
- border-radius: 8px;
63
- display: flex;
64
- align-items: center;
65
- justify-content: center;
66
- }
67
-
68
- .logo-icon::before {
69
- content: "♪";
70
- font-size: 18px;
71
- }
72
-
73
- .header-controls {
74
- display: flex;
75
- gap: 12px;
76
- align-items: center;
77
- }
78
-
79
- /* Toolbar */
80
- .toolbar {
81
- background: var(--bg-tertiary);
82
- border-bottom: 1px solid var(--border-color);
83
- padding: 8px 20px;
84
- display: flex;
85
- gap: 16px;
86
- align-items: center;
87
- flex-wrap: wrap;
88
- }
89
-
90
- .tool-group {
91
- display: flex;
92
- gap: 8px;
93
- padding: 0 12px;
94
- border-right: 1px solid var(--border-color);
95
- }
96
-
97
- .tool-group:last-child {
98
- border-right: none;
99
- }
100
-
101
- .tool-btn {
102
- background: var(--bg-secondary);
103
- border: 1px solid var(--border-color);
104
- color: var(--text-secondary);
105
- padding: 8px 12px;
106
- border-radius: 6px;
107
- cursor: pointer;
108
- transition: all 0.2s;
109
- display: flex;
110
- align-items: center;
111
- gap: 6px;
112
- font-size: 13px;
113
- }
114
-
115
- .tool-btn:hover {
116
- background: var(--hover-bg);
117
- color: var(--text-primary);
118
- transform: translateY(-1px);
119
- }
120
-
121
- .tool-btn.active {
122
- background: var(--accent-blue);
123
- color: white;
124
- border-color: var(--accent-blue);
125
- }
126
-
127
- /* Main Content */
128
- .main-content {
129
- flex: 1;
130
- display: flex;
131
- flex-direction: column;
132
- overflow: hidden;
133
- }
134
-
135
- /* Timeline and Canvas Container */
136
- .editor-container {
137
- flex: 1;
138
- position: relative;
139
- background: var(--bg-primary);
140
- overflow: hidden;
141
- }
142
-
143
- .timeline {
144
- position: absolute;
145
- top: 0;
146
- left: 0;
147
- right: 0;
148
- height: 40px;
149
- background: var(--bg-tertiary);
150
- border-bottom: 1px solid var(--border-color);
151
- display: flex;
152
- align-items: center;
153
- padding: 0 10px;
154
- z-index: 10;
155
- }
156
-
157
- .time-ruler {
158
- width: 100%;
159
- height: 20px;
160
- position: relative;
161
- }
162
-
163
- .time-mark {
164
- position: absolute;
165
- top: 0;
166
- transform: translateX(-50%);
167
- font-size: 11px;
168
- color: var(--text-secondary);
169
- }
170
-
171
- .time-mark::before {
172
- content: '';
173
- position: absolute;
174
- top: 16px;
175
- left: 50%;
176
- width: 1px;
177
- height: 4px;
178
- background: var(--text-secondary);
179
- }
180
-
181
- /* Pitch Editor Canvas */
182
- #pitchCanvas {
183
- position: absolute;
184
- top: 40px;
185
- left: 0;
186
- cursor: crosshair;
187
- }
188
-
189
- /* Playback Controls */
190
- .playback-controls {
191
- background: var(--bg-secondary);
192
- border-top: 1px solid var(--border-color);
193
- padding: 16px 20px;
194
- display: flex;
195
- align-items: center;
196
- gap: 20px;
197
- }
198
-
199
- .transport {
200
- display: flex;
201
- gap: 8px;
202
- }
203
-
204
- .transport-btn {
205
- width: 40px;
206
- height: 40px;
207
- border-radius: 50%;
208
- background: var(--bg-tertiary);
209
- border: 1px solid var(--border-color);
210
- color: var(--text-primary);
211
- cursor: pointer;
212
- display: flex;
213
- align-items: center;
214
- justify-content: center;
215
- transition: all 0.2s;
216
- }
217
-
218
- .transport-btn:hover {
219
- background: var(--hover-bg);
220
- transform: scale(1.05);
221
- }
222
-
223
- .transport-btn.play {
224
- width: 48px;
225
- height: 48px;
226
- background: var(--accent-green);
227
- border-color: var(--accent-green);
228
- }
229
-
230
- .transport-btn.play:hover {
231
- background: #059669;
232
- }
233
-
234
- /* Time Display */
235
- .time-display {
236
- display: flex;
237
- align-items: center;
238
- gap: 8px;
239
- padding: 8px 16px;
240
- background: var(--bg-tertiary);
241
- border-radius: 8px;
242
- font-size: 14px;
243
- font-variant-numeric: tabular-nums;
244
- }
245
-
246
- .time-separator {
247
- color: var(--text-secondary);
248
- }
249
-
250
- /* Pitch Display */
251
- .pitch-display {
252
- display: flex;
253
- align-items: center;
254
- gap: 12px;
255
- margin-left: auto;
256
- }
257
-
258
- .pitch-info {
259
- padding: 8px 16px;
260
- background: var(--bg-tertiary);
261
- border-radius: 8px;
262
- font-size: 14px;
263
- }
264
-
265
- .pitch-label {
266
- color: var(--text-secondary);
267
- margin-right: 8px;
268
- }
269
-
270
- .pitch-value {
271
- color: var(--accent-blue);
272
- font-weight: 600;
273
- }
274
-
275
- /* File Upload */
276
- .file-input-wrapper {
277
- position: relative;
278
- }
279
-
280
- .file-input {
281
- display: none;
282
- }
283
-
284
- /* Side Panel */
285
- .side-panel {
286
- position: absolute;
287
- right: 0;
288
- top: 40px;
289
- width: 280px;
290
- height: calc(100% - 40px);
291
- background: var(--bg-secondary);
292
- border-left: 1px solid var(--border-color);
293
- padding: 20px;
294
- transform: translateX(100%);
295
- transition: transform 0.3s;
296
- z-index: 20;
297
- overflow-y: auto;
298
- }
299
-
300
- .side-panel.open {
301
- transform: translateX(0);
302
- }
303
-
304
- .panel-section {
305
- margin-bottom: 24px;
306
- }
307
-
308
- .panel-title {
309
- font-size: 14px;
310
- font-weight: 600;
311
- margin-bottom: 12px;
312
- color: var(--text-primary);
313
- }
314
-
315
- .slider-control {
316
- margin-bottom: 16px;
317
- }
318
-
319
- .slider-label {
320
- display: flex;
321
- justify-content: space-between;
322
- margin-bottom: 8px;
323
- font-size: 13px;
324
- color: var(--text-secondary);
325
- }
326
-
327
- .slider {
328
- width: 100%;
329
- height: 4px;
330
- background: var(--bg-tertiary);
331
- border-radius: 2px;
332
- outline: none;
333
- -webkit-appearance: none;
334
- }
335
-
336
- .slider::-webkit-slider-thumb {
337
- -webkit-appearance: none;
338
- appearance: none;
339
- width: 16px;
340
- height: 16px;
341
- background: var(--accent-blue);
342
- border-radius: 50%;
343
- cursor: pointer;
344
- }
345
-
346
- .slider::-moz-range-thumb {
347
- width: 16px;
348
- height: 16px;
349
- background: var(--accent-blue);
350
- border-radius: 50%;
351
- cursor: pointer;
352
- border: none;
353
- }
354
-
355
- /* Loading Overlay */
356
- .loading-overlay {
357
- position: fixed;
358
- top: 0;
359
- left: 0;
360
- right: 0;
361
- bottom: 0;
362
- background: rgba(0, 0, 0, 0.8);
363
- display: none;
364
- align-items: center;
365
- justify-content: center;
366
- z-index: 1000;
367
- }
368
-
369
- .loading-overlay.active {
370
- display: flex;
371
- }
372
-
373
- .loading-spinner {
374
- width: 60px;
375
- height: 60px;
376
- border: 3px solid var(--border-color);
377
- border-top-color: var(--accent-blue);
378
- border-radius: 50%;
379
- animation: spin 1s linear infinite;
380
- }
381
-
382
- @keyframes spin {
383
- to { transform: rotate(360deg); }
384
- }
385
-
386
- /* Tooltip */
387
- .tooltip {
388
- position: absolute;
389
- background: var(--bg-tertiary);
390
- border: 1px solid var(--border-color);
391
- padding: 8px 12px;
392
- border-radius: 6px;
393
- font-size: 12px;
394
- pointer-events: none;
395
- z-index: 100;
396
- opacity: 0;
397
- transition: opacity 0.2s;
398
- }
399
-
400
- .tooltip.visible {
401
- opacity: 1;
402
- }
403
-
404
- /* Responsive */
405
- @media (max-width: 768px) {
406
- .toolbar {
407
- padding: 8px 12px;
408
- }
409
-
410
- .tool-group {
411
- padding: 0 8px;
412
- }
413
-
414
- .tool-btn {
415
- padding: 6px 8px;
416
- font-size: 12px;
417
- }
418
-
419
- .side-panel {
420
- width: 100%;
421
- }
422
-
423
- .header-controls {
424
- gap: 8px;
425
- }
426
- }
427
-
428
- /* Custom Scrollbar */
429
- ::-webkit-scrollbar {
430
- width: 8px;
431
- height: 8px;
432
- }
433
-
434
- ::-webkit-scrollbar-track {
435
- background: var(--bg-primary);
436
- }
437
-
438
- ::-webkit-scrollbar-thumb {
439
- background: var(--bg-tertiary);
440
- border-radius: 4px;
441
- }
442
-
443
- ::-webkit-scrollbar-thumb:hover {
444
- background: var(--hover-bg);
445
- }
446
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
  </head>
448
  <body>
449
- <!-- Header -->
450
- <header class="header">
451
- <div class="logo">
452
- <div class="logo-icon"></div>
453
- <span>PitchLab</span>
454
- <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" style="color: var(--text-secondary); font-size: 12px; margin-left: 8px;">Built with anycoder</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
  </div>
456
- <div class="header-controls">
457
- <button class="tool-btn" onclick="toggleSidePanel()">
458
- <span>⚙️</span> Settings
459
- </button>
460
- <button class="tool-btn" onclick="exportAudio()">
461
- <span>💾</span> Export
462
- </button>
 
 
 
 
 
 
 
 
 
 
463
  </div>
464
- </header>
465
 
466
- <!-- Toolbar -->
467
- <div class="toolbar">
468
- <div class="tool-group">
469
- <button class="tool-btn active" data-tool="select" onclick="selectTool('select')">
470
- <span>↖️</span> Select
471
- </button>
472
- <button class="tool-btn" data-tool="pitch" onclick="selectTool('pitch')">
473
- <span>🎵</span> Pitch
474
- </button>
475
- <button class="tool-btn" data-tool="time" onclick="selectTool('time')">
476
- <span>⏱️</span> Time
477
  </button>
478
- </div>
479
- <div class="tool-group">
480
- <button class="tool-btn" onclick="resetView()">
481
- <span>🔄</span> Reset View
482
  </button>
483
- <button class="tool-btn" onclick="zoomIn()">
484
- <span>🔍+</span> Zoom In
485
  </button>
486
- <button class="tool-btn" onclick="zoomOut()">
487
- <span>🔍-</span> Zoom Out
488
  </button>
489
- </div>
490
- <div class="tool-group">
491
- <div class="file-input-wrapper">
492
- <button class="tool-btn" onclick="document.getElementById('audioFile').click()">
493
- <span>📁</span> Load Audio
494
- </button>
495
- <input type="file" id="audioFile" class="file-input" accept="audio/*" onchange="loadAudioFile(event)">
496
- </div>
497
- <button class="tool-btn" onclick="clearAll()">
498
- <span>🗑️</span> Clear
499
  </button>
 
500
  </div>
501
- </div>
502
 
503
- <!-- Main Editor -->
504
- <div class="main-content">
505
- <div class="editor-container">
506
- <!-- Timeline -->
507
- <div class="timeline">
508
- <div class="time-ruler" id="timeRuler"></div>
509
- </div>
510
-
511
- <!-- Pitch Canvas -->
512
- <canvas id="pitchCanvas"></canvas>
513
-
514
- <!-- Side Panel -->
515
- <div class="side-panel" id="sidePanel">
516
- <div class="panel-section">
517
- <h3 class="panel-title">Pitch Correction</h3>
518
- <div class="slider-control">
519
- <div class="slider-label">
520
- <span>Snap Strength</span>
521
- <span id="snapValue">50%</span>
522
- </div>
523
- <input type="range" class="slider" min="0" max="100" value="50" oninput="updateSlider('snap', this.value)">
524
- </div>
525
- <div class="slider-control">
526
- <div class="slider-label">
527
- <span>Pitch Range</span>
528
- <span id="rangeValue">±12</span>
529
- </div>
530
- <input type="range" class="slider" min="1" max="24" value="12" oninput="updateSlider('range', this.value)">
531
- </div>
532
- </div>
533
-
534
- <div class="panel-section">
535
- <h3 class="panel-title">Note Detection</h3>
536
- <div class="slider-control">
537
- <div class="slider-label">
538
- <span>Sensitivity</span>
539
- <span id="sensValue">75%</span>
540
- </div>
541
- <input type="range" class="slider" min="0" max="100" value="75" oninput="updateSlider('sens', this.value)">
542
- </div>
543
- <div class="slider-control">
544
- <div class="slider-label">
545
- <span>Min Note Length</span>
546
- <span id="minLengthValue">100ms</span>
547
- </div>
548
- <input type="range" class="slider" min="50" max="500" value="100" step="50" oninput="updateSlider('minLength', this.value)">
549
- </div>
550
- </div>
551
-
552
- <div class="panel-section">
553
- <h3 class="panel-title">Display</h3>
554
- <div class="slider-control">
555
- <div class="slider-label">
556
- <span>Vertical Zoom</span>
557
- <span id="vZoomValue">100%</span>
558
- </div>
559
- <input type="range" class="slider" min="50" max="200" value="100" oninput="updateSlider('vZoom', this.value)">
560
- </div>
561
- </div>
562
  </div>
 
 
563
  </div>
 
564
  </div>
565
-
566
- <!-- Playback Controls -->
567
- <div class="playback-controls">
568
- <div class="transport">
569
- <button class="transport-btn" onclick="skipBackward()">⏮️</button>
570
- <button class="transport-btn" onclick="stepBackward()">⏪</button>
571
- <button class="transport-btn play" onclick="togglePlayback()">▶️</button>
572
- <button class="transport-btn" onclick="stepForward()">⏩</button>
573
- <button class="transport-btn" onclick="skipForward()">⏭️</button>
574
- </div>
575
-
576
- <div class="time-display">
577
- <span id="currentTime">00:00.000</span>
578
- <span class="time-separator">/</span>
579
- <span id="totalTime">00:00.000</span>
580
- </div>
581
-
582
- <div class="pitch-display">
583
- <div class="pitch-info">
584
- <span class="pitch-label">Current Note:</span>
585
- <span class="pitch-value" id="currentNote">--</span>
586
- </div>
587
- <div class="pitch-info">
588
- <span class="pitch-label">Frequency:</span>
589
- <span class="pitch-value" id="currentFreq">-- Hz</span>
590
- </div>
591
- </div>
592
  </div>
593
 
594
- <!-- Loading Overlay -->
595
- <div class="loading-overlay" id="loadingOverlay">
596
- <div class="loading-spinner"></div>
 
597
  </div>
598
 
599
- <!-- Tooltip -->
600
- <div class="tooltip" id="tooltip"></div>
601
-
602
- <script>
603
- // Global variables
604
- let canvas, ctx;
605
- let audioContext;
606
- let audioBuffer;
607
- let source;
608
- let isPlaying = false;
609
- let startTime = 0;
610
- let pauseTime = 0;
611
- let currentTool = 'select';
612
- let zoomLevel = 1;
613
- let verticalZoom = 1;
614
- let notes = [];
615
- let selectedNote = null;
616
- let isDragging = false;
617
- let dragStartX = 0;
618
- let dragStartY = 0;
619
- let viewOffset = 0;
620
- let playbackPosition = 0;
621
-
622
- // Note frequencies for C4 to B5
623
- const noteFrequencies = {
624
- 'C4': 261.63, 'C#4': 277.18, 'D4': 293.66, 'D#4': 311.13,
625
- 'E4': 329.63, 'F4': 349.23, 'F#4': 369.99, 'G4': 392.00,
626
- 'G#4': 415.30, 'A4': 440.00, 'A#4': 466.16, 'B4': 493.88,
627
- 'C5': 523.25, 'C#5': 554.37, 'D5': 587.33, 'D#5': 622.25,
628
- 'E5': 659.25, 'F5': 698.46, 'F#5': 739.99, 'G5': 783.99,
629
- 'G#5': 830.61, 'A5': 880.00, 'A#5': 932.33, 'B5': 987.77
630
- };
631
-
632
- // Initialize
633
- document.addEventListener('DOMContentLoaded', function() {
634
- initCanvas();
635
- initAudioContext();
636
- generateDemoNotes();
637
- drawInterface();
638
- updateTimeRuler();
639
- startAnimationLoop();
640
- });
641
-
642
- function initCanvas() {
643
- canvas = document.getElementById('pitchCanvas');
644
- ctx = canvas.getContext('2d');
645
- resizeCanvas();
646
- window.addEventListener('resize', resizeCanvas);
647
-
648
- canvas.addEventListener('mousedown', handleMouseDown);
649
- canvas.addEventListener('mousemove', handleMouseMove);
650
- canvas.addEventListener('mouseup', handleMouseUp);
651
- canvas.addEventListener('wheel', handleWheel);
652
- }
653
-
654
- function resizeCanvas() {
655
- const container = document.querySelector('.editor-container');
656
- canvas.width = container.clientWidth;
657
- canvas.height = container.clientHeight - 40;
658
- drawInterface();
659
- }
660
-
661
- function initAudioContext() {
662
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
663
- }
664
-
665
- function generateDemoNotes() {
666
- // Generate some demo notes for visualization
667
- const demoNotes = [
668
- { note: 'C4', start: 0.5, duration: 0.5, pitch: 261.63 },
669
- { note: 'E4', start: 1.0, duration: 0.5, pitch: 329.63 },
670
- { note: 'G4', start: 1.5, duration: 0.5, pitch: 392.00 },
671
- { note: 'C5', start: 2.0, duration: 0.5, pitch: 523.25 },
672
- { note: 'G4', start: 2.5, duration: 0.5, pitch: 392.00 },
673
- { note: 'E4', start: 3.0, duration: 0.5, pitch: 329.63 },
674
- { note: 'C4', start: 3.5, duration: 1.0, pitch: 261.63 },
675
- { note: 'D4', start: 4.5, duration: 0.5, pitch: 293.66 },
676
- { note: 'F4', start: 5.0, duration: 0.5, pitch: 349.23 },
677
- { note: 'A4', start: 5.5, duration: 0.5, pitch: 440.00 },
678
- { note: 'D5', start: 6.0, duration: 0.5, pitch: 587.33 },
679
- { note: 'A4', start: 6.5, duration: 0.5, pitch: 440.00 },
680
- { note: 'F4', start: 7.0, duration: 0.5, pitch: 349.23 },
681
- { note: 'D4', start: 7.5, duration: 1.0, pitch: 293.66 },
682
- ];
683
-
684
- notes = demoNotes.map(n => ({
685
- ...n,
686
- id: Math.random().toString(36).substr(2, 9),
687
- color: getNoteColor(n.note)
688
- }));
689
- }
690
-
691
- function getNoteColor(note) {
692
- const colors = {
693
- 'C': '#ef4444', 'C#': '#f97316', 'D': '#f59e0b', 'D#': '#eab308',
694
- 'E': '#84cc16', 'F': '#22c55e', 'F#': '#10b981', 'G': '#14b8a6',
695
- 'G#': '#06b6d4', 'A': '#0ea5e9', 'A#': '#3b82f6', 'B': '#6366f1'
696
- };
697
- return colors[note.replace(/[0-9]/, '')] || '#8b5cf6';
698
- }
699
-
700
- function drawInterface() {
701
- ctx.clearRect(0, 0, canvas.width, canvas.height);
702
-
703
- // Draw background gradient
704
- const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
705
- gradient.addColorStop(0, '#1a1a1a');
706
- gradient.addColorStop(1, '#242424');
707
- ctx.fillStyle = gradient;
708
- ctx.fillRect(0, 0, canvas.width, canvas.height);
709
-
710
- // Draw grid
711
- drawGrid();
712
-
713
- // Draw notes
714
- drawNotes();
715
-
716
- // Draw playback line
717
- drawPlaybackLine();
718
- }
719
-
720
- function drawGrid() {
721
- ctx.strokeStyle = '#3a3a3a';
722
- ctx.lineWidth = 1;
723
-
724
- // Vertical lines (time)
725
- const pixelsPerSecond = 100 * zoomLevel;
726
- const seconds = Math.ceil(canvas.width / pixelsPerSecond);
727
-
728
- for (let i = 0; i <= seconds; i++) {
729
- const x = i * pixelsPerSecond - viewOffset;
730
- if (x >= 0 && x <= canvas.width) {
731
- ctx.beginPath();
732
- ctx.moveTo(x, 0);
733
- ctx.lineTo(x, canvas.height);
734
- ctx.stroke();
735
- }
736
- }
737
-
738
- // Horizontal lines (pitch)
739
- const noteHeight = 30 * verticalZoom;
740
- const notesCount = Math.ceil(canvas.height / noteHeight);
741
-
742
- for (let i = 0; i <= notesCount; i++) {
743
- const y = i * noteHeight;
744
- ctx.beginPath();
745
- ctx.moveTo(0, y);
746
- ctx.lineTo(canvas.width, y);
747
- ctx.stroke();
748
- }
749
- }
750
-
751
- function drawNotes() {
752
- const pixelsPerSecond = 100 * zoomLevel;
753
- const noteHeight = 30 * verticalZoom;
754
-
755
- notes.forEach(note => {
756
- const x = (note.start * pixelsPerSecond) - viewOffset;
757
- const width = note.duration * pixelsPerSecond;
758
- const y = canvas.height - (getNotePosition(note.note) * noteHeight) - noteHeight;
759
-
760
- // Draw note rectangle
761
- ctx.fillStyle = note.color + '88';
762
- ctx.fillRect(x, y, width, noteHeight - 2);
763
-
764
- // Draw note border
765
- ctx.strokeStyle = note.color;
766
- ctx.lineWidth = 2;
767
- ctx.strokeRect(x, y, width, noteHeight - 2);
768
-
769
- // Draw note label
770
- ctx.fillStyle = '#ffffff';
771
- ctx.font = '12px sans-serif';
772
- ctx.fillText(note.note, x + 5, y + noteHeight / 2 + 4);
773
-
774
- // Highlight selected note
775
- if (selectedNote && selectedNote.id === note.id) {
776
- ctx.strokeStyle = '#ffffff';
777
- ctx.lineWidth = 3;
778
- ctx.strokeRect(x - 2, y - 2, width + 4, noteHeight + 2);
779
- }
780
- });
781
- }
782
-
783
- function getNotePosition(noteName) {
784
- const noteOrder = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
785
- const note = noteName.replace(/[0-9]/, '');
786
- const octave = parseInt(noteName.replace(/[A-G#]/, ''));
787
- return (octave - 3) * 12 + noteOrder.indexOf(note);
788
- }
789
-
790
- function drawPlaybackLine() {
791
- if (isPlaying || playbackPosition > 0) {
792
- const pixelsPerSecond = 100 * zoomLevel;
793
- const x = (playbackPosition * pixelsPerSecond) - viewOffset;
794
-
795
- if (x >= 0 && x <= canvas.width) {
796
- ctx.strokeStyle = '#10b981';
797
- ctx.lineWidth = 2;
798
- ctx.beginPath();
799
- ctx.moveTo(x, 0);
800
- ctx.lineTo(x, canvas.height);
801
- ctx.stroke();
802
- }
803
- }
804
- }
805
-
806
- function handleMouseDown(e) {
807
- const rect = canvas.getBoundingClientRect();
808
- const x = e.clientX - rect.left;
809
- const y = e.clientY - rect.top;
810
-
811
- const pixelsPerSecond = 100 * zoomLevel;
812
- const time = (x + viewOffset) / pixelsPerSecond;
813
-
814
- // Check if clicking on a note
815
- selectedNote = null;
816
- notes.forEach(note => {
817
- const noteX = (note.start * pixelsPerSecond) - viewOffset;
818
- const noteY = canvas.height - (getNotePosition(note.note) * 30 * verticalZoom) - (30 * verticalZoom);
819
- const noteWidth = note.duration * pixelsPerSecond;
820
- const noteHeight = 30 * verticalZoom;
821
-
822
- if (x >= noteX && x <= noteX + noteWidth && y >= noteY && y <= noteY + noteHeight) {
823
- selectedNote = note;
824
- isDragging = true;
825
- dragStartX = x - noteX;
826
- dragStartY = y - noteY;
827
- }
828
- });
829
-
830
- drawInterface();
831
- }
832
-
833
- function handleMouseMove(e) {
834
- const rect = canvas.getBoundingClientRect();
835
- const x = e.clientX - rect.left;
836
- const y = e.clientY - rect.top;
837
-
838
- if (isDragging && selectedNote) {
839
- const pixelsPerSecond = 100 * zoomLevel;
840
- const newStart = (x + viewOffset - dragStartX) / pixelsPerSecond;
841
- selectedNote.start = Math.max(0, newStart);
842
- drawInterface();
843
- }
844
-
845
- // Update cursor
846
- canvas.style.cursor = isDragging ? 'grabbing' : 'crosshair';
847
- }
848
-
849
- function handleMouseUp(e) {
850
- isDragging = false;
851
- }
852
-
853
- function handleWheel(e) {
854
- e.preventDefault();
855
- if (e.deltaY < 0) {
856
- zoomIn();
857
- } else {
858
- zoomOut();
859
- }
860
- }
861
-
862
- function selectTool(tool) {
863
- currentTool = tool;
864
- document.querySelectorAll('.tool-btn[data-tool]').forEach(btn => {
865
- btn.classList.remove('active');
866
- });
867
- document.querySelector(`[data-tool="${tool}"]`).classList.add('active');
868
- }
869
-
870
- function toggleSidePanel() {
871
- document.getElementById('sidePanel').classList.toggle('open');
872
- }
873
-
874
- function updateSlider(type, value) {
875
- switch(type) {
876
- case 'snap':
877
- document.getElementById('snapValue').textContent = value + '%';
878
- break;
879
- case 'range':
880
- document.getElementById('rangeValue').textContent = '±' + value;
881
- break;
882
- case 'sens':
883
- document.getElementById('sensValue').textContent = value + '%';
884
- break;
885
- case 'minLength':
886
- document.getElementById('minLengthValue').textContent = value + 'ms';
887
- break;
888
- case 'vZoom':
889
- verticalZoom = value / 100;
890
- document.getElementById('vZoomValue').textContent = value + '%';
891
- drawInterface();
892
- break;
893
- }
894
- }
895
-
896
- function zoomIn() {
897
- zoomLevel = Math.min(zoomLevel * 1.2, 5);
898
- drawInterface();
899
- updateTimeRuler();
900
- }
901
-
902
- function zoomOut() {
903
- zoomLevel = Math.max(zoomLevel / 1.2, 0.5);
904
- drawInterface();
905
- updateTimeRuler();
906
- }
907
-
908
- function resetView() {
909
- zoomLevel = 1;
910
- viewOffset = 0;
911
- drawInterface();
912
- updateTimeRuler();
913
- }
914
-
915
- function updateTimeRuler() {
916
- const ruler = document.getElementById('timeRuler');
917
- ruler.innerHTML = '';
918
-
919
- const pixelsPerSecond = 100 * zoomLevel;
920
- const seconds = Math.ceil(canvas.width / pixelsPerSecond);
921
-
922
- for (let i = 0; i <= seconds; i++) {
923
- const mark = document.createElement('div');
924
- mark.className = 'time-mark';
925
- mark.style.left = (i * pixelsPerSecond) + 'px';
926
- mark.textContent = formatTime(i);
927
- ruler.appendChild(mark);
928
- }
929
- }
930
-
931
- function formatTime(seconds) {
932
- const mins = Math.floor(seconds / 60);
933
- const secs = Math.floor(seconds % 60);
934
- return `${mins}:${secs.toString().padStart(2, '0')}`;
935
- }
936
-
937
- function loadAudioFile(event) {
938
- const file = event.target.files[0];
939
- if (!file) return;
940
-
941
- showLoading(true);
942
-
943
- const reader = new FileReader();
944
- reader.onload = function(e) {
945
- audioContext.decodeAudioData(e.target.result, function(buffer) {
946
- audioBuffer = buffer;
947
- document.getElementById('totalTime').textContent = formatAudioTime(buffer.duration);
948
- showLoading(false);
949
-
950
- // Simulate note detection
951
- detectNotes();
952
- });
953
- };
954
- reader.readAsArrayBuffer(file);
955
- }
956
-
957
- function detectNotes() {
958
- // Simulate note detection with random notes
959
- const detectedNotes = [];
960
- const noteNames = Object.keys(noteFrequencies);
961
-
962
- for (let i = 0; i < 20; i++) {
963
- const noteName = noteNames[Math.floor(Math.random() * noteNames.length)];
964
- detectedNotes.push({
965
- id: Math.random().toString(36).substr(2, 9),
966
- note: noteName,
967
- start: i * 0.5,
968
- duration: 0.3 + Math.random() * 0.4,
969
- pitch: noteFrequencies[noteName],
970
- color: getNoteColor(noteName)
971
- });
972
- }
973
-
974
- notes = detectedNotes;
975
- drawInterface();
976
- }
977
-
978
- function togglePlayback() {
979
- if (!audioBuffer) {
980
- // Simulate playback with demo
981
- simulatePlayback();
982
- return;
983
- }
984
-
985
- if (isPlaying) {
986
- pauseAudio();
987
- } else {
988
- playAudio();
989
- }
990
- }
991
-
992
- function simulatePlayback() {
993
- isPlaying = !isPlaying;
994
- const playBtn = document.querySelector('.transport-btn.play');
995
- playBtn.innerHTML = isPlaying ? '⏸️' : '▶️';
996
-
997
- if (isPlaying) {
998
- startTime = Date.now() - (playbackPosition * 1000);
999
- updatePlayback();
1000
- }
1001
- }
1002
-
1003
- function updatePlayback() {
1004
- if (!isPlaying) return;
1005
-
1006
- const elapsed = (Date.now() - startTime) / 1000;
1007
- playbackPosition = elapsed % 10; // Loop every 10 seconds
1008
-
1009
- document.getElementById('currentTime').textContent = formatAudioTime(playbackPosition);
1010
-
1011
- // Update current note display
1012
- const currentNoteObj = notes.find(n =>
1013
- playbackPosition >= n.start && playbackPosition <= n.start + n.duration
1014
- );
1015
-
1016
- if (currentNoteObj) {
1017
- document.getElementById('currentNote').textContent = currentNoteObj.note;
1018
- document.getElementById('currentFreq').textContent = Math.round(currentNoteObj.pitch) + ' Hz';
1019
- } else {
1020
- document.getElementById('currentNote').textContent = '--';
1021
- document.getElementById('currentFreq').textContent = '-- Hz';
1022
- }
1023
-
1024
- drawInterface();
1025
- requestAnimationFrame(updatePlayback);
1026
- }
1027
-
1028
- function playAudio() {
1029
- if (!audioBuffer) return;
1030
-
1031
- source = audioContext.createBufferSource();
1032
- source.buffer = audioBuffer;
1033
- source.connect(audioContext.destination);
1034
-
1035
- source.start(0, pauseTime);
1036
- startTime = audioContext.currentTime - pauseTime;
1037
- isPlaying = true;
1038
-
1039
- const playBtn = document.querySelector('.transport-btn.play');
1040
- playBtn.innerHTML = '⏸️';
1041
-
1042
- source.onended = function() {
1043
- if (isPlaying) {
1044
- stopAudio();
1045
- }
1046
- };
1047
-
1048
- updatePlaybackTime();
1049
- }
1050
-
1051
- function pauseAudio() {
1052
- if (source) {
1053
- source.stop();
1054
- source = null;
1055
- }
1056
- pauseTime = audioContext.currentTime - startTime;
1057
- isPlaying = false;
1058
-
1059
- const playBtn = document.querySelector('.transport-btn.play');
1060
- playBtn.innerHTML = '▶️';
1061
- }
1062
-
1063
- function stopAudio() {
1064
- if (source) {
1065
- source.stop();
1066
- source = null;
1067
- }
1068
- isPlaying = false;
1069
- pauseTime = 0;
1070
- playbackPosition = 0;
1071
-
1072
- const playBtn = document.querySelector('.transport-btn.play');
1073
- playBtn.innerHTML = '▶️';
1074
-
1075
- document.getElementById('currentTime').textContent = '00:00.000';
1076
- drawInterface();
1077
- }
1078
-
1079
- function updatePlaybackTime() {
1080
- if (!isPlaying) return;
1081
-
1082
- const currentTime = audioContext.currentTime - startTime;
1083
- document.getElementById('currentTime').textContent = formatAudioTime(currentTime);
1084
- playbackPosition = currentTime;
1085
-
1086
- if (currentTime < audioBuffer.duration) {
1087
- requestAnimationFrame(updatePlaybackTime);
1088
- } else {
1089
- stopAudio();
1090
- }
1091
- }
1092
-
1093
- function formatAudioTime(seconds) {
1094
- const mins = Math.floor(seconds / 60);
1095
- const secs = Math.floor(seconds % 60);
1096
- const ms = Math.floor((seconds % 1) * 1000);
1097
- return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}`;
1098
- }
1099
 
1100
- function skipBackward() {
1101
- playbackPosition = Math.max(0, playbackPosition - 5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>PitchLab Pro - DAW-Integrated Audio Pitch Editor</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ :root {
15
+ --bg-primary: #0f0f0f;
16
+ --bg-secondary: #1a1a1a;
17
+ --bg-tertiary: #252525;
18
+ --bg-hover: #2e2e2e;
19
+ --text-primary: #ffffff;
20
+ --text-secondary: #a0a0a0;
21
+ --accent-blue: #3b82f6;
22
+ --accent-purple: #8b5cf6;
23
+ --accent-green: #10b981;
24
+ --accent-orange: #f59e0b;
25
+ --accent-red: #ef4444;
26
+ --accent-yellow: #eab308;
27
+ --border-color: #333333;
28
+ --grid-color: #1e1e1e;
29
+ --success-green: #22c55e;
30
+ --warning-yellow: #fbbf24;
31
+ }
32
+
33
+ body {
34
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
35
+ background: var(--bg-primary);
36
+ color: var(--text-primary);
37
+ height: 100vh;
38
+ overflow: hidden;
39
+ display: flex;
40
+ flex-direction: column;
41
+ }
42
+
43
+ /* Header */
44
+ .header {
45
+ background: linear-gradient(180deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%);
46
+ border-bottom: 1px solid var(--border-color);
47
+ padding: 10px 20px;
48
+ display: flex;
49
+ justify-content: space-between;
50
+ align-items: center;
51
+ flex-shrink: 0;
52
+ box-shadow: 0 2px 10px rgba(0,0,0,0.3);
53
+ }
54
+
55
+ .logo {
56
+ display: flex;
57
+ align-items: center;
58
+ gap: 12px;
59
+ font-size: 22px;
60
+ font-weight: 600;
61
+ background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
62
+ -webkit-background-clip: text;
63
+ -webkit-text-fill-color: transparent;
64
+ }
65
+
66
+ .logo-icon {
67
+ width: 36px;
68
+ height: 36px;
69
+ background: linear-gradient(135deg, var(--accent-purple), var(--accent-blue));
70
+ border-radius: 10px;
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ font-size: 20px;
75
+ }
76
+
77
+ .header-controls {
78
+ display: flex;
79
+ gap: 10px;
80
+ align-items: center;
81
+ }
82
+
83
+ .header-btn {
84
+ background: var(--bg-tertiary);
85
+ border: 1px solid var(--border-color);
86
+ color: var(--text-secondary);
87
+ padding: 8px 16px;
88
+ border-radius: 8px;
89
+ cursor: pointer;
90
+ transition: all 0.3s;
91
+ font-size: 13px;
92
+ display: flex;
93
+ align-items: center;
94
+ gap: 6px;
95
+ }
96
+
97
+ .header-btn:hover {
98
+ background: var(--bg-hover);
99
+ color: var(--text-primary);
100
+ transform: translateY(-2px);
101
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
102
+ }
103
+
104
+ /* Toolbar */
105
+ .toolbar {
106
+ background: var(--bg-secondary);
107
+ border-bottom: 1px solid var(--border-color);
108
+ padding: 10px 20px;
109
+ display: flex;
110
+ gap: 20px;
111
+ align-items: center;
112
+ flex-wrap: wrap;
113
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
114
+ }
115
+
116
+ .tool-group {
117
+ display: flex;
118
+ gap: 8px;
119
+ padding: 0 12px;
120
+ border-right: 1px solid var(--border-color);
121
+ }
122
+
123
+ .tool-group:last-child {
124
+ border-right: none;
125
+ }
126
+
127
+ .tool-btn {
128
+ background: var(--bg-tertiary);
129
+ border: 1px solid var(--border-color);
130
+ color: var(--text-secondary);
131
+ padding: 8px 12px;
132
+ border-radius: 6px;
133
+ cursor: pointer;
134
+ transition: all 0.2s;
135
+ display: flex;
136
+ align-items: center;
137
+ gap: 6px;
138
+ font-size: 13px;
139
+ position: relative;
140
+ }
141
+
142
+ .tool-btn:hover {
143
+ background: var(--bg-hover);
144
+ color: var(--text-primary);
145
+ transform: translateY(-1px);
146
+ }
147
+
148
+ .tool-btn.active {
149
+ background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
150
+ color: white;
151
+ border-color: var(--accent-blue);
152
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
153
+ }
154
+
155
+ /* Main Content */
156
+ .main-content {
157
+ flex: 1;
158
+ display: flex;
159
+ flex-direction: column;
160
+ overflow: hidden;
161
+ }
162
+
163
+ /* DAW Info Bar */
164
+ .daw-info {
165
+ background: linear-gradient(90deg, var(--bg-tertiary), var(--bg-secondary));
166
+ padding: 8px 20px;
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 30px;
170
+ border-bottom: 1px solid var(--border-color);
171
+ font-size: 12px;
172
+ }
173
+
174
+ .daw-info-item {
175
+ display: flex;
176
+ align-items: center;
177
+ gap: 8px;
178
+ }
179
+
180
+ .daw-info-label {
181
+ color: var(--text-secondary);
182
+ }
183
+
184
+ .daw-info-value {
185
+ color: var(--accent-blue);
186
+ font-weight: 600;
187
+ }
188
+
189
+ /* Editor Container */
190
+ .editor-container {
191
+ flex: 1;
192
+ position: relative;
193
+ background: var(--bg-primary);
194
+ overflow: hidden;
195
+ }
196
+
197
+ /* Timeline */
198
+ .timeline {
199
+ position: absolute;
200
+ top: 0;
201
+ left: 0;
202
+ right: 0;
203
+ height: 45px;
204
+ background: linear-gradient(180deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%);
205
+ border-bottom: 1px solid var(--border-color);
206
+ display: flex;
207
+ align-items: center;
208
+ padding: 0 15px;
209
+ z-index: 10;
210
+ }
211
+
212
+ .time-ruler {
213
+ width: 100%;
214
+ height: 30px;
215
+ position: relative;
216
+ }
217
+
218
+ .time-mark {
219
+ position: absolute;
220
+ top: 0;
221
+ transform: translateX(-50%);
222
+ font-size: 11px;
223
+ color: var(--text-secondary);
224
+ font-weight: 500;
225
+ }
226
+
227
+ .time-mark::before {
228
+ content: '';
229
+ position: absolute;
230
+ top: 20px;
231
+ left: 50%;
232
+ width: 1px;
233
+ height: 5px;
234
+ background: var(--text-secondary);
235
+ }
236
+
237
+ /* Piano Roll */
238
+ .piano-roll {
239
+ position: absolute;
240
+ top: 45px;
241
+ left: 0;
242
+ width: 60px;
243
+ bottom: 0;
244
+ background: linear-gradient(90deg, var(--bg-tertiary), var(--bg-secondary));
245
+ border-right: 1px solid var(--border-color);
246
+ z-index: 5;
247
+ }
248
+
249
+ .piano-key {
250
+ height: 20px;
251
+ border-bottom: 1px solid var(--border-color);
252
+ display: flex;
253
+ align-items: center;
254
+ justify-content: center;
255
+ font-size: 10px;
256
+ color: var(--text-secondary);
257
+ position: relative;
258
+ }
259
+
260
+ .piano-key.black {
261
+ background: #1a1a1a;
262
+ color: var(--text-primary);
263
+ height: 15px;
264
+ }
265
+
266
+ .piano-key.white {
267
+ background: #252525;
268
+ }
269
+
270
+ /* Canvas */
271
+ #pitchCanvas {
272
+ position: absolute;
273
+ top: 45px;
274
+ left: 60px;
275
+ cursor: crosshair;
276
+ }
277
+
278
+ /* Playback Controls */
279
+ .playback-controls {
280
+ background: linear-gradient(180deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%);
281
+ border-top: 1px solid var(--border-color);
282
+ padding: 15px 20px;
283
+ display: flex;
284
+ align-items: center;
285
+ gap: 25px;
286
+ box-shadow: 0 -2px 10px rgba(0,0,0,0.2);
287
+ }
288
+
289
+ .transport {
290
+ display: flex;
291
+ gap: 5px;
292
+ align-items: center;
293
+ }
294
+
295
+ .transport-btn {
296
+ width: 42px;
297
+ height: 42px;
298
+ border-radius: 50%;
299
+ background: var(--bg-tertiary);
300
+ border: 1px solid var(--border-color);
301
+ color: var(--text-primary);
302
+ cursor: pointer;
303
+ display: flex;
304
+ align-items: center;
305
+ justify-content: center;
306
+ transition: all 0.3s;
307
+ font-size: 16px;
308
+ }
309
+
310
+ .transport-btn:hover {
311
+ background: var(--bg-hover);
312
+ transform: scale(1.1);
313
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
314
+ }
315
+
316
+ .transport-btn.play {
317
+ width: 50px;
318
+ height: 50px;
319
+ background: linear-gradient(135deg, var(--success-green), #16a34a);
320
+ border-color: var(--success-green);
321
+ }
322
+
323
+ .transport-btn.play:hover {
324
+ background: linear-gradient(135deg, #16a34a, #15803d);
325
+ }
326
+
327
+ /* Time Display */
328
+ .time-display {
329
+ display: flex;
330
+ align-items: center;
331
+ gap: 10px;
332
+ padding: 10px 18px;
333
+ background: var(--bg-tertiary);
334
+ border-radius: 10px;
335
+ font-size: 14px;
336
+ font-variant-numeric: tabular-nums;
337
+ border: 1px solid var(--border-color);
338
+ }
339
+
340
+ .time-separator {
341
+ color: var(--text-secondary);
342
+ }
343
+
344
+ /* Pitch Display */
345
+ .pitch-display {
346
+ display: flex;
347
+ align-items: center;
348
+ gap: 15px;
349
+ margin-left: auto;
350
+ }
351
+
352
+ .pitch-info {
353
+ padding: 10px 18px;
354
+ background: var(--bg-tertiary);
355
+ border-radius: 10px;
356
+ font-size: 14px;
357
+ border: 1px solid var(--border-color);
358
+ }
359
+
360
+ .pitch-label {
361
+ color: var(--text-secondary);
362
+ margin-right: 8px;
363
+ }
364
+
365
+ .pitch-value {
366
+ color: var(--accent-blue);
367
+ font-weight: 600;
368
+ }
369
+
370
+ /* Side Panel */
371
+ .side-panel {
372
+ position: absolute;
373
+ right: 0;
374
+ top: 45px;
375
+ width: 320px;
376
+ height: calc(100% - 45px);
377
+ background: linear-gradient(180deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%);
378
+ border-left: 1px solid var(--border-color);
379
+ padding: 25px;
380
+ transform: translateX(100%);
381
+ transition: transform 0.3s;
382
+ z-index: 20;
383
+ overflow-y: auto;
384
+ box-shadow: -2px 0 10px rgba(0,0,0,0.2);
385
+ }
386
+
387
+ .side-panel.open {
388
+ transform: translateX(0);
389
+ }
390
+
391
+ .panel-section {
392
+ margin-bottom: 30px;
393
+ }
394
+
395
+ .panel-title {
396
+ font-size: 16px;
397
+ font-weight: 600;
398
+ margin-bottom: 15px;
399
+ color: var(--text-primary);
400
+ display: flex;
401
+ align-items: center;
402
+ gap: 8px;
403
+ }
404
+
405
+ .panel-title::before {
406
+ content: '';
407
+ width: 4px;
408
+ height: 16px;
409
+ background: linear-gradient(180deg, var(--accent-blue), var(--accent-purple));
410
+ border-radius: 2px;
411
+ }
412
+
413
+ .slider-control {
414
+ margin-bottom: 20px;
415
+ }
416
+
417
+ .slider-label {
418
+ display: flex;
419
+ justify-content: space-between;
420
+ margin-bottom: 10px;
421
+ font-size: 13px;
422
+ color: var(--text-secondary);
423
+ }
424
+
425
+ .slider {
426
+ width: 100%;
427
+ height: 6px;
428
+ background: var(--bg-hover);
429
+ border-radius: 3px;
430
+ outline: none;
431
+ -webkit-appearance: none;
432
+ }
433
+
434
+ .slider::-webkit-slider-thumb {
435
+ -webkit-appearance: none;
436
+ appearance: none;
437
+ width: 18px;
438
+ height: 18px;
439
+ background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
440
+ border-radius: 50%;
441
+ cursor: pointer;
442
+ box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
443
+ }
444
+
445
+ .slider::-moz-range-thumb {
446
+ width: 18px;
447
+ height: 18px;
448
+ background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
449
+ border-radius: 50%;
450
+ cursor: pointer;
451
+ border: none;
452
+ box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
453
+ }
454
+
455
+ /* Export Options */
456
+ .export-options {
457
+ display: flex;
458
+ flex-direction: column;
459
+ gap: 12px;
460
+ margin-top: 15px;
461
+ }
462
+
463
+ .export-btn {
464
+ background: linear-gradient(135deg, var(--bg-tertiary), var(--bg-hover));
465
+ border: 1px solid var(--border-color);
466
+ color: var(--text-primary);
467
+ padding: 12px;
468
+ border-radius: 8px;
469
+ cursor: pointer;
470
+ transition: all 0.3s;
471
+ font-size: 13px;
472
+ display: flex;
473
+ align-items: center;
474
+ justify-content: center;
475
+ gap: 8px;
476
+ }
477
+
478
+ .export-btn:hover {
479
+ background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
480
+ transform: translateY(-2px);
481
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
482
+ }
483
+
484
+ /* Loading Overlay */
485
+ .loading-overlay {
486
+ position: fixed;
487
+ top: 0;
488
+ left: 0;
489
+ right: 0;
490
+ bottom: 0;
491
+ background: rgba(0, 0, 0, 0.9);
492
+ display: none;
493
+ align-items: center;
494
+ justify-content: center;
495
+ z-index: 1000;
496
+ }
497
+
498
+ .loading-overlay.active {
499
+ display: flex;
500
+ }
501
+
502
+ .loading-content {
503
+ text-align: center;
504
+ }
505
+
506
+ .loading-spinner {
507
+ width: 60px;
508
+ height: 60px;
509
+ border: 3px solid var(--border-color);
510
+ border-top-color: var(--accent-blue);
511
+ border-radius: 50%;
512
+ animation: spin 1s linear infinite;
513
+ margin: 0 auto 20px;
514
+ }
515
+
516
+ .loading-text {
517
+ color: var(--text-primary);
518
+ font-size: 14px;
519
+ }
520
+
521
+ @keyframes spin {
522
+ to {
523
+ transform: rotate(360deg);
524
+ }
525
+ }
526
+
527
+ /* Tooltip */
528
+ .tooltip {
529
+ position: absolute;
530
+ background: var(--bg-tertiary);
531
+ border: 1px solid var(--border-color);
532
+ padding: 8px 12px;
533
+ border-radius: 6px;
534
+ font-size: 12px;
535
+ pointer-events: none;
536
+ z-index: 100;
537
+ opacity: 0;
538
+ transition: opacity 0.2s;
539
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
540
+ }
541
+
542
+ .tooltip.visible {
543
+ opacity: 1;
544
+ }
545
+
546
+ /* DAW Integration Modal */
547
+ .daw-modal {
548
+ position: fixed;
549
+ top: 0;
550
+ left: 0;
551
+ right: 0;
552
+ bottom: 0;
553
+ background: rgba(0, 0, 0, 0.9);
554
+ display: none;
555
+ align-items: center;
556
+ justify-content: center;
557
+ z-index: 2000;
558
+ }
559
+
560
+ .daw-modal.active {
561
+ display: flex;
562
+ }
563
+
564
+ .modal-content {
565
+ background: var(--bg-secondary);
566
+ border-radius: 15px;
567
+ padding: 30px;
568
+ max-width: 600px;
569
+ width: 90%;
570
+ max-height: 80vh;
571
+ overflow-y: auto;
572
+ box-shadow: 0 10px 40px rgba(0,0,0,0.5);
573
+ }
574
+
575
+ .modal-header {
576
+ font-size: 20px;
577
+ font-weight: 600;
578
+ margin-bottom: 20px;
579
+ color: var(--text-primary);
580
+ }
581
+
582
+ .modal-section {
583
+ margin-bottom: 25px;
584
+ }
585
+
586
+ .modal-section h3 {
587
+ font-size: 16px;
588
+ margin-bottom: 10px;
589
+ color: var(--accent-blue);
590
+ }
591
+
592
+ .modal-section p {
593
+ color: var(--text-secondary);
594
+ line-height: 1.6;
595
+ margin-bottom: 10px;
596
+ }
597
+
598
+ .modal-close {
599
+ background: var(--accent-red);
600
+ color: white;
601
+ border: none;
602
+ padding: 10px 20px;
603
+ border-radius: 8px;
604
+ cursor: pointer;
605
+ margin-top: 20px;
606
+ }
607
+
608
+ /* Responsive */
609
+ @media (max-width: 768px) {
610
+ .toolbar {
611
+ padding: 8px 12px;
612
+ }
613
+
614
+ .tool-group {
615
+ padding: 0 8px;
616
+ }
617
+
618
+ .tool-btn {
619
+ padding: 6px 8px;
620
+ font-size: 12px;
621
+ }
622
+
623
+ .side-panel {
624
+ width: 100%;
625
+ }
626
+
627
+ .header-controls {
628
+ gap: 8px;
629
+ }
630
+
631
+ .daw-info {
632
+ flex-wrap: wrap;
633
+ gap: 15px;
634
+ }
635
+ }
636
+
637
+ /* Custom Scrollbar */
638
+ ::-webkit-scrollbar {
639
+ width: 10px;
640
+ height: 10px;
641
+ }
642
+
643
+ ::-webkit-scrollbar-track {
644
+ background: var(--bg-primary);
645
+ }
646
+
647
+ ::-webkit-scrollbar-thumb {
648
+ background: var(--bg-hover);
649
+ border-radius: 5px;
650
+ }
651
+
652
+ ::-webkit-scrollbar-thumb:hover {
653
+ background: var(--border-color);
654
+ }
655
+ </style>
656
  </head>
657
  <body>
658
+ <!-- Header -->
659
+ <header class="header">
660
+ <div class="logo">
661
+ <div class="logo-icon">♪</div>
662
+ <span>PitchLab Pro</span>
663
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank"
664
+ style="color: var(--text-secondary); font-size: 12px; margin-left: 8px;">Built with anycoder</a>
665
+ </div>
666
+ <div class="header-controls">
667
+ <button class="header-btn" onclick="showDAWIntegration()">
668
+ 🎹 DAW Integration
669
+ </button>
670
+ <button class="header-btn" onclick="toggleSidePanel()">
671
+ ⚙️ Settings
672
+ </button>
673
+ <button class="header-btn" onclick="exportProject()">
674
+ 💾 Export Project
675
+ </button>
676
+ </div>
677
+ </header>
678
+
679
+ <!-- Toolbar -->
680
+ <div class="toolbar">
681
+ <div class="tool-group">
682
+ <button class="tool-btn active" data-tool="select" onclick="selectTool('select')">
683
+ ↖️ Select
684
+ </button>
685
+ <button class="tool-btn" data-tool="pitch" onclick="selectTool('pitch')">
686
+ 🎵 Pitch
687
+ </button>
688
+ <button class="tool-btn" data-tool="time" onclick="selectTool('time')">
689
+ ⏱️ Time
690
+ </button>
691
+ <button class="tool-btn" data-tool="slice" onclick="selectTool('slice')">
692
+ ✂️ Slice
693
+ </button>
694
+ </div>
695
+ <div class="tool-group">
696
+ <button class="tool-btn" onclick="resetView()">
697
+ 🔄 Reset View
698
+ </button>
699
+ <button class="tool-btn" onclick="zoomIn()">
700
+ 🔍+ Zoom In
701
+ </button>
702
+ <button class="tool-btn" onclick="zoomOut()">
703
+ 🔍- Zoom Out
704
+ </button>
705
+ </div>
706
+ <div class="tool-group">
707
+ <div class="file-input-wrapper">
708
+ <button class="tool-btn" onclick="document.getElementById('audioFile').click()">
709
+ 📁 Load Audio
710
+ </button>
711
+ <input type="file" id="audioFile" class="file-input" accept="audio/*" onchange="loadAudioFile(event)">
712
+ </div>
713
+ <button class="tool-btn" onclick="document.getElementById('midiFile').click()">
714
+ 🎹 Import MIDI
715
+ </button>
716
+ <input type="file" id="midiFile" class="file-input" accept=".mid,.midi" onchange="loadMIDIFile(event)">
717
+ <button class="tool-btn" onclick="clearAll()">
718
+ 🗑️ Clear
719
+ </button>
720
+ </div>
721
+ </div>
722
+
723
+ <!-- DAW Info Bar -->
724
+ <div class="daw-info">
725
+ <div class="daw-info-item">
726
+ <span class="daw-info-label">Tempo:</span>
727
+ <span class="daw-info-value" id="bpmValue">120 BPM</span>
728
+ </div>
729
+ <div class="daw-info-item">
730
+ <span class="daw-info-label">Key:</span>
731
+ <span class="daw-info-value" id="keyValue">C Major</span>
732
+ </div>
733
+ <div class="daw-info-item">
734
+ <span class="daw-info-label">Time Sig:</span>
735
+ <span class="daw-info-value" id="timeSigValue">4/4</span>
736
+ </div>
737
+ <div class="daw-info-item">
738
+ <span class="daw-info-label">Sample Rate:</span>
739
+ <span class="daw-info-value" id="sampleRateValue">44.1 kHz</span>
740
+ </div>
741
+ <div class="daw-info-item">
742
+ <span class="daw-info-label">Format:</span>
743
+ <span class="daw-info-value" id="formatValue">Ready for DAW</span>
744
+ </div>
745
+ </div>
746
+
747
+ <!-- Main Editor -->
748
+ <div class="main-content">
749
+ <div class="editor-container">
750
+ <!-- Timeline -->
751
+ <div class="timeline">
752
+ <div class="time-ruler" id="timeRuler"></div>
753
+ </div>
754
+
755
+ <!-- Piano Roll -->
756
+ <div class="piano-roll" id="pianoRoll"></div>
757
+
758
+ <!-- Pitch Canvas -->
759
+ <canvas id="pitchCanvas"></canvas>
760
+
761
+ <!-- Side Panel -->
762
+ <div class="side-panel" id="sidePanel">
763
+ <div class="panel-section">
764
+ <h3 class="panel-title">DAW Settings</h3>
765
+ <div class="slider-control">
766
+ <div class="slider-label">
767
+ <span>Tempo (BPM)</span>
768
+ <span id="bpmSlider">120</span>
769
+ </div>
770
+ <input type="range" class="slider" min="60" max="200" value="120" oninput="updateBPM(this.value)">
771
+ </div>
772
+ <div class="slider-control">
773
+ <div class="slider-label">
774
+ <span>Quantize Strength</span>
775
+ <span id="quantizeValue">50%</span>
776
+ </div>
777
+ <input type="range" class="slider" min="0" max="100" value="50" oninput="updateSlider('quantize', this.value)">
778
+ </div>
779
  </div>
780
+
781
+ <div class="panel-section">
782
+ <h3 class="panel-title">Pitch Correction</h3>
783
+ <div class="slider-control">
784
+ <div class="slider-label">
785
+ <span>Snap Strength</span>
786
+ <span id="snapValue">50%</span>
787
+ </div>
788
+ <input type="range" class="slider" min="0" max="100" value="50" oninput="updateSlider('snap', this.value)">
789
+ </div>
790
+ <div class="slider-control">
791
+ <div class="slider-label">
792
+ <span>Pitch Range</span>
793
+ <span id="rangeValue">±12</span>
794
+ </div>
795
+ <input type="range" class="slider" min="1" max="24" value="12" oninput="updateSlider('range', this.value)">
796
+ </div>
797
  </div>
 
798
 
799
+ <div class="panel-section">
800
+ <h3 class="panel-title">Export Options</h3>
801
+ <div class="export-options">
802
+ <button class="export-btn" onclick="exportMIDI()">
803
+ 🎹 Export as MIDI
 
 
 
 
 
 
804
  </button>
805
+ <button class="export-btn" onclick="exportAudioWithPitch()">
806
+ 🎵 Export Corrected Audio
 
 
807
  </button>
808
+ <button class="export-btn" onclick="exportMelodyneXML()">
809
+ 📄 Export Melodyne XML
810
  </button>
811
+ <button class="export-btn" onclick="exportAbletonClip()">
812
+ 🎛️ Export Ableton Clip
813
  </button>
814
+ <button class="export-btn" onclick="exportLogicPX()">
815
+ 🎼 Export Logic Pro PX
 
 
 
 
 
 
 
 
816
  </button>
817
+ </div>
818
  </div>
 
819
 
820
+ <div class="panel-section">
821
+ <h3 class="panel-title">Display</h3>
822
+ <div class="slider-control">
823
+ <div class="slider-label">
824
+ <span>Vertical Zoom</span>
825
+ <span id="vZoomValue">100%</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
826
  </div>
827
+ <input type="range" class="slider" min="50" max="200" value="100" oninput="updateSlider('vZoom', this.value)">
828
+ </div>
829
  </div>
830
+ </div>
831
  </div>
832
+ </div>
833
+
834
+ <!-- Playback Controls -->
835
+ <div class="playback-controls">
836
+ <div class="transport">
837
+ <button class="transport-btn" onclick="skipBackward()">⏮️</button>
838
+ <button class="transport-btn" onclick="stepBackward()">⏪</button>
839
+ <button class="transport-btn play" onclick="togglePlayback()">▶️</button>
840
+ <button class="transport-btn" onclick="stepForward()">⏩</button>
841
+ <button class="transport-btn" onclick="skipForward()">⏭️</button>
842
+ <button class="transport-btn" onclick="loopToggle()">🔁</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
843
  </div>
844
 
845
+ <div class="time-display">
846
+ <span id="currentTime">00:00.000</span>
847
+ <span class="time-separator">/</span>
848
+ <span id="totalTime">00:00.000</span>
849
  </div>
850
 
851
+ <div class="pitch-display">
852
+ <div class="pitch-info">
853
+ <span class="pitch-label">Current Note:</span>
854
+ <span class="pitch-value" id="currentNote">--</span>
855
+ </div>
856
+ <div class="pitch-info">
857
+ <span class="pitch-label">Frequency:</span>
858
+ <span class="pitch-value" id="currentFreq">-- Hz</span>
859
+ </div>
860
+ </div>
861
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
862
 
863
+ <!-- Loading Overlay -->
864
+ <div class="loading-overlay" id="loadingOverlay">
865
+ <div class="loading-content">
866
+ <div class="loading-spinner"></div>
867
+ <div class="loading-text">Processing audio...</div>
868
+ </div>
869
+ </div>
870
+
871
+ <!-- DAW Integration Modal -->
872
+ <div class="daw-modal" id="dawModal">
873
+ <div class="modal-content">
874
+ <h2 class="modal-header">🎹 DAW Integration Guide</h2>
875
+
876
+ <div class="modal-section">
877
+ <h3>How to use PitchLab Pro with your DAW:</h3>
878
+ <p><strong>1. Export Options:</strong></p>
879
+ <p>• <strong>MIDI Export:</strong> Export your edited melody as a MIDI file that can be dragged into any DAW</p>
880
+ <p>• <strong>Audio Export:</strong> Export the pitch-corrected audio with applied changes</p>
881
+ <p>• <strong>DAW-Specific Formats:</strong> Export in formats compatible with Ableton Live, Logic Pro, and more</p>
882
+ </div>
883
+
884
+ <div class="modal-section">
885
+ <h3>Workflow Integration:</h3>
886
+ <p><strong>Ableton Live:</strong></p>
887
+ <p>• Export as MIDI and drag onto a MIDI track</p>
888
+ <p>• Use "Export Ableton Clip" for seamless integration</p>
889
+ <p>• Tempo and time signature are automatically matched</p>
890
+
891
+ <p><strong>Logic Pro:</strong></p>
892
+ <p>• Export as Logic PX format for direct import</p>
893
+ <p>• Maintains all pitch correction data</p>
894
+ <p>• Compatible with Logic's Flex Pitch</p>
895
+
896
+ <p><strong>FL Studio:</strong></p>
897
+ <p>• Export as MIDI file</p>
898
+ <p>• Drag into Piano Roll</p>
899
+ <p>• Use with Newtone or Pitcher for further processing</p>
900
+ </div>
901
+
902
+ <div class="modal-section">
903
+ <h3>Real-time Integration:</h3>
904
+ <p>• Use your DAW's ReWire or virtual audio cable to route audio</p>
905
+ <p>• Set BPM to match your project tempo</p>
906
+ <p>• Export stems for each edited section</p>
907
+ </div>
908
+
909
+ <button class="modal-close" onclick="closeDAWModal()">Got it!</button>
910
+ </div>
911
+ </div>
912
+
913
+ <script>
914
+ // Global variables
915
+ let canvas, ctx;
916
+ let audioContext;
917
+ let audioBuffer;
918
+ let source;
919
+ let isPlaying = false;
920
+ let startTime = 0;
921
+ let pauseTime = 0;
922
+ let currentTool = 'select';
923
+ let zoomLevel = 1;
924
+ let verticalZoom = 1;
925
+ let notes = [];
926
+ let selectedNote = null;
927
+ let isDragging = false;
928
+ let dragStartX = 0;
929
+ let dragStartY = 0;
930
+ let viewOffset = 0;
931
+ let playbackPosition = 0;
932
+ let projectBPM = 120;
933
+ let projectKey = 'C';
934
+ let timeSignature = '4/4';
935
+ let isLooping = false;
936
+
937
+ // Note frequencies for C2 to B6
938
+ const noteFrequencies = {
939
+ 'C2': 65.41, 'C#2': 69.30, 'D2': 73.42, 'D#2': 77.78, 'E2': 82.41, 'F2': 87.31,
940
+ 'F#2': 92.50, 'G2': 98.00, 'G#2': 103.83, 'A2': 110.00, 'A#2': 116.54, 'B2': 123.47,
941
+ 'C3': 130.81, 'C#3': 138.59, 'D3': 146.83, 'D#3': 155.56, 'E3': 164.81, 'F3': 174.61,
942
+ 'F#3': 185.00, 'G3': 196.00, 'G#3': 207.65, 'A3': 220.00, 'A#3': 233.08, 'B3': 246.94,
943
+ 'C4': 261.63, 'C#4': 277.18, 'D4': 293.66, 'D#4': 311.13, 'E4': 329.63, 'F4': 349.23,
944
+ 'F#4': 369.99, 'G4': 392.00, 'G#4': 415.30, 'A4': 440.00, 'A#4': 466.16, 'B4': 493.88,
945
+ 'C5': 523.25, 'C#5': 554.37, 'D5': 587.33, 'D#5': 622.25, 'E5': 659.25, 'F5': 698.46,
946
+ 'F#5': 739.99, 'G5': 783.99, 'G#5': 830.61, 'A5': 880.00, 'A#5': 932.33, 'B5': 987.77,
947
+ 'C6': 1046.50, 'C#6': 1108.73, 'D6': 1174.66, 'D#6': 1244.51, 'E6': 1318.51, 'F6': 1396.91
948
+ };
949
+
950
+ // Initialize
951
+ document.addEventListener('DOMContentLoaded', function() {
952
+ initCanvas();
953
+ initAudioContext();
954
+ initPianoRoll();
955
+ generateDemoNotes();
956
+ drawInterface();
957
+ updateTimeRuler();
958
+ startAnimationLoop();
959
+ });
960
+
961
+ function initCanvas() {
962
+ canvas = document.getElementById('pitchCanvas');
963
+ ctx = canvas.getContext('2d');
964
+ resizeCanvas();
965
+ window.addEventListener('resize', resizeCanvas);
966
+
967
+ canvas.addEventListener('mousedown', handleMouseDown);
968
+ canvas.addEventListener('mousemove', handleMouseMove);
969
+ canvas.addEventListener('mouseup', handleMouseUp);
970
+ canvas.addEventListener('wheel', handleWheel);
971
+ }
972
+
973
+ function resizeCanvas() {
974
+ const container = document.querySelector('.editor-container');
975
+ canvas.width = container.clientWidth - 60;
976
+ canvas.height = container.clientHeight - 45;
977
+ drawInterface();
978
+ }
979
+
980
+ function initPianoRoll() {
981
+ const pianoRoll = document.getElementById('pianoRoll');
982
+ pianoRoll.innerHTML = '';
983
+
984
+ const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
985
+
986
+ for (let octave = 6; octave >= 2; octave--) {
987
+ for (let i = noteNames.length - 1; i >= 0; i--) {
988
+ const noteName = noteNames[i] + octave;
989
+ const key = document.createElement('div');
990
+ key.className = 'piano-key';
991
+
992
+ if (noteNames[i].includes('#')) {
993
+ key.classList.add('black');
994
+ } else {
995
+ key.classList.add('white');
996
+ }
997
+
998
+ key.textContent = noteName;
999
+ pianoRoll.appendChild(key);
1000
+ }
1001
+ }
1002
+ }
1003
+
1004
+ function initAudioContext() {
1005
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
1006
+ }
1007
+
1008
+ function generateDemoNotes() {
1009
+ const demoNotes = [
1010
+ { note: 'C4', start: 0, duration: 0.5, pitch: 261.63, velocity: 80 },
1011
+ { note: 'E4', start: 0.5, duration: 0.5, pitch: 329.63, velocity: 75 },
1012
+ { note: 'G4', start: 1.0, duration: 0.5, pitch: 392.00, velocity: 70 },
1013
+ { note: 'C5', start: 1.5, duration: 0.5, pitch: 523.25, velocity: 85 },
1014
+ { note: 'G4', start: 2.0, duration: 0.5, pitch: 392.00, velocity: 70 },
1015
+ { note: 'E4', start: 2.5, duration: 0.5, pitch: 329