mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
taut: sample viewer wip
This commit is contained in:
@@ -870,6 +870,22 @@ function loadTaudSongList(filePath) {
|
||||
}
|
||||
|
||||
let projectName = ''
|
||||
// 0x1E-separated UTF-8 strings; slot 0 is always present (typically empty)
|
||||
// because converters write a leading separator. Read all entries that exist.
|
||||
const instNames = []
|
||||
const sampleNames = []
|
||||
|
||||
function parseNameTable(payloadStart, secLen) {
|
||||
const out = []
|
||||
let s = ''
|
||||
for (let k = 0; k < secLen; k++) {
|
||||
const b = sys.peek(ptr + payloadStart + k) & 0xFF
|
||||
if (b === 0x1E) { out.push(s); s = '' }
|
||||
else { s += String.fromCharCode(b) }
|
||||
}
|
||||
out.push(s)
|
||||
return out
|
||||
}
|
||||
|
||||
// Parse Project Data section (\x1ETaudPrJ) for song names / project name.
|
||||
// See terranmon.txt "Project Data" / "sMet" for the format.
|
||||
@@ -900,6 +916,16 @@ function loadTaudSongList(filePath) {
|
||||
}
|
||||
projectName = s
|
||||
}
|
||||
// 'INam' = 0x49,0x4E,0x61,0x6D
|
||||
else if (fc0 === 0x49 && fc1 === 0x4E && fc2 === 0x61 && fc3 === 0x6D) {
|
||||
const names = parseNameTable(payloadStart, secLen)
|
||||
for (let k = 0; k < names.length; k++) instNames[k] = names[k]
|
||||
}
|
||||
// 'SNam' = 0x53,0x4E,0x61,0x6D
|
||||
else if (fc0 === 0x53 && fc1 === 0x4E && fc2 === 0x61 && fc3 === 0x6D) {
|
||||
const names = parseNameTable(payloadStart, secLen)
|
||||
for (let k = 0; k < names.length; k++) sampleNames[k] = names[k]
|
||||
}
|
||||
// 'sMet' = 0x73,0x4D,0x65,0x74
|
||||
else if (fc0 === 0x73 && fc1 === 0x4D && fc2 === 0x65 && fc3 === 0x74) {
|
||||
let q = payloadStart
|
||||
@@ -939,7 +965,7 @@ function loadTaudSongList(filePath) {
|
||||
}
|
||||
|
||||
sys.free(ptr)
|
||||
return { numSongs, projectName, songs }
|
||||
return { numSongs, projectName, songs, instNames, sampleNames }
|
||||
}
|
||||
|
||||
|
||||
@@ -1493,7 +1519,17 @@ function drawControlHint() {
|
||||
['sep'],
|
||||
['!','Help'],
|
||||
]
|
||||
let hintElems = [hintElemTimeline, hintElemOrders, hintElemPatterns, hintElemExternal, hintElemExternal, hintElemProject, hintElemExternal]
|
||||
const hintElemSamples = [
|
||||
[`\u008428u\u008429u`,'Nav'],
|
||||
['sep'],
|
||||
['e','Edit'],
|
||||
['ent','View inst'],
|
||||
['sep'],
|
||||
['tab','Panel'],
|
||||
['sep'],
|
||||
['!','Help'],
|
||||
]
|
||||
let hintElems = [hintElemTimeline, hintElemOrders, hintElemPatterns, hintElemSamples, hintElemExternal, hintElemProject, hintElemExternal]
|
||||
let hintElemPat = [hintElemEditNoteValue, hintElemEditInstValue, hintElemEditVolEff, hintElemEditPanEff, hintElemEditFxSym, hintElemEditFxVal]
|
||||
|
||||
// erase current line
|
||||
@@ -1921,6 +1957,7 @@ function switchSong(newIndex) {
|
||||
|
||||
currentSongIndex = newIndex
|
||||
song = loadTaud(fullPathObj.full, newIndex)
|
||||
refreshSamplesCache()
|
||||
|
||||
const newPitchIdx = songsMeta.songs[newIndex].pitchPresetIdx
|
||||
PITCH_PRESET_IDX = (newPitchIdx != null && pitchTablePresets[newPitchIdx])
|
||||
@@ -3021,7 +3058,438 @@ function projectInput(wo, event) {
|
||||
|
||||
function externalPanelInput(wo, event) {}
|
||||
|
||||
const panelSamples = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, externalPanelInput, makeExternalPanelDraw('taut_sampleedit'), undefined, ()=>{})
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// SAMPLES VIEWER
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// The Samples tab is an internal viewer: sample list on the left, properties +
|
||||
// "used by" instrument list + waveform graphics on the right, and an Edit
|
||||
// button that launches the external taut_sampleedit sub-program.
|
||||
//
|
||||
// Sample identity in .taud is derived from (samplePtr, sampleLen) inside the
|
||||
// 256-byte instrument records (terranmon.txt §"Instrument bin"). Conversion
|
||||
// scripts pack samples into the 8 MB pool in slot order, so sorting unique
|
||||
// pointers ascending lines up with SNam[i] in the project-data block.
|
||||
|
||||
// Peripheral memory window offsets, from terranmon.txt:1994-1999.
|
||||
const TAUT_SBANK_SIZE = 524288 // 512 K window for sample bin
|
||||
const TAUT_INST_WINDOW_OFF = 720896 // instrument bin starts here in peri space
|
||||
const TAUT_INST_RECORD_SIZE = 256
|
||||
const TAUT_INST_COUNT = 256 // slots 0..255; slot 0 is unused
|
||||
|
||||
// Read one 256-byte instrument record straight out of the audio adapter.
|
||||
function readInstRecord(slot) {
|
||||
const memBase = audio.getMemAddr()
|
||||
const base = TAUT_INST_WINDOW_OFF + slot * TAUT_INST_RECORD_SIZE
|
||||
const rec = new Uint8Array(TAUT_INST_RECORD_SIZE)
|
||||
for (let i = 0; i < TAUT_INST_RECORD_SIZE; i++) {
|
||||
rec[i] = sys.peek(memBase - (base + i)) & 0xFF
|
||||
}
|
||||
return rec
|
||||
}
|
||||
|
||||
// Decode the fields the viewer actually cares about. Offsets from terranmon.txt:2071+.
|
||||
function decodeInstRecord(rec) {
|
||||
const samplePtr = (rec[0]) | (rec[1] << 8) | (rec[2] << 16) | (rec[3] * 0x1000000)
|
||||
const sampleLen = rec[4] | (rec[5] << 8)
|
||||
const c4Rate = rec[6] | (rec[7] << 8)
|
||||
const playStart = rec[8] | (rec[9] << 8)
|
||||
const loopStart = rec[10] | (rec[11] << 8)
|
||||
const loopEnd = rec[12] | (rec[13] << 8)
|
||||
const sampleFlags = rec[14]
|
||||
const instGV = rec[171]
|
||||
const defNoteVol = rec[196]
|
||||
const detune = rec[184] | (rec[185] << 8)
|
||||
return {
|
||||
samplePtr, sampleLen, c4Rate, playStart, loopStart, loopEnd,
|
||||
sampleFlags, instGV, defNoteVol, detune
|
||||
}
|
||||
}
|
||||
|
||||
// Scan all 256 instruments and build the deduped sample list. Each returned
|
||||
// entry is { ptr, len, c4Rate, playStart, loopStart, loopEnd, sampleFlags,
|
||||
// usedBy[], name }. usedBy is a list of instrument slot numbers (1..255).
|
||||
let samplesCache = null
|
||||
|
||||
function buildSampleIndex() {
|
||||
const byPtr = new Map()
|
||||
for (let i = 1; i < TAUT_INST_COUNT; i++) {
|
||||
const d = decodeInstRecord(readInstRecord(i))
|
||||
if (d.sampleLen === 0) continue
|
||||
const key = d.samplePtr + ':' + d.sampleLen
|
||||
if (!byPtr.has(key)) {
|
||||
byPtr.set(key, {
|
||||
ptr: d.samplePtr,
|
||||
len: d.sampleLen,
|
||||
c4Rate: d.c4Rate,
|
||||
playStart: d.playStart,
|
||||
loopStart: d.loopStart,
|
||||
loopEnd: d.loopEnd,
|
||||
sampleFlags:d.sampleFlags,
|
||||
usedBy: [],
|
||||
name: ''
|
||||
})
|
||||
}
|
||||
byPtr.get(key).usedBy.push(i)
|
||||
}
|
||||
const list = Array.from(byPtr.values()).sort((a, b) => a.ptr - b.ptr)
|
||||
const names = (songsMeta && songsMeta.sampleNames) || []
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
// SNam is slot-indexed (entry 0 unused); converters keep sample order
|
||||
// identical to pool order, so list[i] corresponds to names[i+1].
|
||||
const n = names[i + 1]
|
||||
list[i].name = (n != null) ? n : ''
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
function refreshSamplesCache() { samplesCache = buildSampleIndex() }
|
||||
|
||||
// ── Layout ───────────────────────────────────────────────────────────────────
|
||||
// Panel area is rows PTNVIEW_OFFSET_Y .. SCRH-1 (the hint bar lives at SCRH).
|
||||
// Columns mirror the Patterns tab: list body | scroll-bar col | VERT separator | right pane.
|
||||
const SMP_LIST_X = 1
|
||||
const SMP_LIST_BODY_W = 26 // text width of one list row
|
||||
const SMP_LIST_W = SMP_LIST_BODY_W + 1 // body + 1-col scroll indicator
|
||||
const SMP_LIST_SCROLL_X = SMP_LIST_X + SMP_LIST_BODY_W // scroll-indicator column
|
||||
const SMP_LIST_Y = PTNVIEW_OFFSET_Y
|
||||
const SMP_LIST_H = PTNVIEW_HEIGHT // full panel height
|
||||
const SMP_SEP_X = SMP_LIST_X + SMP_LIST_W // vertical separator column
|
||||
const SMP_RIGHT_X = SMP_SEP_X + 1
|
||||
const SMP_RIGHT_Y = PTNVIEW_OFFSET_Y
|
||||
const SMP_PROP_H = 10 // rows 5..14
|
||||
const SMP_USED_Y = SMP_RIGHT_Y + SMP_PROP_H // header row
|
||||
const SMP_USED_HDR_H = 1
|
||||
const SMP_USED_LIST_H = 5
|
||||
const SMP_WAVE_Y = SMP_USED_Y + SMP_USED_HDR_H + SMP_USED_LIST_H // row 21
|
||||
const SMP_BTN_Y = SCRH - 1 // bottom-most panel row, reserved for Edit button
|
||||
const SMP_WAVE_H_ROWS = SMP_BTN_Y - SMP_WAVE_Y // visual rows used by the waveform
|
||||
|
||||
const colSmpListBg = colBackPtn
|
||||
const colSmpListSel = colHighlight
|
||||
const colSmpListNumFg = colInst
|
||||
const colSmpListNameFg = colStatus
|
||||
const colSmpPropLabel = colVoiceHdr
|
||||
const colSmpPropValue = colWHITE
|
||||
const colSmpUsedHdr = colVoiceHdr
|
||||
const colSmpUsedFg = colInst
|
||||
const colSmpWaveLine = 77 // bright cyan-ish; visible on dark bg
|
||||
const colSmpWaveMid = 246 // dim grey for zero-line
|
||||
|
||||
let smpListScroll = 0
|
||||
let smpListCursor = 0
|
||||
|
||||
function clampSamplesCursor() {
|
||||
const n = samplesCache ? samplesCache.length : 0
|
||||
if (smpListCursor < 0) smpListCursor = 0
|
||||
if (smpListCursor >= n) smpListCursor = Math.max(0, n - 1)
|
||||
if (smpListCursor < smpListScroll) smpListScroll = smpListCursor
|
||||
if (smpListCursor >= smpListScroll + SMP_LIST_H)
|
||||
smpListScroll = smpListCursor - SMP_LIST_H + 1
|
||||
if (smpListScroll < 0) smpListScroll = 0
|
||||
}
|
||||
|
||||
function drawSamplesListColumn() {
|
||||
const n = samplesCache ? samplesCache.length : 0
|
||||
for (let row = 0; row < SMP_LIST_H; row++) {
|
||||
const idx = smpListScroll + row
|
||||
const y = SMP_LIST_Y + row
|
||||
con.move(y, SMP_LIST_X)
|
||||
if (idx >= n) {
|
||||
con.color_pair(colSmpListNameFg, colSmpListBg)
|
||||
print(' '.repeat(SMP_LIST_BODY_W))
|
||||
continue
|
||||
}
|
||||
const s = samplesCache[idx]
|
||||
const isSel = (idx === smpListCursor)
|
||||
const back = isSel ? colSmpListSel : colSmpListBg
|
||||
const numStr = (idx + 1).toString(16).toUpperCase().padStart(2, '0')
|
||||
const nameRaw = (s.name && s.name.length) ? s.name : '(sample ' + (idx + 1) + ')'
|
||||
const nameW = SMP_LIST_BODY_W - 6 // ' NN name ' totals 6 + N chars
|
||||
const nameStr = (nameRaw.length > nameW ? nameRaw.substring(0, nameW) : nameRaw.padEnd(nameW))
|
||||
con.color_pair(colSmpListNumFg, back); print(' ' + numStr + ' ')
|
||||
con.color_pair(colSmpListNameFg, back); print(' ')
|
||||
con.color_pair(isSel ? colWHITE : colSmpListNameFg, back); print(nameStr)
|
||||
con.color_pair(colSmpListNameFg, back); print(' ')
|
||||
}
|
||||
// scroll indicator on the rightmost column of the list area (left of the separator)
|
||||
if (n > SMP_LIST_H) {
|
||||
const maxScroll = n - SMP_LIST_H
|
||||
const indPos = (maxScroll === 0) ? 0 : ((smpListScroll * (SMP_LIST_H - 1) / maxScroll) | 0)
|
||||
for (let r = 0; r < SMP_LIST_H; r++) {
|
||||
con.move(SMP_LIST_Y + r, SMP_LIST_SCROLL_X)
|
||||
con.color_pair(colStatus, colSmpListBg)
|
||||
print(r === indPos ? sym.ticked : sym.unticked)
|
||||
}
|
||||
} else {
|
||||
for (let r = 0; r < SMP_LIST_H; r++) {
|
||||
con.move(SMP_LIST_Y + r, SMP_LIST_SCROLL_X)
|
||||
con.color_pair(colStatus, colSmpListBg); print(' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawSamplesSeparator() {
|
||||
con.color_pair(colSep, colBackPtn)
|
||||
for (let y = SMP_LIST_Y; y < SCRH; y++) {
|
||||
con.move(y, SMP_SEP_X); con.prnch(VERT)
|
||||
}
|
||||
}
|
||||
|
||||
function loopModeName(flags) {
|
||||
const lp = flags & 3
|
||||
const sus = (flags >>> 2) & 1
|
||||
const names = ['none', 'forward', 'pingpong', 'oneshot']
|
||||
return names[lp] + (sus ? ' (sustain)' : '')
|
||||
}
|
||||
|
||||
function drawSamplesProperties() {
|
||||
const rightW = SCRW - SMP_RIGHT_X + 1
|
||||
// Clear right side
|
||||
for (let r = 0; r < SMP_PROP_H + SMP_USED_HDR_H + SMP_USED_LIST_H; r++) {
|
||||
con.move(SMP_RIGHT_Y + r, SMP_RIGHT_X)
|
||||
con.color_pair(colSmpPropValue, colBackPtn)
|
||||
print(' '.repeat(rightW))
|
||||
}
|
||||
|
||||
const n = samplesCache ? samplesCache.length : 0
|
||||
if (n === 0) {
|
||||
con.move(SMP_RIGHT_Y, SMP_RIGHT_X)
|
||||
con.color_pair(colSmpPropLabel, colBackPtn)
|
||||
print('No samples in this project.')
|
||||
return
|
||||
}
|
||||
|
||||
const s = samplesCache[smpListCursor]
|
||||
if (!s) return
|
||||
|
||||
const rows = [
|
||||
['Sample #', (smpListCursor + 1).toString(16).toUpperCase().padStart(2, '0') + ' ($' + s.ptr.toString(16).toUpperCase().padStart(6, '0') + ')'],
|
||||
['Name', s.name && s.name.length ? s.name : '(unnamed)'],
|
||||
['Length', s.len + ' bytes ($' + s.len.toString(16).toUpperCase().padStart(4, '0') + ')'],
|
||||
['Rate@C4', s.c4Rate + ' Hz'],
|
||||
['Play st.', '$' + s.playStart.toString(16).toUpperCase().padStart(4, '0')],
|
||||
['Loop', loopModeName(s.sampleFlags) +
|
||||
' [$' + s.loopStart.toString(16).toUpperCase().padStart(4, '0') +
|
||||
'..$' + s.loopEnd.toString(16).toUpperCase().padStart(4, '0') + ']'],
|
||||
['Bank', ((s.ptr / TAUT_SBANK_SIZE) | 0) + '/15'],
|
||||
['Used by', s.usedBy.length + ' instrument' + (s.usedBy.length === 1 ? '' : 's')],
|
||||
]
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const y = SMP_RIGHT_Y + i
|
||||
con.move(y, SMP_RIGHT_X)
|
||||
con.color_pair(colSmpPropLabel, colBackPtn)
|
||||
print((rows[i][0] + ' ').substring(0, 10))
|
||||
con.color_pair(colSmpPropValue, colBackPtn)
|
||||
const v = rows[i][1]
|
||||
const valMax = rightW - 11
|
||||
print(v.length > valMax ? v.substring(0, valMax) : v)
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical scroll for the "Used by instruments" list (small in this viewer).
|
||||
let smpUsedScroll = 0
|
||||
|
||||
function drawSamplesUsedBy() {
|
||||
const rightW = SCRW - SMP_RIGHT_X + 1
|
||||
con.move(SMP_USED_Y, SMP_RIGHT_X)
|
||||
con.color_pair(colSmpUsedHdr, colBackPtn)
|
||||
print('Used by instruments:'.padEnd(rightW))
|
||||
|
||||
const s = (samplesCache && samplesCache[smpListCursor]) || null
|
||||
const used = s ? s.usedBy : []
|
||||
const names = (songsMeta && songsMeta.instNames) || []
|
||||
const visible = SMP_USED_LIST_H
|
||||
|
||||
if (smpUsedScroll > Math.max(0, used.length - visible))
|
||||
smpUsedScroll = Math.max(0, used.length - visible)
|
||||
if (smpUsedScroll < 0) smpUsedScroll = 0
|
||||
|
||||
for (let r = 0; r < visible; r++) {
|
||||
const y = SMP_USED_Y + 1 + r
|
||||
con.move(y, SMP_RIGHT_X)
|
||||
con.color_pair(colSmpPropValue, colBackPtn)
|
||||
const idx = smpUsedScroll + r
|
||||
if (idx >= used.length) {
|
||||
print(' '.repeat(rightW))
|
||||
continue
|
||||
}
|
||||
const slot = used[idx]
|
||||
const iname = names[slot] || '(unnamed)'
|
||||
const numStr = '$' + slot.toString(16).toUpperCase().padStart(2, '0')
|
||||
con.color_pair(colSmpUsedFg, colBackPtn)
|
||||
print(' ' + numStr + ' ')
|
||||
con.color_pair(colSmpPropValue, colBackPtn)
|
||||
const nameW = rightW - 5
|
||||
print(iname.length > nameW ? iname.substring(0, nameW) : iname.padEnd(nameW))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Waveform rendering ──────────────────────────────────────────────────────
|
||||
// Renders one sample under the right panel as a min/max envelope, using the
|
||||
// graphics layer. Samples are unsigned 8-bit; bank-switch is required because
|
||||
// only 512 K of the 8 MB pool is mapped at a time. We restore bank 0 (the
|
||||
// playback-expected default) when done.
|
||||
|
||||
// Pixel rect occupied by the waveform inside the Samples viewer. Both the
|
||||
// waveform painter and the leave-Samples cleanup need to reach for the same
|
||||
// geometry, so it lives in one helper.
|
||||
function sampleWaveformRect() {
|
||||
return {
|
||||
x: (SMP_RIGHT_X - 1) * CELL_PW,
|
||||
y: (SMP_WAVE_Y - 1) * CELL_PH,
|
||||
w: (SCRW - SMP_RIGHT_X + 1) * CELL_PW,
|
||||
h: SMP_WAVE_H_ROWS * CELL_PH,
|
||||
}
|
||||
}
|
||||
|
||||
function clearSampleWaveformArea() {
|
||||
const r = sampleWaveformRect()
|
||||
graphics.plotRect(r.x, r.y, r.w, r.h, 255) // 255 = transparent
|
||||
}
|
||||
|
||||
function drawSampleWaveform() {
|
||||
const r = sampleWaveformRect()
|
||||
const wx0 = r.x, wy0 = r.y, wW = r.w, wH = r.h
|
||||
|
||||
// Clear waveform area to transparent (255 = transparent against text bg)
|
||||
graphics.plotRect(wx0, wy0, wW, wH, 255)
|
||||
|
||||
const s = (samplesCache && samplesCache[smpListCursor]) || null
|
||||
if (!s || s.len === 0) return
|
||||
|
||||
const bankIdxFirst = (s.ptr / TAUT_SBANK_SIZE) | 0
|
||||
const bankOff = s.ptr - bankIdxFirst * TAUT_SBANK_SIZE
|
||||
const memBase = audio.getMemAddr()
|
||||
const prevBank = audio.getSampleBank() || 0
|
||||
|
||||
// Centre line
|
||||
graphics.plotRect(wx0, wy0 + (wH >>> 1), wW, 1, colSmpWaveMid)
|
||||
|
||||
// Walk the sample at one column per output pixel. For each column we read
|
||||
// a chunk and reduce to min/max; vertical extent comes from (max-min).
|
||||
// Bank switching is per-step: each output column may straddle banks.
|
||||
const samplesPerCol = Math.max(1, (s.len / wW) | 0)
|
||||
let pos = 0 // byte offset into the sample, 0..len-1
|
||||
let curBank = -1
|
||||
for (let col = 0; col < wW; col++) {
|
||||
const start = (col * s.len / wW) | 0
|
||||
const end = Math.min(s.len, (((col + 1) * s.len / wW) | 0))
|
||||
if (end <= start) continue
|
||||
|
||||
let mn = 255, mx = 0
|
||||
// Step in coarse strides for speed when samples are long.
|
||||
const step = Math.max(1, ((end - start) / 8) | 0)
|
||||
for (let p = start; p < end; p += step) {
|
||||
const abs = s.ptr + p
|
||||
const bank = (abs / TAUT_SBANK_SIZE) | 0
|
||||
if (bank !== curBank) {
|
||||
audio.setSampleBank(bank)
|
||||
curBank = bank
|
||||
}
|
||||
const off = abs - bank * TAUT_SBANK_SIZE
|
||||
const v = sys.peek(memBase - off) & 0xFF
|
||||
if (v < mn) mn = v
|
||||
if (v > mx) mx = v
|
||||
}
|
||||
// unsigned 8-bit → centred around 128
|
||||
const yTop = wy0 + ((wH * (255 - mx)) / 255) | 0
|
||||
const yBot = wy0 + ((wH * (255 - mn)) / 255) | 0
|
||||
const h = Math.max(1, yBot - yTop + 1)
|
||||
graphics.plotRect(wx0 + col, yTop, 1, h, colSmpWaveLine)
|
||||
}
|
||||
|
||||
// Restore bank 0 for playback (engine expects bank 0 as default)
|
||||
audio.setSampleBank(prevBank)
|
||||
}
|
||||
|
||||
function drawSamplesEditButton() {
|
||||
const y = SMP_BTN_Y
|
||||
con.move(y, SMP_RIGHT_X)
|
||||
con.color_pair(colSmpUsedHdr, colBackPtn)
|
||||
print('[ E ]')
|
||||
con.color_pair(colSmpPropValue, colBackPtn)
|
||||
const label = ' Edit sample'
|
||||
print(label)
|
||||
const rest = SCRW - (SMP_RIGHT_X + 5 + label.length) + 1
|
||||
if (rest > 0) print(' '.repeat(rest))
|
||||
}
|
||||
|
||||
function clearSamplesPanel() {
|
||||
// Panel area only — leave the hint row (SCRH) alone; drawControlHint owns it.
|
||||
for (let y = PTNVIEW_OFFSET_Y; y < SCRH; y++) fillLine(y, colSmpPropValue, colBackPtn)
|
||||
}
|
||||
|
||||
function drawSamplesContents(wo) {
|
||||
if (samplesCache === null) refreshSamplesCache()
|
||||
clampSamplesCursor()
|
||||
clearSamplesPanel()
|
||||
drawSamplesListColumn()
|
||||
drawSamplesSeparator()
|
||||
drawSamplesProperties()
|
||||
drawSamplesUsedBy()
|
||||
drawSampleWaveform()
|
||||
drawSamplesEditButton()
|
||||
}
|
||||
|
||||
let pendingEditorLaunch = null // { progName, args[] }
|
||||
|
||||
function requestEditorLaunch(progName, args) {
|
||||
pendingEditorLaunch = { progName, args: args || [] }
|
||||
}
|
||||
|
||||
// Stub: instrument viewer will live in taut.js once implemented; until then,
|
||||
// VIEW_INSTRMNT still routes through the external taut_instredit editor. The
|
||||
// `_G.TAUT.UI.PRELOAD_INST` hand-off is read by whichever piece grows into the
|
||||
// viewer first.
|
||||
function launchInstrumentViewerFor(instSlot) {
|
||||
_G.TAUT.UI.PRELOAD_INST = instSlot
|
||||
// Re-use the panel-switch machinery the Tab key uses.
|
||||
switchToPanel(VIEW_INSTRMNT)
|
||||
}
|
||||
|
||||
function samplesInput(wo, event) {
|
||||
if (event[0] !== 'key_down') return
|
||||
const keysym = event[1]
|
||||
const keyJustHit = (1 == event[2])
|
||||
const shiftDown = (event.includes(59) || event.includes(60))
|
||||
const moveDelta = shiftDown ? 8 : 1
|
||||
|
||||
const n = samplesCache ? samplesCache.length : 0
|
||||
if (n === 0) {
|
||||
if (keysym === 'e' || keysym === 'E') {
|
||||
requestEditorLaunch('taut_sampleedit', [fullPathObj.full, VIEW_SAMPLES, -1])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (keysym === '<UP>') { smpListCursor -= moveDelta; clampSamplesCursor(); smpUsedScroll = 0; drawSamplesContents(); return }
|
||||
if (keysym === '<DOWN>') { smpListCursor += moveDelta; clampSamplesCursor(); smpUsedScroll = 0; drawSamplesContents(); return }
|
||||
if (keysym === '<PAGE_UP>') { smpListCursor -= SMP_LIST_H; clampSamplesCursor(); smpUsedScroll = 0; drawSamplesContents(); return }
|
||||
if (keysym === '<PAGE_DOWN>') { smpListCursor += SMP_LIST_H; clampSamplesCursor(); smpUsedScroll = 0; drawSamplesContents(); return }
|
||||
if (keysym === '<HOME>') { smpListCursor = 0; clampSamplesCursor(); smpUsedScroll = 0; drawSamplesContents(); return }
|
||||
if (keysym === '<END>') { smpListCursor = n - 1; clampSamplesCursor(); smpUsedScroll = 0; drawSamplesContents(); return }
|
||||
|
||||
if (keysym === 'e' || keysym === 'E') {
|
||||
requestEditorLaunch('taut_sampleedit', [fullPathObj.full, VIEW_SAMPLES, smpListCursor])
|
||||
return
|
||||
}
|
||||
|
||||
if (keysym === '\n') {
|
||||
// Open the first instrument that uses this sample in the (stub) inst viewer
|
||||
const s = samplesCache[smpListCursor]
|
||||
if (s && s.usedBy.length > 0) {
|
||||
launchInstrumentViewerFor(s.usedBy[0])
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// END SAMPLES VIEWER
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const panelSamples = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, samplesInput, drawSamplesContents, undefined, ()=>{})
|
||||
const panelInstrmnt = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, externalPanelInput, makeExternalPanelDraw('taut_instredit'), undefined, ()=>{})
|
||||
const panelProject = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, projectInput, drawProjectContents, undefined, ()=>{})
|
||||
const panelFile = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, externalPanelInput, makeExternalPanelDraw('taut_fileop'), undefined, ()=>{})
|
||||
@@ -3706,6 +4174,7 @@ drawAll()
|
||||
|
||||
resetAudioDevice()
|
||||
taud.uploadTaudFile(fullPathObj.full, currentSongIndex, PLAYHEAD)
|
||||
refreshSamplesCache()
|
||||
audio.setMasterVolume(PLAYHEAD, 255)
|
||||
audio.setMasterPan(PLAYHEAD, 128)
|
||||
let initialTrackerMixerflags = audio.getTrackerMixerFlags(PLAYHEAD)
|
||||
@@ -3713,7 +4182,7 @@ let initialGlobalVolume = audio.getSongGlobalVolume(PLAYHEAD)
|
||||
let initialMixingVolume = audio.getSongMixingVolume(PLAYHEAD)
|
||||
|
||||
function isExternalPanel(p) {
|
||||
return p === VIEW_SAMPLES || p === VIEW_INSTRMNT || p === VIEW_FILE
|
||||
return p === VIEW_INSTRMNT || p === VIEW_FILE
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -3808,9 +4277,11 @@ function addGlobalMouseRegion(x, y, w, h, handlers) { MOUSE_GLOBAL.push(Object.a
|
||||
function switchToPanel(newPanel) {
|
||||
if (newPanel === currentPanel) return
|
||||
const wasTimeline = (currentPanel === VIEW_TIMELINE)
|
||||
const wasSamples = (currentPanel === VIEW_SAMPLES)
|
||||
currentPanel = newPanel
|
||||
applyMuteTransition(currentPanel)
|
||||
if (wasTimeline && currentPanel !== VIEW_TIMELINE) clearVoiceMeters()
|
||||
if (wasSamples && currentPanel !== VIEW_SAMPLES) clearSampleWaveformArea()
|
||||
if (isExternalPanel(currentPanel)) {
|
||||
clearPanelMouseRegions()
|
||||
con.clear(); drawAlwaysOnElems(); drawControlHint()
|
||||
@@ -3870,9 +4341,56 @@ function rebuildPanelMouseRegions() {
|
||||
if (currentPanel === VIEW_TIMELINE) registerTimelineMouse()
|
||||
else if (currentPanel === VIEW_CUES) registerOrdersMouse()
|
||||
else if (currentPanel === VIEW_PATTERN_DETAILS) registerPatternsMouse()
|
||||
else if (currentPanel === VIEW_SAMPLES) registerSamplesMouse()
|
||||
else if (currentPanel === VIEW_PROJECT) registerProjectMouse()
|
||||
}
|
||||
|
||||
function registerSamplesMouse() {
|
||||
// Left list (incl. scroll-indicator column, but excluding the separator).
|
||||
addPanelMouseRegion(SMP_LIST_X, SMP_LIST_Y, SMP_SEP_X - SMP_LIST_X, SMP_LIST_H, {
|
||||
onClick: (cy, cx, btn) => {
|
||||
if (btn !== 1) return
|
||||
const n = samplesCache ? samplesCache.length : 0
|
||||
const target = smpListScroll + (cy - SMP_LIST_Y)
|
||||
if (target < 0 || target >= n) return
|
||||
smpListCursor = target
|
||||
smpUsedScroll = 0
|
||||
clampSamplesCursor()
|
||||
drawSamplesContents()
|
||||
},
|
||||
onWheel: (cy, cx, dy) => {
|
||||
smpListCursor += dy * 3
|
||||
clampSamplesCursor()
|
||||
smpUsedScroll = 0
|
||||
drawSamplesContents()
|
||||
}
|
||||
})
|
||||
// Right "Used by" list: click launches inst viewer for that slot
|
||||
addPanelMouseRegion(SMP_RIGHT_X, SMP_USED_Y + 1, SCRW - SMP_RIGHT_X + 1, SMP_USED_LIST_H, {
|
||||
onClick: (cy, cx, btn) => {
|
||||
if (btn !== 1) return
|
||||
const s = samplesCache && samplesCache[smpListCursor]
|
||||
if (!s) return
|
||||
const idx = smpUsedScroll + (cy - (SMP_USED_Y + 1))
|
||||
if (idx < 0 || idx >= s.usedBy.length) return
|
||||
launchInstrumentViewerFor(s.usedBy[idx])
|
||||
},
|
||||
onWheel: (cy, cx, dy) => {
|
||||
const s = samplesCache && samplesCache[smpListCursor]
|
||||
if (!s) return
|
||||
smpUsedScroll += dy
|
||||
drawSamplesUsedBy()
|
||||
}
|
||||
})
|
||||
// Bottom-row Edit button
|
||||
addPanelMouseRegion(SMP_RIGHT_X, SMP_BTN_Y, 18, 1, {
|
||||
onClick: (cy, cx, btn) => {
|
||||
if (btn !== 1) return
|
||||
requestEditorLaunch('taut_sampleedit', [fullPathObj.full, VIEW_SAMPLES, smpListCursor])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function registerTimelineMouse() {
|
||||
addPanelMouseRegion(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, {
|
||||
onClick: (cy, cx, btn) => {
|
||||
@@ -4097,11 +4615,13 @@ while (!exitFlag) {
|
||||
|
||||
if (keyJustHit && keysym === "<TAB>") {
|
||||
const wasTimeline = (currentPanel === VIEW_TIMELINE)
|
||||
const wasSamples = (currentPanel === VIEW_SAMPLES)
|
||||
currentPanel = (currentPanel + (shiftDown ? -1 : 1))
|
||||
if (currentPanel < 0) currentPanel += panels.length
|
||||
currentPanel = currentPanel % panels.length
|
||||
applyMuteTransition(currentPanel)
|
||||
if (wasTimeline && currentPanel !== VIEW_TIMELINE) clearVoiceMeters()
|
||||
if (wasSamples && currentPanel !== VIEW_SAMPLES) clearSampleWaveformArea()
|
||||
if (isExternalPanel(currentPanel)) {
|
||||
// Redraw header now so the tab highlight is visible immediately,
|
||||
// but defer the actual sub-program launch to after withEvent returns.
|
||||
@@ -4150,6 +4670,45 @@ while (!exitFlag) {
|
||||
}
|
||||
}
|
||||
|
||||
// Launch the sample / instrument editor as a sub-program. Same deferral
|
||||
// reason as pendingExternalDraw: avoid leaking the trigger key into the
|
||||
// sub-program's own withEvent loop. The editor sets NEXTPANEL on exit
|
||||
// (typically back to its parent viewer), so the loop above will repaint.
|
||||
if (pendingEditorLaunch) {
|
||||
const { progName, args } = pendingEditorLaunch
|
||||
pendingEditorLaunch = null
|
||||
stopPlayback()
|
||||
clearSampleWaveformArea()
|
||||
clearPanelMouseRegions()
|
||||
con.clear(); drawAlwaysOnElems(); drawControlHint()
|
||||
_G.TAUT.UI.NEXTPANEL = undefined
|
||||
_G.shell.execute(`${progName} ${args.join(' ')}`)
|
||||
// After the editor returns, instruments / samples may have changed —
|
||||
// rebuild the deduped sample list before redrawing whatever panel comes next.
|
||||
refreshSamplesCache()
|
||||
if (_G.TAUT.UI.NEXTPANEL === undefined || _G.TAUT.UI.NEXTPANEL === null) {
|
||||
// Editor declined to switch panels — repaint the current panel.
|
||||
rebuildPanelMouseRegions()
|
||||
drawAll()
|
||||
} else {
|
||||
while (_G.TAUT.UI.NEXTPANEL !== undefined && _G.TAUT.UI.NEXTPANEL !== null) {
|
||||
const wasTimeline = (currentPanel === VIEW_TIMELINE)
|
||||
currentPanel = _G.TAUT.UI.NEXTPANEL
|
||||
_G.TAUT.UI.NEXTPANEL = undefined
|
||||
applyMuteTransition(currentPanel)
|
||||
if (wasTimeline && currentPanel !== VIEW_TIMELINE) clearVoiceMeters()
|
||||
if (isExternalPanel(currentPanel)) {
|
||||
clearPanelMouseRegions()
|
||||
con.clear(); drawAlwaysOnElems(); drawControlHint()
|
||||
redrawPanel()
|
||||
} else {
|
||||
rebuildPanelMouseRegions()
|
||||
drawAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (playbackMode !== PLAYMODE_NONE) updatePlayback()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
/**
|
||||
* TAUT Sample Editor
|
||||
* Sub-program launched by taut.js when the Samples tab is active.
|
||||
* Rows 1-3 are owned by the parent; this program draws rows 4+.
|
||||
* TAUT Sample Editor (stub)
|
||||
* Sub-program launched from taut.js's Samples viewer. Rows 1-3 are owned by
|
||||
* the parent; this program draws rows 4+.
|
||||
*
|
||||
* exec_args[1] = path to .taud file
|
||||
* Sets _G.TAUT.UI.NEXTPANEL before returning to request a panel switch.
|
||||
* exec_args:
|
||||
* [1] = path to .taud file
|
||||
* [2] = parent panel index (where to return)
|
||||
* [3] = sample index to preload (-1 if none)
|
||||
*
|
||||
* Sets _G.TAUT.UI.NEXTPANEL on return to request a panel switch back.
|
||||
*
|
||||
* Created by minjaesong on 2026-04-27
|
||||
* Stub editing UI added on 2026-05-26
|
||||
*/
|
||||
|
||||
const win = require("wintex")
|
||||
|
||||
const PANEL_COUNT = 7
|
||||
const MY_PANEL = 3 // VIEW_SAMPLES
|
||||
const PARENT_PANEL = (exec_args[2] !== undefined) ? (exec_args[2] | 0) : 3 // VIEW_SAMPLES
|
||||
const SAMPLE_IDX = (exec_args[3] !== undefined) ? (exec_args[3] | 0) : -1
|
||||
|
||||
const [SCRH, SCRW] = con.getmaxyx()
|
||||
const PANEL_Y = 4
|
||||
@@ -21,38 +26,122 @@ const PANEL_H = SCRH - PANEL_Y
|
||||
const colStatus = 253
|
||||
const colContent = 240
|
||||
const colHdr = 230
|
||||
const colEmph = 211
|
||||
const colDim = 246
|
||||
const colBack = 255
|
||||
const colSel = 41
|
||||
|
||||
function drawSampleEditContents(wo) {
|
||||
// Stub editor "fields": pretend toolbar. None of these write anything yet.
|
||||
const TOOLS = [
|
||||
{ key: 'L', label: 'Load .raw / .wav from disk' },
|
||||
{ key: 'S', label: 'Save current sample to disk' },
|
||||
{ key: 'D', label: 'Draw waveform freehand' },
|
||||
{ key: 'X', label: 'Crop / trim selection' },
|
||||
{ key: 'R', label: 'Resample' },
|
||||
{ key: 'V', label: 'Reverse' },
|
||||
{ key: 'N', label: 'Normalise to peak' },
|
||||
{ key: 'F', label: 'Fade in / out' },
|
||||
]
|
||||
|
||||
let toolCursor = 0
|
||||
|
||||
function drawSampleEditFrame() {
|
||||
for (let y = PANEL_Y; y < SCRH; y++) {
|
||||
con.move(y, 1)
|
||||
con.color_pair(colContent, 255)
|
||||
con.color_pair(colContent, colBack)
|
||||
print(' '.repeat(SCRW))
|
||||
}
|
||||
// Title
|
||||
con.move(PANEL_Y + 1, 3)
|
||||
con.color_pair(colHdr, 255)
|
||||
print('[ Sample Editor ]')
|
||||
con.move(PANEL_Y + 3, 3)
|
||||
con.color_pair(colStatus, 255)
|
||||
print('placeholder — not yet implemented')
|
||||
con.color_pair(colHdr, colBack); print('[ Sample Editor ] ')
|
||||
con.color_pair(colEmph, colBack); print('Sample ')
|
||||
con.color_pair(colStatus, colBack)
|
||||
if (SAMPLE_IDX >= 0) print('#' + (SAMPLE_IDX + 1).toString(16).toUpperCase().padStart(2, '0'))
|
||||
else print('(none)')
|
||||
|
||||
con.move(PANEL_Y + 2, 3)
|
||||
con.color_pair(colDim, colBack)
|
||||
print('stub editor — actions below are placeholders only.')
|
||||
}
|
||||
|
||||
function drawToolList() {
|
||||
const x = 5
|
||||
const y0 = PANEL_Y + 4
|
||||
con.move(y0, x)
|
||||
con.color_pair(colHdr, colBack); print('Editing actions')
|
||||
con.move(y0 + 1, x)
|
||||
con.color_pair(colDim, colBack); print('-'.repeat(16))
|
||||
|
||||
for (let i = 0; i < TOOLS.length; i++) {
|
||||
const y = y0 + 3 + i
|
||||
const t = TOOLS[i]
|
||||
const sel = (i === toolCursor)
|
||||
const back = sel ? colSel : colBack
|
||||
con.move(y, x)
|
||||
con.color_pair(colHdr, back); print(' ' + t.key + ' ')
|
||||
con.color_pair(colStatus, back); print(' ')
|
||||
con.color_pair(sel ? colEmph : colStatus, back)
|
||||
const w = SCRW - x - 6
|
||||
const lbl = t.label.length > w ? t.label.substring(0, w) : t.label.padEnd(w)
|
||||
print(lbl)
|
||||
}
|
||||
|
||||
// Drawing-area placeholder on the right
|
||||
const dx = 38
|
||||
const dy0 = PANEL_Y + 4
|
||||
const dw = SCRW - dx - 2
|
||||
const dh = SCRH - dy0 - 2
|
||||
con.move(dy0, dx)
|
||||
con.color_pair(colHdr, colBack); print('Waveform editor')
|
||||
con.move(dy0 + 1, dx)
|
||||
con.color_pair(colDim, colBack); print('-'.repeat(16))
|
||||
|
||||
// Empty drawing rectangle made of dots
|
||||
for (let r = 0; r < dh; r++) {
|
||||
con.move(dy0 + 3 + r, dx)
|
||||
con.color_pair(colDim, colBack)
|
||||
if (r === (dh >>> 1)) print('-'.repeat(dw)) // zero line
|
||||
else print(' '.repeat(dw))
|
||||
}
|
||||
con.move(dy0 + 3 + (dh >>> 1) + 1, dx)
|
||||
con.color_pair(colDim, colBack)
|
||||
print('(drawing surface — not yet implemented)')
|
||||
}
|
||||
|
||||
function drawHints() {
|
||||
con.move(SCRH, 1)
|
||||
con.color_pair(colStatus, 255)
|
||||
con.color_pair(colStatus, colBack)
|
||||
print(' '.repeat(SCRW - 1))
|
||||
con.move(SCRH, 1)
|
||||
con.color_pair(colHdr, 255); print('Tab ')
|
||||
con.color_pair(colStatus, 255); print('Panel')
|
||||
con.color_pair(colHdr, colBack); print('28u29u ')
|
||||
con.color_pair(colStatus, colBack); print('Tool ')
|
||||
con.color_pair(colHdr, colBack); print('Enter ')
|
||||
con.color_pair(colStatus, colBack); print('Apply ')
|
||||
con.color_pair(colHdr, colBack); print('Esc/Tab ')
|
||||
con.color_pair(colStatus, colBack); print('Back to viewer')
|
||||
}
|
||||
|
||||
function flashAction(idx) {
|
||||
const t = TOOLS[idx]
|
||||
if (!t) return
|
||||
con.move(SCRH - 2, 5)
|
||||
con.color_pair(colEmph, colBack)
|
||||
print(('Action: ' + t.label + ' (stub, no-op)').padEnd(SCRW - 8))
|
||||
}
|
||||
|
||||
function sampleEditInput(wo, event) {
|
||||
// placeholder — no interaction yet
|
||||
// wintex panel input — wired up but the loop below handles keys directly.
|
||||
}
|
||||
|
||||
const panel = new win.WindowObject(1, PANEL_Y, SCRW, PANEL_H, sampleEditInput, drawSampleEditContents, undefined, ()=>{})
|
||||
function drawAll() {
|
||||
drawSampleEditFrame()
|
||||
drawToolList()
|
||||
drawHints()
|
||||
}
|
||||
|
||||
const panel = new win.WindowObject(1, PANEL_Y, SCRW, PANEL_H, sampleEditInput, drawAll, undefined, ()=>{})
|
||||
|
||||
panel.drawContents()
|
||||
drawHints()
|
||||
|
||||
let done = false
|
||||
while (!done) {
|
||||
@@ -60,17 +149,32 @@ while (!done) {
|
||||
if (event[0] !== 'key_down') return
|
||||
const keysym = event[1]
|
||||
const keyJustHit = (1 == event[2])
|
||||
const shiftDown = (event.includes(59) || event.includes(60))
|
||||
|
||||
if (!keyJustHit) return
|
||||
|
||||
if (keysym === '<TAB>') {
|
||||
_G.TAUT.UI.NEXTPANEL = (MY_PANEL + (shiftDown ? -1 : 1) + PANEL_COUNT) % PANEL_COUNT
|
||||
if (keysym === '<ESCAPE>' || keysym === '<TAB>') {
|
||||
_G.TAUT.UI.NEXTPANEL = PARENT_PANEL
|
||||
done = true
|
||||
return
|
||||
}
|
||||
|
||||
panel.processInput(event)
|
||||
if (keysym === '<UP>') { if (toolCursor > 0) toolCursor--; drawToolList(); return }
|
||||
if (keysym === '<DOWN>') { if (toolCursor < TOOLS.length-1) toolCursor++; drawToolList(); return }
|
||||
|
||||
if (keysym === '\n') {
|
||||
flashAction(toolCursor)
|
||||
return
|
||||
}
|
||||
|
||||
// Direct key shortcuts
|
||||
for (let i = 0; i < TOOLS.length; i++) {
|
||||
if (keysym === TOOLS[i].key.toLowerCase() || keysym === TOOLS[i].key) {
|
||||
toolCursor = i
|
||||
drawToolList()
|
||||
flashAction(i)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -2409,6 +2409,8 @@ TODO:
|
||||
[x] expose song table on UI (test with `insaniq2.taud`)
|
||||
[x] 0x0000 - no-op; 0x0001 - key-off; 0x0002 - note-cut 0x0010..0x001F - INT0..INTF
|
||||
[ ] establish hooks for the interrupts
|
||||
[ ] Samples and Instruments view (viewer on taut.js; editor on separate .js)
|
||||
follow the ImpulseTracker design first, then improve from there
|
||||
|
||||
TODO - list of demo songs that MUST ship with Microtone:
|
||||
* 4THSYM (rename to Fourth Symmetriad) — excellent piece for demonstrating NNAs and filter envelopes
|
||||
|
||||
Reference in New Issue
Block a user