mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 13:38:30 +09:00
testing nearest-cadence retuning
This commit is contained in:
@@ -255,11 +255,48 @@ function rebuildPitchLut() {
|
|||||||
}
|
}
|
||||||
rebuildPitchLut()
|
rebuildPitchLut()
|
||||||
|
|
||||||
|
// Tonal-tension function used by the 'cadence' retune method. Implements
|
||||||
|
// the tonal-distance term D_tonic from cadential_motion.md §3-§4 by locating
|
||||||
|
// each pitch in fifth-circle space relative to `tonic`. The abstract 3:2
|
||||||
|
// fifth (0x95A in 0x1000-per-octave units, ≈ 702 cents) is used as the
|
||||||
|
// fifth-circle generator, which is tuning-agnostic — the same landscape
|
||||||
|
// applies whether the candidate sits in 5-TET, 12-TET, 22-TET, etc.
|
||||||
|
//
|
||||||
|
// For each integer k in [-6, 6], target_k = (k * 0x95A) mod 0x1000 is the
|
||||||
|
// k-th fifth-stack position above the tonic (in pitch-class space). Tension
|
||||||
|
// = |k|*0x100 + |d - target_k|_cyclic, so well-tuned fifth-circle positions
|
||||||
|
// get low values: tonic 0, P5/P4 ≈ 0x105, M2/m7 ≈ 0x209, M6/m3 ≈ 0x30E,
|
||||||
|
// M3/m6 ≈ 0x413, M7/m2 ≈ 0x517, tritone ≈ 0x61C. Pitches that don't sit on
|
||||||
|
// any fifth-stack position degrade gracefully via the residual term.
|
||||||
|
//
|
||||||
|
// The k=0 path is gated to a narrow tonic neighbourhood (TONIC_TOL ≈ 30c).
|
||||||
|
// Otherwise a leading tone would score as "very close to tonic in pitch-
|
||||||
|
// class space" and pick up an artificially low tension via k=0, masking the
|
||||||
|
// real musical fact that it's at fifth-circle distance 5 from tonic and
|
||||||
|
// hence highly tense (cf. Krumhansl's tonal hierarchy: B is the least
|
||||||
|
// stable diatonic note in C, despite sitting a semitone below C).
|
||||||
|
function _cadTension(p, tonic) {
|
||||||
|
const FIFTH_PC = 0x95A
|
||||||
|
const TONIC_TOL = 0x40
|
||||||
|
const d = ((p - tonic) % 0x1000 + 0x1000) % 0x1000
|
||||||
|
const cyclic = (d <= 0x800) ? d : (0x1000 - d)
|
||||||
|
let bestT = (cyclic <= TONIC_TOL) ? cyclic : Infinity
|
||||||
|
for (let k = -6; k <= 6; k++) {
|
||||||
|
if (k === 0) continue
|
||||||
|
const target = ((k * FIFTH_PC) % 0x1000 + 0x1000) % 0x1000
|
||||||
|
let dist = Math.abs(d - target)
|
||||||
|
if (dist > 0x800) dist = 0x1000 - dist
|
||||||
|
const candT = Math.abs(k) * 0x100 + dist
|
||||||
|
if (candT < bestT) bestT = candT
|
||||||
|
}
|
||||||
|
return bestT
|
||||||
|
}
|
||||||
|
|
||||||
// Remap every note in every pattern of the current song to `newIdx`'s pitch
|
// Remap every note in every pattern of the current song to `newIdx`'s pitch
|
||||||
// table, then switch PITCH_PRESET_IDX. Special note values (empty/cut/keyoff)
|
// table, then switch PITCH_PRESET_IDX. Special note values (empty/cut/keyoff)
|
||||||
// are left alone.
|
// are left alone.
|
||||||
//
|
//
|
||||||
// Two mapping methods are supported:
|
// Three mapping methods are supported:
|
||||||
// 'pitch' (nearest-note) — each note's lower 12 bits snap to the closest
|
// 'pitch' (nearest-note) — each note's lower 12 bits snap to the closest
|
||||||
// entry in the new table. Pitches closer to the next octave's root
|
// entry in the new table. Pitches closer to the next octave's root
|
||||||
// (0x1000) than to any table entry wrap up by one octave (mirrors
|
// (0x1000) than to any table entry wrap up by one octave (mirrors
|
||||||
@@ -270,8 +307,18 @@ rebuildPitchLut()
|
|||||||
// between the corresponding original notes. Candidates are drawn from
|
// between the corresponding original notes. Candidates are drawn from
|
||||||
// the table across adjacent octaves so the mapping can cross octave
|
// the table across adjacent octaves so the mapping can cross octave
|
||||||
// boundaries naturally.
|
// boundaries naturally.
|
||||||
|
// 'cadence' (nearest-cadence) — per pattern, the first non-empty note's
|
||||||
|
// pitch class is taken as the tonic and the first note uses the
|
||||||
|
// nearest-pitch rule. Each subsequent note is chosen so that the
|
||||||
|
// change in tonal tension (see _cadTension) from the previously
|
||||||
|
// mapped note matches the change in the original sequence, with raw
|
||||||
|
// pitch displacement as a tiebreaker. This preserves cadential
|
||||||
|
// trajectories — V→I-style descents stay V→I-style — rather than
|
||||||
|
// absolute pitch positions or raw intervals, mirroring the framing in
|
||||||
|
// cadential_motion.md §2 (motion along -∇T) and §9 (trajectories
|
||||||
|
// carry cadentiality better than coordinates).
|
||||||
function retuneAllPatterns(newIdx, method) {
|
function retuneAllPatterns(newIdx, method) {
|
||||||
method = (method === 'delta') ? 'delta' : 'pitch'
|
method = (method === 'delta' || method === 'cadence') ? method : 'pitch'
|
||||||
const preset = pitchTablePresets[newIdx]
|
const preset = pitchTablePresets[newIdx]
|
||||||
if (!preset) return
|
if (!preset) return
|
||||||
const table = preset.table
|
const table = preset.table
|
||||||
@@ -280,6 +327,16 @@ function retuneAllPatterns(newIdx, method) {
|
|||||||
const ptn = song.patterns[p]
|
const ptn = song.patterns[p]
|
||||||
let prevOrigAbs = -1
|
let prevOrigAbs = -1
|
||||||
let prevMappedAbs = 0
|
let prevMappedAbs = 0
|
||||||
|
let tonic = 0
|
||||||
|
if (method === 'cadence') {
|
||||||
|
for (let row = 0; row < ROWS_PER_PAT; row++) {
|
||||||
|
const off = 8 * row
|
||||||
|
const note = ptn[off] | (ptn[off+1] << 8)
|
||||||
|
if (note === 0xFFFF || note === 0xFFFE || note === 0x0000) continue
|
||||||
|
tonic = note & 0xFFF
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
for (let row = 0; row < ROWS_PER_PAT; row++) {
|
for (let row = 0; row < ROWS_PER_PAT; row++) {
|
||||||
const off = 8 * row
|
const off = 8 * row
|
||||||
const note = ptn[off] | (ptn[off+1] << 8)
|
const note = ptn[off] | (ptn[off+1] << 8)
|
||||||
@@ -288,26 +345,41 @@ function retuneAllPatterns(newIdx, method) {
|
|||||||
const pitch = note & 0xFFF
|
const pitch = note & 0xFFF
|
||||||
const origAbs = (octave << 12) | pitch
|
const origAbs = (octave << 12) | pitch
|
||||||
let newAbs
|
let newAbs
|
||||||
if (method === 'delta' && prevOrigAbs >= 0) {
|
if ((method === 'delta' || method === 'cadence') && prevOrigAbs >= 0) {
|
||||||
const targetAbs = prevMappedAbs + (origAbs - prevOrigAbs)
|
const targetAbs = prevMappedAbs + (origAbs - prevOrigAbs)
|
||||||
const baseOc = (targetAbs >> 12)
|
const baseOc = (targetAbs >> 12)
|
||||||
let bestAbs = 0, bestDist = Infinity
|
let targetDeltaT = 0, tMappedPrev = 0
|
||||||
|
if (method === 'cadence') {
|
||||||
|
targetDeltaT = _cadTension(origAbs, tonic) - _cadTension(prevOrigAbs, tonic)
|
||||||
|
tMappedPrev = _cadTension(prevMappedAbs, tonic)
|
||||||
|
}
|
||||||
|
let bestAbs = 0, bestScore = Infinity
|
||||||
for (let dOc = -1; dOc <= 1; dOc++) {
|
for (let dOc = -1; dOc <= 1; dOc++) {
|
||||||
const oc = baseOc + dOc
|
const oc = baseOc + dOc
|
||||||
if (oc < 0 || oc > 0xF) continue
|
if (oc < 0 || oc > 0xF) continue
|
||||||
const ocAbs = oc << 12
|
const ocAbs = oc << 12
|
||||||
for (let i = 0; i < table.length; i++) {
|
for (let i = 0; i < table.length; i++) {
|
||||||
const cand = ocAbs + table[i]
|
const cand = ocAbs + table[i]
|
||||||
const d = Math.abs(cand - targetAbs)
|
const pitchErr = Math.abs(cand - targetAbs)
|
||||||
if (d < bestDist) { bestDist = d; bestAbs = cand }
|
let score = pitchErr
|
||||||
|
if (method === 'cadence') {
|
||||||
|
const candDeltaT = _cadTension(cand, tonic) - tMappedPrev
|
||||||
|
score = Math.abs(candDeltaT - targetDeltaT) * 2 + pitchErr
|
||||||
|
}
|
||||||
|
if (score < bestScore) { bestScore = score; bestAbs = cand }
|
||||||
}
|
}
|
||||||
// Also consider the next octave's root (0x1000 above
|
// Also consider the next octave's root (0x1000 above
|
||||||
// this octave's base) so an interval that lands just
|
// this octave's base) so an interval that lands just
|
||||||
// past the top entry can snap up to the octave.
|
// past the top entry can snap up to the octave.
|
||||||
if (oc < 0xF) {
|
if (oc < 0xF) {
|
||||||
const cand = ocAbs + 0x1000
|
const cand = ocAbs + 0x1000
|
||||||
const d = Math.abs(cand - targetAbs)
|
const pitchErr = Math.abs(cand - targetAbs)
|
||||||
if (d < bestDist) { bestDist = d; bestAbs = cand }
|
let score = pitchErr
|
||||||
|
if (method === 'cadence') {
|
||||||
|
const candDeltaT = _cadTension(cand, tonic) - tMappedPrev
|
||||||
|
score = Math.abs(candDeltaT - targetDeltaT) * 2 + pitchErr
|
||||||
|
}
|
||||||
|
if (score < bestScore) { bestScore = score; bestAbs = cand }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newAbs = bestAbs
|
newAbs = bestAbs
|
||||||
@@ -3196,7 +3268,8 @@ function openRetunePopup() {
|
|||||||
const entries = Object.values(pitchTablePresets).sort((a, b) => a.index - b.index)
|
const entries = Object.values(pitchTablePresets).sort((a, b) => a.index - b.index)
|
||||||
const n = entries.length
|
const n = entries.length
|
||||||
|
|
||||||
const methodLabels = { pitch: 'Nearest-note', delta: 'Nearest-delta' }
|
const methodLabels = { pitch: 'Nearest-note', delta: 'Nearest-delta', cadence: 'Nearest-cadence' }
|
||||||
|
const methodCycle = ['pitch', 'delta', 'cadence']
|
||||||
let method = 'pitch'
|
let method = 'pitch'
|
||||||
|
|
||||||
const pw = 44
|
const pw = 44
|
||||||
@@ -3299,7 +3372,7 @@ function openRetunePopup() {
|
|||||||
if (ks === 'Q') { done = true }
|
if (ks === 'Q') { done = true }
|
||||||
else if (ks === '\n') { confirmed = true; done = true }
|
else if (ks === '\n') { confirmed = true; done = true }
|
||||||
else if (ks === 'M' || ks === 'm') {
|
else if (ks === 'M' || ks === 'm') {
|
||||||
method = (method === 'pitch') ? 'delta' : 'pitch'
|
method = methodCycle[(methodCycle.indexOf(method) + 1) % methodCycle.length]
|
||||||
repaint()
|
repaint()
|
||||||
}
|
}
|
||||||
else if (ks === '<UP>') {
|
else if (ks === '<UP>') {
|
||||||
|
|||||||
Reference in New Issue
Block a user