From fa99395580f7849df44a3124e6398d945be00264 Mon Sep 17 00:00:00 2001 From: Rekushi <170186554+RekuNote@users.noreply.github.com> Date: Tue, 20 Oct 2026 04:01:33 +0100 Subject: [PATCH] corrected switchnote speed --- sppm-js/index.js | 15 ++++++++----- sppm-js/test/sppm.test.js | 12 +++++++++-- .../__pycache__/sppm_tools.cpython-310.pyc | Bin 0 -> 18457 bytes .../test_sppm_tools.cpython-310.pyc | Bin 0 -> 3787 bytes sppm-python/sppm_tools.py | 20 +++++++++++++----- sppm-python/test_sppm_tools.py | 14 ++++++++---- 6 files changed, 45 insertions(+), 16 deletions(-) create mode 100644 sppm-python/__pycache__/sppm_tools.cpython-310.pyc create mode 100644 sppm-python/__pycache__/test_sppm_tools.cpython-310.pyc diff --git a/sppm-js/index.js b/sppm-js/index.js index ab1a18b..9a060c6 100644 --- a/sppm-js/index.js +++ b/sppm-js/index.js @@ -159,7 +159,7 @@ export async function readSppm(input, { legacyHmacKey = null } = {}) { r.take(4); const version = r.u16(), headerSize = r.u16(), totalBytes = r.u64(), expectedCrc = r.u32(); const width = r.u16(), height = r.u16(), layerCount = r.u8(), paletteCount = r.u16(), fps = r.u8(), flags = r.u8(), frameCount = r.u32(), audioCount = r.u8(), reserved = r.u16(); - if ((version !== 1 && version !== 2 && version !== 3) || headerSize !== HEADER_SIZE || totalBytes !== body.length || width !== WIDTH || height !== HEIGHT || layerCount !== 4 || paletteCount !== 256 || fps < 1 || fps > 30 || frameCount < 1 || frameCount > 9999 || audioCount > 4 || reserved !== 0 || ((version === 1 ? flags & ~3 : flags & ~15) !== 0)) fail("unsupported SPPM header"); + if ((version !== 1 && version !== 2 && version !== 3) || headerSize !== HEADER_SIZE || totalBytes !== body.length || width !== WIDTH || height !== HEIGHT || layerCount !== 4 || paletteCount !== 256 || fps < 1 || fps > 30 || frameCount < 1 || frameCount > 9999 || audioCount > 4 || (version < 3 ? reserved !== 0 : reserved > 8) || ((version === 1 ? flags & ~3 : flags & ~15) !== 0)) fail("unsupported SPPM header"); if (crc32(body.subarray(HEADER_SIZE)) !== expectedCrc) fail("payload CRC mismatch"); const paletteBytes = r.take(256 * 4), palette = new Array(256); @@ -167,7 +167,9 @@ export async function readSppm(input, { legacyHmacKey = null } = {}) { const metadata = { title: r.string(256), description: r.string(4096), authorName: r.string(128), originalAuthorName: r.string(128), lastEditorName: r.string(128), authorFsid: r.id(), originalAuthorFsid: r.id(), lastEditorFsid: r.id(), currentId: r.id(), rootId: r.id(), parentId: r.id(true), - createdUnixSeconds: r.u64(), modifiedUnixSeconds: r.u64(), coverFrame: r.u32(), width, height, fps, + createdUnixSeconds: r.u64(), modifiedUnixSeconds: r.u64(), coverFrame: r.u32(), width, height, + speed: version >= 3 ? reserved : 0, nominalFps: fps, + fps: version >= 3 && reserved >= 1 ? [0, 0.4985, 1, 2, 4, 5.9972, 11.986, 19.9459, 29.9275][reserved] : fps, locked: !!(flags & 1), allowSpinoffs: !!(flags & 2), bgmFollowsSpeed: version === 1 || !!(flags & 4), loopPlayback: version === 1 ? true : !(flags & 8), frameCount, audioTrackCount: audioCount, formatVersion: version, signature: signature.status, }; @@ -201,7 +203,7 @@ export async function readSppm(input, { legacyHmacKey = null } = {}) { const audioTracks = []; let audioSamples = 0; for (let ti = 0; ti < audioCount; ti++) { - const index = r.u8(), channels = r.u8(), referenceFps = version >= 2 ? r.u8() : 0, sampleRate = r.u32(), name = r.string(64), triggerCount = r.u16(); + const index = r.u8(), channels = r.u8(), storedReference = version >= 2 ? r.u8() : 0, sampleRate = r.u32(), name = r.string(64), triggerCount = r.u16(); if (index !== ti || (channels !== 1 && channels !== 2) || sampleRate < 8000 || sampleRate > 48000 || triggerCount > frameCount || (ti === 0 && triggerCount !== 0)) fail("invalid audio metadata"); const triggers = Array.from({ length: triggerCount }, () => r.u16()); for (let i = 0; i < triggers.length; i++) if (triggers[i] >= frameCount || (i && triggers[i] <= triggers[i - 1])) fail("invalid sound-effect frame cues"); @@ -212,7 +214,10 @@ export async function readSppm(input, { legacyHmacKey = null } = {}) { const pcm = new Int16Array(sampleCount); const sampleView = new DataView(pcmBytes.buffer, pcmBytes.byteOffset, pcmBytes.byteLength); for (let i = 0; i < sampleCount; i++) pcm[i] = sampleView.getInt16(i * 2, true); - audioTracks.push({ index, name, channels, referenceFps: ti === 0 && version === 1 ? fps : referenceFps, sampleRate, frameTriggers: triggers, pcm }); + const referenceSpeed = version >= 3 && reserved !== 0 && ti === 0 ? storedReference : 0; + const referenceFps = version >= 3 && reserved !== 0 ? 0 : ti === 0 && version === 1 ? fps : storedReference; + if (version >= 3 && reserved !== 0 && (ti === 0 ? storedReference > 8 : storedReference !== 0)) fail("invalid audio speed metadata"); + audioTracks.push({ index, name, channels, referenceFps, referenceSpeed, sampleRate, frameTriggers: triggers, pcm }); } if (r.at !== body.length) fail("unexpected trailing bytes"); return { width, height, palette, metadata, frames, audioTracks, signature }; @@ -333,7 +338,7 @@ export class SppmPlayer { } if (track.index === 0) { const source = this.audioContext.createBufferSource(); source.buffer = buffer; - if (this.note.metadata.bgmFollowsSpeed) source.playbackRate.value = fps / (track.referenceFps || fps); + if (this.note.metadata.bgmFollowsSpeed) source.playbackRate.value = fps / (track.referenceSpeed ? [0, 0.4985, 1, 2, 4, 5.9972, 11.986, 19.9459, 29.9275][track.referenceSpeed] : (track.referenceFps || fps)); source.loop = !!this.loop; source.connect(this.audioContext.destination); source.start(start); this.sources.push(source); } else for (const cue of track.frameTriggers) { const source = this.audioContext.createBufferSource(); source.buffer = buffer; source.connect(this.audioContext.destination); diff --git a/sppm-js/test/sppm.test.js b/sppm-js/test/sppm.test.js index 5c1ec69..c74b6e2 100644 --- a/sppm-js/test/sppm.test.js +++ b/sppm-js/test/sppm.test.js @@ -7,14 +7,14 @@ function crc32(bytes) { for (const b of bytes) { crc ^= b; for (let i = 0; i < 8; i++) crc = (crc >>> 1) ^ ((crc & 1) ? 0xedb88320 : 0); } return (crc ^ 0xffffffff) >>> 0; } -function fixture() { +function fixture(version = 2, speed = 0) { const parts = []; const u8 = n => parts.push(Buffer.from([n & 255])); const u16 = n => { const b = Buffer.alloc(2); b.writeUInt16LE(n); parts.push(b); }; const u32 = n => { const b = Buffer.alloc(4); b.writeUInt32LE(n); parts.push(b); }; const u64 = n => { const b = Buffer.alloc(8); b.writeBigUInt64LE(BigInt(n)); parts.push(b); }; const str = s => { const b = Buffer.from(s); u16(b.length); parts.push(b); }; - parts.push(Buffer.from("SPPM")); u16(2); u16(36); u64(0); u32(0); u16(640); u16(360); u8(4); u16(256); u8(12); u8(2); u32(1); u8(0); u16(0); + parts.push(Buffer.from("SPPM")); u16(version); u16(36); u64(0); u32(0); u16(640); u16(360); u8(4); u16(256); u8(12); u8(2); u32(1); u8(0); u16(speed); const palette = Buffer.alloc(1024); palette.set([255, 0, 0, 255], 4); parts.push(palette); ["JS fixture", "", "Tester", "Tester", "Tester"].forEach(str); ["1111111111111111", "1111111111111111", "1111111111111111", "2222222222222222", "2222222222222222", "0000000000000000"].forEach(id => parts.push(Buffer.from(id))); @@ -45,3 +45,11 @@ test("rejects damaged payload CRC", async () => { const bytes = fixture(); bytes[80] ^= 1; await assert.rejects(readSppm(bytes), /CRC/); }); + +test("reads DSi speed indices using the measured frame rates", async () => { + const bytes = fixture(3, 1); + const note = await readSppm(bytes); + assert.equal(note.metadata.speed, 1); + assert.equal(note.metadata.fps, 0.4985); + assert.equal(note.metadata.nominalFps, 12); +}); diff --git a/sppm-python/__pycache__/sppm_tools.cpython-310.pyc b/sppm-python/__pycache__/sppm_tools.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2dc6ab0e4e6bc7a1bddae640f15e1524bfaf5023 GIT binary patch literal 18457 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 LTMMfd?rr@SoCcJJ literal 0 HcmV?d00001 diff --git a/sppm-python/sppm_tools.py b/sppm-python/sppm_tools.py index 6a459e2..55e334c 100644 --- a/sppm-python/sppm_tools.py +++ b/sppm-python/sppm_tools.py @@ -40,6 +40,7 @@ class AudioTrack: name: str frame_triggers: list[int] pcm: array.array + reference_speed: int = 0 @dataclass @@ -208,7 +209,7 @@ def read_sppm(source: Input, *, legacy_hmac_key: Optional[bytes] = None) -> Sppm frame_count, audio_count, reserved = r.u32(), r.u8(), r.u16() if (version not in (1, 2, 3) or header_size != HEADER_SIZE or total_size != len(body) or width != WIDTH or height != HEIGHT or layer_count != 4 or palette_count != 256 or - not 1 <= fps <= 30 or not 1 <= frame_count <= 9999 or audio_count > 4 or reserved != 0 or + not 1 <= fps <= 30 or not 1 <= frame_count <= 9999 or audio_count > 4 or (reserved != 0 if version < 3 else reserved > 8) or (flags & ~(3 if version == 1 else 15))): raise SppmError("unsupported SPPM header") if zlib.crc32(body[HEADER_SIZE:]) & 0xFFFFFFFF != expected_crc: @@ -228,7 +229,10 @@ def read_sppm(source: Input, *, legacy_hmac_key: Optional[bytes] = None) -> Sppm "authorFsid": r.id(), "originalAuthorFsid": r.id(), "lastEditorFsid": r.id(), "currentId": r.id(), "rootId": r.id(), "parentId": r.id(True), "createdUnixSeconds": r.u64(), "modifiedUnixSeconds": r.u64(), "coverFrame": r.u32(), - "width": width, "height": height, "fps": fps, "locked": bool(flags & 1), + "width": width, "height": height, + "speed": reserved if version >= 3 else 0, "nominalFps": fps, + "fps": (0.4985, 1.0, 2.0, 4.0, 5.9972, 11.986, 19.9459, 29.9275)[reserved - 1] if version >= 3 and reserved else fps, + "locked": bool(flags & 1), "allowSpinoffs": bool(flags & 2), "bgmFollowsSpeed": version == 1 or bool(flags & 4), "loopPlayback": True if version == 1 else not bool(flags & 8), "frameCount": frame_count, "audioTrackCount": audio_count, "formatVersion": version, @@ -280,8 +284,12 @@ def read_sppm(source: Input, *, legacy_hmac_key: Optional[bytes] = None) -> Sppm pcm.frombytes(raw_pcm) if struct.pack("=H", 1) != b"\x01\x00": pcm.byteswap() - tracks.append(AudioTrack(index, channels, fps if ti == 0 and version == 1 else reference_fps, - sample_rate, name, cues, pcm)) + reference_speed = reference_fps if version >= 3 and reserved != 0 and ti == 0 else 0 + if version >= 3 and reserved != 0 and (reference_speed > 8 or (ti > 0 and reference_fps != 0)): + raise SppmError("invalid audio speed metadata") + legacy_reference_fps = (fps if ti == 0 else reference_fps) if version < 3 or reserved == 0 else 0 + tracks.append(AudioTrack(index, channels, legacy_reference_fps, + sample_rate, name, cues, pcm, reference_speed)) if r.at != body_end: raise SppmError("unexpected trailing bytes") return SppmNote(meta, palette, frames, tracks, signature) @@ -358,7 +366,9 @@ def _mix_wav(note: SppmNote, wav_path: str, sample_rate: int = 48000, chunk_seco times = (base + np.arange(count, dtype=np.float64)) / sample_rate for track, source in sources: if track.index == 0: - rate = track.sample_rate * (fps / (track.reference_fps or fps) if note.metadata["bgmFollowsSpeed"] else 1.0) + reference_fps = ((0.4985, 1.0, 2.0, 4.0, 5.9972, 11.986, 19.9459, 29.9275)[track.reference_speed - 1] + if track.reference_speed else (track.reference_fps or fps)) + rate = track.sample_rate * (fps / reference_fps if note.metadata["bgmFollowsSpeed"] else 1.0) positions = times * rate valid = np.ones(count, dtype=bool) if note.metadata["loopPlayback"] else positions < len(source) if note.metadata["loopPlayback"]: diff --git a/sppm-python/test_sppm_tools.py b/sppm-python/test_sppm_tools.py index 3aca121..4af9954 100644 --- a/sppm-python/test_sppm_tools.py +++ b/sppm-python/test_sppm_tools.py @@ -9,7 +9,7 @@ from pathlib import Path from sppm_tools import get_sppm_metadata, get_sppm_thumbnail, read_sppm, sppm_to_mp4 -def fixture() -> bytes: +def fixture(version: int = 2, speed: int = 0) -> bytes: payload = bytearray() palette = bytearray(256 * 4) palette[4:8] = bytes((255, 0, 0, 255)) @@ -37,15 +37,15 @@ def fixture() -> bytes: payload.extend(encoded) pcm = struct.pack("<800h", *([1200, -1200] * 400)) - payload.extend(struct.pack("= 3 and speed else 12), 8000)) text("BGM") payload.extend(struct.pack("