Spaces:
Running on Zero
Running on Zero
Upload 8 files
Browse files- frontend/app.css +170 -27
- frontend/studio.css +287 -92
- frontend/studio.html +79 -71
- frontend/studio.js +147 -83
frontend/app.css
CHANGED
|
@@ -1,28 +1,171 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
color-scheme: dark;
|
| 3 |
+
--ms-bg: #090b0f;
|
| 4 |
+
--ms-bg-elevated: #0d1016;
|
| 5 |
+
--ms-panel: rgba(18, 21, 29, .86);
|
| 6 |
+
--ms-border: rgba(255, 255, 255, .085);
|
| 7 |
+
--ms-border-strong: rgba(255, 255, 255, .15);
|
| 8 |
+
--ms-text: #f4f5f7;
|
| 9 |
+
--ms-text-soft: #b5bac6;
|
| 10 |
+
--ms-muted: #747b8c;
|
| 11 |
+
--ms-faint: #4f5564;
|
| 12 |
+
--ms-accent: #8b7cff;
|
| 13 |
+
--ms-success: #63d8a2;
|
| 14 |
+
--ms-warning: #f4bf75;
|
| 15 |
+
--ms-radius-xl: 22px;
|
| 16 |
+
--ms-radius-lg: 16px;
|
| 17 |
+
--ms-radius-md: 12px;
|
| 18 |
+
--ms-shadow: 0 28px 80px rgba(0,0,0,.34), inset 0 1px rgba(255,255,255,.025);
|
| 19 |
+
--ms-font: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
| 20 |
+
--ms-mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
:host {
|
| 24 |
+
display: block;
|
| 25 |
+
min-height: 100vh;
|
| 26 |
+
color-scheme: dark;
|
| 27 |
+
background:
|
| 28 |
+
radial-gradient(1000px 620px at 10% -10%, rgba(92,75,202,.18), transparent 62%),
|
| 29 |
+
radial-gradient(780px 500px at 96% 8%, rgba(53,103,175,.12), transparent 66%),
|
| 30 |
+
linear-gradient(180deg, #0a0c11 0%, #090b0f 50%, #080a0d 100%);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
html, body { min-height: 100%; background: var(--ms-bg) !important; }
|
| 34 |
+
body { margin: 0; color: var(--ms-text); font-family: var(--ms-font); -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
|
| 35 |
+
body::before {
|
| 36 |
+
position: fixed;
|
| 37 |
+
z-index: -2;
|
| 38 |
+
inset: 0;
|
| 39 |
+
content: "";
|
| 40 |
+
pointer-events: none;
|
| 41 |
+
background:
|
| 42 |
+
radial-gradient(1000px 620px at 10% -10%, rgba(92,75,202,.18), transparent 62%),
|
| 43 |
+
radial-gradient(780px 500px at 96% 8%, rgba(53,103,175,.12), transparent 66%),
|
| 44 |
+
linear-gradient(180deg, #0a0c11 0%, #090b0f 50%, #080a0d 100%);
|
| 45 |
+
}
|
| 46 |
+
body::after {
|
| 47 |
+
position: fixed;
|
| 48 |
+
z-index: -1;
|
| 49 |
+
inset: 0;
|
| 50 |
+
content: "";
|
| 51 |
+
pointer-events: none;
|
| 52 |
+
opacity: .28;
|
| 53 |
+
background-image: linear-gradient(rgba(255,255,255,.012) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.012) 1px, transparent 1px);
|
| 54 |
+
background-size: 32px 32px;
|
| 55 |
+
mask-image: linear-gradient(to bottom, black, transparent 78%);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.gradio-container, .gradio-container.dark {
|
| 59 |
+
min-height: 100vh !important;
|
| 60 |
+
max-width: none !important;
|
| 61 |
+
margin: 0 !important;
|
| 62 |
+
padding: 0 !important;
|
| 63 |
+
background: transparent !important;
|
| 64 |
+
color: var(--ms-text) !important;
|
| 65 |
+
font-family: var(--ms-font) !important;
|
| 66 |
+
}
|
| 67 |
+
.gradio-container .prose, .gradio-container .prose * { color: inherit; }
|
| 68 |
+
.gradio-container footer { display: none !important; }
|
| 69 |
+
|
| 70 |
+
#app-shell {
|
| 71 |
+
width: min(1240px, calc(100% - 40px));
|
| 72 |
+
margin: 0 auto;
|
| 73 |
+
padding: 34px 0 58px;
|
| 74 |
+
gap: 18px;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
#masthead, #intro { border: 0 !important; background: transparent !important; }
|
| 78 |
+
#masthead { padding: 16px 4px 24px; }
|
| 79 |
+
#masthead .masthead-inner { display: flex; align-items: center; justify-content: space-between; gap: 18px; }
|
| 80 |
+
#masthead .brand { display: inline-flex; align-items: center; gap: 11px; color: var(--ms-text) !important; font-size: .92rem; font-weight: 680; letter-spacing: -.025em; text-decoration: none; }
|
| 81 |
+
.brand-mark { display: flex; align-items: end; justify-content: center; width: 30px; height: 30px; gap: 2px; padding: 7px; border: 1px solid rgba(139,124,255,.3); border-radius: 9px; background: linear-gradient(145deg, rgba(139,124,255,.2), rgba(95,140,255,.08)); box-shadow: 0 8px 28px rgba(75,60,180,.2), inset 0 1px rgba(255,255,255,.13); }
|
| 82 |
+
.brand-mark i { display: block; width: 2px; border-radius: 3px; background: #d8d4ff; }
|
| 83 |
+
.brand-mark i:nth-child(1) { height: 6px; opacity: .62; }
|
| 84 |
+
.brand-mark i:nth-child(2) { height: 12px; }
|
| 85 |
+
.brand-mark i:nth-child(3) { height: 9px; opacity: .8; }
|
| 86 |
+
.brand-mark i:nth-child(4) { height: 14px; }
|
| 87 |
+
.header-meta { display: flex; align-items: center; gap: 8px; }
|
| 88 |
+
#model-badge, .zerogpu-badge { display: inline-flex; align-items: center; min-height: 30px; padding: 5px 10px; border: 1px solid var(--ms-border); border-radius: 999px; font: 620 .64rem/1 var(--ms-mono); letter-spacing: .07em; text-transform: uppercase; }
|
| 89 |
+
#model-badge { border-color: rgba(139,124,255,.22); background: rgba(139,124,255,.09); color: #cec8ff; }
|
| 90 |
+
#model-badge::before { width: 6px; height: 6px; margin-right: 7px; border-radius: 50%; background: var(--ms-success); box-shadow: 0 0 0 4px rgba(99,216,162,.1), 0 0 12px rgba(99,216,162,.62); content: ""; }
|
| 91 |
+
.zerogpu-badge { background: rgba(255,255,255,.025); color: var(--ms-muted); }
|
| 92 |
+
|
| 93 |
+
#intro { padding: 28px 4px 32px; }
|
| 94 |
+
.intro-copy { max-width: 850px; }
|
| 95 |
+
.eyebrow { display: inline-block; margin-bottom: 16px; color: #aaa1ff; font: 670 .67rem var(--ms-mono); letter-spacing: .13em; text-transform: uppercase; }
|
| 96 |
+
.intro-copy h1 { margin: 0; color: var(--ms-text); font-size: clamp(2.7rem, 7vw, 5.9rem); font-weight: 590; letter-spacing: -.072em; line-height: .94; }
|
| 97 |
+
.intro-copy h1 em { color: transparent; background: linear-gradient(112deg, #d9d5ff 4%, #8b7cff 48%, #5f9cff 94%); -webkit-background-clip: text; background-clip: text; font-style: normal; }
|
| 98 |
+
.intro-copy p { max-width: 690px; margin: 23px 0 0; color: var(--ms-text-soft); font-size: clamp(.95rem,1.8vw,1.1rem); line-height: 1.68; letter-spacing: -.015em; }
|
| 99 |
+
|
| 100 |
+
#studio-grid { align-items: flex-start; gap: 16px; }
|
| 101 |
+
.section-heading { display: flex; align-items: center; gap: 12px; padding: 2px 2px 12px; }
|
| 102 |
+
.section-heading .step-number { color: var(--ms-faint); font: .61rem var(--ms-mono); letter-spacing: .08em; }
|
| 103 |
+
.section-heading h2 { margin: 0 0 4px; color: var(--ms-text); font-size: .9rem; font-weight: 630; letter-spacing: -.02em; }
|
| 104 |
+
.section-heading p { margin: 0; color: var(--ms-muted); font-size: .68rem; line-height: 1.4; }
|
| 105 |
+
|
| 106 |
+
#upload-card, #result-panel {
|
| 107 |
+
position: relative;
|
| 108 |
+
min-width: 0;
|
| 109 |
+
border: 1px solid var(--ms-border) !important;
|
| 110 |
+
border-radius: var(--ms-radius-xl) !important;
|
| 111 |
+
background: linear-gradient(145deg, rgba(20,23,32,.93), rgba(13,16,22,.9)) !important;
|
| 112 |
+
box-shadow: var(--ms-shadow) !important;
|
| 113 |
+
backdrop-filter: blur(22px);
|
| 114 |
+
}
|
| 115 |
+
#upload-card::before, #result-panel::before { position: absolute; top: 0; right: 12%; left: 12%; height: 1px; content: ""; pointer-events: none; background: linear-gradient(90deg, transparent, rgba(255,255,255,.16), transparent); }
|
| 116 |
+
#upload-card { padding: clamp(16px,2.5vw,25px) !important; }
|
| 117 |
+
#result-panel { padding: clamp(12px,2vw,19px) !important; }
|
| 118 |
+
|
| 119 |
+
#audio-input { overflow: hidden; min-height: 214px; border: 1px dashed rgba(255,255,255,.16) !important; border-radius: var(--ms-radius-lg) !important; background: rgba(7,9,13,.46) !important; transition: border-color 180ms ease, background 180ms ease; }
|
| 120 |
+
#audio-input:hover, #audio-input:focus-within { border-color: rgba(139,124,255,.54) !important; background: rgba(139,124,255,.045) !important; }
|
| 121 |
+
#audio-input .wrap, #audio-input > div, #audio-input .upload-container { border: 0 !important; background: transparent !important; }
|
| 122 |
+
#audio-input .upload-container { min-height: 190px; }
|
| 123 |
+
#audio-input button, #audio-input .upload-button { border-color: var(--ms-border-strong) !important; border-radius: 10px !important; background: rgba(255,255,255,.055) !important; color: var(--ms-text) !important; box-shadow: none !important; }
|
| 124 |
+
#audio-input audio { width: 100%; filter: saturate(.75) contrast(.96); }
|
| 125 |
+
|
| 126 |
+
#advanced-settings, #score-settings, #downloads-panel { overflow: hidden; border: 1px solid var(--ms-border) !important; border-radius: var(--ms-radius-md) !important; background: rgba(8,10,14,.42) !important; box-shadow: none !important; }
|
| 127 |
+
#advanced-settings > button, #advanced-settings summary, #score-settings > button, #score-settings summary, #downloads-panel > button, #downloads-panel summary { min-height: 43px; padding-inline: 13px !important; background: transparent !important; color: var(--ms-text-soft) !important; font-size: .76rem !important; font-weight: 580 !important; }
|
| 128 |
+
#advanced-settings > div, #score-settings > div, #downloads-panel > div { border-color: var(--ms-border) !important; background: transparent !important; }
|
| 129 |
+
#advanced-settings label, #score-settings label, #downloads-panel label { color: var(--ms-text-soft) !important; font-size: .73rem !important; }
|
| 130 |
+
#advanced-settings input, #advanced-settings textarea, #advanced-settings select, #score-settings input, #score-settings textarea, #score-settings select { border-color: var(--ms-border) !important; border-radius: 9px !important; background: rgba(255,255,255,.035) !important; color: var(--ms-text) !important; }
|
| 131 |
+
#advanced-settings input[type="range"], #advanced-settings input[type="checkbox"], #score-settings input[type="range"], #score-settings input[type="checkbox"] { accent-color: var(--ms-accent); }
|
| 132 |
+
|
| 133 |
+
#transcribe-button { position: relative; overflow: hidden; width: 100%; min-height: 50px; margin-top: 3px; border: 1px solid rgba(255,255,255,.15) !important; border-radius: 13px !important; background: linear-gradient(128deg, #7a69eb 0%, #675ad2 48%, #4f75d5 100%) !important; box-shadow: 0 10px 30px rgba(76,66,181,.25), inset 0 1px rgba(255,255,255,.2) !important; color: #fff !important; font-size: .9rem !important; font-weight: 650 !important; transition: transform 150ms ease, box-shadow 150ms ease, filter 150ms ease !important; }
|
| 134 |
+
#transcribe-button:hover:not(:disabled) { filter: brightness(1.08); transform: translateY(-1px); box-shadow: 0 14px 38px rgba(76,66,181,.32), inset 0 1px rgba(255,255,255,.22) !important; }
|
| 135 |
+
#transcribe-button:disabled, #transcribe-button[aria-busy="true"] { cursor: wait; filter: saturate(.55) brightness(.8); }
|
| 136 |
+
.cold-note { display: flex; align-items: flex-start; gap: 8px; margin: 2px !important; color: var(--ms-faint); font-size: .64rem; line-height: 1.5; }
|
| 137 |
+
.cold-note > span { width: 6px; height: 6px; flex: none; margin-top: 3px; border-radius: 50%; background: var(--ms-warning); box-shadow: 0 0 8px rgba(244,191,117,.38); }
|
| 138 |
+
|
| 139 |
+
#studio-viewer, #visualizer-host { min-width: 0; margin: 0; padding: 0 !important; border: 0 !important; background: transparent !important; }
|
| 140 |
+
#studio-viewer > .wrap, #studio-viewer > div, #studio-viewer .html-container { min-width: 0; padding: 0 !important; border: 0 !important; background: transparent !important; }
|
| 141 |
+
#score-settings { margin-top: 3px; }
|
| 142 |
+
#generate-score-button { min-height: 42px; margin-top: 5px; border: 1px solid rgba(139,124,255,.44) !important; border-radius: 10px !important; background: rgba(139,124,255,.14) !important; color: #ede9fe !important; font-weight: 680 !important; }
|
| 143 |
+
#generate-score-button:hover:not(:disabled) { background: rgba(139,124,255,.22) !important; }
|
| 144 |
+
#generate-score-button:disabled { border-color: var(--ms-border) !important; background: rgba(255,255,255,.025) !important; color: var(--ms-faint) !important; }
|
| 145 |
+
#downloads-panel { margin-top: 0; }
|
| 146 |
+
#downloads-row { margin: 0; padding: 3px 11px 12px; display: flex !important; flex-wrap: wrap !important; gap: 8px; border: 0; background: transparent; }
|
| 147 |
+
#downloads-row > div { min-width: min(190px,100%); flex: 1 1 190px !important; overflow: hidden; border: 1px solid var(--ms-border) !important; border-radius: 9px !important; background: rgba(255,255,255,.022) !important; }
|
| 148 |
+
|
| 149 |
+
.footnote { display: flex; justify-content: space-between; gap: 16px; padding: 14px 5px 0; color: var(--ms-faint); font: .57rem/1.5 var(--ms-mono); }
|
| 150 |
+
|
| 151 |
+
@media (max-width: 860px) {
|
| 152 |
+
#app-shell { width: min(100% - 24px,1240px); padding-top: 22px; }
|
| 153 |
+
#intro { padding: 22px 3px 27px; }
|
| 154 |
+
#studio-grid { flex-direction: column !important; }
|
| 155 |
+
#upload-card, #result-panel { width: 100% !important; min-width: 100% !important; }
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
@media (max-width: 600px) {
|
| 159 |
+
#app-shell { width: min(100% - 16px,1240px); padding-bottom: 30px; gap: 12px; }
|
| 160 |
+
#masthead .masthead-inner, .footnote { align-items: flex-start; flex-direction: column; }
|
| 161 |
+
.header-meta { width: 100%; }
|
| 162 |
+
.intro-copy h1 { font-size: clamp(2.65rem,15vw,4.2rem); letter-spacing: -.062em; }
|
| 163 |
+
#upload-card, #result-panel { border-radius: 17px !important; }
|
| 164 |
+
#upload-card { padding: 12px !important; }
|
| 165 |
+
#result-panel { padding: 10px !important; }
|
| 166 |
+
#audio-input { min-height: 180px; }
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
@media (prefers-reduced-motion: reduce) {
|
| 170 |
+
*, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
|
| 171 |
}
|
frontend/studio.css
CHANGED
|
@@ -1,96 +1,291 @@
|
|
| 1 |
-
:root {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
* { box-sizing: border-box; }
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
.ms-root [hidden] { display: none !important; }
|
| 5 |
-
.ms-root button, .ms-root input { font: inherit; }
|
| 6 |
-
|
| 7 |
-
.ms-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
.ms-
|
| 17 |
-
.ms-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
.ms-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
.ms-
|
| 38 |
-
.ms-
|
| 39 |
-
|
| 40 |
-
.ms-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
.ms-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
.ms-track-copy strong, .ms-track-copy span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 57 |
-
.ms-track-copy strong { font-size:
|
| 58 |
-
.ms-track-copy span { margin-top: 4px; color: var(--
|
| 59 |
-
.ms-program-select { width: 100%
|
| 60 |
.ms-track-controls { display: flex; gap: 4px; }
|
| 61 |
-
.ms-track-controls button, .ms-track-controls a { width: 27px; height: 27px;
|
| 62 |
-
.ms-track-controls button[aria-pressed="true"] {
|
| 63 |
-
|
| 64 |
-
.ms-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
.ms-
|
| 70 |
-
.ms-
|
| 71 |
-
.ms-
|
| 72 |
-
.ms-
|
| 73 |
-
.ms-
|
| 74 |
-
.ms-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
.ms-
|
| 79 |
-
.ms-
|
| 80 |
-
.ms-
|
| 81 |
-
.ms-
|
| 82 |
-
.ms-
|
| 83 |
-
.ms-
|
| 84 |
-
.ms-
|
| 85 |
-
.ms-
|
| 86 |
-
.ms-
|
| 87 |
-
.ms-
|
| 88 |
-
.ms-
|
| 89 |
-
.ms-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
@container (max-width: 760px) { .ms-root { height: 920px; min-height: 920px; } .ms-statusbar { min-height: 66px; flex-basis: 66px; grid-template-columns: 1fr; gap: 8px; } .ms-song, .ms-progress { display: none; } .ms-transport { min-height: 94px; flex-basis: 94px; flex-wrap: wrap; } .ms-transport > input { min-width: calc(100% - 135px); } .ms-source-mix { margin-left: 86px; } .ms-grid { grid-template-columns: 1fr; grid-template-rows: 230px minmax(0, 1fr); } .ms-tracks-panel { border-right: 0; } .ms-track-list { grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); } .ms-main-panel { border-top: 1px solid var(--line); } .ms-score-view { padding: 10px; } }
|
| 96 |
-
@container (max-width: 500px) { .ms-root { height: 940px; min-height: 940px; } .ms-statusbar { padding: 12px; } .ms-transport { padding: 8px; gap: 6px; } .ms-transport > span { min-width: 38px; font-size: 9px; } .ms-source-mix { margin-left: 0; } .ms-source-mix input { width: 42px; } .ms-roll-view { padding: 9px; } .ms-track-list { grid-template-columns: 1fr; } }
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
color-scheme: dark;
|
| 3 |
+
--ms-bg: #090b0f;
|
| 4 |
+
--ms-panel: #11151d;
|
| 5 |
+
--ms-line: rgba(255, 255, 255, .085);
|
| 6 |
+
--ms-line-strong: rgba(255, 255, 255, .15);
|
| 7 |
+
--ms-text: #f4f5f7;
|
| 8 |
+
--ms-soft: #b5bac6;
|
| 9 |
+
--ms-muted: #747b8c;
|
| 10 |
+
--ms-faint: #505766;
|
| 11 |
+
--ms-accent: #8b7cff;
|
| 12 |
+
--ms-success: #63d8a2;
|
| 13 |
+
--ms-warning: #f4bf75;
|
| 14 |
+
--ms-danger: #ff758e;
|
| 15 |
+
--ms-mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
* { box-sizing: border-box; }
|
| 19 |
+
|
| 20 |
+
.ms-root {
|
| 21 |
+
display: grid;
|
| 22 |
+
min-width: 0;
|
| 23 |
+
gap: 14px;
|
| 24 |
+
color: var(--ms-text);
|
| 25 |
+
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
| 26 |
+
container-type: inline-size;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
.ms-root [hidden] { display: none !important; }
|
| 30 |
+
.ms-root button, .ms-root input, .ms-root select { font: inherit; }
|
| 31 |
+
|
| 32 |
+
.ms-topbar {
|
| 33 |
+
display: flex;
|
| 34 |
+
align-items: flex-start;
|
| 35 |
+
justify-content: space-between;
|
| 36 |
+
gap: 18px;
|
| 37 |
+
min-width: 0;
|
| 38 |
+
padding: 2px 2px 0;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.ms-heading { min-width: 0; }
|
| 42 |
+
.ms-heading h2 {
|
| 43 |
+
overflow: hidden;
|
| 44 |
+
max-width: 620px;
|
| 45 |
+
margin: 0;
|
| 46 |
+
color: var(--ms-text);
|
| 47 |
+
font-size: clamp(1.04rem, 2vw, 1.3rem);
|
| 48 |
+
font-weight: 620;
|
| 49 |
+
letter-spacing: -.032em;
|
| 50 |
+
text-overflow: ellipsis;
|
| 51 |
+
white-space: nowrap;
|
| 52 |
+
}
|
| 53 |
+
.ms-subtitle {
|
| 54 |
+
display: flex;
|
| 55 |
+
align-items: center;
|
| 56 |
+
gap: 8px;
|
| 57 |
+
margin: 6px 0 0;
|
| 58 |
+
color: var(--ms-muted);
|
| 59 |
+
font-size: .74rem;
|
| 60 |
+
line-height: 1.45;
|
| 61 |
+
}
|
| 62 |
+
.ms-subtitle b { color: var(--ms-faint); font: 560 .57rem var(--ms-mono); white-space: nowrap; }
|
| 63 |
+
.ms-subtitle b::before { margin-right: 8px; content: "·"; }
|
| 64 |
+
|
| 65 |
+
.ms-eyebrow {
|
| 66 |
+
display: flex;
|
| 67 |
+
align-items: center;
|
| 68 |
+
gap: 8px;
|
| 69 |
+
margin: 0 0 7px;
|
| 70 |
+
color: var(--ms-muted);
|
| 71 |
+
font: 650 .62rem/1 var(--ms-mono);
|
| 72 |
+
letter-spacing: .09em;
|
| 73 |
+
text-transform: uppercase;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.ms-state-dot {
|
| 77 |
+
width: 7px;
|
| 78 |
+
height: 7px;
|
| 79 |
+
flex: none;
|
| 80 |
+
border-radius: 50%;
|
| 81 |
+
background: var(--ms-faint);
|
| 82 |
+
box-shadow: 0 0 0 3px rgba(255,255,255,.035);
|
| 83 |
+
}
|
| 84 |
+
.ms-root[data-state="queued"] .ms-state-dot,
|
| 85 |
+
.ms-root[data-state="loading"] .ms-state-dot,
|
| 86 |
+
.ms-root[data-state="transcribing"] .ms-state-dot {
|
| 87 |
+
background: var(--ms-warning);
|
| 88 |
+
box-shadow: 0 0 0 4px rgba(244,191,117,.1), 0 0 14px rgba(244,191,117,.38);
|
| 89 |
+
animation: ms-pulse 1.7s ease-in-out infinite;
|
| 90 |
+
}
|
| 91 |
+
.ms-root[data-state="complete"] .ms-state-dot,
|
| 92 |
+
.ms-root[data-state="ready"] .ms-state-dot {
|
| 93 |
+
background: var(--ms-success);
|
| 94 |
+
box-shadow: 0 0 0 4px rgba(99,216,162,.09), 0 0 13px rgba(99,216,162,.32);
|
| 95 |
+
}
|
| 96 |
+
.ms-root[data-state="error"] .ms-state-dot,
|
| 97 |
+
.ms-root[data-state="notation_error"] .ms-state-dot { background: var(--ms-danger); }
|
| 98 |
+
|
| 99 |
+
.ms-quick-downloads { display: flex; flex: none; gap: 6px; }
|
| 100 |
+
.ms-quick-downloads a,
|
| 101 |
+
.ms-tracks-heading button,
|
| 102 |
+
.ms-track-controls button,
|
| 103 |
+
.ms-track-controls a {
|
| 104 |
+
display: inline-flex;
|
| 105 |
+
align-items: center;
|
| 106 |
+
justify-content: center;
|
| 107 |
+
border: 1px solid var(--ms-line);
|
| 108 |
+
border-radius: 9px;
|
| 109 |
+
background: rgba(255,255,255,.035);
|
| 110 |
+
color: var(--ms-soft);
|
| 111 |
+
cursor: pointer;
|
| 112 |
+
text-decoration: none;
|
| 113 |
+
transition: color 140ms ease, border-color 140ms ease, background 140ms ease, transform 140ms ease;
|
| 114 |
+
}
|
| 115 |
+
.ms-quick-downloads a { min-height: 33px; padding: 0 10px; font-size: .66rem; font-weight: 620; }
|
| 116 |
+
.ms-quick-downloads a:hover:not([aria-disabled="true"]),
|
| 117 |
+
.ms-tracks-heading button:hover,
|
| 118 |
+
.ms-track-controls button:hover,
|
| 119 |
+
.ms-track-controls a:hover:not([aria-disabled="true"]) {
|
| 120 |
+
border-color: rgba(139,124,255,.38);
|
| 121 |
+
background: rgba(139,124,255,.13);
|
| 122 |
+
color: #e4e1ff;
|
| 123 |
+
transform: translateY(-1px);
|
| 124 |
+
}
|
| 125 |
+
.ms-quick-downloads a[aria-disabled="true"], .ms-track-controls a[aria-disabled="true"] {
|
| 126 |
+
pointer-events: none;
|
| 127 |
+
opacity: .3;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.ms-progress {
|
| 131 |
+
display: grid;
|
| 132 |
+
gap: 8px;
|
| 133 |
+
padding: 11px 13px;
|
| 134 |
+
border: 1px solid var(--ms-line);
|
| 135 |
+
border-radius: 11px;
|
| 136 |
+
background: rgba(7,9,13,.45);
|
| 137 |
+
}
|
| 138 |
+
.ms-progress > div { display: flex; justify-content: space-between; gap: 12px; color: var(--ms-soft); font-size: .7rem; }
|
| 139 |
+
.ms-progress b { color: var(--ms-muted); font: 600 .64rem var(--ms-mono); }
|
| 140 |
+
.ms-progress i { display: block; overflow: hidden; height: 4px; border-radius: 99px; background: rgba(255,255,255,.055); }
|
| 141 |
+
.ms-progress i span { display: block; width: 0; height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--ms-accent), #6b9bff); box-shadow: 0 0 18px rgba(119,102,255,.45); transition: width 320ms ease; }
|
| 142 |
+
|
| 143 |
+
.ms-empty {
|
| 144 |
+
min-height: 420px;
|
| 145 |
+
display: grid;
|
| 146 |
+
align-content: center;
|
| 147 |
+
justify-items: center;
|
| 148 |
+
padding: 34px 20px;
|
| 149 |
+
border: 1px solid var(--ms-line);
|
| 150 |
+
border-radius: 14px;
|
| 151 |
+
background: radial-gradient(circle at 50% 42%, rgba(139,124,255,.06), transparent 36%), #0a0d12;
|
| 152 |
+
text-align: center;
|
| 153 |
+
}
|
| 154 |
+
.ms-empty-icon { display: grid; width: 44px; height: 44px; border: 1px solid var(--ms-line); border-radius: 12px; background: rgba(255,255,255,.025); color: #aaa1ff; font-size: 20px; place-items: center; }
|
| 155 |
+
.ms-empty h3 { margin: 14px 0 6px; color: var(--ms-soft); font-size: .88rem; }
|
| 156 |
+
.ms-empty p { max-width: 430px; margin: 0; color: var(--ms-faint); font-size: .7rem; line-height: 1.6; }
|
| 157 |
+
|
| 158 |
+
.ms-workspace { display: grid; min-width: 0; gap: 12px; }
|
| 159 |
+
|
| 160 |
+
.ms-stats {
|
| 161 |
+
display: grid;
|
| 162 |
+
grid-template-columns: repeat(4, minmax(0,1fr));
|
| 163 |
+
overflow: hidden;
|
| 164 |
+
border: 1px solid var(--ms-line);
|
| 165 |
+
border-radius: 11px;
|
| 166 |
+
background: rgba(7,9,13,.36);
|
| 167 |
+
}
|
| 168 |
+
.ms-stats > div { min-width: 0; padding: 10px 12px; }
|
| 169 |
+
.ms-stats > div + div { border-left: 1px solid var(--ms-line); }
|
| 170 |
+
.ms-stats span { display: block; margin-bottom: 4px; color: var(--ms-faint); font: 650 .55rem var(--ms-mono); letter-spacing: .07em; text-transform: uppercase; }
|
| 171 |
+
.ms-stats strong { display: block; overflow: hidden; color: var(--ms-soft); font-size: .75rem; font-weight: 590; text-overflow: ellipsis; white-space: nowrap; }
|
| 172 |
+
|
| 173 |
+
.ms-transport {
|
| 174 |
+
display: grid;
|
| 175 |
+
gap: 9px;
|
| 176 |
+
padding: 10px;
|
| 177 |
+
border: 1px solid var(--ms-line);
|
| 178 |
+
border-radius: 12px;
|
| 179 |
+
background: rgba(7,9,13,.46);
|
| 180 |
+
}
|
| 181 |
+
.ms-transport-main, .ms-transport-options { display: flex; align-items: center; min-width: 0; gap: 8px; }
|
| 182 |
+
.ms-play, .ms-stop {
|
| 183 |
+
display: grid;
|
| 184 |
+
width: 34px;
|
| 185 |
+
height: 34px;
|
| 186 |
+
flex: none;
|
| 187 |
+
border: 1px solid var(--ms-line-strong);
|
| 188 |
+
border-radius: 50%;
|
| 189 |
+
color: #fff;
|
| 190 |
+
background: linear-gradient(135deg, #7a69eb, #5576d7);
|
| 191 |
+
cursor: pointer;
|
| 192 |
+
place-items: center;
|
| 193 |
+
}
|
| 194 |
+
.ms-stop { width: 29px; height: 29px; border-radius: 8px; color: var(--ms-soft); background: rgba(255,255,255,.04); }
|
| 195 |
+
.ms-play:disabled, .ms-stop:disabled { cursor: not-allowed; filter: saturate(.3); opacity: .38; }
|
| 196 |
+
.ms-transport-main > span { min-width: 44px; color: var(--ms-muted); font: 600 .62rem var(--ms-mono); text-align: center; }
|
| 197 |
+
.ms-seek { min-width: 80px; flex: 1; accent-color: var(--ms-accent); }
|
| 198 |
+
.ms-transport-options { flex-wrap: wrap; padding-left: 71px; }
|
| 199 |
+
|
| 200 |
+
.ms-source-mix { display: flex; align-items: center; gap: 5px; min-height: 32px; padding: 3px; border: 1px solid var(--ms-line); border-radius: 9px; background: rgba(255,255,255,.025); }
|
| 201 |
+
.ms-source-mix button { height: 24px; padding: 0 7px; border: 0; border-radius: 6px; color: var(--ms-muted); background: transparent; cursor: pointer; font-size: .58rem; font-weight: 720; }
|
| 202 |
+
.ms-source-mix button[aria-pressed="true"] { color: #fff; background: rgba(139,124,255,.32); }
|
| 203 |
+
.ms-source-mix input { width: 82px; accent-color: var(--ms-accent); }
|
| 204 |
+
.ms-stereo-toggle, .ms-volume { display: flex; align-items: center; gap: 6px; min-height: 32px; padding: 0 8px; border: 1px solid var(--ms-line); border-radius: 9px; color: var(--ms-muted); background: rgba(255,255,255,.025); font-size: .6rem; white-space: nowrap; }
|
| 205 |
+
.ms-stereo-toggle { cursor: pointer; }
|
| 206 |
+
.ms-stereo-toggle input { accent-color: var(--ms-accent); }
|
| 207 |
+
.ms-stereo-toggle[data-active="true"] { border-color: rgba(139,124,255,.46); color: #dcd7ff; background: rgba(139,124,255,.12); }
|
| 208 |
+
.ms-volume input { width: 64px; accent-color: var(--ms-accent); }
|
| 209 |
+
.ms-synth-state { margin-left: auto; color: var(--ms-faint); font: 570 .56rem var(--ms-mono); }
|
| 210 |
+
.ms-synth-state[data-kind="loading"] { color: var(--ms-warning); }
|
| 211 |
+
.ms-synth-state[data-kind="ready"] { color: var(--ms-success); }
|
| 212 |
+
.ms-synth-state[data-kind="error"] { color: var(--ms-danger); }
|
| 213 |
+
|
| 214 |
+
.ms-view-shell { overflow: hidden; min-width: 0; border: 1px solid var(--ms-line); border-radius: 14px; background: #0a0d12; }
|
| 215 |
+
.ms-tabs { display: flex; align-items: center; min-height: 42px; gap: 20px; padding: 0 13px; border-bottom: 1px solid var(--ms-line); background: rgba(255,255,255,.018); }
|
| 216 |
+
.ms-tabs button { align-self: stretch; padding: 0 1px; border: 0; border-bottom: 2px solid transparent; color: var(--ms-muted); background: transparent; cursor: pointer; font-size: .64rem; font-weight: 720; text-transform: uppercase; }
|
| 217 |
+
.ms-tabs button[aria-selected="true"] { border-bottom-color: var(--ms-accent); color: var(--ms-text); }
|
| 218 |
+
.ms-tabs > span { overflow: hidden; margin-left: auto; color: var(--ms-faint); font: .57rem var(--ms-mono); text-overflow: ellipsis; white-space: nowrap; }
|
| 219 |
+
.ms-tabs label { display: flex; align-items: center; gap: 5px; color: var(--ms-faint); font-size: .58rem; }
|
| 220 |
+
.ms-tabs input { width: 62px; accent-color: var(--ms-accent); }
|
| 221 |
+
|
| 222 |
+
.ms-roll-view { height: 420px; min-width: 0; padding: 11px; }
|
| 223 |
+
.ms-roll-view canvas { display: block; width: 100%; height: 370px; border-radius: 8px; background: #090c11; cursor: crosshair; }
|
| 224 |
+
.ms-roll-view p { margin: 7px 1px 0; color: var(--ms-faint); font-size: .58rem; text-align: right; }
|
| 225 |
+
.ms-score-view { height: 420px; overflow: auto; padding: 14px; background: #11151b; overscroll-behavior: contain; }
|
| 226 |
+
.ms-score-empty { min-height: 100%; display: grid; align-content: center; justify-items: center; color: var(--ms-muted); text-align: center; }
|
| 227 |
+
.ms-score-empty span { color: var(--ms-accent); font-size: 36px; }
|
| 228 |
+
.ms-score-empty strong { margin-top: 9px; color: var(--ms-soft); font-size: .82rem; }
|
| 229 |
+
.ms-score-empty p { max-width: 420px; margin: 5px 0 0; font-size: .66rem; line-height: 1.5; }
|
| 230 |
+
.ms-score-paper { min-width: 720px; padding: 18px; border-radius: 4px; background: #fffdf7; box-shadow: 0 12px 36px rgba(0,0,0,.3); }
|
| 231 |
+
.ms-score-paper svg { display: block; width: 100%; height: auto; color: #111; }
|
| 232 |
+
|
| 233 |
+
.ms-notation-info { display: flex; align-items: center; gap: 12px; padding: 9px 11px; border: 1px solid var(--ms-line); border-radius: 10px; background: rgba(255,255,255,.022); }
|
| 234 |
+
.ms-notation-info > span { color: #aaa1ff; font: 650 .58rem var(--ms-mono); letter-spacing: .08em; text-transform: uppercase; }
|
| 235 |
+
.ms-notation-info dl { display: flex; flex: 1; gap: 14px; margin: 0; }
|
| 236 |
+
.ms-notation-info dl div { display: flex; gap: 5px; }
|
| 237 |
+
.ms-notation-info dt, .ms-notation-info dd { margin: 0; font-size: .58rem; }
|
| 238 |
+
.ms-notation-info dt { color: var(--ms-faint); }
|
| 239 |
+
.ms-notation-info dd { color: var(--ms-soft); }
|
| 240 |
+
|
| 241 |
+
.ms-tracks-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 4px 2px 0; }
|
| 242 |
+
.ms-tracks-heading strong, .ms-kicker { display: block; }
|
| 243 |
+
.ms-kicker { color: var(--ms-soft); font-size: .72rem; font-weight: 620; }
|
| 244 |
+
.ms-tracks-heading strong { margin-top: 3px; color: var(--ms-faint); font: .58rem var(--ms-mono); }
|
| 245 |
+
.ms-tracks-heading button { min-height: 29px; padding: 0 9px; font-size: .59rem; }
|
| 246 |
+
.ms-track-list { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 8px; }
|
| 247 |
+
.ms-track-placeholder { grid-column: 1 / -1; padding: 15px; border: 1px dashed var(--ms-line); border-radius: 11px; color: var(--ms-faint); font-size: .68rem; text-align: center; }
|
| 248 |
+
.ms-track-row { display: grid; grid-template-columns: 6px minmax(0,1fr) auto; align-items: center; min-width: 0; min-height: 68px; gap: 10px; padding: 10px 11px; border: 1px solid var(--ms-line); border-radius: 11px; background: rgba(255,255,255,.022); transition: opacity 150ms ease, border-color 150ms ease, background 150ms ease; }
|
| 249 |
+
.ms-track-row:hover { border-color: var(--ms-line-strong); background: rgba(255,255,255,.034); }
|
| 250 |
+
.ms-track-row[data-inactive="true"] { opacity: .48; }
|
| 251 |
+
.ms-track-color { width: 6px; height: 36px; border-radius: 99px; background: var(--track); box-shadow: 0 0 15px color-mix(in srgb, var(--track) 35%, transparent); }
|
| 252 |
+
.ms-track-copy { min-width: 0; overflow: hidden; }
|
| 253 |
.ms-track-copy strong, .ms-track-copy span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 254 |
+
.ms-track-copy strong { color: var(--ms-soft); font-size: .74rem; font-weight: 600; }
|
| 255 |
+
.ms-track-copy span { margin-top: 4px; color: var(--ms-faint); font: .56rem var(--ms-mono); }
|
| 256 |
+
.ms-program-select { width: min(100%, 158px); margin-top: 6px; padding: 3px 20px 3px 5px; border: 1px solid var(--ms-line); border-radius: 6px; background: #111721; color: var(--ms-soft); font-size: .57rem; }
|
| 257 |
.ms-track-controls { display: flex; gap: 4px; }
|
| 258 |
+
.ms-track-controls button, .ms-track-controls a { width: 27px; height: 27px; padding: 0; font: 700 .58rem var(--ms-mono); }
|
| 259 |
+
.ms-track-controls button[aria-pressed="true"] { border-color: color-mix(in srgb, var(--track) 52%, transparent); background: color-mix(in srgb, var(--track) 18%, transparent); color: #fff; }
|
| 260 |
+
|
| 261 |
+
.ms-announcer { position: absolute; overflow: hidden; width: 1px; height: 1px; clip: rect(0 0 0 0); }
|
| 262 |
+
|
| 263 |
+
@keyframes ms-pulse { 0%,100% { opacity:.62; transform:scale(.88); } 50% { opacity:1; transform:scale(1); } }
|
| 264 |
+
|
| 265 |
+
@container (max-width: 760px) {
|
| 266 |
+
.ms-topbar { display: grid; }
|
| 267 |
+
.ms-quick-downloads { width: 100%; }
|
| 268 |
+
.ms-quick-downloads a { flex: 1; }
|
| 269 |
+
.ms-transport-options { padding-left: 0; }
|
| 270 |
+
.ms-synth-state { width: 100%; margin-left: 0; }
|
| 271 |
+
.ms-track-list { grid-template-columns: 1fr; }
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
@container (max-width: 520px) {
|
| 275 |
+
.ms-stats { grid-template-columns: repeat(2,minmax(0,1fr)); }
|
| 276 |
+
.ms-stats > div:nth-child(3) { border-top: 1px solid var(--ms-line); border-left: 0; }
|
| 277 |
+
.ms-stats > div:nth-child(4) { border-top: 1px solid var(--ms-line); }
|
| 278 |
+
.ms-transport-main { flex-wrap: wrap; }
|
| 279 |
+
.ms-seek { min-width: calc(100% - 134px); }
|
| 280 |
+
.ms-source-mix { width: 100%; }
|
| 281 |
+
.ms-source-mix input { flex: 1; }
|
| 282 |
+
.ms-tabs > span { display: none; }
|
| 283 |
+
.ms-roll-view, .ms-score-view { height: 330px; }
|
| 284 |
+
.ms-roll-view canvas { height: 282px; }
|
| 285 |
+
.ms-notation-info { align-items: flex-start; }
|
| 286 |
+
.ms-notation-info dl { display: grid; grid-template-columns: repeat(2,1fr); gap: 6px 12px; }
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
@media (prefers-reduced-motion: reduce) {
|
| 290 |
+
.ms-root *, .ms-root *::before, .ms-root *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
|
| 291 |
+
}
|
|
|
|
|
|
frontend/studio.html
CHANGED
|
@@ -1,87 +1,95 @@
|
|
| 1 |
-
<
|
| 2 |
-
<
|
| 3 |
-
<div class="ms-
|
| 4 |
-
<span class="ms-state-dot"></span>
|
| 5 |
-
<
|
|
|
|
| 6 |
</div>
|
| 7 |
-
<div class="ms-
|
| 8 |
-
|
| 9 |
-
<
|
| 10 |
-
<
|
| 11 |
</div>
|
| 12 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
<section class="ms-empty">
|
| 15 |
-
<div class="ms-empty-icon">♫</div>
|
| 16 |
-
<
|
| 17 |
-
<p>
|
| 18 |
</section>
|
| 19 |
|
| 20 |
<section class="ms-workspace" hidden>
|
| 21 |
-
<div class="ms-
|
| 22 |
-
<
|
| 23 |
-
<
|
| 24 |
-
<span data-field="
|
| 25 |
-
<
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
<button type="button" data-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
</div>
|
| 32 |
-
<label class="ms-stereo-toggle" title="Place l’original à gauche et le MIDI à droite"><input data-action="stereo" type="checkbox"> L/R</label>
|
| 33 |
-
<label class="ms-volume">Volume <input data-action="volume" type="range" min="0" max="1" step="0.01" value="0.65"></label>
|
| 34 |
-
<span data-field="synth-state" class="ms-synth-state">SoundFont au premier play</span>
|
| 35 |
</div>
|
| 36 |
|
| 37 |
-
<
|
| 38 |
-
<
|
| 39 |
-
<
|
| 40 |
-
<
|
| 41 |
-
|
|
|
|
|
|
|
| 42 |
|
| 43 |
-
<
|
| 44 |
-
<
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
</nav>
|
| 48 |
-
<div data-view="roll" class="ms-roll-view">
|
| 49 |
-
<div class="ms-roll-toolbar"><span data-field="pitch-range">—</span><label>Zoom <input data-action="zoom" type="range" min="1" max="8" step="1" value="1"></label></div>
|
| 50 |
-
<canvas data-canvas="roll" height="470" aria-label="Piano roll multipiste"></canvas>
|
| 51 |
-
<p>Cliquez dans le piano roll pour déplacer la tête de lecture.</p>
|
| 52 |
-
</div>
|
| 53 |
-
<div data-view="score" class="ms-score-view" hidden>
|
| 54 |
-
<div data-region="score-empty" class="ms-score-empty"><span>♩</span><strong>Aucune partition générée</strong><p>Après la transcription, utilisez le bouton « Generate score » dans les paramètres.</p></div>
|
| 55 |
-
<div data-region="score" class="ms-score-paper"></div>
|
| 56 |
-
</div>
|
| 57 |
-
</section>
|
| 58 |
|
| 59 |
-
<
|
| 60 |
-
<
|
| 61 |
-
|
| 62 |
-
<
|
| 63 |
-
<
|
| 64 |
-
<div><dt>Pistes</dt><dd data-field="info-tracks">—</dd></div>
|
| 65 |
-
<div><dt>Calcul</dt><dd data-field="elapsed">—</dd></div>
|
| 66 |
-
</dl>
|
| 67 |
-
<div class="ms-notation-info" hidden>
|
| 68 |
-
<span class="ms-kicker">PARTITION</span>
|
| 69 |
-
<dl>
|
| 70 |
-
<div><dt>Tempo</dt><dd data-field="tempo">—</dd></div>
|
| 71 |
-
<div><dt>Mesure</dt><dd data-field="meter">—</dd></div>
|
| 72 |
-
<div><dt>Grille</dt><dd data-field="quantization">—</dd></div>
|
| 73 |
-
<div><dt>Pages</dt><dd data-field="pages">—</dd></div>
|
| 74 |
-
</dl>
|
| 75 |
-
</div>
|
| 76 |
-
<div class="ms-downloads">
|
| 77 |
-
<span class="ms-kicker">EXPORT RAPIDE</span>
|
| 78 |
-
<a data-download="midi" aria-disabled="true">MIDI complet</a>
|
| 79 |
-
<a data-download="musicxml" aria-disabled="true">MusicXML</a>
|
| 80 |
-
<a data-download="svg" aria-disabled="true">SVG de la partition</a>
|
| 81 |
-
<small>Les exports PDF, pistes séparées et ZIP sont disponibles sous le visualiseur.</small>
|
| 82 |
</div>
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
</section>
|
|
|
|
| 86 |
<div class="ms-announcer" aria-live="polite"></div>
|
| 87 |
-
</
|
|
|
|
| 1 |
+
<section class="ms-root" data-state="idle" aria-label="MuScriptor transcription studio">
|
| 2 |
+
<header class="ms-topbar">
|
| 3 |
+
<div class="ms-heading">
|
| 4 |
+
<p class="ms-eyebrow"><span class="ms-state-dot" aria-hidden="true"></span><span data-field="state">Ready for audio</span></p>
|
| 5 |
+
<h2 data-field="audio-name">Your transcription</h2>
|
| 6 |
+
<p class="ms-subtitle"><span data-field="status">Upload a recording to reveal its notes and instruments.</span><b data-field="summary">—</b></p>
|
| 7 |
</div>
|
| 8 |
+
<div class="ms-quick-downloads" aria-label="Quick exports">
|
| 9 |
+
<a data-download="midi" aria-disabled="true">MIDI</a>
|
| 10 |
+
<a data-download="musicxml" aria-disabled="true">MusicXML</a>
|
| 11 |
+
<a data-download="svg" aria-disabled="true">Score SVG</a>
|
| 12 |
</div>
|
| 13 |
+
</header>
|
| 14 |
+
|
| 15 |
+
<div class="ms-progress" hidden>
|
| 16 |
+
<div><span data-field="progress-label">Preparing inference…</span><b data-field="progress-value">0%</b></div>
|
| 17 |
+
<i role="progressbar" aria-valuemin="0" aria-valuemax="100"><span data-field="progress-bar"></span></i>
|
| 18 |
+
</div>
|
| 19 |
|
| 20 |
<section class="ms-empty">
|
| 21 |
+
<div class="ms-empty-icon" aria-hidden="true">♫</div>
|
| 22 |
+
<h3>A piano roll will appear here</h3>
|
| 23 |
+
<p>MuScriptor separates notes into color-coded instrument tracks. Generate an editable score after transcription when you need one.</p>
|
| 24 |
</section>
|
| 25 |
|
| 26 |
<section class="ms-workspace" hidden>
|
| 27 |
+
<div class="ms-stats" aria-label="Transcription statistics">
|
| 28 |
+
<div><span>Duration</span><strong data-field="info-duration">—</strong></div>
|
| 29 |
+
<div><span>Notes</span><strong data-field="note-count">—</strong></div>
|
| 30 |
+
<div><span>Tracks</span><strong data-field="info-tracks">—</strong></div>
|
| 31 |
+
<div><span>Elapsed</span><strong data-field="elapsed">—</strong></div>
|
| 32 |
+
</div>
|
| 33 |
+
|
| 34 |
+
<div class="ms-transport" aria-label="Playback controls">
|
| 35 |
+
<div class="ms-transport-main">
|
| 36 |
+
<button type="button" data-action="play" class="ms-play" aria-label="Play" disabled>▶</button>
|
| 37 |
+
<button type="button" data-action="stop" class="ms-stop" aria-label="Return to start" disabled>■</button>
|
| 38 |
+
<span data-field="clock">0:00.0</span>
|
| 39 |
+
<input data-action="seek" class="ms-seek" type="range" min="0" max="1" step="0.01" value="0" aria-label="Playback position">
|
| 40 |
+
<span data-field="duration">0:00.0</span>
|
| 41 |
+
</div>
|
| 42 |
+
<div class="ms-transport-options">
|
| 43 |
+
<div class="ms-source-mix" title="Blend the original recording and MIDI transcription">
|
| 44 |
+
<button type="button" data-source="original">Original</button>
|
| 45 |
+
<input data-action="source-mix" type="range" min="0" max="1" step="0.01" value="0.5" aria-label="Original to MIDI mix">
|
| 46 |
+
<button type="button" data-source="midi">MIDI</button>
|
| 47 |
+
</div>
|
| 48 |
+
<label class="ms-stereo-toggle" title="Original on the left, MIDI on the right"><input data-action="stereo" type="checkbox"> Split L/R</label>
|
| 49 |
+
<label class="ms-volume">Volume <input data-action="volume" type="range" min="0" max="1" step="0.01" value="0.72"></label>
|
| 50 |
+
<span data-field="synth-state" class="ms-synth-state">SoundFont loads on first play</span>
|
| 51 |
</div>
|
|
|
|
|
|
|
|
|
|
| 52 |
</div>
|
| 53 |
|
| 54 |
+
<section class="ms-view-shell">
|
| 55 |
+
<nav class="ms-tabs" aria-label="Visualization">
|
| 56 |
+
<button type="button" data-tab="roll" aria-selected="true">Piano roll</button>
|
| 57 |
+
<button type="button" data-tab="score" aria-selected="false">Score</button>
|
| 58 |
+
<span data-field="pitch-range">Waiting for notes</span>
|
| 59 |
+
<label>Zoom <input data-action="zoom" type="range" min="1" max="8" step="1" value="1"></label>
|
| 60 |
+
</nav>
|
| 61 |
|
| 62 |
+
<div data-view="roll" class="ms-roll-view">
|
| 63 |
+
<canvas data-canvas="roll" height="410" aria-label="Color-coded multitrack piano roll"></canvas>
|
| 64 |
+
<p>Click the piano roll to seek. Use zoom to inspect dense passages.</p>
|
| 65 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
+
<div data-view="score" class="ms-score-view" hidden>
|
| 68 |
+
<div data-region="score-empty" class="ms-score-empty">
|
| 69 |
+
<span aria-hidden="true">♩</span>
|
| 70 |
+
<strong>No score generated yet</strong>
|
| 71 |
+
<p>After transcription, open “Create an editable score” below and generate it manually.</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
</div>
|
| 73 |
+
<div data-region="score" class="ms-score-paper" hidden></div>
|
| 74 |
+
</div>
|
| 75 |
+
</section>
|
| 76 |
+
|
| 77 |
+
<div class="ms-notation-info" hidden>
|
| 78 |
+
<span>Score</span>
|
| 79 |
+
<dl>
|
| 80 |
+
<div><dt>Tempo</dt><dd data-field="tempo">—</dd></div>
|
| 81 |
+
<div><dt>Time</dt><dd data-field="meter">—</dd></div>
|
| 82 |
+
<div><dt>Grid</dt><dd data-field="quantization">—</dd></div>
|
| 83 |
+
<div><dt>Pages</dt><dd data-field="pages">—</dd></div>
|
| 84 |
+
</dl>
|
| 85 |
</div>
|
| 86 |
+
|
| 87 |
+
<div class="ms-tracks-heading">
|
| 88 |
+
<div><span class="ms-kicker">Instrument tracks</span><strong data-field="track-count">No tracks yet</strong></div>
|
| 89 |
+
<button type="button" data-action="reset-mix">Reset mute & solo</button>
|
| 90 |
+
</div>
|
| 91 |
+
<div data-region="tracks" class="ms-track-list" aria-label="Instrument tracks"></div>
|
| 92 |
</section>
|
| 93 |
+
|
| 94 |
<div class="ms-announcer" aria-live="polite"></div>
|
| 95 |
+
</section>
|
frontend/studio.js
CHANGED
|
@@ -20,7 +20,8 @@ const ui = {
|
|
| 20 |
const state = {
|
| 21 |
data: null, audio: null, master: null, compressor: null, gains: new Map(),
|
| 22 |
midiGain: null, midiPanner: null, originalGain: null, originalPanner: null,
|
| 23 |
-
originalElement: null,
|
|
|
|
| 24 |
sourceMix: Number(ui.sourceMix.value), stereo: Boolean(ui.stereo.checked),
|
| 25 |
synth: null, synthPromise: null, synthFailed: false, channels: new Map(), nextChannel: 0,
|
| 26 |
programOverrides: new Map(),
|
|
@@ -35,7 +36,7 @@ const formatTime = (seconds) => {
|
|
| 35 |
const minutes = Math.floor(safe / 60);
|
| 36 |
return `${minutes}:${(safe % 60).toFixed(1).padStart(4, '0')}`;
|
| 37 |
};
|
| 38 |
-
const noteName = (pitch) => `${['
|
| 39 |
const setText = (selector, value) => { const target = $(selector); if (target) target.textContent = value; };
|
| 40 |
|
| 41 |
function normalize(value) {
|
|
@@ -94,21 +95,21 @@ function setSynthStatus(label, kind = '') {
|
|
| 94 |
async function initSoundFont() {
|
| 95 |
const library = globalThis.MuScriptorSynthLib;
|
| 96 |
if (!library?.WorkletSynthesizer || !library?.processorUrl) {
|
| 97 |
-
throw new Error('SpessaSynth
|
| 98 |
}
|
| 99 |
-
setSynthStatus('
|
| 100 |
await state.audio.audioWorklet.addModule(library.processorUrl);
|
| 101 |
const synth = new library.WorkletSynthesizer(state.audio);
|
| 102 |
synth.connect(state.midiGain);
|
| 103 |
await synth.isReady;
|
| 104 |
-
setSynthStatus('
|
| 105 |
const response = await fetch(SOUNDFONT_URL);
|
| 106 |
if (!response.ok) throw new Error(`SoundFont HTTP ${response.status}`);
|
| 107 |
await synth.soundBankManager.addSoundBank(await response.arrayBuffer(), 'MuseScore General');
|
| 108 |
state.synth = synth;
|
| 109 |
state.channels.clear();
|
| 110 |
state.nextChannel = 0;
|
| 111 |
-
setSynthStatus('MuseScore General
|
| 112 |
refreshMix();
|
| 113 |
return synth;
|
| 114 |
}
|
|
@@ -132,12 +133,11 @@ function ensureAudio() {
|
|
| 132 |
state.originalGain.connect(state.originalPanner).connect(state.master);
|
| 133 |
state.master.connect(state.compressor).connect(state.audio.destination);
|
| 134 |
rebuildGains();
|
| 135 |
-
setupOriginalAudio();
|
| 136 |
applySourceMix();
|
| 137 |
state.synthPromise = initSoundFont().catch((error) => {
|
| 138 |
state.synthFailed = true;
|
| 139 |
-
console.warn('SoundFont
|
| 140 |
-
setSynthStatus('
|
| 141 |
return null;
|
| 142 |
});
|
| 143 |
}
|
|
@@ -164,48 +164,101 @@ function findOriginalAudio() {
|
|
| 164 |
return null;
|
| 165 |
}
|
| 166 |
|
| 167 |
-
function
|
| 168 |
-
if (!state.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
const audio = findOriginalAudio();
|
| 170 |
-
if (!audio)
|
| 171 |
-
|
| 172 |
-
if (
|
| 173 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
state.originalElement = audio;
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
}
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
}
|
| 195 |
|
| 196 |
function applySourceMix() {
|
| 197 |
const mix = clamp(Number(state.sourceMix), 0, 1);
|
| 198 |
-
const hasOriginal = Boolean(state.
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
| 201 |
if (state.audio) {
|
| 202 |
const now = state.audio.currentTime;
|
| 203 |
-
state.originalGain?.gain.setTargetAtTime(
|
| 204 |
state.midiGain?.gain.setTargetAtTime(Math.max(.0001, midiLevel), now, .018);
|
| 205 |
state.originalPanner?.pan.setTargetAtTime(state.stereo && hasOriginal ? -1 : 0, now, .018);
|
| 206 |
state.midiPanner?.pan.setTargetAtTime(state.stereo && hasOriginal ? 1 : 0, now, .018);
|
| 207 |
}
|
| 208 |
-
if (state.originalElement && state.originalFallback) state.originalElement.volume = originalLevel;
|
| 209 |
$$('[data-source]').forEach((button) => {
|
| 210 |
const selected = button.dataset.source === 'original' ? mix <= .01 : mix >= .99;
|
| 211 |
button.setAttribute('aria-pressed', String(selected));
|
|
@@ -254,18 +307,19 @@ function waveform(track) {
|
|
| 254 |
}
|
| 255 |
|
| 256 |
function scheduleNote(track, note, nowSong) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
if (state.synth) {
|
| 258 |
const channel = channelFor(track);
|
| 259 |
-
const startAt = state.audio.currentTime + Math.max(.006, note.start - nowSong);
|
| 260 |
-
const endAt = startAt + clamp(note.end - Math.max(note.start, nowSong), .03, 12);
|
| 261 |
state.synth.noteOn(channel, note.pitch, Math.round(note.velocity), { time: startAt });
|
| 262 |
-
state.synth.noteOff(channel, note.pitch, { time: endAt });
|
| 263 |
return;
|
| 264 |
}
|
| 265 |
const gain = state.gains.get(track.id);
|
| 266 |
if (!state.audio || !gain) return;
|
| 267 |
-
const
|
| 268 |
-
const duration = clamp(note.end - Math.max(note.start, nowSong), .03, 8);
|
| 269 |
const oscillator = state.audio.createOscillator();
|
| 270 |
const envelope = state.audio.createGain();
|
| 271 |
oscillator.type = waveform(track);
|
|
@@ -279,10 +333,12 @@ function scheduleNote(track, note, nowSong) {
|
|
| 279 |
}
|
| 280 |
|
| 281 |
function exactTime() {
|
| 282 |
-
if (state.playing
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
|
|
|
|
|
|
| 286 |
}
|
| 287 |
|
| 288 |
function lowerBound(time) {
|
|
@@ -294,14 +350,11 @@ function lowerBound(time) {
|
|
| 294 |
function stopPlayback(reset = false) {
|
| 295 |
if (state.playing) state.songTime = clamp(exactTime(), 0, state.data?.duration || 0);
|
| 296 |
state.playing = false; cancelAnimationFrame(state.animation);
|
| 297 |
-
|
| 298 |
state.nodes.forEach((node) => { try { node.stop(); } catch (_) {} }); state.nodes.clear();
|
| 299 |
state.synth?.stopAll();
|
| 300 |
-
if (reset)
|
| 301 |
-
|
| 302 |
-
if (state.originalElement) { try { state.originalElement.currentTime = 0; } catch (_) {} }
|
| 303 |
-
}
|
| 304 |
-
ui.play.textContent = '▶'; ui.play.setAttribute('aria-label', 'Lecture');
|
| 305 |
updateTimeline();
|
| 306 |
}
|
| 307 |
|
|
@@ -309,20 +362,21 @@ async function startPlayback() {
|
|
| 309 |
if (!state.data || !state.notes.length || state.playing) return;
|
| 310 |
ensureAudio(); if (!state.audio) return;
|
| 311 |
await state.audio.resume();
|
| 312 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
if (state.songTime >= state.data.duration - .02) state.songTime = 0;
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
state.originSong = state.songTime; state.originAudio = state.audio.currentTime;
|
| 324 |
-
state.nextNote = lowerBound(Math.max(0, state.songTime - .01)); state.playing = true;
|
| 325 |
-
ui.play.textContent = '❚❚'; ui.play.setAttribute('aria-label', 'Pause'); frame();
|
| 326 |
}
|
| 327 |
|
| 328 |
function frame() {
|
|
@@ -338,7 +392,6 @@ function frame() {
|
|
| 338 |
|
| 339 |
function seekTo(time) {
|
| 340 |
const resume = state.playing; stopPlayback(false); state.songTime = clamp(time, 0, state.data.duration);
|
| 341 |
-
if (state.originalElement) { try { state.originalElement.currentTime = state.songTime; } catch (_) {} }
|
| 342 |
updateTimeline(); if (resume) startPlayback();
|
| 343 |
}
|
| 344 |
|
|
@@ -349,15 +402,24 @@ function updateTimeline() {
|
|
| 349 |
|
| 350 |
function renderTracks() {
|
| 351 |
ui.tracks.replaceChildren();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
state.data.tracks.forEach((track) => {
|
| 353 |
const row = document.createElement('article'); row.className = 'ms-track-row'; row.dataset.track = track.id; row.style.setProperty('--track', track.color);
|
| 354 |
const dot = document.createElement('i'); dot.className = 'ms-track-color';
|
| 355 |
const copy = document.createElement('div'); copy.className = 'ms-track-copy';
|
| 356 |
const name = document.createElement('strong'); name.textContent = track.name;
|
| 357 |
-
const meta = document.createElement('span'); meta.textContent = `${track.note_count.toLocaleString('
|
| 358 |
if (!track.is_drum) {
|
| 359 |
const select = document.createElement('select');
|
| 360 |
-
select.className = 'ms-program-select'; select.dataset.program = track.id; select.setAttribute('aria-label', `
|
| 361 |
const selectedProgram = state.programOverrides.get(track.id) ?? track.program;
|
| 362 |
GM_PROGRAMS.forEach(([program, label]) => {
|
| 363 |
const option = document.createElement('option'); option.value = String(program); option.textContent = label; option.selected = program === selectedProgram; select.append(option);
|
|
@@ -369,22 +431,23 @@ function renderTracks() {
|
|
| 369 |
}
|
| 370 |
const controls = document.createElement('div'); controls.className = 'ms-track-controls';
|
| 371 |
['solo', 'mute'].forEach((kind) => { const button = document.createElement('button'); button.type = 'button'; button.dataset.mix = kind; button.textContent = kind === 'solo' ? 'S' : 'M'; button.title = kind === 'solo' ? 'Solo' : 'Mute'; button.setAttribute('aria-pressed', 'false'); controls.append(button); });
|
| 372 |
-
const download = document.createElement('a'); download.textContent = '↓'; download.title = `
|
| 373 |
if (track.midi) { download.href = track.midi; download.download = `${track.key}.mid`; } else download.setAttribute('aria-disabled', 'true');
|
| 374 |
controls.append(download); row.append(dot, copy, controls); ui.tracks.append(row);
|
| 375 |
});
|
| 376 |
}
|
| 377 |
|
| 378 |
function drawRoll() {
|
| 379 |
-
if (!state.data ||
|
| 380 |
const canvas = ui.canvas, rect = canvas.getBoundingClientRect(); if (!rect.width) return;
|
| 381 |
const dpr = Math.min(window.devicePixelRatio || 1, 2), width = Math.round(rect.width * dpr), height = Math.round(rect.height * dpr);
|
| 382 |
if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; }
|
| 383 |
const ctx = canvas.getContext('2d'); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); const w = rect.width, h = rect.height;
|
| 384 |
ctx.clearRect(0, 0, w, h); ctx.fillStyle = '#090c11'; ctx.fillRect(0, 0, w, h);
|
| 385 |
const allPitches = state.notes.map((item) => item.note.pitch); const minPitch = Math.min(...allPitches, 48) - 2, maxPitch = Math.max(...allPitches, 72) + 2, pitchSpan = maxPitch - minPitch + 1;
|
| 386 |
-
const
|
| 387 |
-
const
|
|
|
|
| 388 |
state.viewStart = start; state.viewEnd = end;
|
| 389 |
ctx.lineWidth = 1; ctx.strokeStyle = 'rgba(255,255,255,.055)';
|
| 390 |
for (let pitch = minPitch; pitch <= maxPitch; pitch++) { const y = h - ((pitch - minPitch + 1) / pitchSpan) * h; if ([1,3,6,8,10].includes(pitch % 12)) { ctx.fillStyle = 'rgba(255,255,255,.012)'; ctx.fillRect(0, y - h / pitchSpan, w, h / pitchSpan); } if (pitch % 12 === 0) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); } }
|
|
@@ -406,6 +469,7 @@ function render(data) {
|
|
| 406 |
if (state.playing && previousSignature !== nextSignature) stopPlayback(true);
|
| 407 |
if (audioChanged) {
|
| 408 |
stopPlayback(true);
|
|
|
|
| 409 |
state.synth?.stopAll();
|
| 410 |
state.channels.clear();
|
| 411 |
state.nextChannel = 0;
|
|
@@ -415,15 +479,15 @@ function render(data) {
|
|
| 415 |
root.dataset.state = data.state; const active = ['queued', 'loading', 'transcribing'].includes(data.state);
|
| 416 |
const hasResults = data.tracks.length > 0 || active;
|
| 417 |
ui.empty.hidden = hasResults; ui.workspace.hidden = !hasResults;
|
| 418 |
-
ui.state.textContent = ({ idle: '
|
| 419 |
-
ui.status.textContent = data.status; ui.audioName.textContent = data.audio_name || '
|
| 420 |
-
ui.summary.textContent = data.audio_name ? `${data.note_count.toLocaleString('
|
| 421 |
ui.progress.hidden = !active; ui.progressLabel.textContent = data.status; ui.progressValue.textContent = `${Math.round(data.progress * 100)}%`; ui.progressBar.style.width = `${data.progress * 100}%`;
|
| 422 |
ui.duration.textContent = formatTime(data.duration); ui.seek.max = String(Math.max(.01, data.duration));
|
| 423 |
-
setText('[data-field="track-count"]', `${data.tracks.length}
|
| 424 |
-
setText('[data-field="info-duration"]', formatTime(data.duration)); setText('[data-field="note-count"]', data.note_count ? data.note_count.toLocaleString('
|
| 425 |
setText('[data-field="info-tracks"]', data.tracks.length || '—'); setText('[data-field="elapsed"]', data.elapsed ? `${data.elapsed.toFixed(1)} s` : '—');
|
| 426 |
-
const pitches = state.notes.map((item) => item.note.pitch); setText('[data-field="pitch-range"]', pitches.length ? `${noteName(Math.min(...pitches))} → ${noteName(Math.max(...pitches))}` : '
|
| 427 |
state.muted.forEach((id) => { if (!data.tracks.some((track) => track.id === id)) state.muted.delete(id); }); state.solo.forEach((id) => { if (!data.tracks.some((track) => track.id === id)) state.solo.delete(id); });
|
| 428 |
renderTracks(); rebuildGains();
|
| 429 |
ui.score.replaceChildren();
|
|
@@ -435,7 +499,7 @@ function render(data) {
|
|
| 435 |
const stem = (data.audio_name || 'muscriptor').replace(/\.[^.]+$/, '');
|
| 436 |
updateDownload('[data-download="midi"]', data.full_midi, `${stem}.mid`); updateDownload('[data-download="musicxml"]', notation.musicxml || '', `${stem}.musicxml`); updateDownload('[data-download="svg"]', notation.svg || '', `${stem}.svg`);
|
| 437 |
ui.play.disabled = !state.notes.length; ui.stop.disabled = !state.notes.length; updateTimeline();
|
| 438 |
-
if (state.audio)
|
| 439 |
applySourceMix();
|
| 440 |
if (data.state === 'ready') switchTab('score');
|
| 441 |
announce(`${ui.state.textContent}. ${data.status}`);
|
|
@@ -445,14 +509,14 @@ ui.play.addEventListener('click', () => {
|
|
| 445 |
if (state.playing) stopPlayback(false);
|
| 446 |
else startPlayback().catch((error) => {
|
| 447 |
console.error(error);
|
| 448 |
-
setSynthStatus('
|
| 449 |
});
|
| 450 |
});
|
| 451 |
ui.stop.addEventListener('click', () => stopPlayback(true));
|
| 452 |
ui.seek.addEventListener('input', () => state.data && seekTo(Number(ui.seek.value)));
|
| 453 |
ui.volume.addEventListener('input', () => { if (state.master && state.audio) state.master.gain.setTargetAtTime(Number(ui.volume.value), state.audio.currentTime, .02); });
|
| 454 |
ui.sourceMix.addEventListener('input', () => { state.sourceMix = Number(ui.sourceMix.value); applySourceMix(); });
|
| 455 |
-
ui.stereo.addEventListener('change', () => { state.stereo = ui.stereo.checked; applySourceMix(); announce(state.stereo ? '
|
| 456 |
$$('[data-source]').forEach((button) => button.addEventListener('click', () => {
|
| 457 |
state.sourceMix = button.dataset.source === 'original' ? 0 : 1;
|
| 458 |
ui.sourceMix.value = String(state.sourceMix);
|
|
@@ -473,7 +537,7 @@ ui.tracks.addEventListener('change', (event) => {
|
|
| 473 |
state.synth.stopAll();
|
| 474 |
state.synth.programChange(channel, program);
|
| 475 |
}
|
| 476 |
-
announce(`
|
| 477 |
});
|
| 478 |
$$('[data-tab]').forEach((button) => button.addEventListener('click', () => switchTab(button.dataset.tab)));
|
| 479 |
new ResizeObserver(() => drawRoll()).observe(ui.canvas);
|
|
|
|
| 20 |
const state = {
|
| 21 |
data: null, audio: null, master: null, compressor: null, gains: new Map(),
|
| 22 |
midiGain: null, midiPanner: null, originalGain: null, originalPanner: null,
|
| 23 |
+
originalElement: null, originalUrl: '', originalBuffer: null,
|
| 24 |
+
originalBufferPromise: null, originalBufferSource: null, originalFailed: false,
|
| 25 |
sourceMix: Number(ui.sourceMix.value), stereo: Boolean(ui.stereo.checked),
|
| 26 |
synth: null, synthPromise: null, synthFailed: false, channels: new Map(), nextChannel: 0,
|
| 27 |
programOverrides: new Map(),
|
|
|
|
| 36 |
const minutes = Math.floor(safe / 60);
|
| 37 |
return `${minutes}:${(safe % 60).toFixed(1).padStart(4, '0')}`;
|
| 38 |
};
|
| 39 |
+
const noteName = (pitch) => `${['C','C♯','D','E♭','E','F','F♯','G','A♭','A','B♭','B'][pitch % 12]}${Math.floor(pitch / 12) - 1}`;
|
| 40 |
const setText = (selector, value) => { const target = $(selector); if (target) target.textContent = value; };
|
| 41 |
|
| 42 |
function normalize(value) {
|
|
|
|
| 95 |
async function initSoundFont() {
|
| 96 |
const library = globalThis.MuScriptorSynthLib;
|
| 97 |
if (!library?.WorkletSynthesizer || !library?.processorUrl) {
|
| 98 |
+
throw new Error('SpessaSynth is unavailable');
|
| 99 |
}
|
| 100 |
+
setSynthStatus('Loading audio engine…', 'loading');
|
| 101 |
await state.audio.audioWorklet.addModule(library.processorUrl);
|
| 102 |
const synth = new library.WorkletSynthesizer(state.audio);
|
| 103 |
synth.connect(state.midiGain);
|
| 104 |
await synth.isReady;
|
| 105 |
+
setSynthStatus('Loading 38 MB SoundFont…', 'loading');
|
| 106 |
const response = await fetch(SOUNDFONT_URL);
|
| 107 |
if (!response.ok) throw new Error(`SoundFont HTTP ${response.status}`);
|
| 108 |
await synth.soundBankManager.addSoundBank(await response.arrayBuffer(), 'MuseScore General');
|
| 109 |
state.synth = synth;
|
| 110 |
state.channels.clear();
|
| 111 |
state.nextChannel = 0;
|
| 112 |
+
setSynthStatus('MuseScore General ready', 'ready');
|
| 113 |
refreshMix();
|
| 114 |
return synth;
|
| 115 |
}
|
|
|
|
| 133 |
state.originalGain.connect(state.originalPanner).connect(state.master);
|
| 134 |
state.master.connect(state.compressor).connect(state.audio.destination);
|
| 135 |
rebuildGains();
|
|
|
|
| 136 |
applySourceMix();
|
| 137 |
state.synthPromise = initSoundFont().catch((error) => {
|
| 138 |
state.synthFailed = true;
|
| 139 |
+
console.warn('SoundFont unavailable, using WebAudio fallback:', error);
|
| 140 |
+
setSynthStatus('Lightweight synth fallback', 'error');
|
| 141 |
return null;
|
| 142 |
});
|
| 143 |
}
|
|
|
|
| 164 |
return null;
|
| 165 |
}
|
| 166 |
|
| 167 |
+
function stopOriginalBufferSource() {
|
| 168 |
+
if (!state.originalBufferSource) return;
|
| 169 |
+
try { state.originalBufferSource.stop(); } catch (_) {}
|
| 170 |
+
try { state.originalBufferSource.disconnect(); } catch (_) {}
|
| 171 |
+
state.originalBufferSource = null;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
function resetOriginalAudio() {
|
| 175 |
+
stopOriginalBufferSource();
|
| 176 |
+
state.originalElement = null;
|
| 177 |
+
state.originalUrl = '';
|
| 178 |
+
state.originalBuffer = null;
|
| 179 |
+
state.originalBufferPromise = null;
|
| 180 |
+
state.originalFailed = false;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
function discoverOriginalAudio() {
|
| 184 |
const audio = findOriginalAudio();
|
| 185 |
+
if (!audio) return null;
|
| 186 |
+
const url = String(audio.currentSrc || audio.src || '');
|
| 187 |
+
if (!url) return null;
|
| 188 |
+
if (url !== state.originalUrl) {
|
| 189 |
+
stopOriginalBufferSource();
|
| 190 |
+
state.originalUrl = url;
|
| 191 |
+
state.originalBuffer = null;
|
| 192 |
+
state.originalBufferPromise = null;
|
| 193 |
+
state.originalFailed = false;
|
| 194 |
+
}
|
| 195 |
state.originalElement = audio;
|
| 196 |
+
if (!audio.paused) audio.pause();
|
| 197 |
+
return url;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
async function loadOriginalBuffer() {
|
| 201 |
+
if (!state.audio) return null;
|
| 202 |
+
const url = discoverOriginalAudio();
|
| 203 |
+
if (!url) return null;
|
| 204 |
+
if (state.originalBuffer) return state.originalBuffer;
|
| 205 |
+
if (!state.originalBufferPromise) {
|
| 206 |
+
const requestedUrl = url;
|
| 207 |
+
state.originalBufferPromise = fetch(url)
|
| 208 |
+
.then((response) => {
|
| 209 |
+
if (!response.ok) throw new Error(`Original audio HTTP ${response.status}`);
|
| 210 |
+
return response.arrayBuffer();
|
| 211 |
+
})
|
| 212 |
+
.then((data) => state.audio.decodeAudioData(data.slice(0)))
|
| 213 |
+
.then((buffer) => {
|
| 214 |
+
if (state.originalUrl !== requestedUrl) return null;
|
| 215 |
+
state.originalBuffer = buffer;
|
| 216 |
+
state.originalFailed = false;
|
| 217 |
+
applySourceMix();
|
| 218 |
+
return buffer;
|
| 219 |
+
})
|
| 220 |
+
.catch((error) => {
|
| 221 |
+
console.warn('Original audio could not be decoded:', error);
|
| 222 |
+
if (state.originalUrl === requestedUrl) {
|
| 223 |
+
state.originalBufferPromise = null;
|
| 224 |
+
state.originalFailed = true;
|
| 225 |
+
applySourceMix();
|
| 226 |
+
}
|
| 227 |
+
return null;
|
| 228 |
+
});
|
| 229 |
}
|
| 230 |
+
return state.originalBufferPromise;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
function startOriginalBufferSource(startAt, offset) {
|
| 234 |
+
stopOriginalBufferSource();
|
| 235 |
+
if (!state.audio || !state.originalGain || !state.originalBuffer) return;
|
| 236 |
+
const safeOffset = clamp(offset, 0, Math.max(0, state.originalBuffer.duration - .001));
|
| 237 |
+
if (safeOffset >= state.originalBuffer.duration) return;
|
| 238 |
+
const source = state.audio.createBufferSource();
|
| 239 |
+
source.buffer = state.originalBuffer;
|
| 240 |
+
source.connect(state.originalGain);
|
| 241 |
+
source.addEventListener('ended', () => {
|
| 242 |
+
if (state.originalBufferSource === source) state.originalBufferSource = null;
|
| 243 |
+
}, { once: true });
|
| 244 |
+
source.start(startAt, safeOffset);
|
| 245 |
+
state.originalBufferSource = source;
|
| 246 |
}
|
| 247 |
|
| 248 |
function applySourceMix() {
|
| 249 |
const mix = clamp(Number(state.sourceMix), 0, 1);
|
| 250 |
+
const hasOriginal = Boolean(state.originalBuffer || (!state.originalFailed && discoverOriginalAudio()));
|
| 251 |
+
// Match the official client: a predictable linear blend in normal mode,
|
| 252 |
+
// with split mode overriding the blend so both sources remain audible.
|
| 253 |
+
const originalLevel = state.stereo && hasOriginal ? .62 : (hasOriginal ? 1 - mix : 0);
|
| 254 |
+
const midiLevel = state.stereo && hasOriginal ? .62 : (hasOriginal ? mix : 1);
|
| 255 |
if (state.audio) {
|
| 256 |
const now = state.audio.currentTime;
|
| 257 |
+
state.originalGain?.gain.setTargetAtTime(Math.max(.0001, originalLevel), now, .018);
|
| 258 |
state.midiGain?.gain.setTargetAtTime(Math.max(.0001, midiLevel), now, .018);
|
| 259 |
state.originalPanner?.pan.setTargetAtTime(state.stereo && hasOriginal ? -1 : 0, now, .018);
|
| 260 |
state.midiPanner?.pan.setTargetAtTime(state.stereo && hasOriginal ? 1 : 0, now, .018);
|
| 261 |
}
|
|
|
|
| 262 |
$$('[data-source]').forEach((button) => {
|
| 263 |
const selected = button.dataset.source === 'original' ? mix <= .01 : mix >= .99;
|
| 264 |
button.setAttribute('aria-pressed', String(selected));
|
|
|
|
| 307 |
}
|
| 308 |
|
| 309 |
function scheduleNote(track, note, nowSong) {
|
| 310 |
+
const scheduledStart = state.originAudio + (note.start - state.originSong);
|
| 311 |
+
const scheduledEnd = state.originAudio + (note.end - state.originSong);
|
| 312 |
+
const startAt = Math.max(state.audio.currentTime + .006, scheduledStart);
|
| 313 |
+
const endAt = Math.max(startAt + .03, scheduledEnd);
|
| 314 |
if (state.synth) {
|
| 315 |
const channel = channelFor(track);
|
|
|
|
|
|
|
| 316 |
state.synth.noteOn(channel, note.pitch, Math.round(note.velocity), { time: startAt });
|
| 317 |
+
state.synth.noteOff(channel, note.pitch, { time: Math.min(endAt, startAt + 12) });
|
| 318 |
return;
|
| 319 |
}
|
| 320 |
const gain = state.gains.get(track.id);
|
| 321 |
if (!state.audio || !gain) return;
|
| 322 |
+
const duration = clamp(endAt - startAt, .03, 8);
|
|
|
|
| 323 |
const oscillator = state.audio.createOscillator();
|
| 324 |
const envelope = state.audio.createGain();
|
| 325 |
oscillator.type = waveform(track);
|
|
|
|
| 333 |
}
|
| 334 |
|
| 335 |
function exactTime() {
|
| 336 |
+
if (!state.playing || !state.audio) return state.songTime;
|
| 337 |
+
return clamp(
|
| 338 |
+
state.originSong + Math.max(0, state.audio.currentTime - state.originAudio),
|
| 339 |
+
0,
|
| 340 |
+
state.data?.duration || Number.POSITIVE_INFINITY,
|
| 341 |
+
);
|
| 342 |
}
|
| 343 |
|
| 344 |
function lowerBound(time) {
|
|
|
|
| 350 |
function stopPlayback(reset = false) {
|
| 351 |
if (state.playing) state.songTime = clamp(exactTime(), 0, state.data?.duration || 0);
|
| 352 |
state.playing = false; cancelAnimationFrame(state.animation);
|
| 353 |
+
stopOriginalBufferSource();
|
| 354 |
state.nodes.forEach((node) => { try { node.stop(); } catch (_) {} }); state.nodes.clear();
|
| 355 |
state.synth?.stopAll();
|
| 356 |
+
if (reset) state.songTime = 0;
|
| 357 |
+
ui.play.textContent = '▶'; ui.play.setAttribute('aria-label', 'Play');
|
|
|
|
|
|
|
|
|
|
| 358 |
updateTimeline();
|
| 359 |
}
|
| 360 |
|
|
|
|
| 362 |
if (!state.data || !state.notes.length || state.playing) return;
|
| 363 |
ensureAudio(); if (!state.audio) return;
|
| 364 |
await state.audio.resume();
|
| 365 |
+
const originalPromise = loadOriginalBuffer();
|
| 366 |
+
await Promise.all([
|
| 367 |
+
state.synthPromise && !state.synthFailed ? state.synthPromise : Promise.resolve(null),
|
| 368 |
+
originalPromise,
|
| 369 |
+
]);
|
| 370 |
if (state.songTime >= state.data.duration - .02) state.songTime = 0;
|
| 371 |
+
state.originSong = state.songTime;
|
| 372 |
+
state.originAudio = state.audio.currentTime + .065;
|
| 373 |
+
startOriginalBufferSource(state.originAudio, state.songTime);
|
| 374 |
+
// Include sustained notes that began before a seek position. Twelve seconds
|
| 375 |
+
// matches the scheduler's maximum rendered note length.
|
| 376 |
+
state.nextNote = lowerBound(Math.max(0, state.songTime - 12)); state.playing = true;
|
| 377 |
+
ui.play.textContent = '❚❚'; ui.play.setAttribute('aria-label', 'Pause');
|
| 378 |
+
applySourceMix();
|
| 379 |
+
frame();
|
|
|
|
|
|
|
|
|
|
| 380 |
}
|
| 381 |
|
| 382 |
function frame() {
|
|
|
|
| 392 |
|
| 393 |
function seekTo(time) {
|
| 394 |
const resume = state.playing; stopPlayback(false); state.songTime = clamp(time, 0, state.data.duration);
|
|
|
|
| 395 |
updateTimeline(); if (resume) startPlayback();
|
| 396 |
}
|
| 397 |
|
|
|
|
| 402 |
|
| 403 |
function renderTracks() {
|
| 404 |
ui.tracks.replaceChildren();
|
| 405 |
+
if (!state.data.tracks.length) {
|
| 406 |
+
const empty = document.createElement('div');
|
| 407 |
+
empty.className = 'ms-track-placeholder';
|
| 408 |
+
empty.textContent = ['queued', 'loading', 'transcribing'].includes(state.data.state)
|
| 409 |
+
? 'Instrument tracks will appear as transcription progresses.'
|
| 410 |
+
: 'Tracks will appear after transcription.';
|
| 411 |
+
ui.tracks.append(empty);
|
| 412 |
+
return;
|
| 413 |
+
}
|
| 414 |
state.data.tracks.forEach((track) => {
|
| 415 |
const row = document.createElement('article'); row.className = 'ms-track-row'; row.dataset.track = track.id; row.style.setProperty('--track', track.color);
|
| 416 |
const dot = document.createElement('i'); dot.className = 'ms-track-color';
|
| 417 |
const copy = document.createElement('div'); copy.className = 'ms-track-copy';
|
| 418 |
const name = document.createElement('strong'); name.textContent = track.name;
|
| 419 |
+
const meta = document.createElement('span'); meta.textContent = `${track.note_count.toLocaleString('en-US')} notes${track.is_drum ? ' · drums' : ''}`; copy.append(name, meta);
|
| 420 |
if (!track.is_drum) {
|
| 421 |
const select = document.createElement('select');
|
| 422 |
+
select.className = 'ms-program-select'; select.dataset.program = track.id; select.setAttribute('aria-label', `Sound for ${track.name}`);
|
| 423 |
const selectedProgram = state.programOverrides.get(track.id) ?? track.program;
|
| 424 |
GM_PROGRAMS.forEach(([program, label]) => {
|
| 425 |
const option = document.createElement('option'); option.value = String(program); option.textContent = label; option.selected = program === selectedProgram; select.append(option);
|
|
|
|
| 431 |
}
|
| 432 |
const controls = document.createElement('div'); controls.className = 'ms-track-controls';
|
| 433 |
['solo', 'mute'].forEach((kind) => { const button = document.createElement('button'); button.type = 'button'; button.dataset.mix = kind; button.textContent = kind === 'solo' ? 'S' : 'M'; button.title = kind === 'solo' ? 'Solo' : 'Mute'; button.setAttribute('aria-pressed', 'false'); controls.append(button); });
|
| 434 |
+
const download = document.createElement('a'); download.textContent = '↓'; download.title = `Download ${track.name}`;
|
| 435 |
if (track.midi) { download.href = track.midi; download.download = `${track.key}.mid`; } else download.setAttribute('aria-disabled', 'true');
|
| 436 |
controls.append(download); row.append(dot, copy, controls); ui.tracks.append(row);
|
| 437 |
});
|
| 438 |
}
|
| 439 |
|
| 440 |
function drawRoll() {
|
| 441 |
+
if (!state.data || state.activeTab !== 'roll') return;
|
| 442 |
const canvas = ui.canvas, rect = canvas.getBoundingClientRect(); if (!rect.width) return;
|
| 443 |
const dpr = Math.min(window.devicePixelRatio || 1, 2), width = Math.round(rect.width * dpr), height = Math.round(rect.height * dpr);
|
| 444 |
if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; }
|
| 445 |
const ctx = canvas.getContext('2d'); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); const w = rect.width, h = rect.height;
|
| 446 |
ctx.clearRect(0, 0, w, h); ctx.fillStyle = '#090c11'; ctx.fillRect(0, 0, w, h);
|
| 447 |
const allPitches = state.notes.map((item) => item.note.pitch); const minPitch = Math.min(...allPitches, 48) - 2, maxPitch = Math.max(...allPitches, 72) + 2, pitchSpan = maxPitch - minPitch + 1;
|
| 448 |
+
const duration = Math.max(.1, state.data.duration);
|
| 449 |
+
const zoom = Number(ui.zoom.value), span = Math.max(.1, duration / zoom);
|
| 450 |
+
const start = zoom === 1 ? 0 : clamp(state.songTime - span * .32, 0, Math.max(0, duration - span)); const end = Math.min(duration, start + span);
|
| 451 |
state.viewStart = start; state.viewEnd = end;
|
| 452 |
ctx.lineWidth = 1; ctx.strokeStyle = 'rgba(255,255,255,.055)';
|
| 453 |
for (let pitch = minPitch; pitch <= maxPitch; pitch++) { const y = h - ((pitch - minPitch + 1) / pitchSpan) * h; if ([1,3,6,8,10].includes(pitch % 12)) { ctx.fillStyle = 'rgba(255,255,255,.012)'; ctx.fillRect(0, y - h / pitchSpan, w, h / pitchSpan); } if (pitch % 12 === 0) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); } }
|
|
|
|
| 469 |
if (state.playing && previousSignature !== nextSignature) stopPlayback(true);
|
| 470 |
if (audioChanged) {
|
| 471 |
stopPlayback(true);
|
| 472 |
+
resetOriginalAudio();
|
| 473 |
state.synth?.stopAll();
|
| 474 |
state.channels.clear();
|
| 475 |
state.nextChannel = 0;
|
|
|
|
| 479 |
root.dataset.state = data.state; const active = ['queued', 'loading', 'transcribing'].includes(data.state);
|
| 480 |
const hasResults = data.tracks.length > 0 || active;
|
| 481 |
ui.empty.hidden = hasResults; ui.workspace.hidden = !hasResults;
|
| 482 |
+
ui.state.textContent = ({ idle: 'Ready for audio', queued: 'ZeroGPU queued', loading: 'Loading model', transcribing: 'Transcribing', complete: 'MIDI ready', ready: 'Score ready', error: 'Error', notation_error: 'MIDI ready' })[data.state] || data.state;
|
| 483 |
+
ui.status.textContent = data.status; ui.audioName.textContent = data.audio_name || 'Your transcription';
|
| 484 |
+
ui.summary.textContent = data.audio_name ? `${data.note_count.toLocaleString('en-US')} notes · ${data.tracks.length} tracks` : '—';
|
| 485 |
ui.progress.hidden = !active; ui.progressLabel.textContent = data.status; ui.progressValue.textContent = `${Math.round(data.progress * 100)}%`; ui.progressBar.style.width = `${data.progress * 100}%`;
|
| 486 |
ui.duration.textContent = formatTime(data.duration); ui.seek.max = String(Math.max(.01, data.duration));
|
| 487 |
+
setText('[data-field="track-count"]', data.tracks.length ? `${data.tracks.length} track${data.tracks.length > 1 ? 's' : ''}` : 'No tracks yet');
|
| 488 |
+
setText('[data-field="info-duration"]', formatTime(data.duration)); setText('[data-field="note-count"]', data.note_count ? data.note_count.toLocaleString('en-US') : '—');
|
| 489 |
setText('[data-field="info-tracks"]', data.tracks.length || '—'); setText('[data-field="elapsed"]', data.elapsed ? `${data.elapsed.toFixed(1)} s` : '—');
|
| 490 |
+
const pitches = state.notes.map((item) => item.note.pitch); setText('[data-field="pitch-range"]', pitches.length ? `${noteName(Math.min(...pitches))} → ${noteName(Math.max(...pitches))}` : 'Waiting for notes');
|
| 491 |
state.muted.forEach((id) => { if (!data.tracks.some((track) => track.id === id)) state.muted.delete(id); }); state.solo.forEach((id) => { if (!data.tracks.some((track) => track.id === id)) state.solo.delete(id); });
|
| 492 |
renderTracks(); rebuildGains();
|
| 493 |
ui.score.replaceChildren();
|
|
|
|
| 499 |
const stem = (data.audio_name || 'muscriptor').replace(/\.[^.]+$/, '');
|
| 500 |
updateDownload('[data-download="midi"]', data.full_midi, `${stem}.mid`); updateDownload('[data-download="musicxml"]', notation.musicxml || '', `${stem}.musicxml`); updateDownload('[data-download="svg"]', notation.svg || '', `${stem}.svg`);
|
| 501 |
ui.play.disabled = !state.notes.length; ui.stop.disabled = !state.notes.length; updateTimeline();
|
| 502 |
+
if (state.audio) discoverOriginalAudio();
|
| 503 |
applySourceMix();
|
| 504 |
if (data.state === 'ready') switchTab('score');
|
| 505 |
announce(`${ui.state.textContent}. ${data.status}`);
|
|
|
|
| 509 |
if (state.playing) stopPlayback(false);
|
| 510 |
else startPlayback().catch((error) => {
|
| 511 |
console.error(error);
|
| 512 |
+
setSynthStatus('Playback unavailable', 'error');
|
| 513 |
});
|
| 514 |
});
|
| 515 |
ui.stop.addEventListener('click', () => stopPlayback(true));
|
| 516 |
ui.seek.addEventListener('input', () => state.data && seekTo(Number(ui.seek.value)));
|
| 517 |
ui.volume.addEventListener('input', () => { if (state.master && state.audio) state.master.gain.setTargetAtTime(Number(ui.volume.value), state.audio.currentTime, .02); });
|
| 518 |
ui.sourceMix.addEventListener('input', () => { state.sourceMix = Number(ui.sourceMix.value); applySourceMix(); });
|
| 519 |
+
ui.stereo.addEventListener('change', () => { state.stereo = ui.stereo.checked; applySourceMix(); announce(state.stereo ? 'Split mode: original left, MIDI right.' : 'Split mode off.'); });
|
| 520 |
$$('[data-source]').forEach((button) => button.addEventListener('click', () => {
|
| 521 |
state.sourceMix = button.dataset.source === 'original' ? 0 : 1;
|
| 522 |
ui.sourceMix.value = String(state.sourceMix);
|
|
|
|
| 537 |
state.synth.stopAll();
|
| 538 |
state.synth.programChange(channel, program);
|
| 539 |
}
|
| 540 |
+
announce(`Sound changed to ${select.options[select.selectedIndex].textContent}.`);
|
| 541 |
});
|
| 542 |
$$('[data-tab]').forEach((button) => button.addEventListener('click', () => switchTab(button.dataset.tab)));
|
| 543 |
new ResizeObserver(() => drawRoll()).observe(ui.canvas);
|