Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion doc/spec_SGU.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ WavetableとPCMは、位相の整数部が指す標本 `a` と次の標本 `b`
- Wavetable: 次の標本はインデックス`+1`(`0xFF`の次は`0x00`)。
- PCM: 次の標本はアドレス`+1`。ただし`+1`が PCM_END 以上になる場合は、ループ有効なら PCM_LOOP の標本、無効なら現在の標本を使う(一回再生の末尾は保持)。
- Phase変調は位相の整数部へ加算する。Ring変調は補間後の値に適用する。Slew Rate/Step は補間後のQ8.8値を入力とし、v2.1では状態の小数部を出力に残す。Hard Sync/Windowの標本置換は補間しない。
- Noise、SQV4 は補間しない。
- SQV4: デコード後の標本(フラグメント音量適用後)を同じ式で補間する。次の標本は次のフレームの先頭でもよい。最後の標本の次は、ループ有無にかかわらず最後の標本を保持する(ループ先頭へは補間しない)。
- Noise は補間しない。

## 3. インターフェース

Expand Down
7 changes: 7 additions & 0 deletions memory/2026-10-24-issue-158-sqv4-interpolation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# SGU v2.1: SQV4 の線形補間 (#158)

- SQV4 の補間は新しい版ではなく、v2.1(3WS8PNA)の補間機能の不足分の追加。`SguVersion` やチップ名は上げない。v1/v2 は従来と bit 一致(`frac` を無視)。
- 次の標本がフレーム境界(63→64)を越えるため、デコード済みフレームを 2 枚(`frame & 1`)保持する。デコーダは順方向にしか進まないので、`ensure_sqv4_frame` は目的フレームより後ろへ戻るときだけ先頭から再デコードする。
- 最後の標本の次の tap は保持(ループ先頭へは補間しない)。ループ先頭の標本を読むとデコーダ再開が毎 tick 起きるため。
- SQV4 の音量は Q8.8 のまま `q8 * volume / (255*256)` で適用する(整数 q8 なら従来の `sample*volume/255` と一致)。
- 検証: `cargo test --lib devices::sgu`(`sqv4_two_tap_interpolation_crosses_frames_and_holds_at_end`)。外部 golden に依存する SQV4 テストは無効のまま。
144 changes: 103 additions & 41 deletions src/devices/sgu/s3w2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,12 @@ pub struct Channel {
sqv4_sample_count: usize,
sqv4_frame_size: usize,
sqv4_loop_sample: usize,
sqv4_cached_frame: Option<usize>,
// Two 64-sample frames are kept (indexed by frame & 1) so a sample can be
// interpolated with the first sample of the following frame.
sqv4_cached_frame: [Option<usize>; 2],
sqv4_next_frame: usize,
sqv4_decoder: Sqv4State,
sqv4_frame: [i16; 64],
sqv4_frame: [[i16; 64]; 2],

// Internal state
pub old_phase: u64,
Expand Down Expand Up @@ -177,10 +179,10 @@ impl Default for Channel {
sqv4_sample_count: 0,
sqv4_frame_size: 0,
sqv4_loop_sample: 0,
sqv4_cached_frame: None,
sqv4_cached_frame: [None; 2],
sqv4_next_frame: 0,
sqv4_decoder: Sqv4State::default(),
sqv4_frame: [0; 64],
sqv4_frame: [[0; 64]; 2],
old_phase: 0,
lfsr_state: 0x12D4_803C,
phase: 0.0,
Expand Down Expand Up @@ -260,10 +262,10 @@ impl Channel {
self.sqv4_sample_count = 0;
self.sqv4_frame_size = 0;
self.sqv4_loop_sample = 0;
self.sqv4_cached_frame = None;
self.sqv4_cached_frame = [None; 2];
self.sqv4_next_frame = 0;
self.sqv4_decoder = Sqv4State::default();
self.sqv4_frame.fill(0);
self.sqv4_frame = [[0; 64]; 2];
}
}

Expand Down Expand Up @@ -644,7 +646,7 @@ impl S3w2Sound {
WaveformType::Pcm => self.generate_pcm_sample(ch),
WaveformType::Noise => i32::from(self.generate_noise_sample(ch)) << 8,
WaveformType::DmaPcm => i32::from(self.generate_dma_pcm_sample(ch)) << 8,
WaveformType::Sqv4 => i32::from(self.generate_sqv4_sample(ch)) << 8,
WaveformType::Sqv4 => self.generate_sqv4_sample(ch),
WaveformType::FifoDac => {
// 16-bit sample, fixed gain: VOLUME does not apply.
let sample = self.generate_fifo_sample(ch);
Expand All @@ -656,7 +658,7 @@ impl S3w2Sound {
self.channels[ch].last_sample = (q8 >> 8) as i16;
let volume = self.channels[ch].volume as i32;
if waveform == WaveformType::Sqv4 {
((q8 >> 8) * volume / 255) as i16
(q8 * volume / (255 * 256)) as i16
} else {
(q8 * volume / (4 * 256)) as i16
}
Expand Down Expand Up @@ -866,23 +868,59 @@ impl S3w2Sound {
channel.sqv4_sample_count = header.samples_per_channel;
channel.sqv4_frame_size = header.frame_size;
channel.sqv4_loop_sample = loop_sample;
channel.sqv4_cached_frame = None;
channel.sqv4_cached_frame = [None; 2];
channel.sqv4_next_frame = 0;
channel.sqv4_decoder = Sqv4State::default();
true
}

fn generate_sqv4_sample(&mut self, ch: usize) -> i16 {
/// Decode frames in order until `target` is cached (restarting from frame 0
/// when it lies before the decoder position).
fn ensure_sqv4_frame(channel: &mut Channel, pcm_ram: &[u8], start: usize, target: usize) {
if channel.sqv4_cached_frame[target & 1] == Some(target) {
return;
}
if target < channel.sqv4_next_frame {
channel.sqv4_decoder = Sqv4State::default();
channel.sqv4_next_frame = 0;
channel.sqv4_cached_frame = [None; 2];
}
let frame_size = channel.sqv4_frame_size;
let variant = channel.sqv4_variant.unwrap();
let version = channel.sqv4_version;
while channel.sqv4_next_frame <= target {
let decode_frame = channel.sqv4_next_frame;
let fragment_offset = start + 16 + decode_frame * frame_size;
let volume = pcm_ram[fragment_offset] as i32;
let payload = &pcm_ram[fragment_offset + 1..fragment_offset + frame_size];
let count = (channel.sqv4_sample_count - decode_frame * 64).min(64);
for within in 0..count {
let code = sqv4::unpack_code(payload, within, variant.bits());
let reconstructed =
sqv4::decode_code(version, variant, &mut channel.sqv4_decoder, code);
channel.sqv4_frame[decode_frame & 1][within] = (reconstructed * volume / 255) as i16;
}
channel.sqv4_cached_frame[decode_frame & 1] = Some(decode_frame);
channel.sqv4_next_frame += 1;
}
}

/// Returns the decoded sample in Q8.8 (sample units << 8). v2.1 blends with
/// the next decoded sample; the tap after the last sample holds the last
/// one (no loop wrap, which would force a decoder restart on every tick).
fn generate_sqv4_sample(&mut self, ch: usize) -> i32 {
if !self.channels[ch].sqv4_validated && !self.initialize_sqv4(ch) {
return 0;
}
let interpolate = self.version.has_3ws8pna();
let start = self.channels[ch].pcm_start_addr as usize;
let mut sample_index = (self.channels[ch].phase * 256.0) as usize;
let sample_count = self.channels[ch].sqv4_sample_count;
if sample_count == 0 {
self.channels[ch].active = false;
return 0;
}
let (sample_index, frac) = Self::split_phase(self.channels[ch].phase);
let mut sample_index = sample_index as usize;
if sample_index >= sample_count {
if self.channels[ch].pcm_control & 0x02 == 0 {
self.channels[ch].active = false;
Expand All @@ -891,38 +929,20 @@ impl S3w2Sound {
sample_index = self.channels[ch].sqv4_loop_sample;
self.channels[ch].phase = sample_index as f64 / 256.0;
}
let frame_index = sample_index / 64;
let within_frame = sample_index % 64;
let frame_size = self.channels[ch].sqv4_frame_size;
let variant = self.channels[ch].sqv4_variant.unwrap();
let version = self.channels[ch].sqv4_version;
let pcm_ram = self.pcm_ram.borrow();
let channels = &mut self.channels;
let channel = &mut channels[ch];
if channel.sqv4_cached_frame != Some(frame_index) {
if frame_index < channel.sqv4_next_frame {
channel.sqv4_decoder = Sqv4State::default();
channel.sqv4_next_frame = 0;
}
while channel.sqv4_next_frame <= frame_index {
let decode_frame = channel.sqv4_next_frame;
let fragment_offset = start + 16 + decode_frame * frame_size;
let volume = pcm_ram[fragment_offset] as i32;
let payload = &pcm_ram[fragment_offset + 1..fragment_offset + frame_size];
let count = (sample_count - decode_frame * 64).min(64);
for within in 0..count {
let code = sqv4::unpack_code(payload, within, variant.bits());
let reconstructed =
sqv4::decode_code(version, variant, &mut channel.sqv4_decoder, code);
channel.sqv4_frame[within] = (reconstructed * volume / 255) as i16;
}
channel.sqv4_cached_frame = Some(decode_frame);
channel.sqv4_next_frame += 1;
}
}
let sample = channel.sqv4_frame[within_frame];
let channel = &mut self.channels[ch];
Self::ensure_sqv4_frame(channel, &pcm_ram, start, sample_index / 64);
let current = i32::from(channel.sqv4_frame[(sample_index / 64) & 1][sample_index % 64]);
let q8 = if interpolate && frac != 0 && sample_index + 1 < sample_count {
let next_index = sample_index + 1;
Self::ensure_sqv4_frame(channel, &pcm_ram, start, next_index / 64);
let next = i32::from(channel.sqv4_frame[(next_index / 64) & 1][next_index % 64]);
current * (256 - frac) + next * frac
} else {
current << 8
};
channel.phase += (channel.frequency as f64) / (SOUND_CLOCK as f64) / 8.0;
sample
q8
}

fn generate_noise_sample(&mut self, ch: usize) -> i16 {
Expand Down Expand Up @@ -1130,6 +1150,48 @@ mod tests {
assert_eq!(sound.generate_sample(0), 50 - 128);
}

fn sqv4_sound(version: SguVersion, bytes: &[u8]) -> S3w2Sound {
let mut sound =
S3w2Sound::with_version(Rc::new(RefCell::new(vec![0; PCM_RAM_SIZE])), version);
for (offset, byte) in bytes.iter().copied().enumerate() {
sound.write_pcm_ram(256 + offset as u32, byte);
}
let end = 256 + bytes.len() as u32;
for (base, address) in [(0x810, 256u32), (0x813, end), (0x816, 256)] {
sound.write_register(base, (address >> 16) as u8);
sound.write_register(base + 1, (address >> 8) as u8);
sound.write_register(base + 2, address as u8);
}
sound.write_register(0x800, 0x0B); // 3000 Hz => 0.5 sample per call
sound.write_register(0x801, 0xB8);
sound.write_register(0x802, 4);
sound.write_register(0x803, 255);
sound.write_register(0x819, 0x01);
sound
}

#[test]
fn sqv4_two_tap_interpolation_crosses_frames_and_holds_at_end() {
let pcm: Vec<i16> = (0..130).map(|i| i * 40).collect();
let bytes = super::sqv4::encode_interleaved(&pcm, 1, 8_000, Sqv4Variant::H, 255).unwrap();
let decoded = super::sqv4::decode(&bytes).unwrap().samples;
assert_eq!(decoded.len(), 130);

let mut v2 = sqv4_sound(SguVersion::V2, &bytes);
let mut v21 = sqv4_sound(SguVersion::V2_1, &bytes);
for k in 0..129usize {
// Even calls land on a sample, odd calls halfway to the next one.
assert_eq!(v2.generate_sample(0), decoded[k], "v2 sample {k}");
assert_eq!(v2.generate_sample(0), decoded[k], "v2 half {k}");
assert_eq!(v21.generate_sample(0), decoded[k], "v2.1 sample {k}");
let half = ((i32::from(decoded[k]) * 128 + i32::from(decoded[k + 1]) * 128) / 256) as i16;
assert_eq!(v21.generate_sample(0), half, "v2.1 half {k}");
}
// The last sample has no following tap: it is held, not blended.
assert_eq!(v21.generate_sample(0), decoded[129]);
assert_eq!(v21.generate_sample(0), decoded[129]);
}

#[test]
fn fifo_dac_is_16_bit_fixed_gain_with_decimation_and_pan() {
let mut sound = v21_sound();
Expand Down
Loading