|
437 | 437 | // volume strip — vertical fill column (skip updates while dragging OR during post-drag cooldown) |
438 | 438 | const volStrip = row.querySelector('.vol-strip'); |
439 | 439 | if (volStrip && !volStrip._dragging && !volStrip._cooldown) { |
440 | | - const rawVol = Math.round(volumeToStripPos(layer.volume || 1)); |
| 440 | + const rawVol = Math.round(volumeToStripPos(layerVolume(layer))); |
441 | 441 | volStrip.dataset.value = rawVol; |
442 | 442 | updateVolStripVisual(volStrip); |
443 | 443 | } |
|
1135 | 1135 |
|
1136 | 1136 | // exponential curve: knob position 0-200 → volume 0-200% |
1137 | 1137 | // gives much more resolution at low volumes for quiet layers |
1138 | | - // pos 0 → vol 0 (true mute), pos 100 → vol 1.0 (unity), pos 200 → vol 2.0 |
| 1138 | + // pos 0 → vol 0 (true mute), pos ~136 → vol 1.0 (unity), pos 200 → vol 2.0 |
1139 | 1139 | function stripPosToVolume(pos) { |
1140 | 1140 | if (pos <= 0) return 0; // true mute at zero |
1141 | 1141 | const norm = pos / 200; // 0..1 |
|
1151 | 1151 | return Math.round(uncurved * 200); |
1152 | 1152 | } |
1153 | 1153 |
|
| 1154 | + function layerVolume(layer) { |
| 1155 | + const vol = Number(layer?.volume); |
| 1156 | + return Number.isFinite(vol) ? Math.max(0, Math.min(2, vol)) : 1; |
| 1157 | + } |
| 1158 | + |
| 1159 | + function stripValue(strip, fallback = 100) { |
| 1160 | + const val = parseInt(strip?.dataset?.value ?? '', 10); |
| 1161 | + return Number.isFinite(val) ? Math.max(0, Math.min(200, val)) : fallback; |
| 1162 | + } |
| 1163 | + |
| 1164 | + function formatVolume(vol) { |
| 1165 | + const clamped = Math.max(0, Math.min(2, vol)); |
| 1166 | + return clamped < 0.1 ? clamped.toFixed(3) : clamped.toFixed(2); |
| 1167 | + } |
| 1168 | + |
| 1169 | + function volumeLabel(vol) { |
| 1170 | + if (vol <= 0) return 'mute'; |
| 1171 | + if (vol < 0.01) return Math.max(0.1, Math.round(vol * 1000) / 10) + '%'; |
| 1172 | + return Math.round(vol * 100) + '%'; |
| 1173 | + } |
| 1174 | + |
1154 | 1175 | function updateVolStripVisual(strip) { |
1155 | | - const val = parseInt(strip.dataset.value) || 100; |
| 1176 | + const val = stripValue(strip); |
1156 | 1177 | const pct = Math.max(0, Math.min(100, (val / 200) * 100)); |
1157 | 1178 | const fill = strip.querySelector('.vol-strip-fill'); |
1158 | 1179 | if (fill) fill.style.height = pct + '%'; |
1159 | 1180 | const vol = stripPosToVolume(val); |
1160 | 1181 | strip.classList.toggle('hot', vol > 1.1); |
1161 | | - strip.classList.toggle('silent', vol < 0.01); |
| 1182 | + strip.classList.toggle('silent', vol <= 0); |
1162 | 1183 | strip.setAttribute('aria-valuenow', String(val)); |
1163 | | - strip.setAttribute('aria-valuetext', vol.toFixed(2) + '×'); |
1164 | | - strip.dataset.display = vol < 0.01 ? 'mute' : Math.round(vol * 100) + '%'; |
| 1184 | + strip.setAttribute('aria-valuetext', formatVolume(vol) + '×'); |
| 1185 | + strip.dataset.display = volumeLabel(vol); |
1165 | 1186 | } |
1166 | 1187 |
|
1167 | 1188 | function commitStripValue(strip) { |
1168 | | - const val = parseInt(strip.dataset.value) || 100; |
1169 | | - const vol = stripPosToVolume(val).toFixed(2); |
| 1189 | + const val = stripValue(strip); |
| 1190 | + const vol = formatVolume(stripPosToVolume(val)); |
1170 | 1191 | sendCommand('set volume layer ' + strip.dataset.target + ' ' + vol); |
1171 | 1192 | } |
1172 | 1193 |
|
|
1197 | 1218 | didLongPress = false; |
1198 | 1219 | strip.classList.add('dragging'); |
1199 | 1220 | startY = e.clientY; |
1200 | | - startVal = parseInt(strip.dataset.value) || 100; |
| 1221 | + startVal = stripValue(strip); |
1201 | 1222 | strip.setPointerCapture(e.pointerId); |
1202 | 1223 | showFloatingValue(); |
1203 | 1224 |
|
|
1253 | 1274 | e.preventDefault(); |
1254 | 1275 | const step = e.shiftKey ? 1 : 4; |
1255 | 1276 | const delta = e.deltaY < 0 ? step : -step; |
1256 | | - const cur = parseInt(strip.dataset.value) || 100; |
| 1277 | + const cur = stripValue(strip); |
1257 | 1278 | const newVal = Math.max(0, Math.min(200, cur + delta)); |
1258 | 1279 | strip.dataset.value = newVal; |
1259 | 1280 | updateVolStripVisual(strip); |
|
1270 | 1291 |
|
1271 | 1292 | strip.addEventListener('keydown', (e) => { |
1272 | 1293 | const step = e.shiftKey ? 1 : 5; |
1273 | | - const cur = parseInt(strip.dataset.value) || 100; |
| 1294 | + const cur = stripValue(strip); |
1274 | 1295 | let newVal = cur; |
1275 | 1296 | if (e.key === 'ArrowUp' || e.key === 'ArrowRight') { newVal = Math.min(200, cur + step); e.preventDefault(); } |
1276 | 1297 | else if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') { newVal = Math.max(0, cur - step); e.preventDefault(); } |
|
1651 | 1672 | unity.className = 'mixer-fader-unity'; |
1652 | 1673 | fader.appendChild(fill); |
1653 | 1674 | fader.appendChild(unity); |
1654 | | - const vol = layer.volume || 1; |
| 1675 | + const vol = layerVolume(layer); |
1655 | 1676 | const pos = volumeToStripPos(vol); |
1656 | 1677 | const pct = Math.max(0, Math.min(100, (pos / 200) * 100)); |
1657 | 1678 | fill.style.height = pct + '%'; |
|
1663 | 1684 | // dB readout |
1664 | 1685 | const dbLabel = document.createElement('div'); |
1665 | 1686 | dbLabel.className = 'mixer-fader-db'; |
1666 | | - dbLabel.textContent = vol < 0.01 ? 'mute' : Math.round(vol * 100) + '%'; |
| 1687 | + dbLabel.textContent = volumeLabel(vol); |
1667 | 1688 | ch.appendChild(dbLabel); |
1668 | 1689 |
|
1669 | 1690 | // pan track |
|
1709 | 1730 |
|
1710 | 1731 | // mute/solo clicks |
1711 | 1732 | muteBtn.addEventListener('click', () => { |
1712 | | - sendCommand('mute ' + muteBtn.dataset.target); |
| 1733 | + sendCommand('mute layer ' + muteBtn.dataset.target); |
1713 | 1734 | }); |
1714 | 1735 | soloBtn.addEventListener('click', () => { |
1715 | | - sendCommand('solo ' + soloBtn.dataset.target); |
| 1736 | + sendCommand('solo layer ' + soloBtn.dataset.target); |
1716 | 1737 | }); |
1717 | 1738 | }); |
1718 | 1739 |
|
|
1751 | 1772 | // update fader (if not dragging) |
1752 | 1773 | const fader = ch.querySelector('.mixer-fader-wrap'); |
1753 | 1774 | if (fader && !fader._dragging && !fader._cooldown) { |
1754 | | - const vol = layer.volume || 1; |
| 1775 | + const vol = layerVolume(layer); |
1755 | 1776 | const pos = volumeToStripPos(vol); |
1756 | 1777 | const pct = Math.max(0, Math.min(100, (pos / 200) * 100)); |
1757 | 1778 | const fill = fader.querySelector('.mixer-fader-fill'); |
1758 | 1779 | if (fill) fill.style.height = pct + '%'; |
1759 | 1780 | fader.classList.toggle('hot', vol > 1.1); |
1760 | 1781 | const dbLabel = ch.querySelector('.mixer-fader-db'); |
1761 | | - if (dbLabel) dbLabel.textContent = vol < 0.01 ? 'mute' : Math.round(vol * 100) + '%'; |
| 1782 | + if (dbLabel) dbLabel.textContent = volumeLabel(vol); |
1762 | 1783 | } |
1763 | 1784 |
|
1764 | 1785 | // update mute/solo states |
|
1791 | 1812 | fader._dragging = true; |
1792 | 1813 | startY = e.clientY; |
1793 | 1814 | const layerData = state.layers?.[target - 1]; |
1794 | | - startVal = volumeToStripPos(layerData?.volume || 1); |
| 1815 | + startVal = volumeToStripPos(layerVolume(layerData)); |
1795 | 1816 | fader.setPointerCapture(e.pointerId); |
1796 | 1817 | }); |
1797 | 1818 |
|
|
1804 | 1825 | fill.style.height = pct + '%'; |
1805 | 1826 | fader.classList.toggle('hot', stripPosToVolume(newVal) > 1.1); |
1806 | 1827 | const vol = stripPosToVolume(newVal); |
1807 | | - dbLabel.textContent = vol < 0.01 ? 'mute' : Math.round(vol * 100) + '%'; |
| 1828 | + dbLabel.textContent = volumeLabel(vol); |
1808 | 1829 | fader._currentVal = newVal; |
1809 | 1830 | }); |
1810 | 1831 |
|
1811 | 1832 | fader.addEventListener('pointerup', () => { |
1812 | 1833 | if (!fader._dragging) return; |
1813 | 1834 | fader._dragging = false; |
1814 | 1835 | if (fader._currentVal != null) { |
1815 | | - const vol = stripPosToVolume(fader._currentVal).toFixed(2); |
| 1836 | + const vol = formatVolume(stripPosToVolume(fader._currentVal)); |
1816 | 1837 | sendCommand('set volume layer ' + target + ' ' + vol); |
1817 | 1838 | startCooldown(); |
1818 | 1839 | // sync the corresponding layer vol-strip |
|
1830 | 1851 | if (fader._dragging) { |
1831 | 1852 | fader._dragging = false; |
1832 | 1853 | if (fader._currentVal != null) { |
1833 | | - const vol = stripPosToVolume(fader._currentVal).toFixed(2); |
| 1854 | + const vol = formatVolume(stripPosToVolume(fader._currentVal)); |
1834 | 1855 | sendCommand('set volume layer ' + target + ' ' + vol); |
1835 | 1856 | startCooldown(); |
1836 | 1857 | } |
|
1918 | 1939 | muteBtn.className = 'vol-expanded-mute' + (layer.muted ? ' on' : ''); |
1919 | 1940 | muteBtn.textContent = layer.muted ? 'unmute' : 'mute'; |
1920 | 1941 | muteBtn.addEventListener('click', () => { |
1921 | | - sendCommand('mute ' + target); |
| 1942 | + sendCommand('mute layer ' + target); |
1922 | 1943 | muteBtn.classList.toggle('on'); |
1923 | 1944 | muteBtn.textContent = muteBtn.classList.contains('on') ? 'unmute' : 'mute'; |
1924 | 1945 | }); |
|
1928 | 1949 | document.body.appendChild(overlay); |
1929 | 1950 |
|
1930 | 1951 | // sync initial value from strip |
1931 | | - let currentVal = parseInt(strip.dataset.value) || 100; |
| 1952 | + let currentVal = stripValue(strip); |
1932 | 1953 |
|
1933 | 1954 | function updateVisual() { |
1934 | 1955 | const pct = Math.max(0, Math.min(100, (currentVal / 200) * 100)); |
1935 | 1956 | fill.style.height = pct + '%'; |
1936 | 1957 | const vol = stripPosToVolume(currentVal); |
1937 | | - valueEl.textContent = vol < 0.01 ? 'mute' : Math.round(vol * 100) + '%'; |
| 1958 | + valueEl.textContent = volumeLabel(vol); |
1938 | 1959 | } |
1939 | 1960 | updateVisual(); |
1940 | 1961 |
|
|
1961 | 1982 | fader.addEventListener('pointerup', () => { |
1962 | 1983 | if (!isDragging) return; |
1963 | 1984 | isDragging = false; |
1964 | | - const vol = stripPosToVolume(currentVal).toFixed(2); |
| 1985 | + const vol = formatVolume(stripPosToVolume(currentVal)); |
1965 | 1986 | sendCommand('set volume layer ' + target + ' ' + vol); |
1966 | 1987 | // sync back to layer strip |
1967 | 1988 | strip.dataset.value = currentVal; |
|
0 commit comments