mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
linear freq pitch mode
This commit is contained in:
@@ -151,12 +151,13 @@ D's 16-bit argument encodes four mutually exclusive modes using the top nibble a
|
||||
|
||||
## E $xxxx — Pitch slide down by $xxxx
|
||||
|
||||
**Plain.** Lowers the channel's pitch by the argument per tick. The coarse argument has **two distinct interpretations** chosen by the song-table `f` flag (effect `1`, bit 1):
|
||||
**Plain.** Lowers the channel's pitch by the argument per tick. The coarse argument has **three distinct interpretations** chosen by the song-table `ff` field (effect `1`, bits 1-2):
|
||||
|
||||
- **Linear mode** (`f` unset, default): the argument is a value in the 4096-TET pitch grid, subtracted directly from the stored pitch. `E $0155` ≈ one semitone per tick.
|
||||
- **Amiga (cycle-based) mode** (`f` set): the argument is a **raw ProTracker/ST3 period unit count** — the same byte the original tracker stored on disk, *unscaled*. The engine converts the channel's stored 4096-TET pitch back to an Amiga period, subtracts the argument from that period directly, then converts the result back to 4096-TET. `E $0001` therefore corresponds to PT `201` and produces the characteristic non-linear pitch drift of ProTracker-style slides (lower pitches drift more slowly in semitone terms than higher pitches).
|
||||
- **Linear mode** (`ff = 0`, default): the argument is a value in the 4096-TET pitch grid, subtracted directly from the stored pitch. `E $0155` ≈ one semitone per tick.
|
||||
- **Amiga (cycle-based) mode** (`ff = 1`): the argument is a **raw ProTracker/ST3 period unit count** — the same byte the original tracker stored on disk, *unscaled*. The engine converts the channel's stored 4096-TET pitch back to an Amiga period, subtracts the argument from that period directly, then converts the result back to 4096-TET. `E $0001` therefore corresponds to PT `201` and produces the characteristic non-linear pitch drift of ProTracker-style slides (lower pitches drift more slowly in semitone terms than higher pitches).
|
||||
- **Linear-frequency mode** (`ff = 2`): the argument is **Hz/tick** at A4 = 440 Hz / C4 ≈ 261.6256 Hz reference. The engine converts the stored pitch to audible frequency, subtracts the argument from that frequency, then converts the result back to 4096-TET. `E $0010` is the verbatim Monotone `210` (drop 16 Hz/tick); the slide produces a constant *frequency* delta per tick, so the perceived semitone drop is larger at low pitches and smaller at high pitches — exactly Monotone's tracker semantics.
|
||||
|
||||
Because Amiga period units fit in a single byte (PT/ST3 max value $FF), the coarse range in Amiga mode never approaches the $F000 fine-slide marker, so the same argument-format selector still distinguishes coarse from fine. **Fine slides (`E $Fxxx`) follow the same dual-interpretation rule as coarse**: in linear mode the low 12 bits are 4096-TET units; in Amiga mode they are raw tracker period units (the PT `E2x` / ST3 `EFx` or `EEx` digit), applied once per row at tick 0 in period space. A coarse slide uses the full value range; a fine slide applies only once per row.
|
||||
Because Amiga period units (and Monotone Hz/tick) fit in a single byte (PT/ST3 max value $FF, MONOTONE max $3F), the coarse range never approaches the $F000 fine-slide marker, so the same argument-format selector still distinguishes coarse from fine across all three modes. **Fine slides (`E $Fxxx`) follow the same mode-selection rule as coarse**: linear mode reads the low 12 bits as 4096-TET units, Amiga mode reads them as raw tracker period units, and linear-frequency mode reads them as Hz. A coarse slide uses the full value range; a fine slide applies only once per row.
|
||||
|
||||
Coarse and fine modes are distinguished by the high nibble of the argument:
|
||||
|
||||
@@ -166,16 +167,19 @@ Coarse and fine modes are distinguished by the high nibble of the argument:
|
||||
|
||||
**Compatibility.** ST3 pitch slides operate on Amiga periods or linear slide units; Taud's storage depends on the song-table mode flag:
|
||||
|
||||
- **Linear-source ST3 song** (`linear_slides` set in S3M flags → Taud `f` bit clear):
|
||||
- **Linear-source ST3 song** (`linear_slides` set in S3M flags → Taud `ff = 0`):
|
||||
- ST3 `Exx` coarse (where `xx < $E0`) → Taud `E round($00xx × 64/3)` (1 ST3 coarse unit = 1/16 semitone = 64/3 ≈ 21.33 Taud units, rounded).
|
||||
- ST3 `EFx` fine → Taud `E $F0 round(x × 16/3)` (1 ST3 fine unit = 1/64 semitone = 16/3 ≈ 5.33 Taud units, applied once per row).
|
||||
- ST3 `EEx` extra-fine → Taud `E $F0 round(x × 16/3)` (same unit as fine, applied once per row).
|
||||
|
||||
- **Amiga-source ST3/PT song** (`linear_slides` clear → Taud `f` bit set):
|
||||
- **Amiga-source ST3/PT song** (`linear_slides` clear → Taud `ff = 1`):
|
||||
- ST3 `Exx` coarse / PT `2xx` → Taud `E $00xx` **verbatim**, with no `× 64/3` scaling. The engine reads the stored byte as Amiga period units and applies it in period space, recovering the original tracker's exact period-step count.
|
||||
- ST3 `EFx` fine / `EEx` extra-fine / PT `E2x` → Taud `E $F00x` **verbatim** (raw period-unit nibble in the low 4 bits), with no `× 16/3` scaling. The engine performs the once-per-row fine slide in Amiga period space, mirroring the coarse arithmetic.
|
||||
|
||||
The Amiga-mode flag therefore controls **two** decoder behaviours simultaneously: (a) which numeric scale the converter should have used when emitting coarse arguments, and (b) which arithmetic the engine performs on those arguments per tick. Converters MUST set bit 1 (`f`) of the song-table flags byte whenever they emit raw period-unit coarse arguments, and MUST NOT mix the two scales within one Taud song.
|
||||
- **MONOTONE source** (Taud `ff = 2`):
|
||||
- MONOTONE `2xx` → Taud `E $00xx` **verbatim** (Hz/tick). The engine converts the stored pitch to frequency, subtracts the argument, and converts back. MONOTONE has no fine-slide form; converters never emit `E $Fxxx` for ff=2 sources.
|
||||
|
||||
The mode flag therefore controls **two** decoder behaviours simultaneously: (a) which numeric scale the converter should have used when emitting coarse arguments, and (b) which arithmetic the engine performs on those arguments per tick. Converters MUST set bits 1-2 (`ff`) of the song-table flags byte to match the units they emit, and MUST NOT mix scales within one Taud song.
|
||||
|
||||
Because E and F share memory in Taud (narrower than ST3's broad shared memory), an ST3 song that used `E00` or `F00` to recall a D, G, or Q argument will break on import; the converter must eagerly resolve ST3 recalls into explicit Taud arguments rather than relying on memory.
|
||||
|
||||
@@ -188,10 +192,11 @@ on row start:
|
||||
else: memory_EF = raw
|
||||
if (raw & $F000) == $F000: # fine, applied once on tick 0
|
||||
mag = raw & $0FFF
|
||||
if amiga_mode:
|
||||
# mag is a raw tracker period-unit count; subtract pitch ⇒ add period.
|
||||
if tone_mode == 1: # Amiga: mag is raw period units; pitch down ⇒ +period
|
||||
pitch = amiga_slide_down(pitch, mag)
|
||||
else:
|
||||
elif tone_mode == 2: # linear-freq: mag is Hz/tick; pitch down ⇒ −freq
|
||||
pitch = linear_freq_slide(pitch, −mag)
|
||||
else: # linear: mag is 4096-TET units
|
||||
pitch -= mag
|
||||
mode_this_row = FINE
|
||||
else: # coarse
|
||||
@@ -200,12 +205,18 @@ on row start:
|
||||
|
||||
on tick > 0:
|
||||
if mode_this_row == COARSE:
|
||||
if amiga_mode:
|
||||
if tone_mode == 1:
|
||||
# slide_amount_this_row is a raw tracker period-unit count (no × 64/3 scaling).
|
||||
# period = AMIGA_BASE_PERIOD × 2^(−(pitch − C4) / 4096)
|
||||
# period_new = period + slide_amount_this_row # E subtracts pitch ⇒ adds period
|
||||
# pitch = C4 + 4096 × log2(AMIGA_BASE_PERIOD / period_new)
|
||||
pitch = amiga_slide_down(pitch, slide_amount_this_row)
|
||||
elif tone_mode == 2:
|
||||
# slide_amount_this_row is Hz/tick (verbatim from MONOTONE 2xx).
|
||||
# freq = LINEAR_FREQ_C4_HZ × 2^((pitch − C4) / 4096)
|
||||
# freq_new = max(freq − slide_amount_this_row, 1)
|
||||
# pitch = C4 + 4096 × log2(freq_new / LINEAR_FREQ_C4_HZ)
|
||||
pitch = linear_freq_slide(pitch, −slide_amount_this_row)
|
||||
else:
|
||||
pitch -= slide_amount_this_row
|
||||
```
|
||||
@@ -216,9 +227,9 @@ Glissando control (S $1x) snaps the output pitch to the nearest semitone after e
|
||||
|
||||
## F $xxxx — Pitch slide up by $xxxx
|
||||
|
||||
**Plain.** Raises the channel's pitch by the argument per tick, with the same mode-selection scheme as E. Coarse, fine, memory behaviour, and Amiga-mode handling are identical in form but inverted in direction. The same dual-interpretation rule applies to **both** coarse and fine arguments: 4096-TET units in linear mode, raw tracker period units in Amiga mode.
|
||||
**Plain.** Raises the channel's pitch by the argument per tick, with the same mode-selection scheme as E. Coarse, fine, memory behaviour, and Amiga / linear-freq mode handling are identical in form but inverted in direction. The same triple-interpretation rule applies to **both** coarse and fine arguments: 4096-TET units in linear mode, raw tracker period units in Amiga mode, Hz/tick in linear-frequency mode.
|
||||
|
||||
**Compatibility.** Same as E. In linear-source songs, ST3 `Fxx` coarse converts using `round(x × 64/3)` and `FFx`/`FEx` fine/extra-fine use `round(x × 16/3)`. In Amiga-source songs (PT or S3M with `linear_slides` clear), both forms are stored verbatim: `Fxx` coarse → `F $00xx`, and `FFx`/`FEx` fine/extra-fine / PT `E1x` → `F $F00x`. F and E share one memory slot in Taud. Amiga-mode behaviour is controlled by the same `f` flag as E; under that flag, both coarse (per-tick) and fine (tick-0 only) F slides are applied in period space.
|
||||
**Compatibility.** Same as E. In linear-source songs, ST3 `Fxx` coarse converts using `round(x × 64/3)` and `FFx`/`FEx` fine/extra-fine use `round(x × 16/3)`. In Amiga-source songs (PT or S3M with `linear_slides` clear), both forms are stored verbatim: `Fxx` coarse → `F $00xx`, and `FFx`/`FEx` fine/extra-fine / PT `E1x` → `F $F00x`. In MONOTONE-source songs (ff=2), `1xx` → `F $00xx` verbatim (Hz/tick); MONOTONE has no fine-slide form. F and E share one memory slot in Taud. Slide-mode behaviour is controlled by the same `ff` field as E; under any non-linear mode, both coarse (per-tick) and fine (tick-0 only) F slides are applied in the corresponding mode's space.
|
||||
|
||||
**Implementation.** As for E, but add instead of subtract. No upper pitch cap is defined by the effect itself, but the sample-rate conversion at the mixer will saturate well before arithmetic overflow at reasonable playing ranges.
|
||||
|
||||
@@ -226,26 +237,39 @@ Glissando control (S $1x) snaps the output pitch to the nearest semitone after e
|
||||
|
||||
## G $xxxx — Tone portamento with speed $xxxx
|
||||
|
||||
**Plain.** Slides the channel's current pitch toward the note specified in the same row, at $xxxx Taud units per tick (after tick 0), stopping when the target is reached. A row with G and a note does **not** re-trigger the sample — the note's pitch becomes the portamento target and the already-sounding sample continues at its current pitch.
|
||||
**Plain.** Slides the channel's current pitch toward the note specified in the same row, at $xxxx units per tick (after tick 0), stopping when the target is reached. A row with G and a note does **not** re-trigger the sample — the note's pitch becomes the portamento target and the already-sounding sample continues at its current pitch.
|
||||
|
||||
**Compatibility.** ST3 `Gxx` uses an 8-bit value in period-table units; convert to Taud using the same `round(× 64/3)` scale as E/F coarse (1/16 semitone per ST3 slide unit). ST3 linear mode is the expected import source; Amiga-mode G sources should be treated as linear. G has its **own** memory slot in both ST3 and Taud, so conversion is straightforward and does not suffer the shared-memory problem of E/F.
|
||||
The unit of `$xxxx` depends on the song-table tone mode (effect `1`, bits 1-2):
|
||||
|
||||
- `ff = 0` (linear) and `ff = 1` (Amiga): 4096-TET pitch units per tick. Amiga sources should be converted to linear units on G, since the original PT G slide already operated semi-linearly within a small range and the shared-memory pitfall of E/F does not apply here.
|
||||
- `ff = 2` (linear-frequency): Hz/tick. The engine walks the channel's *frequency* toward the target note's frequency by `±$xxxx` Hz each non-first tick. This is MONOTONE's `3xx` behaviour verbatim (MTSRC/MT_PLAY.PAS:620-630).
|
||||
|
||||
**Compatibility.** ST3 `Gxx` uses an 8-bit value in period-table units; convert to Taud using the same `round(× 64/3)` scale as E/F coarse (1/16 semitone per ST3 slide unit). ST3 linear mode is the expected import source; Amiga-mode G sources should be treated as linear. MONOTONE `3xx` → Taud `G $00xx` verbatim under ff=2. G has its **own** memory slot in both ST3 and Taud, so conversion is straightforward and does not suffer the shared-memory problem of E/F.
|
||||
|
||||
**Implementation.**
|
||||
|
||||
```
|
||||
on row parse:
|
||||
if row has note and G effect:
|
||||
target_pitch = period_for(note)
|
||||
target_pitch = pitch_for(note)
|
||||
# do NOT re-trigger sample
|
||||
if arg != 0:
|
||||
memory_G = arg
|
||||
speed_this_row = memory_G
|
||||
|
||||
on tick > 0:
|
||||
on tick > 0 (linear / Amiga modes):
|
||||
if target_pitch set:
|
||||
delta = sign(target_pitch - pitch) × speed_this_row
|
||||
pitch += delta
|
||||
if sign crossed target: pitch = target_pitch; target_pitch = None
|
||||
|
||||
on tick > 0 (linear-frequency mode):
|
||||
if target_pitch set:
|
||||
target_freq = LINEAR_FREQ_C4_HZ × 2^((target_pitch − C4) / 4096)
|
||||
cur_freq = cached freq (or recomputed from pitch on first use)
|
||||
cur_freq += sign(target_freq − cur_freq) × speed_this_row
|
||||
if sign crossed target_freq: cur_freq = target_freq; target_pitch = None
|
||||
pitch = C4 + 4096 × log2(cur_freq / LINEAR_FREQ_C4_HZ)
|
||||
```
|
||||
|
||||
Glissando (S $1x) snaps the output frequency to the nearest semitone ($0155 step approximation) after each advance without changing the internal pitch counter; it affects only what the mixer sees.
|
||||
@@ -956,14 +980,14 @@ Effects in this section modifies the behaviour of the mixer. Primary intention o
|
||||
|
||||
**Plain.** Sets mixer-wide behaviour flags. Available flags are:
|
||||
|
||||
0b 0000 0Ffp
|
||||
0b 0000 0ffp
|
||||
|
||||
- p unset: Linear panning mode (tracker-accurate). Centre panning gets 3 dB boost. Default setting.
|
||||
- p set: Equal-power panning mode. L/R amplitude is at 0.707 when centre-panned.
|
||||
|
||||
- Ff = 0: Linear tone mode. Pitch shift will behave like MIDI/ImpulseTracker/ScreamTracker linear mode. **Coarse and fine E/F arguments are stored as 4096-TET pitch units** and subtracted/added directly from the stored pitch.
|
||||
- Ff = 1: Amiga (cycle-based) tone mode. Pitch shift will behave like ProTracker/ScreamTracker default mode. **Coarse and fine E/F arguments are stored as raw tracker period units** (the unscaled byte/nibble from the source PT/S3M/IT file) and applied in Amiga period space. Tone portamento (G) remains linear regardless of mode.
|
||||
- Ff = 2: Linear frequency mode. Pitch shift will behave against frequency number.
|
||||
- ff = 0: Linear tone mode. Pitch shift will behave like MIDI/ImpulseTracker/ScreamTracker linear mode. **Coarse and fine E/F arguments are stored as 4096-TET pitch units** and subtracted/added directly from the stored pitch.
|
||||
- ff = 1: Amiga (cycle-based) tone mode. Pitch shift will behave like ProTracker/ScreamTracker default mode. **Coarse and fine E/F arguments are stored as raw tracker period units** (the unscaled byte/nibble from the source PT/S3M/IT file) and applied in Amiga period space. Tone portamento (G) remains linear regardless of mode.
|
||||
- ff = 2: Linear-frequency tone mode (MONOTONE compat). **E, F, and G arguments are stored as Hz/tick** (a signed change in audible frequency per song tick), and the engine converts the channel's stored 4096-TET pitch back to a frequency, adds/subtracts the argument, then converts back to 4096-TET. Reference is fixed at 12-TET A4 = 440 Hz / C4 ≈ 261.6256 Hz, which matches MONOTONE's MT_PLAY.PAS `notesHz` table (A0 = 27.5 Hz, equal-temperament). Unlike Amiga mode, *all three* slide effects use the new arithmetic — Monotone's `1xx`, `2xx`, and `3xx` are all in Hz/tick (see MTSRC/MT_PLAY.PAS:606-630).
|
||||
|
||||
(Bit 2 is reserved. It previously held an `m` "fadeout-zero policy" flag intended to swap between IT and FT2 semantics for `storedFadeout = 0`. That flag was removed once both trackers were verified to share identical "stored 0 ⇒ no fade" semantics — see schismtracker `player/sndmix.c:330-342` and ft2-clone `src/ft2_replayer.c:1467-1481`. Fadeout scaling now lives in the converters; see "Volume Fadeout" below.)
|
||||
|
||||
@@ -1021,6 +1045,13 @@ There is no separate "use fadeout" flag — both extremes share the same field,
|
||||
- period = AMIGA_BASE_PERIOD × 2^(−(noteVal − C4) / 4096)
|
||||
- period_new = period − slideArg (E subtracts pitch ⇒ adds period; F adds pitch ⇒ subtracts period)
|
||||
- noteVal_new = C4 + 4096 × log2(AMIGA_BASE_PERIOD / period_new)
|
||||
- Linear-frequency tone (E / F / G in Hz/tick). The `slideArg` is a **signed Hz delta per tick** at the audible reference 12-TET A4 = 440 Hz / C4 ≈ 261.6256 Hz, identical to the value MONOTONE stores in its 1xx/2xx/3xx commands. Sign convention matches linear/Amiga modes (negative for E, positive for F):
|
||||
- LINEAR_FREQ_C4_HZ = 261.625565... (12-TET, so A4 = 440 Hz exactly)
|
||||
- freq = LINEAR_FREQ_C4_HZ × 2^((noteVal − C4) / 4096)
|
||||
- freq_new = max(freq + slideArg, 1.0)
|
||||
- noteVal_new = C4 + 4096 × log2(freq_new / LINEAR_FREQ_C4_HZ)
|
||||
- For tone portamento (G), `tonePortaSpeed` is also in Hz/tick: each tick walks `freq` toward `noteValToFreq(target)` by `±tonePortaSpeed` until the target frequency is reached.
|
||||
- Like Amiga mode, the per-voice intermediate frequency is cached across ticks (no round-trip rounding) and reseeded on note trigger, S$2x finetune, fine slides, and the start of a fresh multi-tick coarse slide.
|
||||
|
||||
**Initialisation from the song table.** The same flags byte is stored in the song-table entry (see file format §Song Table). A Taud player should write this byte to MMIO playhead register 7 before starting playback; the mixer then applies it as the initial state on every reset, and subsequent in-pattern `1` effects may override it.
|
||||
|
||||
|
||||
@@ -1704,7 +1704,7 @@ function simulateRowState(ptnDat, uptoRow) {
|
||||
let bpm = audio.getBPM(PLAYHEAD) // best-effort starting tempo
|
||||
let speed = audio.getTickRate(PLAYHEAD)
|
||||
let globalVol = 0xFF
|
||||
let panLaw = 0, amigaMode = false
|
||||
let panLaw = 0, toneMode = 0 // toneMode: 0=linear, 1=Amiga, 2=linear-freq, 3=reserved
|
||||
|
||||
let memEF = 0, memG = 0
|
||||
let memHU = { speed: 0, depth: 0 }
|
||||
@@ -1808,8 +1808,8 @@ function simulateRowState(ptnDat, uptoRow) {
|
||||
if (effop !== 0 || effarg !== 0) {
|
||||
if (effop === OP_1) {
|
||||
const flags = (effarg >>> 8) & 0xFF
|
||||
panLaw = flags & 1
|
||||
amigaMode = (flags & 2) !== 0
|
||||
panLaw = flags & 1
|
||||
toneMode = (flags >>> 1) & 3
|
||||
// bit 2 reserved (was 'm' fadeout-zero policy; removed)
|
||||
}
|
||||
else if (effop === OP_8) {
|
||||
@@ -1948,7 +1948,7 @@ function simulateRowState(ptnDat, uptoRow) {
|
||||
|
||||
return { lastNote, lastInst, volAbs, panAbs, pitchOff,
|
||||
bpm, speed, globalVol,
|
||||
panLaw, amigaMode,
|
||||
panLaw, toneMode,
|
||||
bitcrushDepth, bitcrushSkip, overdriveAmp, clipMode,
|
||||
glissandoOn, vibratoWave, tremoloWave, panbrelloWave,
|
||||
memEF, memG, memHU, memR, memY,
|
||||
@@ -2153,15 +2153,14 @@ function drawProjectContents(wo) {
|
||||
let flagstr = [
|
||||
['Linear pan','EquNrg pan'],
|
||||
['Linear pitch','Amiga pitch', 'Linear freq', ''], // TODO MONOTONE uses linear-freq pitch
|
||||
['IT fade','FT2 fade'],
|
||||
]
|
||||
for (let i = 0; i < flagstr.length; i++) {
|
||||
if (i != 1 && 1 != 3) {
|
||||
if (i != 1 && 1 != 2) {
|
||||
let s = flagstr[i][(mixerflag >>> i) & 1 != 0]
|
||||
flagStrSelected.push(s)
|
||||
}
|
||||
}
|
||||
let toneMode = (((mixerflag >>> 1) & 1)) | (((mixerflag >>> 3) & 1) << 1)
|
||||
let toneMode = (mixerflag >>> 1) & 3
|
||||
flagStrSelected.splice(1, 0, flagstr[1][toneMode])
|
||||
|
||||
|
||||
|
||||
@@ -11,15 +11,36 @@ Tags:
|
||||
<l> - align left
|
||||
<o> - create virtual typesetting box. Left anchor: where the text cursor is. Right anchor: end of the line
|
||||
µtone; - replace with the brand string (<col 211>Micro</col><col 239>tone</col>)
|
||||
|
||||
&bul; - replace with bullet (\u00F9)
|
||||
&ddot; - replace with double-dot (\u008419u)
|
||||
&mdot; - replace with BIGDOT (\u00FA)
|
||||
&updn; - up-down arrow (\u008418u)
|
||||
&udlr; - four direction arrow (\u008428u\u008429u)
|
||||
|
||||
&keyoffsym; - pattern view key-off symbol (\u00A0\u00CD\u00CD\u00A1)
|
||||
¬ecutsym; - pattern view note-cut symbol (\u00A4\u00A4\u00A4\u00A4)
|
||||
|
||||
&demisharp;
|
||||
♯
|
||||
&sesquisharp;
|
||||
&doublesharp;
|
||||
&triplesharp;
|
||||
&quadsharp;
|
||||
&demiflat;
|
||||
♭
|
||||
&sesquiflat;
|
||||
&doubleflat;
|
||||
&tripleflat;
|
||||
&quadflat;
|
||||
&accuptick;
|
||||
&accdntick;
|
||||
&accupup;
|
||||
&accdndn;
|
||||
|
||||
- nonbreakable space (only meaningful for typesetters)
|
||||
­ - soft hyphen (only meaningful for typesetters)
|
||||
|
||||
default alignment: fully justified
|
||||
*/
|
||||
|
||||
@@ -29,7 +50,8 @@ let helpNotation = `<c>CONTROL NOTATON</c>
|
||||
&bul;<b>a</b>&ddot;<b>z</b> : <O>alphabet without shift-in</O>
|
||||
&bul;<b>A</b>&ddot;<b>Z</b> : <O>alphabet with shift-in</O>
|
||||
&bul;<b>^q</b> : <O>hit 'q' with control key</O>
|
||||
&bul;<b>^Q</b> : <O>hit 'q' with control and shift key</O>`
|
||||
&bul;<b>^Q</b> : <O>hit 'q' with control and shift key</O>
|
||||
`
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -37,7 +59,8 @@ let helpJam = `<c>NOTE JAMMING</c>
|
||||
|
||||
Push keys to play or insert notes.
|
||||
w e t y u
|
||||
a s d f g h j k`
|
||||
a s d f g h j k
|
||||
`
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -50,7 +73,8 @@ let helpCommon = `<c>COMMON CONTROLS</c>
|
||||
&bul;<b>O</b> : <O>stop the playback</O>
|
||||
&bul;<b>tab</b> : <O>switch forward a tab</O>
|
||||
&bul;<b>TAB</b> : <O>switch backward a tab</O>
|
||||
&bul;<b>q</b> : <O>close µtone;</O>`
|
||||
&bul;<b>q</b> : <O>close µtone;</O>
|
||||
`
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -79,7 +103,12 @@ Timeline has two distinct modes: view and edit mode. Two modes are toggled using
|
||||
&bul;<b><</b>&mdot;<b>></b>: <O>(panning column) slide left/right</O>
|
||||
&bul;<b>-</b>&mdot;<b>=</b> : <O>(vol/pan col) fine slide down/up</O>
|
||||
&bul;<b>&udlr;</b> : <O>move the viewing cursor by columns and rows</O>
|
||||
&bul;<b>pg&updn;</b> : <O>go to previous/next cue</O>`
|
||||
&bul;<b>pg&updn;</b> : <O>go to previous/next cue</O>
|
||||
|
||||
<b>ACCIDENTALS</b>
|
||||
&demisharp; ♯ &doublesharp; &triplesharp; &quadsharp; &demiflat; ♭ &doubleflat; &tripleflat; &accuptick; &accupup; &accdntick; &accdndn;
|
||||
<b>C c x cx xx B b bb bbb ^ ^^ v vv</b>
|
||||
`
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -114,6 +143,22 @@ function expandEntities(s) {
|
||||
.replaceAll('­', '')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('&demisharp;', '\u0080\u0081')
|
||||
.replaceAll('♯', '\u0082\u0083')
|
||||
.replaceAll('&sesquisharp;', '\u0084132u\u0085')
|
||||
.replaceAll('&doublesharp;', '\u0086\u0087')
|
||||
.replaceAll('&triplesharp;', '\u0088\u0089')
|
||||
.replaceAll('&quadsharp;', '\u008A\u008B')
|
||||
.replaceAll('&demiflat;', '\u008C\u008D')
|
||||
.replaceAll('♭', '\u008E\u008F')
|
||||
.replaceAll('&sesquiflat;', '\u0090\u0091')
|
||||
.replaceAll('&doubleflat;', '\u0092\u0093')
|
||||
.replaceAll('&tripleflat;', '\u0094\u0095')
|
||||
.replaceAll('&quadflat;', '\u0096\u0097')
|
||||
.replaceAll('&accuptick;', '\u009A')
|
||||
.replaceAll('&accdntick;', '\u009B')
|
||||
.replaceAll('&accupup;', '\u009C')
|
||||
.replaceAll('&accdndn;', '\u009D')
|
||||
}
|
||||
|
||||
// Tokenise a (post-entity-expansion) line. Returns an array of:
|
||||
@@ -324,13 +369,13 @@ function typeset(text, customWidth) {
|
||||
}
|
||||
|
||||
let helpMessages = [ // index: taut.js PANEL_NAMES
|
||||
[helpJam, helpTimeline, helpCommon, helpNotation].join('\n\n'),
|
||||
[helpCommon, helpNotation].join('\n\n'), // placeholder
|
||||
[helpCommon, helpNotation].join('\n\n'), // placeholder
|
||||
[helpCommon, helpNotation].join('\n\n'), // placeholder
|
||||
[helpCommon, helpNotation].join('\n\n'), // placeholder
|
||||
[helpCommon, helpNotation].join('\n\n'), // placeholder
|
||||
[helpCommon, helpNotation].join('\n\n'), // placeholder
|
||||
[helpJam, helpTimeline, helpCommon, helpNotation].join('\n'),
|
||||
[helpCommon, helpNotation].join('\n'), // placeholder
|
||||
[helpCommon, helpNotation].join('\n'), // placeholder
|
||||
[helpCommon, helpNotation].join('\n'), // placeholder
|
||||
[helpCommon, helpNotation].join('\n'), // placeholder
|
||||
[helpCommon, helpNotation].join('\n'), // placeholder
|
||||
[helpCommon, helpNotation].join('\n'), // placeholder
|
||||
]
|
||||
|
||||
help.MSG_BY_TABS = helpMessages.map(it => typeset(it))
|
||||
|
||||
37
mon2taud.py
37
mon2taud.py
@@ -14,14 +14,15 @@ This converter:
|
||||
- splits each Monotone pattern (64 × N voices) into N Taud patterns
|
||||
- converts notes (A0=27.5 Hz chromatic) to Taud 4096-TET centred on C4
|
||||
- maps the 8 Monotone effects to their closest Taud equivalents
|
||||
- approximates Hz/tick slides (1xx/2xx/3xx) at an A4=440 Hz reference
|
||||
- emits Hz/tick slides (1xx/2xx/3xx) verbatim and turns on Taud's
|
||||
linear-frequency tone mode (Effect 1 ff=2) so the engine interprets
|
||||
E/F/G arguments as Hz at A4=440 Hz reference — no scaling drift
|
||||
|
||||
Limits: numVoices ≤ 20, numPatterns × numVoices ≤ 4095.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import gzip
|
||||
import math
|
||||
import struct
|
||||
import sys
|
||||
|
||||
@@ -51,11 +52,13 @@ MON_EFFECT_LETTERS = ['0', '1', '2', '3', '4', 'B', 'D', 'F']
|
||||
# Note value 1 = A0; C4 sits at value 40 (A0 + 39 semitones).
|
||||
MON_NOTE_C4 = 40
|
||||
|
||||
# Slides are linear-in-Hz on Monotone but linear-in-4096-TET on Taud. Take A4
|
||||
# (440 Hz) as the reference: 1 Hz at A4 ≈ 12/(440·ln 2) semitones, scaled by
|
||||
# 4096/12 to Taud units. ≈ 161.0. Off by ±1 octave at the extremes; documented
|
||||
# in the script header.
|
||||
SLIDE_UNITS_PER_HZ = 12.0 / (440.0 * math.log(2.0)) * 4096.0 / 12.0
|
||||
# Global behaviour flags byte (Taud Effect 1 / song-table byte 15):
|
||||
# bit 0 (p) : pan law — leave 0 (linear) for tracker accuracy
|
||||
# bits 1-2 (ff): tone mode — 2 = linear-frequency (Hz/tick)
|
||||
# Selecting ff=2 makes the engine interpret 1xx/2xx/3xx slide arguments in
|
||||
# audible Hz at the A4=440 Hz reference, matching Monotone's MT_PLAY.PAS
|
||||
# `Frequency:=Frequency±parm1` arithmetic (see MTSRC/MT_PLAY.PAS:606-630).
|
||||
GLOBAL_FLAGS_LINEAR_FREQ = 0b100
|
||||
|
||||
|
||||
# ── Taud container ───────────────────────────────────────────────────────────
|
||||
@@ -150,17 +153,14 @@ def encode_effect(eff_code: int, data: int) -> tuple:
|
||||
y = data & 0x7
|
||||
return (TOP_J, (J_SEMI_TABLE[x] << 8) | J_SEMI_TABLE[y])
|
||||
|
||||
if letter == '1': # slide up Hz/tick → Taud F
|
||||
arg = round(data * SLIDE_UNITS_PER_HZ) & 0xFFFF
|
||||
return (TOP_F, arg)
|
||||
if letter == '1': # slide up Hz/tick → Taud F (Hz/tick under ff=2)
|
||||
return (TOP_F, data & 0xFFFF)
|
||||
|
||||
if letter == '2': # slide down Hz/tick → Taud E
|
||||
arg = round(data * SLIDE_UNITS_PER_HZ) & 0xFFFF
|
||||
return (TOP_E, arg)
|
||||
if letter == '2': # slide down Hz/tick → Taud E (Hz/tick under ff=2)
|
||||
return (TOP_E, data & 0xFFFF)
|
||||
|
||||
if letter == '3': # tone porta Hz/tick → Taud G
|
||||
arg = round(data * SLIDE_UNITS_PER_HZ) & 0xFFFF
|
||||
return (TOP_G, arg)
|
||||
if letter == '3': # tone porta Hz/tick → Taud G (Hz/tick under ff=2)
|
||||
return (TOP_G, data & 0xFFFF)
|
||||
|
||||
if letter == '4': # vibrato xy → Taud H
|
||||
x = (data >> 3) & 0x7 # speed (3 bits)
|
||||
@@ -366,10 +366,11 @@ def assemble_taud(mon: dict) -> bytes:
|
||||
|
||||
# BPM 150 + ticks=mon_speed → row rate = 60/mon_speed (matches Monotone).
|
||||
bpm_stored = 150 - 24
|
||||
# bit 2 reserved (was 'm' fadeout-zero policy; removed). Monotone has no instrument-level
|
||||
# Linear-frequency tone mode (ff=2) so 1xx/2xx/3xx Hz/tick semantics survive verbatim;
|
||||
# pan law stays 0 (linear), bit 2 stays 0 (reserved). Monotone has no instrument-level
|
||||
# fadeout, so every Taud instrument carries fadeout=0 ("no fade") — notes retire on
|
||||
# sample-end or pattern note-cut instead.
|
||||
flags_byte = 0x00
|
||||
flags_byte = GLOBAL_FLAGS_LINEAR_FREQ
|
||||
|
||||
song_table = encode_song_entry(
|
||||
song_offset = song_offset,
|
||||
|
||||
@@ -2328,7 +2328,17 @@ TODO:
|
||||
for pan/pf envelopes to play. See byte 15/17/19 spec for the LOOP
|
||||
word bit layout.
|
||||
[x] slumberjack.xm: E6x commands are not processed
|
||||
[ ] implement linear-freq tone mode (MONOTONE compat)
|
||||
[x] implement linear-freq tone mode (MONOTONE compat)
|
||||
Resolution: ff=2 in song-table flags byte (was reserved). E / F / G
|
||||
arguments are interpreted as Hz/tick at A4 = 440 Hz / C4 ≈ 261.6256 Hz
|
||||
reference, exactly matching MONOTONE's MT_PLAY.PAS `Frequency`
|
||||
arithmetic (MTSRC/MT_PLAY.PAS:606-630). Per-voice `linearFreq` cache
|
||||
in AudioAdapter.kt preserves sub-noteVal precision across ticks; the
|
||||
Voice cache reseeds on note trigger, fine slides, S$2x finetune, and
|
||||
the start of a fresh multi-tick coarse slide. mon2taud.py now emits
|
||||
Hz values verbatim (no SLIDE_UNITS_PER_HZ scaling) and sets the
|
||||
linear-freq flag in the song-table flags byte. Spec details in
|
||||
TAUD_NOTE_EFFECTS.md §1, §E, §F, §G.
|
||||
[ ] milkytracker-style volume ramping (on sample-end only)
|
||||
|
||||
|
||||
@@ -2430,9 +2440,9 @@ Play Head Flags
|
||||
Byte 2
|
||||
- PCM Mode: Write non-zero value to start uploading; always 0 when read
|
||||
- Tracker Mode: Global mixer flags. Maps directly to Taud effect symbol '1'
|
||||
0b 0000 00fp
|
||||
0b 0000 0ffp
|
||||
p: panning mode (0: linear, 1: equal-power)
|
||||
f: pitchshift mode (0: tone-linear, 1: Amiga)
|
||||
ff: pitchshift mode (0: linear pitch slides, 1: Amiga period slides, 2: linear-frequency slides, 3: reserved)
|
||||
Tracker command may change the mixer state, but the changes WILL NOT BE REFLECTED BACK.
|
||||
Starting a new song will use whatever written to this register. In other words, changes
|
||||
made by songs will not persist.
|
||||
@@ -2551,9 +2561,9 @@ Endianness: Little
|
||||
Uint16 Current Tuning base note (1..65533). A4 (western default) is 0x5C00. C9 (tracker default) is 0xA000. If zero, assume the tracker default value
|
||||
Float32 Frequency at the base note. Tracker default is 8363.0. If zero, assume the tracker default
|
||||
Uint8 Flags for Global Behaviour (effect symbol '1')
|
||||
0b 0000 0Ffp
|
||||
p: panning law (0=linear, 1=equal-power)
|
||||
Ff: tone mode (0=linear pitch slides, 1=Amiga period slides, 2=linear-frequency slides, 3=reserved)
|
||||
0b 0000 0ffp
|
||||
p: panning law (0: linear, 1: equal-power)
|
||||
ff: tone mode (0: linear pitch slides, 1: Amiga period slides, 2: linear-frequency slides, 3: reserved)
|
||||
(bit 2 reserved — was 'm' fadeout-zero policy, removed; fadeout
|
||||
scaling now lives entirely in the converter — see byte 172/173
|
||||
of the instrument record for engine semantics)
|
||||
|
||||
@@ -136,7 +136,7 @@ class AudioJSR223Delegate(private val vm: VM) {
|
||||
ph.initialGlobalFlags = flags
|
||||
ph.trackerState?.let { ts ->
|
||||
ts.panLaw = flags and 1
|
||||
ts.amigaMode = (flags and 2) != 0
|
||||
ts.toneMode = (flags ushr 1) and 3
|
||||
// bit 2 reserved (was 'm' fadeout-zero policy; removed — see AudioAdapter.kt
|
||||
// and TAUD_NOTE_EFFECTS.md §1 "Volume Fadeout")
|
||||
}
|
||||
|
||||
@@ -133,6 +133,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
// Trackers may use different labelling conventions (e.g. C5) for Middle C.
|
||||
// For non-tracker context, Middle C shall be labelled as C4.
|
||||
const val AMIGA_BASE_PERIOD = 428.0
|
||||
// Reference frequency for linear-freq tone mode (toneMode == 2). Fixed at 12-TET
|
||||
// A4 = 440 Hz so that 1 Hz/tick at C4 ≈ 1 Hz at the audible output: 261.6256 ×
|
||||
// 2^(9/12) = 440 Hz exactly. MONOTONE (.MON) — the only source format using
|
||||
// linear-freq slides — uses A0 = 27.5 Hz with the same equal-temperament tuning,
|
||||
// so emitted Hz values map directly to audible Hz at any pitch.
|
||||
const val LINEAR_FREQ_C4_HZ = 261.6255653005986
|
||||
}
|
||||
|
||||
// Memory map (terranmon.txt:1985-1997, updated 2026-05-06):
|
||||
@@ -1209,6 +1215,34 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
return amigaPeriodToNoteVal(newPeriod)
|
||||
}
|
||||
|
||||
// Linear-frequency mode (toneMode == 2): E / F / G arguments are interpreted as Hz/tick.
|
||||
// The reference is fixed at 12-TET A4 = 440 Hz (so MIDDLE_C ≈ 261.6256 Hz). MONOTONE
|
||||
// (.MON) is the canonical source — its 1xx/2xx/3xx commands use Hz/tick directly, so
|
||||
// mon2taud.py emits the raw byte and relies on this mode. Like Amiga mode, a per-voice
|
||||
// linearFreq cache (`voice.linearFreq`) preserves sub-noteVal precision across ticks;
|
||||
// -1.0 means stale and must be reseeded from current noteVal.
|
||||
private fun noteValToFreqHz(noteVal: Int): Double =
|
||||
LINEAR_FREQ_C4_HZ * 2.0.pow((noteVal - MIDDLE_C).toDouble() / 4096.0)
|
||||
|
||||
private fun freqHzToNoteVal(freq: Double): Int =
|
||||
(MIDDLE_C + 4096.0 * log2(freq / LINEAR_FREQ_C4_HZ)).roundToInt()
|
||||
|
||||
// Per-tick linear-freq slide. Sign convention matches linear/Amiga modes: positive
|
||||
// slideArg = pitch up = freq rises; negative = pitch down = freq falls.
|
||||
private fun linearFreqSlideTick(voice: Voice, slideArg: Int): Int {
|
||||
if (voice.linearFreq < 0.0) voice.linearFreq = noteValToFreqHz(voice.noteVal)
|
||||
voice.linearFreq = (voice.linearFreq + slideArg).coerceAtLeast(1.0)
|
||||
return freqHzToNoteVal(voice.linearFreq)
|
||||
}
|
||||
|
||||
// One-shot linear-freq slide for fine E/F (applied once per row at tick 0); does
|
||||
// not mutate persistent state.
|
||||
private fun linearFreqSlideOnce(noteVal: Int, slideArg: Int): Int {
|
||||
val freq = noteValToFreqHz(noteVal)
|
||||
val newFreq = (freq + slideArg).coerceAtLeast(1.0)
|
||||
return freqHzToNoteVal(newFreq)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the active wrap region for an envelope based on the LOOP and
|
||||
* SUSTAIN words and key state.
|
||||
@@ -1679,6 +1713,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
voice.noteVal = noteVal
|
||||
voice.basePitch = noteVal
|
||||
voice.amigaPeriod = -1.0 // fresh trigger: period state must reseed from the new noteVal
|
||||
voice.linearFreq = -1.0 // ditto for linear-freq mode (toneMode == 2)
|
||||
voice.playbackRate = computePlaybackRate(inst, noteVal)
|
||||
// Fresh trigger resets channel volume to full ($3F). Per-instrument scaling lives in
|
||||
// instGlobalVolume (byte 171), which the mixer applies as a multiplier. Converters
|
||||
@@ -1824,6 +1859,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
v.noteVal = src.noteVal
|
||||
v.basePitch = src.basePitch
|
||||
v.amigaPeriod = src.amigaPeriod
|
||||
v.linearFreq = src.linearFreq
|
||||
v.volEnvOn = src.volEnvOn
|
||||
v.panEnvOn = src.panEnvOn
|
||||
v.pfEnvOn = src.pfEnvOn
|
||||
@@ -1997,13 +2033,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
EffectOp.OP_NONE -> {}
|
||||
EffectOp.OP_1 -> {
|
||||
// 1 $xx00 — Global behaviour flags byte in the high byte (see TAUD_NOTE_EFFECTS.md §1).
|
||||
// bit 0 (p): 0=linear pan, 1=equal-power pan
|
||||
// bit 1 (f): 0=linear pitch slides, 1=Amiga-mode pitch slides
|
||||
// bit 2 : reserved (was 'm' fadeout-zero policy; removed — converters now scale
|
||||
// source fadeout into Taud-native units, so the engine has a single divisor)
|
||||
// bit 0 (p): 0=linear pan, 1=equal-power pan
|
||||
// bits 1-2 (ff): 0=linear pitch, 1=Amiga period, 2=linear frequency (Hz/tick),
|
||||
// 3=reserved
|
||||
val flags = rawArg ushr 8
|
||||
ts.panLaw = flags and 1
|
||||
ts.amigaMode = (flags and 2) != 0
|
||||
ts.toneMode = (flags ushr 1) and 3
|
||||
}
|
||||
EffectOp.OP_8 -> {
|
||||
// 8 $xyzz — Bitcrusher. See TAUD_NOTE_EFFECTS.md §8.
|
||||
@@ -2073,32 +2108,38 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
val arg = resolveArg(rawArg, voice.mem.ef).also { if (rawArg != 0) voice.mem.ef = it }
|
||||
if ((arg and 0xF000) == 0xF000) {
|
||||
val mag = arg and 0x0FFF
|
||||
voice.noteVal = if (ts.amigaMode)
|
||||
amigaSlideOnce(voice.noteVal, -mag).coerceIn(0, 0xFFFE)
|
||||
else
|
||||
(voice.noteVal - mag).coerceIn(0, 0xFFFE)
|
||||
voice.noteVal = when (ts.toneMode) {
|
||||
1 -> amigaSlideOnce(voice.noteVal, -mag) // Amiga: subtract from pitch ⇒ adds period
|
||||
2 -> linearFreqSlideOnce(voice.noteVal, -mag) // Hz/tick: pitch down ⇒ -Hz
|
||||
else -> voice.noteVal - mag // linear 4096-TET
|
||||
}.coerceIn(0, 0xFFFE)
|
||||
voice.basePitch = voice.noteVal
|
||||
voice.amigaPeriod = -1.0 // reseed on next per-tick slide
|
||||
voice.linearFreq = -1.0
|
||||
voice.playbackRate = computePlaybackRate(instruments[voice.instrumentId], voice.noteVal)
|
||||
} else {
|
||||
voice.slideMode = 1; voice.slideArg = -arg
|
||||
voice.amigaPeriod = -1.0 // reseed at the start of a fresh multi-tick slide
|
||||
voice.linearFreq = -1.0
|
||||
}
|
||||
}
|
||||
EffectOp.OP_F -> {
|
||||
val arg = resolveArg(rawArg, voice.mem.ef).also { if (rawArg != 0) voice.mem.ef = it }
|
||||
if ((arg and 0xF000) == 0xF000) {
|
||||
val mag = arg and 0x0FFF
|
||||
voice.noteVal = if (ts.amigaMode)
|
||||
amigaSlideOnce(voice.noteVal, mag).coerceIn(0, 0xFFFE)
|
||||
else
|
||||
(voice.noteVal + mag).coerceIn(0, 0xFFFE)
|
||||
voice.noteVal = when (ts.toneMode) {
|
||||
1 -> amigaSlideOnce(voice.noteVal, mag)
|
||||
2 -> linearFreqSlideOnce(voice.noteVal, mag)
|
||||
else -> voice.noteVal + mag
|
||||
}.coerceIn(0, 0xFFFE)
|
||||
voice.basePitch = voice.noteVal
|
||||
voice.amigaPeriod = -1.0
|
||||
voice.linearFreq = -1.0
|
||||
voice.playbackRate = computePlaybackRate(instruments[voice.instrumentId], voice.noteVal)
|
||||
} else {
|
||||
voice.slideMode = 2; voice.slideArg = arg
|
||||
voice.amigaPeriod = -1.0
|
||||
voice.linearFreq = -1.0
|
||||
}
|
||||
}
|
||||
EffectOp.OP_G -> {
|
||||
@@ -2214,6 +2255,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
voice.noteVal = (voice.noteVal + FINETUNE_OFFSET[x]).coerceIn(0, 0xFFFE)
|
||||
voice.basePitch = voice.noteVal
|
||||
voice.amigaPeriod = -1.0
|
||||
voice.linearFreq = -1.0
|
||||
voice.playbackRate = computePlaybackRate(instruments[voice.instrumentId], voice.noteVal)
|
||||
}
|
||||
0x3 -> { voice.vibratoWave = x and 3; voice.vibratoRetrig = (x and 4) == 0 }
|
||||
@@ -2299,24 +2341,46 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
|
||||
// Pitch slides (E/F coarse on tick > 0).
|
||||
if (ts.tickInRow > 0 && (voice.slideMode == 1 || voice.slideMode == 2)) {
|
||||
voice.noteVal = if (ts.amigaMode)
|
||||
amigaSlideTick(voice, voice.slideArg).coerceIn(0, 0xFFFE)
|
||||
else
|
||||
(voice.noteVal + voice.slideArg).coerceIn(0, 0xFFFE)
|
||||
voice.noteVal = when (ts.toneMode) {
|
||||
1 -> amigaSlideTick(voice, voice.slideArg)
|
||||
2 -> linearFreqSlideTick(voice, voice.slideArg)
|
||||
else -> voice.noteVal + voice.slideArg
|
||||
}.coerceIn(0, 0xFFFE)
|
||||
voice.basePitch = voice.noteVal
|
||||
}
|
||||
|
||||
// Tone portamento (G).
|
||||
// Tone portamento (G). In linear-freq mode the speed is interpreted as Hz/tick
|
||||
// so MONOTONE 3xx (port-to-note in Hz) round-trips faithfully; in linear and
|
||||
// Amiga modes the speed is in 4096-TET pitch units (Amiga period units would be
|
||||
// backwards relative to PT semantics — see TAUD_NOTE_EFFECTS.md §G).
|
||||
if (voice.tonePortaTarget >= 0 && ts.tickInRow > 0) {
|
||||
val target = voice.tonePortaTarget
|
||||
val sp = voice.tonePortaSpeed
|
||||
val delta = if (target > voice.noteVal) sp else -sp
|
||||
voice.noteVal += delta
|
||||
if ((delta > 0 && voice.noteVal >= target) || (delta < 0 && voice.noteVal <= target)) {
|
||||
voice.noteVal = target; voice.tonePortaTarget = -1
|
||||
if (ts.toneMode == 2) {
|
||||
if (voice.linearFreq < 0.0) voice.linearFreq = noteValToFreqHz(voice.noteVal)
|
||||
val targetFreq = noteValToFreqHz(target)
|
||||
val dir = if (targetFreq > voice.linearFreq) +1.0 else -1.0
|
||||
voice.linearFreq += dir * sp
|
||||
if ((dir > 0 && voice.linearFreq >= targetFreq) ||
|
||||
(dir < 0 && voice.linearFreq <= targetFreq)) {
|
||||
voice.linearFreq = targetFreq
|
||||
voice.noteVal = target
|
||||
voice.tonePortaTarget = -1
|
||||
} else {
|
||||
voice.noteVal = freqHzToNoteVal(voice.linearFreq).coerceIn(0, 0xFFFE)
|
||||
}
|
||||
voice.basePitch = voice.noteVal
|
||||
voice.amigaPeriod = -1.0
|
||||
} else {
|
||||
val delta = if (target > voice.noteVal) sp else -sp
|
||||
voice.noteVal += delta
|
||||
if ((delta > 0 && voice.noteVal >= target) || (delta < 0 && voice.noteVal <= target)) {
|
||||
voice.noteVal = target; voice.tonePortaTarget = -1
|
||||
}
|
||||
voice.basePitch = voice.noteVal
|
||||
voice.amigaPeriod = -1.0 // tone porta works in linear noteVal space; reseed period
|
||||
voice.linearFreq = -1.0
|
||||
}
|
||||
voice.basePitch = voice.noteVal
|
||||
voice.amigaPeriod = -1.0 // tone porta works in linear noteVal space; reseed period
|
||||
}
|
||||
|
||||
// Volume slides (D coarse on tick > 0).
|
||||
@@ -2904,6 +2968,9 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
// sub-noteVal precision through repeated round-trip rounding (see amigaSlideTick).
|
||||
// -1.0 means "needs reseed from current noteVal".
|
||||
var amigaPeriod: Double = -1.0
|
||||
// Linear-frequency-mode state (Hz). Same -1.0 = stale convention as amigaPeriod.
|
||||
// Used by toneMode == 2 (MONOTONE compat) for E / F coarse slides and G tone porta.
|
||||
var linearFreq: Double = -1.0
|
||||
|
||||
// Per-row effect state (set in applyTrackerRow, consumed by applyTrackerTick).
|
||||
var rowEffect = 0
|
||||
@@ -3009,7 +3076,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
|
||||
// Global mixer config (effect 1).
|
||||
var panLaw = 0 // 0 = linear balance (default), 1 = equal-power
|
||||
var amigaMode = false // false = linear pitch slides, true = Amiga period-space slides
|
||||
// Tone-slide mode for E / F / G effects (terranmon.txt §Song Table flags byte):
|
||||
// 0 = linear pitch slides (4096-TET units, default)
|
||||
// 1 = Amiga period slides (raw PT period units, applied in period space)
|
||||
// 2 = linear-frequency slides (Hz/tick — MONOTONE compat)
|
||||
// 3 = reserved
|
||||
var toneMode = 0
|
||||
|
||||
// Pending row-end events (set during a row by B/C; consumed at row end).
|
||||
var pendingOrderJump = -1 // -1 = none; otherwise the order index to jump to
|
||||
@@ -3122,7 +3194,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
initialGlobalFlags = byte
|
||||
trackerState?.let { ts ->
|
||||
ts.panLaw = byte and 1
|
||||
ts.amigaMode = (byte and 2) != 0
|
||||
ts.toneMode = (byte ushr 1) and 3
|
||||
}
|
||||
}
|
||||
8 -> { bpm = byte + 24 }
|
||||
@@ -3157,7 +3229,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
ts.sexWinningChannel = -1
|
||||
ts.finePatternDelayExtra = 0
|
||||
ts.panLaw = initialGlobalFlags and 1
|
||||
ts.amigaMode = (initialGlobalFlags and 2) != 0
|
||||
ts.toneMode = (initialGlobalFlags ushr 1) and 3
|
||||
ts.voices.forEach {
|
||||
it.active = false
|
||||
it.channelVolume = 0x3F
|
||||
|
||||
Reference in New Issue
Block a user