mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 13:38:30 +09:00
taud: amiga mode pitchbend
This commit is contained in:
@@ -45,9 +45,9 @@ mix = sample × note_vol × channel_vol × global_vol >> normalisation_shift
|
|||||||
|
|
||||||
with saturation applied before the 8-bit stereo output.
|
with saturation applied before the 8-bit stereo output.
|
||||||
|
|
||||||
## 4. Rows, ticks, patterns, orders
|
## 4. Rows, ticks, patterns, cues
|
||||||
|
|
||||||
A pattern is a rectangular grid of rows and channels; each cell holds one note event. Playback divides each row into `speed` ticks (effect A); tempo (effect T) sets the duration of one tick. At 125 BPM and speed 6, one row takes 120 ms and one tick 20 ms. Songs play patterns in an order sequence; effects B and C navigate this sequence.
|
A pattern is a rectangular grid of rows and channels; each cell holds one note event. Playback divides each row into `speed` ticks (effect A); tempo (effect T) sets the duration of one tick. At 125 BPM and speed 6, one row takes 120 ms and one tick 20 ms. Songs play patterns in a cue sequence; effects B and C navigate this sequence.
|
||||||
|
|
||||||
## 5. Default parameters at song start
|
## 5. Default parameters at song start
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ A pattern is a rectangular grid of rows and channels; each cell holds one note e
|
|||||||
| Global volume | $80 (mid-scale) |
|
| Global volume | $80 (mid-scale) |
|
||||||
| Channel volume | $3F (full) |
|
| Channel volume | $3F (full) |
|
||||||
| Pan (all channels) | $80 (centre) |
|
| Pan (all channels) | $80 (centre) |
|
||||||
| Order index | $0000 |
|
| cue index | $0000 |
|
||||||
|
|
||||||
## 6. Effect memory groups
|
## 6. Effect memory groups
|
||||||
|
|
||||||
@@ -89,25 +89,25 @@ Opcodes are single base-36 digits (0-9, then A-Z); arguments are 16-bit hexadeci
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## B $xxyy — Jump to order $xxyy
|
## B $xxyy — Jump to cue $xxyy
|
||||||
|
|
||||||
**Plain.** Finishes the current row, then continues playback at row 0 of the pattern at order position $xxyy. Use this to create song-level jumps, loops, or branching structures.
|
**Plain.** Finishes the current row, then continues playback at row 0 of the pattern at cue position $xxyy. Use this to create song-level jumps, loops, or branching structures.
|
||||||
|
|
||||||
**Compatibility.** ST3 `Bxx` jumps to an 8-bit order and maps to Taud `B $00xx`. The extended 16-bit range means Taud songs may have up to $10000 order entries.
|
**Compatibility.** ST3 `Bxx` jumps to an 8-bit cue and maps to Taud `B $00xx`. The extended 16-bit range means Taud songs may have up to $10000 cue entries.
|
||||||
|
|
||||||
**Implementation.** On the last tick of the current row, set the next order index to the argument and the next row to 0. If the argument exceeds the song length, wrap to the song's defined restart position (order $0000 by default). Jumps are detected by a visited `(order, row)` set so that pathological loops do not prevent song-length computation, though they do not interrupt actual playback. There is no memory for B.
|
**Implementation.** On the last tick of the current row, set the next cue index to the argument and the next row to 0. If the argument exceeds the song length, wrap to the song's defined restart position (cue $0000 by default). Jumps are detected by a visited `(cue, row)` set so that pathological loops do not prevent song-length computation, though they do not interrupt actual playback. There is no memory for B.
|
||||||
|
|
||||||
**Simultaneous B and C on the same row.** If a B command appears in the same row as a C command (on any channel), both fire: B chooses the order, C chooses the row within that order. If the two commands appear on different channels, channel priority is **ascending channel index** — the lowest-numbered channel carrying either effect wins its parameter. If both appear on the same channel row (only possible if one is a volume-column equivalent), the effect column takes precedence.
|
**Simultaneous B and C on the same row.** If a B command appears in the same row as a C command (on any channel), both fire: B chooses the cue, C chooses the row within that cue. If the two commands appear on different channels, channel priority is **ascending channel index** — the lowest-numbered channel carrying either effect wins its parameter. If both appear on the same channel row (only possible if one is a volume-column equivalent), the effect column takes precedence.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## C $xxyy — Break pattern to row $xxyy
|
## C $xxyy — Break pattern to row $xxyy
|
||||||
|
|
||||||
**Plain.** Finishes the current row, then skips ahead to row $xxyy of the **next** pattern in the order sequence.
|
**Plain.** Finishes the current row, then skips ahead to row $xxyy of the **next** pattern in the cue sequence.
|
||||||
|
|
||||||
**Compatibility.** ST3 stores `Cxx` as **BCD** (so on-disk `$10` means decimal row 10); Taud stores the argument as plain binary. When converting from ST3, decode with `row = (byte >> 4) × 10 + (byte & $0F)`. Valid ST3 source bytes are those representing decimal 0..63; out-of-range BCD bytes should clamp to row 0 on import. When exporting back to ST3, encode with `byte = ((row / 10) << 4) | (row % 10)`, clamped at row 63.
|
**Compatibility.** ST3 stores `Cxx` as **BCD** (so on-disk `$10` means decimal row 10); Taud stores the argument as plain binary. When converting from ST3, decode with `row = (byte >> 4) × 10 + (byte & $0F)`. Valid ST3 source bytes are those representing decimal 0..63; out-of-range BCD bytes should clamp to row 0 on import. When exporting back to ST3, encode with `byte = ((row / 10) << 4) | (row % 10)`, clamped at row 63.
|
||||||
|
|
||||||
**Implementation.** On the last tick of the current row, advance the order index by 1 (or honour a co-occurring B), then set the next row to the argument. If the argument exceeds the destination pattern's row count, start the destination pattern at row 0. There is no memory for C.
|
**Implementation.** On the last tick of the current row, advance the cue index by 1 (or honour a co-occurring B), then set the next row to the argument. If the argument exceeds the destination pattern's row count, start the destination pattern at row 0. There is no memory for C.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -149,9 +149,9 @@ D's 16-bit argument encodes four mutually exclusive modes using the top nibble a
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## E $xxxx — Pitch slide down by $xxxx (linear)
|
## E $xxxx — Pitch slide down by $xxxx
|
||||||
|
|
||||||
**Plain.** Lowers the channel's pitch by the argument per tick. Taud's pitch slides are **linear in the 4096-TET grid** — the slide value is subtracted directly from the stored pitch, without any period-table indirection. A coarse slide uses the full value range; a fine slide applies only once per row; an extra-fine slide is not provided (the 16-bit argument already gives microtonal precision below 1/64 semitone).
|
**Plain.** Lowers the channel's pitch by the argument per tick. By default (linear mode, `f` bit unset in effect `1`) the coarse slide value is subtracted directly from the stored pitch in the 4096-TET grid. When Amiga mode is active (`f` bit set), coarse slides are instead applied in Amiga period space: the stored value is converted back to Amiga period units and subtracted from the equivalent period, producing the characteristic non-linear pitch drift of ProTracker-style slides. Fine slides (`E $Fxxx`) are always applied in linear pitch-unit space regardless of mode. 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:
|
Coarse and fine modes are distinguished by the high nibble of the argument:
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ Coarse and fine modes are distinguished by the high nibble of the argument:
|
|||||||
- 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 `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).
|
- ST3 `EEx` extra-fine → Taud `E $F0 round(x × 16/3)` (same unit as fine, applied once per row).
|
||||||
|
|
||||||
ST3 Amiga-mode slides do not have a clean conversion and should be treated as linear-mode equivalents during import.
|
ST3 Amiga-mode coarse slides do not have a clean conversion and should be treated as linear-mode equivalents during import (same `round(× 64/3)` scale). The Amiga-mode flag (`f` bit in effect `1` or the song-table flags byte) is set in the output file to signal the mixer to apply the stored values in period space rather than directly in pitch space. This preserves the characteristic non-linearity of Amiga slides (lower pitches slide more slowly in semitone terms) without requiring a different numeric encoding. Fine and extra-fine slides (`E $Fxxx`) are always applied in linear pitch-unit space regardless of the Amiga-mode flag, as they are ST3-specific extensions absent from ProTracker.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -185,18 +185,24 @@ on row start:
|
|||||||
|
|
||||||
on tick > 0:
|
on tick > 0:
|
||||||
if mode_this_row == COARSE:
|
if mode_this_row == COARSE:
|
||||||
pitch -= slide_amount_this_row
|
if amiga_mode:
|
||||||
|
# period = AMIGA_BASE_PERIOD × 2^(−(pitch − C3) / 4096)
|
||||||
|
# period += slide_amount_this_row × (3/64) # convert Taud units → Amiga period units
|
||||||
|
# pitch = C3 + 4096 × log2(AMIGA_BASE_PERIOD / period)
|
||||||
|
pitch = amiga_slide_down(pitch, slide_amount_this_row)
|
||||||
|
else:
|
||||||
|
pitch -= slide_amount_this_row
|
||||||
```
|
```
|
||||||
|
|
||||||
Glissando control (S $1x) snaps the output pitch to the nearest semitone after every slide application; see S $1x.
|
Glissando control (S $1x) snaps the output pitch to the nearest semitone after every slide application; see S $1x.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## F $xxxx — Pitch slide up by $xxxx (linear)
|
## 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, and memory behaviour are identical in form but inverted in direction.
|
**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.
|
||||||
|
|
||||||
**Compatibility.** Same as E. ST3 `Fxx` coarse converts using `round(x × 64/3)`; `FFx` fine and `FEx` extra-fine convert using `round(x × 16/3)`. F and E share one memory slot in Taud.
|
**Compatibility.** Same as E. ST3 `Fxx` coarse converts using `round(x × 64/3)`; `FFx` fine and `FEx` extra-fine convert using `round(x × 16/3)`. F and E share one memory slot in Taud. Amiga-mode behaviour is controlled by the same `f` flag as E; coarse F slides are applied in period space when the flag is set, while fine slides remain linear.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
@@ -527,6 +533,21 @@ Peak at maximum settings: $7F × $FF >> 9 = $3F — the full panning range. Retr
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 8 $xyzz — Bitcrusher
|
||||||
|
|
||||||
|
**Plain.** Applies Bitcrusher to the current voice.
|
||||||
|
|
||||||
|
- x: clipping mode. 0: clamp, 1: fold, 2: modulus
|
||||||
|
- y: bit depth (1..15). 8..15 has no effect on TSVM audio adapter (already operates on 8 bits)
|
||||||
|
- z: sample skip (0..255). 0: no skip, 1: use every 2nd samples, 2: use every 3rd samples, ..., 255: use every 256th samples
|
||||||
|
- `8 0000` will disable the bitcrusher
|
||||||
|
|
||||||
|
**Compatibility.** Unique to Taud. No compatible equivalent exists.
|
||||||
|
|
||||||
|
**Implementation.** TODO
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# The S subcommand family
|
# The S subcommand family
|
||||||
|
|
||||||
S is a multiplexing opcode; the **high nibble of the high byte** selects the sub-effect, and the remainder is the sub-argument.
|
S is a multiplexing opcode; the **high nibble of the high byte** selects the sub-effect, and the remainder is the sub-argument.
|
||||||
@@ -755,20 +776,33 @@ NOTE: **`3.00` — is No-op**
|
|||||||
|
|
||||||
Effects in this section modifies the behaviour of the mixer. Primary intention of the commands is to provide switches for legacy tracker and modern DAW behaviours.
|
Effects in this section modifies the behaviour of the mixer. Primary intention of the commands is to provide switches for legacy tracker and modern DAW behaviours.
|
||||||
|
|
||||||
## 1 $01xx — Set stereo panning law
|
## 1 $xx00 — Global behaviour flags
|
||||||
|
|
||||||
**Plain.** Sets how the mixer should treat the panning. Available modes are:
|
**Plain.** Sets how the mixer should treat the panning. Available flags are:
|
||||||
|
|
||||||
- 0: Linear panning mode (tracker-accurate). Centre panning gets 3 dB boost. Default setting.
|
0b 0000 00fp
|
||||||
- 1: Equal-power panning mode. L/R amplitude is at 0.707 when centre-panned.
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
- f unset: Linear tone mode. Pitch shift will behave like MIDI/ImpulseTracker/ScreamTracker linear mode.
|
||||||
|
- f set: Amiga tone mode. Pitch shift will behave like ProTracker/ScreamTracker default mode.
|
||||||
|
|
||||||
**Implementation.**
|
**Implementation.**
|
||||||
- Mode 0:
|
- Panning-linear:
|
||||||
- L_gain = if (pan < 0x80) 1.0 else 1.0 - (pan - 128.0) / 128.0
|
- L_gain = if (pan < 0x80) 1.0 else 1.0 - (pan - 128.0) / 128.0
|
||||||
- R_gain = if (pan < 0x80) pan / 128.0 else 1.0
|
- R_gain = if (pan < 0x80) pan / 128.0 else 1.0
|
||||||
- Mode 1:
|
- Panning-equal-power:
|
||||||
- L_gain = cos(pi*x / 512.0)
|
- L_gain = cos(pi*x / 512.0)
|
||||||
- R_gain = sin(pi*x / 512.0)
|
- R_gain = sin(pi*x / 512.0)
|
||||||
|
- Amiga tone (coarse E/F pitch slides only; fine slides are always linear):
|
||||||
|
- AMIGA_BASE_PERIOD = 214.0 (period at the Taud reference pitch C3 for a standard 8363 Hz instrument, NTSC clock)
|
||||||
|
- AMIGA_PERIOD_SCALE = 3.0 / 64.0 (converts stored Taud coarse-slide units back to Amiga period units)
|
||||||
|
- period = AMIGA_BASE_PERIOD × 2^(−(noteVal − C3) / 4096)
|
||||||
|
- period_new = period − slideArg × AMIGA_PERIOD_SCALE (slideArg < 0 for E, > 0 for F)
|
||||||
|
- noteVal_new = C3 + 4096 × log2(AMIGA_BASE_PERIOD / period_new)
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -833,7 +867,7 @@ These quirks of ST3 are worth preserving or flagging when importing S3M files in
|
|||||||
|
|
||||||
**Global volume scale.** ST3's 0..$40 maps to Taud's 0..$FF with a ×4 scale on import, truncated ÷4 on export.
|
**Global volume scale.** ST3's 0..$40 maps to Taud's 0..$FF with a ×4 scale on import, truncated ÷4 on export.
|
||||||
|
|
||||||
**Linear pitch slides.** ST3's slide arithmetic is period-based (Amiga) or linear-table-indexed; Taud's is purely linear in 4096-TET units. ST3 songs in linear mode convert cleanly: coarse forms (Exx/Fxx/Gxx) use `round(× 64/3)` (1/16 semitone per unit), fine/extra-fine forms (EFx/EEx/FFx/FEx) use `round(× 16/3)` (1/64 semitone per unit). Amiga-mode slides change character slightly because the non-linearity of period math is not replicated.
|
**Linear pitch slides.** ST3's slide arithmetic is period-based (Amiga) or linear-table-indexed; Taud's default is purely linear in 4096-TET units. ST3 songs in linear mode convert cleanly: coarse forms (Exx/Fxx/Gxx) use `round(× 64/3)` (1/16 semitone per unit), fine/extra-fine forms (EFx/EEx/FFx/FEx) use `round(× 16/3)` (1/64 semitone per unit). ST3 songs in Amiga mode use the **same numeric conversion** for coarse E/F (the exact period-step count is not preserved), but the converter sets bit 1 (`f`) of the song-table flags byte and Taud's mixer re-applies the stored coarse slide values in Amiga period space at playback, recovering the non-linear pitch character. G is always treated as linear regardless of mode. Fine/extra-fine slides are always linear.
|
||||||
|
|
||||||
**Default tempo byte.** Taud's default $65 equals 125 BPM under the $18 offset; this is not the same as ST3's `$7D` default, which maps to Taud `$65` after subtracting $18. Converters must remap on both import and export.
|
**Default tempo byte.** Taud's default $65 equals 125 BPM under the $18 offset; this is not the same as ST3's `$7D` default, which maps to Taud `$65` after subtracting $18. Converters must remap on both import and export.
|
||||||
|
|
||||||
|
|||||||
@@ -88,14 +88,14 @@ rightshade:'\u00B2',
|
|||||||
|
|
||||||
const fxNames = {
|
const fxNames = {
|
||||||
'0':"-- ",
|
'0':"-- ",
|
||||||
'1':"Mixer config ", // Taud: 1 01xx: set stereo panning law
|
'1':"Mixer config ",
|
||||||
'2':"UNIMPLEMENTED",
|
'2':"UNIMPLEMENTED",
|
||||||
'3':"UNIMPLEMENTED",
|
'3':"UNIMPLEMENTED",
|
||||||
'4':"UNIMPLEMENTED",
|
'4':"UNIMPLEMENTED",
|
||||||
'5':"UNIMPLEMENTED",
|
'5':"UNIMPLEMENTED",
|
||||||
'6':"UNIMPLEMENTED",
|
'6':"UNIMPLEMENTED",
|
||||||
'7':"UNIMPLEMENTED",
|
'7':"UNIMPLEMENTED",
|
||||||
'8':"UNIMPLEMENTED",
|
'8':"Bitcrusher ",
|
||||||
'9':"UNIMPLEMENTED",
|
'9':"UNIMPLEMENTED",
|
||||||
A:"Tick speed ",
|
A:"Tick speed ",
|
||||||
B:"Jump to order",
|
B:"Jump to order",
|
||||||
@@ -674,8 +674,8 @@ function drawStatusBar() {
|
|||||||
con.move(1,4)
|
con.move(1,4)
|
||||||
con.color_pair(colWHITE, 255); print(`Cue `)
|
con.color_pair(colWHITE, 255); print(`Cue `)
|
||||||
con.color_pair(20, 255); print(`${sCueIdx}`)
|
con.color_pair(20, 255); print(`${sCueIdx}`)
|
||||||
con.color_pair(colWHITE, 255); print(`/`)
|
// con.color_pair(colWHITE, 255); print(`/`)
|
||||||
con.color_pair(20, 255); print(`${sCueMax}`)
|
// con.color_pair(20, 255); print(`${sCueMax}`)
|
||||||
con.color_pair(colWHITE, 255); print(` Row `)
|
con.color_pair(colWHITE, 255); print(` Row `)
|
||||||
con.color_pair(130, 255); print(`${sRow}`)
|
con.color_pair(130, 255); print(`${sRow}`)
|
||||||
|
|
||||||
@@ -683,7 +683,7 @@ function drawStatusBar() {
|
|||||||
con.move(2,4)
|
con.move(2,4)
|
||||||
con.color_pair(colWHITE, 255); print(`BPM `)
|
con.color_pair(colWHITE, 255); print(`BPM `)
|
||||||
con.color_pair(161, 255); print(`${sBPM}`)
|
con.color_pair(161, 255); print(`${sBPM}`)
|
||||||
con.color_pair(colWHITE, 255); print(` Tickspeed `)
|
con.color_pair(colWHITE, 255); print(` Tick `)
|
||||||
con.color_pair(235, 255); print(`${sSpd}`)
|
con.color_pair(235, 255); print(`${sSpd}`)
|
||||||
|
|
||||||
// app title
|
// app title
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -219,9 +219,9 @@ function captureTrackerDataToFile(outFile) {
|
|||||||
numPats & 0xFF, (numPats >>> 8) & 0xFF, // numPatterns Uint16 LE
|
numPats & 0xFF, (numPats >>> 8) & 0xFF, // numPatterns Uint16 LE
|
||||||
bpmStored, // BPM with −24 bias
|
bpmStored, // BPM with −24 bias
|
||||||
tickRate, // initial tick-rate
|
tickRate, // initial tick-rate
|
||||||
0x00,0x90, // basenote (0x9000 -- C8)
|
0x00,0xA0, // basenote (0xA000 -- C9)
|
||||||
0x00,0xAC,0x02,0x46, // basefreq (8363 Hz)
|
0x00,0xAC,0x02,0x46, // basefreq (8363 Hz)
|
||||||
0, // padding
|
sys.peek(baseAddr - 7), // mixer flags
|
||||||
]
|
]
|
||||||
|
|
||||||
// -- 7. Write header (creates / truncates file) ---------------------------
|
// -- 7. Write header (creates / truncates file) ---------------------------
|
||||||
|
|||||||
37
it2taud.py
37
it2taud.py
@@ -920,7 +920,8 @@ def encode_effect_it(cmd: int, arg: int, ch: int = 0, row: int = 0) -> tuple:
|
|||||||
# ── IT recall resolution ──────────────────────────────────────────────────────
|
# ── IT recall resolution ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
def resolve_it_recalls(patterns_rows: list, order_list: list,
|
def resolve_it_recalls(patterns_rows: list, order_list: list,
|
||||||
num_channels: int, link_gef: bool) -> None:
|
num_channels: int, link_gef: bool,
|
||||||
|
old_effects: bool = False) -> None:
|
||||||
"""Walk in order, resolve zero-arg recalls per-effect-per-channel.
|
"""Walk in order, resolve zero-arg recalls per-effect-per-channel.
|
||||||
|
|
||||||
IT effect memory groups:
|
IT effect memory groups:
|
||||||
@@ -928,11 +929,19 @@ def resolve_it_recalls(patterns_rows: list, order_list: list,
|
|||||||
- E / F (/ G when link_gef): shared pitch-slide cohort
|
- E / F (/ G when link_gef): shared pitch-slide cohort
|
||||||
- G: own slot (or part of EF cohort when link_gef)
|
- G: own slot (or part of EF cohort when link_gef)
|
||||||
- All others: private slots
|
- All others: private slots
|
||||||
|
|
||||||
|
old_effects=True (IT_FLAG_OLD_EFFECTS): E00/F00 are ST3-style stops —
|
||||||
|
they do NOT recall and are suppressed to TOP_NONE. All other effects
|
||||||
|
still recall normally even in old_effects mode.
|
||||||
"""
|
"""
|
||||||
# last_mem[ch][eff_key] = last_non_zero_arg
|
# last_mem[ch][eff_key] = last_non_zero_arg
|
||||||
# eff_key: integer 1-26 for most effects; we merge cohorts by normalising.
|
# eff_key: integer 1-26 for most effects; we merge cohorts by normalising.
|
||||||
last_mem = [{} for _ in range(num_channels)]
|
last_mem = [{} for _ in range(num_channels)]
|
||||||
|
|
||||||
|
# Effects that stop rather than recall when arg=0 in old_effects mode (ST3 compat).
|
||||||
|
# E/F: pitch slide stop. J: arpeggio stop (J00 = return to normal pitch in ST3).
|
||||||
|
OLD_EFF_STOPS = frozenset({EFF_E, EFF_F, EFF_J})
|
||||||
|
|
||||||
def cohort_key(cmd):
|
def cohort_key(cmd):
|
||||||
if cmd in (EFF_D, EFF_K, EFF_L):
|
if cmd in (EFF_D, EFF_K, EFF_L):
|
||||||
return EFF_D # vol-slide cohort
|
return EFF_D # vol-slide cohort
|
||||||
@@ -957,7 +966,12 @@ def resolve_it_recalls(patterns_rows: list, order_list: list,
|
|||||||
continue
|
continue
|
||||||
key = cohort_key(cell.effect)
|
key = cohort_key(cell.effect)
|
||||||
if cell.effect_arg == 0:
|
if cell.effect_arg == 0:
|
||||||
cell.effect_arg = last_mem[ch].get(key, 0)
|
if old_effects and cell.effect in OLD_EFF_STOPS:
|
||||||
|
# E00/F00 in old_effects = stop slide — suppress entirely.
|
||||||
|
# Taud's E $0000 also recalls, so convert to no-op here.
|
||||||
|
cell.effect = 0
|
||||||
|
else:
|
||||||
|
cell.effect_arg = last_mem[ch].get(key, 0)
|
||||||
else:
|
else:
|
||||||
last_mem[ch][key] = cell.effect_arg
|
last_mem[ch][key] = cell.effect_arg
|
||||||
|
|
||||||
@@ -1141,7 +1155,12 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
|
|||||||
c2spd = min(s.c5_speed, 65535)
|
c2spd = min(s.c5_speed, 65535)
|
||||||
ls = min(s.loop_beg, 65535)
|
ls = min(s.loop_beg, 65535)
|
||||||
le = min(s.loop_end, 65535)
|
le = min(s.loop_end, 65535)
|
||||||
loop_mode = 1 if s.has_loop else 0
|
if s.has_loop and (s.flags & IT_SMP_PINGPONG):
|
||||||
|
loop_mode = 2 # backandforth
|
||||||
|
elif s.has_loop:
|
||||||
|
loop_mode = 1 # forward loop
|
||||||
|
else:
|
||||||
|
loop_mode = 0 # no loop
|
||||||
flags_byte = (ptr_hi << 4) | (loop_mode & 0x3)
|
flags_byte = (ptr_hi << 4) | (loop_mode & 0x3)
|
||||||
|
|
||||||
base = taud_idx * 64
|
base = taud_idx * 64
|
||||||
@@ -1371,7 +1390,8 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
|
|||||||
patterns_rows: list, decompress: bool) -> bytes:
|
patterns_rows: list, decompress: bool) -> bytes:
|
||||||
# ── Resolve IT recalls ───────────────────────────────────────────────────
|
# ── Resolve IT recalls ───────────────────────────────────────────────────
|
||||||
vprint(" resolving IT recalls…")
|
vprint(" resolving IT recalls…")
|
||||||
resolve_it_recalls(patterns_rows, h.order_list, 64, h.link_gef)
|
resolve_it_recalls(patterns_rows, h.order_list, 64, h.link_gef,
|
||||||
|
old_effects=h.old_effects)
|
||||||
|
|
||||||
# ── Check SBx chunk crossing (warn only) ─────────────────────────────────
|
# ── Check SBx chunk crossing (warn only) ─────────────────────────────────
|
||||||
for pi, (grid, rows) in enumerate(patterns_rows):
|
for pi, (grid, rows) in enumerate(patterns_rows):
|
||||||
@@ -1524,12 +1544,15 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
|
|||||||
)
|
)
|
||||||
assert len(header) == TAUD_HEADER_SIZE
|
assert len(header) == TAUD_HEADER_SIZE
|
||||||
|
|
||||||
song_table = struct.pack('<IBHBBHf',
|
# flags byte: bit 1 (f) = Amiga pitch-slide mode (IT linear_slides flag inverted)
|
||||||
|
flags_byte = 0x00 if h.linear_slides else 0x02
|
||||||
|
song_table = struct.pack('<IBHBBHfB',
|
||||||
song_offset, C, num_taud_pats,
|
song_offset, C, num_taud_pats,
|
||||||
bpm_stored, speed,
|
bpm_stored, speed,
|
||||||
0x9000, # C8
|
0xA000, # C9
|
||||||
8363.0,
|
8363.0,
|
||||||
) + b'\x00'
|
flags_byte,
|
||||||
|
)
|
||||||
assert len(song_table) == TAUD_SONG_ENTRY
|
assert len(song_table) == TAUD_SONG_ENTRY
|
||||||
|
|
||||||
return header + compressed + song_table + bytes(pat_bin) + bytes(sheet)
|
return header + compressed + song_table + bytes(pat_bin) + bytes(sheet)
|
||||||
|
|||||||
13
s3m2taud.py
13
s3m2taud.py
@@ -79,7 +79,7 @@ EFF_Z = 26 # sync
|
|||||||
TAUD_MAGIC = bytes([0x1F,0x54,0x53,0x56,0x4D,0x61,0x75,0x64])
|
TAUD_MAGIC = bytes([0x1F,0x54,0x53,0x56,0x4D,0x61,0x75,0x64])
|
||||||
TAUD_VERSION = 1
|
TAUD_VERSION = 1
|
||||||
TAUD_HEADER_SIZE = 32 # magic(8)+ver(1)+numSongs(1)+compSize(4)+rsvd(2)+sig(16)
|
TAUD_HEADER_SIZE = 32 # magic(8)+ver(1)+numSongs(1)+compSize(4)+rsvd(2)+sig(16)
|
||||||
TAUD_SONG_ENTRY = 16 # offset(4)+voices(1)+pats_lo(1)+pats_hi(1)+bpm(1)+tick(1)+pad(7)
|
TAUD_SONG_ENTRY = 16 # offset(4)+voices(1)+pats(2)+bpm(1)+tick(1)+basenote(2)+basefreq(4)+flags(1)
|
||||||
SAMPLEBIN_SIZE = 770048
|
SAMPLEBIN_SIZE = 770048
|
||||||
INSTBIN_SIZE = 16384 # 256 instruments × 64 bytes
|
INSTBIN_SIZE = 16384 # 256 instruments × 64 bytes
|
||||||
SAMPLEINST_SIZE = SAMPLEBIN_SIZE + INSTBIN_SIZE # 786432
|
SAMPLEINST_SIZE = SAMPLEBIN_SIZE + INSTBIN_SIZE # 786432
|
||||||
@@ -898,17 +898,20 @@ def assemble_taud(h: S3MHeader, instruments: list, patterns: list) -> bytes:
|
|||||||
pat_bin, pat_remap, num_taud_pats = deduplicate_patterns(bytes(pat_bin), orig_count)
|
pat_bin, pat_remap, num_taud_pats = deduplicate_patterns(bytes(pat_bin), orig_count)
|
||||||
vprint(f" patterns: {orig_count} → {num_taud_pats} unique ({orig_count - num_taud_pats} deduplicated)")
|
vprint(f" patterns: {orig_count} → {num_taud_pats} unique ({orig_count - num_taud_pats} deduplicated)")
|
||||||
|
|
||||||
# Song table row (16 bytes): offset(4)+voices(1)+patsLo(1)+patsHi(1)+bpm(1)+tick(1)+basenote(2)+basefreq(4)+pad(1)
|
# Song table row (16 bytes): offset(4)+voices(1)+pats(2)+bpm(1)+tick(1)+basenote(2)+basefreq(4)+flags(1)
|
||||||
# Built after dedup so num_taud_pats reflects the unique count.
|
# Built after dedup so num_taud_pats reflects the unique count.
|
||||||
song_table = struct.pack('<IBHBBHf',
|
# flags byte: bit 1 (f) = Amiga pitch-slide mode (mirrors the S3M linear_slides flag inverted)
|
||||||
|
flags_byte = 0x00 if h.linear_slides else 0x02
|
||||||
|
song_table = struct.pack('<IBHBBHfB',
|
||||||
song_offset,
|
song_offset,
|
||||||
C,
|
C,
|
||||||
num_taud_pats,
|
num_taud_pats,
|
||||||
bpm_stored,
|
bpm_stored,
|
||||||
speed,
|
speed,
|
||||||
0x9000, # C8
|
0xA000, # C9
|
||||||
8363.0, # Hz
|
8363.0, # Hz
|
||||||
) + b'\x00'
|
flags_byte,
|
||||||
|
)
|
||||||
assert len(song_table) == TAUD_SONG_ENTRY
|
assert len(song_table) == TAUD_SONG_ENTRY
|
||||||
|
|
||||||
# Cue sheet (using remapped pattern indices)
|
# Cue sheet (using remapped pattern indices)
|
||||||
|
|||||||
@@ -2121,6 +2121,13 @@ Play Head Flags
|
|||||||
NOTE: changing from PCM mode to Tracker mode or vice versa will also reset the parameters as described above
|
NOTE: changing from PCM mode to Tracker mode or vice versa will also reset the parameters as described above
|
||||||
Byte 2
|
Byte 2
|
||||||
- PCM Mode: Write non-zero value to start uploading; always 0 when read
|
- 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
|
||||||
|
p: panning mode (0: linear, 1: equal-power)
|
||||||
|
f: pitchshift mode (0: tone-linear, 1: Amiga)
|
||||||
|
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.
|
||||||
Byte 3 (Tracker Mode)
|
Byte 3 (Tracker Mode)
|
||||||
- BPM (24 to 279. Play Data will change this register)
|
- BPM (24 to 279. Play Data will change this register)
|
||||||
Byte 4 (Tracker Mode)
|
Byte 4 (Tracker Mode)
|
||||||
@@ -2226,24 +2233,24 @@ Endianness: Little
|
|||||||
Uint16 Number of patterns (0 is invalid. pattern bin length = numPats * 8 bytes)
|
Uint16 Number of patterns (0 is invalid. pattern bin length = numPats * 8 bytes)
|
||||||
Uint8 Initial BPM (bias of -24. 0x00=24, 0xFF=279)
|
Uint8 Initial BPM (bias of -24. 0x00=24, 0xFF=279)
|
||||||
Uint8 Initial Tickrate (0 is invalid)
|
Uint8 Initial Tickrate (0 is invalid)
|
||||||
Uint16 Current Tuning base note (1..65533). A3 (western default) is 0x4C00. C8 (tracker default) is 0x9000. If zero, assume the tracker default value
|
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
|
Float32 Frequency at the base note. Tracker default is 8363.0. If zero, assume the tracker default
|
||||||
Byte[1] Reserved for future versions
|
Uint8 Flags for Global Behaviour (effect symbol '1')
|
||||||
|
|
||||||
Taud device can queue up to 2 "playdata" in its buffer, which can be interpreted as a song.
|
Taud device can queue up to 2 "playdata" in its buffer, which can be interpreted as a song.
|
||||||
|
|
||||||
* Known standard tunings
|
* Known standard tunings:
|
||||||
A440. ISO standard
|
A4 @ 440 Hz. ISO standard
|
||||||
A435. Former French standard (year 1859)
|
A4 @ 435 Hz. Former French standard (year 1859)
|
||||||
A452. Old Philharmonic pitch (19th century Britain)
|
A4 @ 452 Hz. Old Philharmonic pitch (19th century Britain)
|
||||||
C256. Power of two
|
C4 @ 256 Hz. Power of two
|
||||||
C262. Modern Chinese a-ak tuning convention
|
C4 @ 262 Hz. Modern Chinese a-ak tuning convention
|
||||||
C311. Korean hyang-ak tuning standard (ROK National Gugak Center)
|
C4 @ 311 Hz. Korean hyang-ak tuning standard (ROK National Gugak Center)
|
||||||
|
|
||||||
For your reference, tracker default tuning at A3 is 439.526 Hz (8363*2^(3/4) / 32)
|
For your reference, tracker default tuning at A4 is 439.526 Hz (8363*2^(3/4) / 32)
|
||||||
|
|
||||||
## Pattern Bin and Cue Sheet
|
## Pattern Bin and Cue Sheet
|
||||||
Pattern Bin/Cue Sheet images (GZip or
|
Pattern Bin/Cue Sheet images
|
||||||
|
|
||||||
## Project Data
|
## Project Data
|
||||||
|
|
||||||
@@ -2294,19 +2301,16 @@ prefixes:
|
|||||||
Byte[*] Song composer, null terminated. Encoding: UTF-8
|
Byte[*] Song composer, null terminated. Encoding: UTF-8
|
||||||
Byte[*] Song copyright string, null terminated. Encoding: UTF-8
|
Byte[*] Song copyright string, null terminated. Encoding: UTF-8
|
||||||
|
|
||||||
* nota. Custom notation definition
|
* nota. Custom notation definition (version 'a')
|
||||||
* Repetition of:
|
* Repetition of:
|
||||||
Uint8 Notation index (starting from zero) used by songs
|
Uint8 Notation index (starting from zero) used by songs
|
||||||
Uint32 Size of this notation following this field
|
Uint32 Size of this notation following this field
|
||||||
Uint8 Flags
|
Uint16 Reserved for flags
|
||||||
0b 0000 000t
|
Float32 Interval size (octave system = 2.0f). If you are not using an interval system (which means you are responsible for defining every note expressible), this must be NaN. 0f and Infinity are considered illegal
|
||||||
t: NOT using interval system (you are responsible for defining every notes expressible)
|
Uint16 Notes between interval MINUS ONE (or octave); 12-TET will have value 11
|
||||||
Uint8 Reserved
|
|
||||||
Float32 Interval size (octave system = 2.0f). If Flag 't' is set, this must be NaN. 0f and Infinity are considered illegal
|
|
||||||
Uint16 Notes between interval MINUS ONE (or octave); 12-TET will have value 11. 0 is considered illegal
|
|
||||||
Byte[8] Reserved
|
Byte[8] Reserved
|
||||||
Byte[*] Name, null terminated. Encoding: UTF-8
|
Byte[*] Name, null terminated. Encoding: UTF-8
|
||||||
Byte[*] Notation table. 0xFF-separated and null-terminated. Encoding: raw bytes
|
Byte[*] Notation table. 0xFF-separated and null-terminated. Encoding: Taud charset
|
||||||
Uint16[*] Frequency table. Size of the table is defined by "Notes between interval MINUS ONE". This is a lookup table of relative pitch offsets (against the base tuning note) in 4096-TET space. Index zero of this table will be 0x0 if you read the spec right
|
Uint16[*] Frequency table. Size of the table is defined by "Notes between interval MINUS ONE". This is a lookup table of relative pitch offsets (against the base tuning note) in 4096-TET space. Index zero of this table will be 0x0 if you read the spec right
|
||||||
|
|
||||||
Note: custom notations will use internal index 65535 down to 65520 (index 0 = 65535, index 15 = 65520)
|
Note: custom notations will use internal index 65535 down to 65520 (index 0 = 65535, index 15 = 65520)
|
||||||
@@ -2318,7 +2322,12 @@ prefixes:
|
|||||||
4. Frequency-Offset Table from the previous step will be applied against the "Base Note at C3" to construct the notes within the notation. Value at index zero of the Frequency Table must be 0
|
4. Frequency-Offset Table from the previous step will be applied against the "Base Note at C3" to construct the notes within the notation. Value at index zero of the Frequency Table must be 0
|
||||||
5. The progress will continue outside the "root interval" (C3..C4) to build a complete note-to-frequency table
|
5. The progress will continue outside the "root interval" (C3..C4) to build a complete note-to-frequency table
|
||||||
|
|
||||||
Note: if your sample is pre-tuned for your system, keep the project setting as A4,440Hz. If you are not working with the conventional octave system, you still need to specify the Interval Size
|
Note: if your sample is pre-tuned for your system, keep the project setting as the defaults. If you are not working with the conventional octave system, you still need to specify the Interval Size
|
||||||
|
|
||||||
|
* Suggested notation serialisation format (for notation editor, etc.)
|
||||||
|
Byte[8] Magic (\x1E T a u d n o t)
|
||||||
|
Uint8 Version (Ascii 'a')
|
||||||
|
Bytes Notation definitions (see above)
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import net.torvald.tsvm.VM
|
|||||||
import net.torvald.tsvm.toInt
|
import net.torvald.tsvm.toInt
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.log2
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
@@ -124,6 +125,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
const val SAMPLING_RATE = 32000
|
const val SAMPLING_RATE = 32000
|
||||||
const val TRACKER_CHUNK = 512
|
const val TRACKER_CHUNK = 512
|
||||||
const val TRACKER_C3 = 0x4000
|
const val TRACKER_C3 = 0x4000
|
||||||
|
// Amiga period at TRACKER_C3 for a standard 8363 Hz instrument (NTSC clock 3579545 Hz).
|
||||||
|
// Used to implement Amiga-mode pitch slides (effect '1' f-bit or song-table flag).
|
||||||
|
const val AMIGA_BASE_PERIOD = 214.0
|
||||||
|
// Scale factor that converts a Taud coarse-slide unit back to one Amiga period unit.
|
||||||
|
// Taud coarse unit = round(ST3_unit × 64/3), so the inverse is × 3/64.
|
||||||
|
const val AMIGA_PERIOD_SCALE = 3.0 / 64.0
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val sampleBin = UnsafeHelper.allocate(770048L, this)
|
internal val sampleBin = UnsafeHelper.allocate(770048L, this)
|
||||||
@@ -1165,6 +1172,16 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
private fun computePlaybackRate(inst: TaudInst, noteVal: Int): Double =
|
private fun computePlaybackRate(inst: TaudInst, noteVal: Int): Double =
|
||||||
inst.samplingRate.toDouble() / SAMPLING_RATE * 2.0.pow((noteVal - TRACKER_C3) / 4096.0)
|
inst.samplingRate.toDouble() / SAMPLING_RATE * 2.0.pow((noteVal - TRACKER_C3) / 4096.0)
|
||||||
|
|
||||||
|
// Applies one tick of Amiga-mode pitch slide. slideArg uses the same sign convention as
|
||||||
|
// linear mode: negative = pitch down (E effect), positive = pitch up (F effect).
|
||||||
|
// The Taud coarse-slide value is converted back to Amiga period units via AMIGA_PERIOD_SCALE.
|
||||||
|
private fun amigaSlide(noteVal: Int, slideArg: Int): Int {
|
||||||
|
val period = AMIGA_BASE_PERIOD * 2.0.pow(-(noteVal - TRACKER_C3).toDouble() / 4096.0)
|
||||||
|
// Negate slideArg: pitch down (slideArg < 0) → period up, pitch up (slideArg > 0) → period down.
|
||||||
|
val newPeriod = (period - slideArg * AMIGA_PERIOD_SCALE).coerceAtLeast(1.0)
|
||||||
|
return (TRACKER_C3 + 4096.0 * log2(AMIGA_BASE_PERIOD / newPeriod)).roundToInt()
|
||||||
|
}
|
||||||
|
|
||||||
private fun advanceEnvelope(voice: Voice, inst: TaudInst, tickSec: Double) {
|
private fun advanceEnvelope(voice: Voice, inst: TaudInst, tickSec: Double) {
|
||||||
// Volume envelope
|
// Volume envelope
|
||||||
// sustain byte: bit7=enabled, bits[5:3]=end_idx, bits[2:0]=start_idx
|
// sustain byte: bit7=enabled, bits[5:3]=end_idx, bits[2:0]=start_idx
|
||||||
@@ -1406,8 +1423,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
when (op) {
|
when (op) {
|
||||||
EffectOp.OP_NONE -> {}
|
EffectOp.OP_NONE -> {}
|
||||||
EffectOp.OP_1 -> {
|
EffectOp.OP_1 -> {
|
||||||
// 1 $01xx — Set stereo panning law. High byte selects subcommand; only $01 is defined.
|
// 1 $xx00 — Global behaviour flags byte in the high byte (see TAUD_NOTE_EFFECTS.md §1).
|
||||||
if ((rawArg ushr 8) == 0x01) ts.panLaw = rawArg and 0xFF
|
// bit 0 (p): 0=linear pan, 1=equal-power pan
|
||||||
|
// bit 1 (f): 0=linear pitch slides, 1=Amiga-mode pitch slides
|
||||||
|
val flags = rawArg ushr 8
|
||||||
|
ts.panLaw = flags and 1
|
||||||
|
ts.amigaMode = (flags and 2) != 0
|
||||||
}
|
}
|
||||||
EffectOp.OP_A -> {
|
EffectOp.OP_A -> {
|
||||||
val tr = (rawArg ushr 8) and 0xFF
|
val tr = (rawArg ushr 8) and 0xFF
|
||||||
@@ -1609,7 +1630,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
|
|
||||||
// Pitch slides (E/F coarse on tick > 0).
|
// Pitch slides (E/F coarse on tick > 0).
|
||||||
if (ts.tickInRow > 0 && (voice.slideMode == 1 || voice.slideMode == 2)) {
|
if (ts.tickInRow > 0 && (voice.slideMode == 1 || voice.slideMode == 2)) {
|
||||||
voice.noteVal = (voice.noteVal + voice.slideArg).coerceIn(0, 0xFFFE)
|
voice.noteVal = if (ts.amigaMode)
|
||||||
|
amigaSlide(voice.noteVal, voice.slideArg).coerceIn(0, 0xFFFE)
|
||||||
|
else
|
||||||
|
(voice.noteVal + voice.slideArg).coerceIn(0, 0xFFFE)
|
||||||
voice.basePitch = voice.noteVal
|
voice.basePitch = voice.noteVal
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2071,7 +2095,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
val voices = Array(20) { Voice() }
|
val voices = Array(20) { Voice() }
|
||||||
|
|
||||||
// Global mixer config (effect 1).
|
// Global mixer config (effect 1).
|
||||||
var panLaw = 0 // 0 = linear balance (default), 1 = equal-power
|
var panLaw = 0 // 0 = linear balance (default), 1 = equal-power
|
||||||
|
var amigaMode = false // false = linear pitch slides, true = Amiga period-space slides
|
||||||
|
|
||||||
// Pending row-end events (set during a row by B/C; consumed at row end).
|
// 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
|
var pendingOrderJump = -1 // -1 = none; otherwise the order index to jump to
|
||||||
@@ -2111,6 +2136,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
) {
|
) {
|
||||||
var trackerState: TrackerState? = TrackerState() // default mode is tracker (isPcmMode=false)
|
var trackerState: TrackerState? = TrackerState() // default mode is tracker (isPcmMode=false)
|
||||||
|
|
||||||
|
// Initial global behaviour flags (song-table byte, written via MMIO register 7 in tracker mode).
|
||||||
|
// Applied to TrackerState on every resetParams(); in-pattern effect '1' can override later.
|
||||||
|
var initialGlobalFlags: Int = 0
|
||||||
|
|
||||||
// flags
|
// flags
|
||||||
var isPcmMode: Boolean = false
|
var isPcmMode: Boolean = false
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -2140,7 +2169,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
4 -> masterVolume.toByte()
|
4 -> masterVolume.toByte()
|
||||||
5 -> masterPan.toByte()
|
5 -> masterPan.toByte()
|
||||||
6 -> (isPcmMode.toInt(7) or isPlaying.toInt(4) or pcmQueueSizeIndex.and(15)).toByte()
|
6 -> (isPcmMode.toInt(7) or isPlaying.toInt(4) or pcmQueueSizeIndex.and(15)).toByte()
|
||||||
7 -> 0
|
7 -> initialGlobalFlags.toByte()
|
||||||
8 -> (bpm - 24).toByte()
|
8 -> (bpm - 24).toByte()
|
||||||
9 -> tickRate.toByte()
|
9 -> tickRate.toByte()
|
||||||
else -> throw InternalError("Bad offset $index")
|
else -> throw InternalError("Bad offset $index")
|
||||||
@@ -2165,7 +2194,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
pcmQueueSizeIndex = (it and 0b00001111)
|
pcmQueueSizeIndex = (it and 0b00001111)
|
||||||
if (it and 0b00100000 != 0) purgeQueue()
|
if (it and 0b00100000 != 0) purgeQueue()
|
||||||
} }
|
} }
|
||||||
7 -> if (isPcmMode) { pcmUpload = true } else {}
|
7 -> if (isPcmMode) { pcmUpload = true } else {
|
||||||
|
initialGlobalFlags = byte
|
||||||
|
trackerState?.let { ts -> ts.panLaw = byte and 1; ts.amigaMode = (byte and 2) != 0 }
|
||||||
|
}
|
||||||
8 -> { bpm = byte + 24 }
|
8 -> { bpm = byte + 24 }
|
||||||
9 -> { tickRate = byte }
|
9 -> { tickRate = byte }
|
||||||
else -> throw InternalError("Bad offset $index")
|
else -> throw InternalError("Bad offset $index")
|
||||||
@@ -2194,7 +2226,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
ts.pendingOrderJump = -1; ts.pendingRowJump = -1
|
ts.pendingOrderJump = -1; ts.pendingRowJump = -1
|
||||||
ts.patternDelayRemaining = 0; ts.patternDelayActive = false
|
ts.patternDelayRemaining = 0; ts.patternDelayActive = false
|
||||||
ts.sexWinningChannel = -1
|
ts.sexWinningChannel = -1
|
||||||
ts.panLaw = 0
|
ts.panLaw = initialGlobalFlags and 1
|
||||||
|
ts.amigaMode = (initialGlobalFlags and 2) != 0
|
||||||
ts.voices.forEach {
|
ts.voices.forEach {
|
||||||
it.active = false
|
it.active = false
|
||||||
it.channelVolume = 0x3F
|
it.channelVolume = 0x3F
|
||||||
|
|||||||
Reference in New Issue
Block a user