mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 13:38:30 +09:00
taut: sample viewer wip
This commit is contained in:
@@ -870,6 +870,22 @@ function loadTaudSongList(filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let projectName = ''
|
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.
|
// Parse Project Data section (\x1ETaudPrJ) for song names / project name.
|
||||||
// See terranmon.txt "Project Data" / "sMet" for the format.
|
// See terranmon.txt "Project Data" / "sMet" for the format.
|
||||||
@@ -900,6 +916,16 @@ function loadTaudSongList(filePath) {
|
|||||||
}
|
}
|
||||||
projectName = s
|
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
|
// 'sMet' = 0x73,0x4D,0x65,0x74
|
||||||
else if (fc0 === 0x73 && fc1 === 0x4D && fc2 === 0x65 && fc3 === 0x74) {
|
else if (fc0 === 0x73 && fc1 === 0x4D && fc2 === 0x65 && fc3 === 0x74) {
|
||||||
let q = payloadStart
|
let q = payloadStart
|
||||||
@@ -939,7 +965,7 @@ function loadTaudSongList(filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sys.free(ptr)
|
sys.free(ptr)
|
||||||
return { numSongs, projectName, songs }
|
return { numSongs, projectName, songs, instNames, sampleNames }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1493,7 +1519,17 @@ function drawControlHint() {
|
|||||||
['sep'],
|
['sep'],
|
||||||
['!','Help'],
|
['!','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]
|
let hintElemPat = [hintElemEditNoteValue, hintElemEditInstValue, hintElemEditVolEff, hintElemEditPanEff, hintElemEditFxSym, hintElemEditFxVal]
|
||||||
|
|
||||||
// erase current line
|
// erase current line
|
||||||
@@ -1921,6 +1957,7 @@ function switchSong(newIndex) {
|
|||||||
|
|
||||||
currentSongIndex = newIndex
|
currentSongIndex = newIndex
|
||||||
song = loadTaud(fullPathObj.full, newIndex)
|
song = loadTaud(fullPathObj.full, newIndex)
|
||||||
|
refreshSamplesCache()
|
||||||
|
|
||||||
const newPitchIdx = songsMeta.songs[newIndex].pitchPresetIdx
|
const newPitchIdx = songsMeta.songs[newIndex].pitchPresetIdx
|
||||||
PITCH_PRESET_IDX = (newPitchIdx != null && pitchTablePresets[newPitchIdx])
|
PITCH_PRESET_IDX = (newPitchIdx != null && pitchTablePresets[newPitchIdx])
|
||||||
@@ -3021,7 +3058,438 @@ function projectInput(wo, event) {
|
|||||||
|
|
||||||
function externalPanelInput(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 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 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, ()=>{})
|
const panelFile = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, externalPanelInput, makeExternalPanelDraw('taut_fileop'), undefined, ()=>{})
|
||||||
@@ -3706,6 +4174,7 @@ drawAll()
|
|||||||
|
|
||||||
resetAudioDevice()
|
resetAudioDevice()
|
||||||
taud.uploadTaudFile(fullPathObj.full, currentSongIndex, PLAYHEAD)
|
taud.uploadTaudFile(fullPathObj.full, currentSongIndex, PLAYHEAD)
|
||||||
|
refreshSamplesCache()
|
||||||
audio.setMasterVolume(PLAYHEAD, 255)
|
audio.setMasterVolume(PLAYHEAD, 255)
|
||||||
audio.setMasterPan(PLAYHEAD, 128)
|
audio.setMasterPan(PLAYHEAD, 128)
|
||||||
let initialTrackerMixerflags = audio.getTrackerMixerFlags(PLAYHEAD)
|
let initialTrackerMixerflags = audio.getTrackerMixerFlags(PLAYHEAD)
|
||||||
@@ -3713,7 +4182,7 @@ let initialGlobalVolume = audio.getSongGlobalVolume(PLAYHEAD)
|
|||||||
let initialMixingVolume = audio.getSongMixingVolume(PLAYHEAD)
|
let initialMixingVolume = audio.getSongMixingVolume(PLAYHEAD)
|
||||||
|
|
||||||
function isExternalPanel(p) {
|
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) {
|
function switchToPanel(newPanel) {
|
||||||
if (newPanel === currentPanel) return
|
if (newPanel === currentPanel) return
|
||||||
const wasTimeline = (currentPanel === VIEW_TIMELINE)
|
const wasTimeline = (currentPanel === VIEW_TIMELINE)
|
||||||
|
const wasSamples = (currentPanel === VIEW_SAMPLES)
|
||||||
currentPanel = newPanel
|
currentPanel = newPanel
|
||||||
applyMuteTransition(currentPanel)
|
applyMuteTransition(currentPanel)
|
||||||
if (wasTimeline && currentPanel !== VIEW_TIMELINE) clearVoiceMeters()
|
if (wasTimeline && currentPanel !== VIEW_TIMELINE) clearVoiceMeters()
|
||||||
|
if (wasSamples && currentPanel !== VIEW_SAMPLES) clearSampleWaveformArea()
|
||||||
if (isExternalPanel(currentPanel)) {
|
if (isExternalPanel(currentPanel)) {
|
||||||
clearPanelMouseRegions()
|
clearPanelMouseRegions()
|
||||||
con.clear(); drawAlwaysOnElems(); drawControlHint()
|
con.clear(); drawAlwaysOnElems(); drawControlHint()
|
||||||
@@ -3870,9 +4341,56 @@ function rebuildPanelMouseRegions() {
|
|||||||
if (currentPanel === VIEW_TIMELINE) registerTimelineMouse()
|
if (currentPanel === VIEW_TIMELINE) registerTimelineMouse()
|
||||||
else if (currentPanel === VIEW_CUES) registerOrdersMouse()
|
else if (currentPanel === VIEW_CUES) registerOrdersMouse()
|
||||||
else if (currentPanel === VIEW_PATTERN_DETAILS) registerPatternsMouse()
|
else if (currentPanel === VIEW_PATTERN_DETAILS) registerPatternsMouse()
|
||||||
|
else if (currentPanel === VIEW_SAMPLES) registerSamplesMouse()
|
||||||
else if (currentPanel === VIEW_PROJECT) registerProjectMouse()
|
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() {
|
function registerTimelineMouse() {
|
||||||
addPanelMouseRegion(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, {
|
addPanelMouseRegion(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, {
|
||||||
onClick: (cy, cx, btn) => {
|
onClick: (cy, cx, btn) => {
|
||||||
@@ -4097,11 +4615,13 @@ while (!exitFlag) {
|
|||||||
|
|
||||||
if (keyJustHit && keysym === "<TAB>") {
|
if (keyJustHit && keysym === "<TAB>") {
|
||||||
const wasTimeline = (currentPanel === VIEW_TIMELINE)
|
const wasTimeline = (currentPanel === VIEW_TIMELINE)
|
||||||
|
const wasSamples = (currentPanel === VIEW_SAMPLES)
|
||||||
currentPanel = (currentPanel + (shiftDown ? -1 : 1))
|
currentPanel = (currentPanel + (shiftDown ? -1 : 1))
|
||||||
if (currentPanel < 0) currentPanel += panels.length
|
if (currentPanel < 0) currentPanel += panels.length
|
||||||
currentPanel = currentPanel % panels.length
|
currentPanel = currentPanel % panels.length
|
||||||
applyMuteTransition(currentPanel)
|
applyMuteTransition(currentPanel)
|
||||||
if (wasTimeline && currentPanel !== VIEW_TIMELINE) clearVoiceMeters()
|
if (wasTimeline && currentPanel !== VIEW_TIMELINE) clearVoiceMeters()
|
||||||
|
if (wasSamples && currentPanel !== VIEW_SAMPLES) clearSampleWaveformArea()
|
||||||
if (isExternalPanel(currentPanel)) {
|
if (isExternalPanel(currentPanel)) {
|
||||||
// Redraw header now so the tab highlight is visible immediately,
|
// Redraw header now so the tab highlight is visible immediately,
|
||||||
// but defer the actual sub-program launch to after withEvent returns.
|
// 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()
|
if (playbackMode !== PLAYMODE_NONE) updatePlayback()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
/**
|
/**
|
||||||
* TAUT Sample Editor
|
* TAUT Sample Editor (stub)
|
||||||
* Sub-program launched by taut.js when the Samples tab is active.
|
* Sub-program launched from taut.js's Samples viewer. Rows 1-3 are owned by
|
||||||
* Rows 1-3 are owned by the parent; this program draws rows 4+.
|
* the parent; this program draws rows 4+.
|
||||||
*
|
*
|
||||||
* exec_args[1] = path to .taud file
|
* exec_args:
|
||||||
* Sets _G.TAUT.UI.NEXTPANEL before returning to request a panel switch.
|
* [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
|
* Created by minjaesong on 2026-04-27
|
||||||
|
* Stub editing UI added on 2026-05-26
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const win = require("wintex")
|
const win = require("wintex")
|
||||||
|
|
||||||
const PANEL_COUNT = 7
|
const PARENT_PANEL = (exec_args[2] !== undefined) ? (exec_args[2] | 0) : 3 // VIEW_SAMPLES
|
||||||
const MY_PANEL = 3 // VIEW_SAMPLES
|
const SAMPLE_IDX = (exec_args[3] !== undefined) ? (exec_args[3] | 0) : -1
|
||||||
|
|
||||||
const [SCRH, SCRW] = con.getmaxyx()
|
const [SCRH, SCRW] = con.getmaxyx()
|
||||||
const PANEL_Y = 4
|
const PANEL_Y = 4
|
||||||
@@ -21,38 +26,122 @@ const PANEL_H = SCRH - PANEL_Y
|
|||||||
const colStatus = 253
|
const colStatus = 253
|
||||||
const colContent = 240
|
const colContent = 240
|
||||||
const colHdr = 230
|
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++) {
|
for (let y = PANEL_Y; y < SCRH; y++) {
|
||||||
con.move(y, 1)
|
con.move(y, 1)
|
||||||
con.color_pair(colContent, 255)
|
con.color_pair(colContent, colBack)
|
||||||
print(' '.repeat(SCRW))
|
print(' '.repeat(SCRW))
|
||||||
}
|
}
|
||||||
|
// Title
|
||||||
con.move(PANEL_Y + 1, 3)
|
con.move(PANEL_Y + 1, 3)
|
||||||
con.color_pair(colHdr, 255)
|
con.color_pair(colHdr, colBack); print('[ Sample Editor ] ')
|
||||||
print('[ Sample Editor ]')
|
con.color_pair(colEmph, colBack); print('Sample ')
|
||||||
con.move(PANEL_Y + 3, 3)
|
con.color_pair(colStatus, colBack)
|
||||||
con.color_pair(colStatus, 255)
|
if (SAMPLE_IDX >= 0) print('#' + (SAMPLE_IDX + 1).toString(16).toUpperCase().padStart(2, '0'))
|
||||||
print('placeholder — not yet implemented')
|
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() {
|
function drawHints() {
|
||||||
con.move(SCRH, 1)
|
con.move(SCRH, 1)
|
||||||
con.color_pair(colStatus, 255)
|
con.color_pair(colStatus, colBack)
|
||||||
print(' '.repeat(SCRW - 1))
|
print(' '.repeat(SCRW - 1))
|
||||||
con.move(SCRH, 1)
|
con.move(SCRH, 1)
|
||||||
con.color_pair(colHdr, 255); print('Tab ')
|
con.color_pair(colHdr, colBack); print('28u29u ')
|
||||||
con.color_pair(colStatus, 255); print('Panel')
|
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) {
|
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()
|
panel.drawContents()
|
||||||
drawHints()
|
|
||||||
|
|
||||||
let done = false
|
let done = false
|
||||||
while (!done) {
|
while (!done) {
|
||||||
@@ -60,17 +149,32 @@ while (!done) {
|
|||||||
if (event[0] !== 'key_down') return
|
if (event[0] !== 'key_down') return
|
||||||
const keysym = event[1]
|
const keysym = event[1]
|
||||||
const keyJustHit = (1 == event[2])
|
const keyJustHit = (1 == event[2])
|
||||||
const shiftDown = (event.includes(59) || event.includes(60))
|
|
||||||
|
|
||||||
if (!keyJustHit) return
|
if (!keyJustHit) return
|
||||||
|
|
||||||
if (keysym === '<TAB>') {
|
if (keysym === '<ESCAPE>' || keysym === '<TAB>') {
|
||||||
_G.TAUT.UI.NEXTPANEL = (MY_PANEL + (shiftDown ? -1 : 1) + PANEL_COUNT) % PANEL_COUNT
|
_G.TAUT.UI.NEXTPANEL = PARENT_PANEL
|
||||||
done = true
|
done = true
|
||||||
return
|
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] expose song table on UI (test with `insaniq2.taud`)
|
||||||
[x] 0x0000 - no-op; 0x0001 - key-off; 0x0002 - note-cut 0x0010..0x001F - INT0..INTF
|
[x] 0x0000 - no-op; 0x0001 - key-off; 0x0002 - note-cut 0x0010..0x001F - INT0..INTF
|
||||||
[ ] establish hooks for the interrupts
|
[ ] 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:
|
TODO - list of demo songs that MUST ship with Microtone:
|
||||||
* 4THSYM (rename to Fourth Symmetriad) — excellent piece for demonstrating NNAs and filter envelopes
|
* 4THSYM (rename to Fourth Symmetriad) — excellent piece for demonstrating NNAs and filter envelopes
|
||||||
|
|||||||
Reference in New Issue
Block a user