From 729e5246c99ac6d36a243f38a7a05511c96100a9 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Thu, 4 Jun 2026 11:46:06 +0900 Subject: [PATCH] taut: mouse-operated slider bar --- assets/disk0/tvdos/bin/taut.js | 224 +++++++++++++++++++++++++++++---- terranmon.txt | 4 +- 2 files changed, 199 insertions(+), 29 deletions(-) diff --git a/assets/disk0/tvdos/bin/taut.js b/assets/disk0/tvdos/bin/taut.js index 4228750..1c7ab41 100644 --- a/assets/disk0/tvdos/bin/taut.js +++ b/assets/disk0/tvdos/bin/taut.js @@ -4007,6 +4007,163 @@ function drawGroupHeader(y, title) { print(txt + `\u00FB`.repeat(dashes)) } +// ── Inline value sliders (Gen.1 / Gen.2 knob editing) ────────────────────── +// A horizontal slider painted alongside a numeric field. The knob is one 7-px +// cell wide and slides with per-pixel precision via the sym.slider1..7 glyphs +// (slider1 = knob snug in one cell; slider2..7 straddle two cells at a 1..6 px +// offset). The trough is a flat colBLACK bar capped by inverse-video round pads +// (0xAB left, 0xAA right). Two trough widths only: small (10) and wide (20). +// +// Clicking/dragging a trough drives the knob: the label updates live as the knob +// moves, and the instrument byte(s) are written only on mouse release (see +// runSliderDrag). instSliders is rebuilt on every Gen.1/Gen.2 body repaint and +// hit-tested by the panel's slider mouse region. +const SLIDER_TW_SMALL = 25 +const SLIDER_TW_WIDE = 36 +const SLIDER_LABEL_W = 10 +const SLIDER_END_COL = SCRW - 1 // common right edge +const SLIDER_SMALL_SX = SLIDER_END_COL - (SLIDER_TW_SMALL + 1) // small left-pad col +const SLIDER_WIDE_SX = SLIDER_END_COL - (SLIDER_TW_WIDE + 1) // wide left-pad col +const SLIDER_VALUE_W = SLIDER_SMALL_SX - (INST_RIGHT_X + SLIDER_LABEL_W) + +const sliderGlyphs = [sym.slider1, sym.slider2, sym.slider3, sym.slider4, + sym.slider5, sym.slider6, sym.slider7] + +// Rebuilt by drawInstTabGeneral1/2; each entry is +// { y, sx, tw, troughLeftPx, min, max, render(val), commit(val) }. +let instSliders = [] + +// Paint the trough + knob for value-fraction `frac` (0..1) at (y, sx). +function drawSlider(y, sx, tw, frac) { + const pmax = (tw - 1) * CELL_PW + const p = Math.round((frac < 0 ? 0 : frac > 1 ? 1 : frac) * pmax) + const cell = (p / CELL_PW) | 0 + const sub = p - cell * CELL_PW + const cells = new Array(tw).fill(' ') + if (sub === 0) cells[cell] = sliderGlyphs[0] + else { + const g = sliderGlyphs[sub] // 2-char glyph straddling cell..cell+1 + cells[cell] = g[0] + if (cell + 1 < tw) cells[cell + 1] = g[1] + } + con.color_pair(colBLACK, colStatus); con.move(y, sx); con.prnch(0xAB) + con.color_pair(colStatus, colBLACK); con.move(y, sx + 1); print(cells.join('')) + con.color_pair(colBLACK, colStatus); con.move(y, sx + tw + 1); con.prnch(0xAA) +} + +// Pixel X (mouse) → quantised slider value, knob centred under the cursor. +function sliderMouseToVal(s, pxX) { + const pmax = (s.tw - 1) * CELL_PW + let knob = Math.round((pxX - s.troughLeftPx) - CELL_PW / 2) + if (knob < 0) knob = 0 + if (knob > pmax) knob = pmax + const frac = (pmax === 0) ? 0 : knob / pmax + let v = Math.round(s.min + frac * (s.max - s.min)) + if (v < s.min) v = s.min + if (v > s.max) v = s.max + return v +} + +// Write byte pairs [[offset, value], ...] into instrument `slot`'s peripheral +// record. The audio adapter decodes these live, so edits take effect at once. +function instWriteBytes(slot, pairs) { + const memBase = audio.getMemAddr() + const base = TAUT_INST_WINDOW_OFF + slot * TAUT_INST_RECORD_SIZE + for (let i = 0; i < pairs.length; i++) { + sys.poke(memBase - (base + pairs[i][0]), pairs[i][1] & 0xFF) + } +} + +// Drag interaction: live label updates while held, commit on release, ESC cancels. +function runSliderDrag(s, downEvent) { + let val = sliderMouseToVal(s, downEvent[1]) + let committed = false + s.render(val) + let dragging = true + while (dragging) { + input.withEvent(e => { + const t = e[0] + if (t === 'mouse_move') { + const nv = sliderMouseToVal(s, e[1]) + if (nv !== val) { val = nv; s.render(val) } + } else if (t === 'mouse_up') { + dragging = false; committed = true + } else if (t === 'key_down' && e[1] === '') { + dragging = false + } + // mouse_down echo and other events are ignored during a drag + }) + } + if (committed) s.commit(val) + drawInstrumentsContents() +} + +// fmt helpers — kept short so the value still fits the SLIDER_VALUE_W field. +function fmtByte255(v) { return '$' + _hex(v, 2) + ' (' + v + ')' } +function fmtSigned(v) { return _signed(v) } +function fmtFilter(v) { return (v === 0xFF) ? 'off' : ('$' + _hex(v, 2) + ' (' + v + ')') } +function fmtFadeout(v) { + if (v <= 0) return '0 none' + if (v >= 1024) return '1024 cut' + return v + ' ~' + (1024 / v).toFixed(1) + 't' +} + +// Emit a small-slider row: label, numeric value, then the knob. `encode(val)` +// returns the byte pairs to poke on commit. +function sliderRow(y, e, label, val0, min, max, fmt, encode) { + const sx = SLIDER_SMALL_SX, tw = SLIDER_TW_SMALL + const render = (val) => { + if (val < min) val = min + if (val > max) val = max + con.move(y, INST_RIGHT_X) + con.color_pair(colInstLabel, colBackPtn) + print((label + ' '.repeat(SLIDER_LABEL_W)).substring(0, SLIDER_LABEL_W)) + con.color_pair(colInstValue, colBackPtn) + print((fmt(val) + ' '.repeat(SLIDER_VALUE_W)).substring(0, SLIDER_VALUE_W)) + drawSlider(y, sx, tw, (max === min) ? 0 : (val - min) / (max - min)) + } + render(val0) + instSliders.push({ + y, sx, tw, troughLeftPx: sx * CELL_PW, min, max, render, + val: (val0 < min ? min : val0 > max ? max : val0), // current value, for wheel ±1 deltas + commit: (v) => { instWriteBytes(e.slot, encode(v)); e.decoded = decodeInstFull(readInstRecord(e.slot)) } + }) +} + +// Emit the wide two-row Detune slider: knob on `y`, numeric labels on `y+1`. +function detuneRow(y, e, val0) { + const sx = SLIDER_WIDE_SX, tw = SLIDER_TW_WIDE + const min = -32768, max = 32767 + const render = (val) => { + if (val < min) val = min + if (val > max) val = max + con.move(y, INST_RIGHT_X) + con.color_pair(colInstLabel, colBackPtn) + print((' Detune:' + ' '.repeat(SLIDER_LABEL_W)).substring(0, SLIDER_LABEL_W)) + drawSlider(y, sx, tw, (val - min) / (max - min)) + con.move(y + 1, INST_RIGHT_X) + con.color_pair(colInstValue, colBackPtn) + const s = ' ' + _signed(val) + ' (' + (val / 0x1000).toFixed(3) + ' octave, 4096-TET)' + print((s + ' '.repeat(INST_RIGHT_W)).substring(0, INST_RIGHT_W)) + } + render(val0) + instSliders.push({ + y, sx, tw, troughLeftPx: sx * CELL_PW, min, max, render, + val: (val0 < min ? min : val0 > max ? max : val0), // current value, for wheel ±1 deltas + commit: (v) => { instWriteBytes(e.slot, [[184, v & 0xFF], [185, (v >> 8) & 0xFF]]); e.decoded = decodeInstFull(readInstRecord(e.slot)) } + }) +} + +// Hit-test the live instSliders list for a cell (cy, cx). Gen.1/Gen.2 only. +function sliderAt(cy, cx) { + if (instSubTab !== INST_TAB_GEN1 && instSubTab !== INST_TAB_GEN2) return null + for (let i = 0; i < instSliders.length; i++) { + const s = instSliders[i] + if (cy === s.y && cx >= s.sx && cx <= s.sx + s.tw + 1) return s + } + return null +} + // ── Tab body: General (page 1 + page 2) ─────────────────────────────────── // Page 1 (Gen.1): // Sample binding — sample link, length, c4Rate, play/loop positions, loop mode @@ -4053,25 +4210,18 @@ function drawInstTabGeneral1(e) { y++ drawGroupHeader(y++, 'Volume') - drawLabelRow(y++, ' Inst. GV:', _hex(d.igv, 2) + ' (' + d.igv + '/255)') - drawLabelRow(y++, ' DefNote:', _hex(d.defNoteVol, 2) + ' (' + d.defNoteVol + '/255' + - (d.defNoteVol === 0 ? ' legacy: row default 63' : '') + ')') - let fadeStr - if (d.fadeout === 0) fadeStr = '0 (no fade)' - else if (d.fadeout >= 1024) fadeStr = d.fadeout + ' (1-tick cut)' - else { - const ticks = (1024 / d.fadeout) - fadeStr = d.fadeout + ' (~' + ticks.toFixed(1) + ' ticks)' - } - drawLabelRow(y++, ' Fadeout:', fadeStr) - drawLabelRow(y++, ' Swing:', _hex(d.volSwing, 2)) + sliderRow(y++, e, ' Inst.GV:', d.igv, 0, 255, fmtByte255, (v) => [[171, v]]) + sliderRow(y++, e, ' DefNote:', d.defNoteVol, 0, 255, fmtByte255, (v) => [[196, v]]) + sliderRow(y++, e, ' Fadeout:', d.fadeout, 0, 1024, fmtFadeout, (v) => [[172, v & 0xFF], [173, (v >> 8) & 0x0F]]) + sliderRow(y++, e, ' Swing:', d.volSwing, 0, 255, fmtByte255, (v) => [[174, v]]) y++ drawGroupHeader(y++, 'Panning') - drawLabelRow(y++, ' Default:', _hex(d.defPan, 2) + ' Use: ' + + sliderRow(y++, e, ' Default:', d.defPan, 0, 255, fmtByte255, (v) => [[177, v]]) + sliderRow(y++, e, ' Sep:', d.pitchPanSep, -128, 127, fmtSigned, (v) => [[180, v & 0xFF]]) + sliderRow(y++, e, ' Swing:', d.panSwing, 0, 255, fmtByte255, (v) => [[181, v]]) + drawLabelRow(y++, ' PPanCnt:', '$' + _hex(d.pitchPanCenter, 4) + ' Use: ' + (d.panEnv.panUseDef ? sym.ticked + ' on' : sym.unticked + ' off')) - drawLabelRow(y++, ' PPanCtr:', '$' + _hex(d.pitchPanCenter, 4) + ' Sep: ' + _signed(d.pitchPanSep)) - drawLabelRow(y++, ' Swing:', _hex(d.panSwing, 2)) } function drawInstTabGeneral2(e) { @@ -4079,27 +4229,27 @@ function drawInstTabGeneral2(e) { let y = INST_BODY_Y drawGroupHeader(y++, 'Filter') - drawLabelRow(y++, ' Cutoff:', (d.defCutoff === 0xFF ? 'off' : ('$' + _hex(d.defCutoff, 2) + - ' (' + d.defCutoff + '/254)'))) - drawLabelRow(y++, ' Reso:', (d.defReso === 0xFF ? 'off' : ('$' + _hex(d.defReso, 2) + - ' (' + d.defReso + '/254)'))) + sliderRow(y++, e, ' Cutoff:', d.defCutoff, 0, 255, fmtFilter, (v) => [[182, v]]) + sliderRow(y++, e, ' Reso:', d.defReso, 0, 255, fmtFilter, (v) => [[183, v]]) y++ drawGroupHeader(y++, 'Vibrato') - drawLabelRow(y++, ' Waveform:', VIB_WF_NAMES[d.vibWaveform & 7]) - drawLabelRow(y++, ' Speed:', _hex(d.vibSpeed, 2) + ' Depth: ' + _hex(d.vibDepth, 2)) - drawLabelRow(y++, ' Sweep:', _hex(d.vibSweep, 2) + ' Rate: ' + _hex(d.vibRate, 2)) + drawLabelRow(y++, ' Wave:', VIB_WF_NAMES[d.vibWaveform & 7], SLIDER_LABEL_W) + sliderRow(y++, e, ' Speed:', d.vibSpeed, 0, 255, fmtByte255, (v) => [[175, v]]) + sliderRow(y++, e, ' Depth:', d.vibDepth, 0, 255, fmtByte255, (v) => [[187, v]]) + sliderRow(y++, e, ' Sweep:', d.vibSweep, 0, 255, fmtByte255, (v) => [[176, v]]) + sliderRow(y++, e, ' Rate:', d.vibRate, 0, 255, fmtByte255, (v) => [[188, v]]) y++ drawGroupHeader(y++, 'Note actions') - drawLabelRow(y++, ' NNA:', NNA_NAMES[d.nna & 3]) - drawLabelRow(y++, ' DCT:', DCT_NAMES[d.dct & 3]) - drawLabelRow(y++, ' DCA:', DCA_NAMES[d.dca & 3]) + drawLabelRow(y++, ' NNA:', NNA_NAMES[d.nna & 3], SLIDER_LABEL_W) + drawLabelRow(y++, ' DCT:', DCT_NAMES[d.dct & 3], SLIDER_LABEL_W) + drawLabelRow(y++, ' DCA:', DCA_NAMES[d.dca & 3], SLIDER_LABEL_W) y++ drawGroupHeader(y++, 'Tuning') - const detStr = _signed(d.detune) + ' (' + (d.detune / 0x1000).toFixed(3) + ' octave, 4096-TET)' - drawLabelRow(y++, ' Detune:', detStr) + detuneRow(y, e, d.detune) + y += 2 } // ── Envelope rendering (shared by Volume/Panning/Pitch tabs) ─────────────── @@ -4320,6 +4470,7 @@ function clearInstrumentsPanel() { function drawInstrumentsContents(wo) { if (instrumentsCache === null) refreshInstrumentsCache() clampInstrumentsCursor() + instSliders.length = 0 // rebuilt by the Gen.1/Gen.2 body drawers below clearInstrumentsPanel() drawInstrumentsListColumn() drawInstrumentsSeparator() @@ -4431,6 +4582,25 @@ function registerInstrumentsMouse() { requestEditorLaunch('taut_instredit', [fullPathObj.full, VIEW_INSTRMNT, slot]) } }) + // Slider body (Gen.1 / Gen.2): one region that hit-tests the live instSliders + // list. Click/drag the matched knob until mouse release; wheel nudges by ±1 + // (wheel up = +1) and commits each notch for fine control. + addPanelMouseRegion(INST_RIGHT_X, INST_BODY_Y, INST_RIGHT_W, INST_BODY_H, { + onClick: (cy, cx, btn, ev) => { + if (btn !== 1) return + const s = sliderAt(cy, cx) + if (s) runSliderDrag(s, ev) + }, + onWheel: (cy, cx, dy) => { + const s = sliderAt(cy, cx) + if (!s) return + const nv = Math.max(s.min, Math.min(s.max, s.val + (dy < 0 ? 1 : -1))) + if (nv === s.val) return + s.val = nv + s.render(nv) + s.commit(nv) + } + }) } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/terranmon.txt b/terranmon.txt index e6940ea..5dfe957 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -2218,7 +2218,7 @@ from source. * FastTracker2 has instrumentwise config (0..255) * The spec follows FastTracker2, and conversion must be performed when importing from FastTracker2 176 Uint8 Vibrato sweep - * FastTracker2 instrument config + * FastTracker2 instrument config (0..255) 177 Uint8 Default pan value (0..255 full range, see offset 17 for the enable flag) * ImpulseTracker has samplewise default pan and instrumentwise default pan, and they must be taken into account because Taud has no samplewise config 178 Uint16 Pitch-pan centre (4096-TET note value) @@ -2226,7 +2226,7 @@ from source. 181 Uint8 Pan swing (0..255 full range) 182 Uint8 Default cutoff (0..254 full range, 255 to off (-1 on IT). Effect range equals to that of ImpulseTracker -- 127 in IT is equal to 254 in Taud) 183 Uint8 Default resonance (0..254 full range, 255 to off (-1 on IT). Effect range equals to that of ImpulseTracker -- 127 in IT is equal to 254 in Taud) -184 Uint16 Sample detune (in 4096-TET unit) (FT2 finetune scale need to be rescaled accordingly) +184 Sint16 Sample detune (in 4096-TET unit) (FT2 finetune scale need to be rescaled accordingly) 186 Bit8 Instrument Flag 0b 000 www nn n: New note action. 00: note off, 01: note cut, 10: continue, 11: note fade (arranged differently to IT)