mirror of
https://github.com/RexiMemo-org/switchnote-tools.git
synced 2026-10-30 16:29:12 +00:00
corrected switchnote speed
This commit is contained in:
1 parent
8f31bdd977
commit
fa99395580
6 files changed
+45
-16
No files matched your search
+10
-5
@@ -159,7 +159,7 @@ export async function readSppm(input, { legacyHmacKey = null } = {}) {
|
|||||||
r.take(4);
|
r.take(4);
|
||||||
const version = r.u16(), headerSize = r.u16(), totalBytes = r.u64(), expectedCrc = r.u32();
|
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();
|
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");
|
if (crc32(body.subarray(HEADER_SIZE)) !== expectedCrc) fail("payload CRC mismatch");
|
||||||
|
|
||||||
const paletteBytes = r.take(256 * 4), palette = new Array(256);
|
const paletteBytes = r.take(256 * 4), palette = new Array(256);
|
||||||
@@ -167,7 +167,9 @@ export async function readSppm(input, { legacyHmacKey = null } = {}) {
|
|||||||
const metadata = {
|
const metadata = {
|
||||||
title: r.string(256), description: r.string(4096), authorName: r.string(128), originalAuthorName: r.string(128), lastEditorName: r.string(128),
|
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),
|
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),
|
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,
|
frameCount, audioTrackCount: audioCount, formatVersion: version, signature: signature.status,
|
||||||
};
|
};
|
||||||
@@ -201,7 +203,7 @@ export async function readSppm(input, { legacyHmacKey = null } = {}) {
|
|||||||
const audioTracks = [];
|
const audioTracks = [];
|
||||||
let audioSamples = 0;
|
let audioSamples = 0;
|
||||||
for (let ti = 0; ti < audioCount; ti++) {
|
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");
|
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());
|
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");
|
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 pcm = new Int16Array(sampleCount);
|
||||||
const sampleView = new DataView(pcmBytes.buffer, pcmBytes.byteOffset, pcmBytes.byteLength);
|
const sampleView = new DataView(pcmBytes.buffer, pcmBytes.byteOffset, pcmBytes.byteLength);
|
||||||
for (let i = 0; i < sampleCount; i++) pcm[i] = sampleView.getInt16(i * 2, true);
|
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");
|
if (r.at !== body.length) fail("unexpected trailing bytes");
|
||||||
return { width, height, palette, metadata, frames, audioTracks, signature };
|
return { width, height, palette, metadata, frames, audioTracks, signature };
|
||||||
@@ -333,7 +338,7 @@ export class SppmPlayer {
|
|||||||
}
|
}
|
||||||
if (track.index === 0) {
|
if (track.index === 0) {
|
||||||
const source = this.audioContext.createBufferSource(); source.buffer = buffer;
|
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);
|
source.loop = !!this.loop; source.connect(this.audioContext.destination); source.start(start); this.sources.push(source);
|
||||||
} else for (const cue of track.frameTriggers) {
|
} else for (const cue of track.frameTriggers) {
|
||||||
const source = this.audioContext.createBufferSource(); source.buffer = buffer; source.connect(this.audioContext.destination);
|
const source = this.audioContext.createBufferSource(); source.buffer = buffer; source.connect(this.audioContext.destination);
|
||||||
|
|||||||
@@ -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); }
|
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;
|
return (crc ^ 0xffffffff) >>> 0;
|
||||||
}
|
}
|
||||||
function fixture() {
|
function fixture(version = 2, speed = 0) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
const u8 = n => parts.push(Buffer.from([n & 255]));
|
const u8 = n => parts.push(Buffer.from([n & 255]));
|
||||||
const u16 = n => { const b = Buffer.alloc(2); b.writeUInt16LE(n); parts.push(b); };
|
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 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 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); };
|
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);
|
const palette = Buffer.alloc(1024); palette.set([255, 0, 0, 255], 4); parts.push(palette);
|
||||||
["JS fixture", "", "Tester", "Tester", "Tester"].forEach(str);
|
["JS fixture", "", "Tester", "Tester", "Tester"].forEach(str);
|
||||||
["1111111111111111", "1111111111111111", "1111111111111111", "2222222222222222", "2222222222222222", "0000000000000000"].forEach(id => parts.push(Buffer.from(id)));
|
["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;
|
const bytes = fixture(); bytes[80] ^= 1;
|
||||||
await assert.rejects(readSppm(bytes), /CRC/);
|
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);
|
||||||
|
});
|
||||||
Binary file not shown.
Binary file not shown.
@@ -40,6 +40,7 @@ class AudioTrack:
|
|||||||
name: str
|
name: str
|
||||||
frame_triggers: list[int]
|
frame_triggers: list[int]
|
||||||
pcm: array.array
|
pcm: array.array
|
||||||
|
reference_speed: int = 0
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@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()
|
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
|
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
|
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))):
|
(flags & ~(3 if version == 1 else 15))):
|
||||||
raise SppmError("unsupported SPPM header")
|
raise SppmError("unsupported SPPM header")
|
||||||
if zlib.crc32(body[HEADER_SIZE:]) & 0xFFFFFFFF != expected_crc:
|
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(),
|
"authorFsid": r.id(), "originalAuthorFsid": r.id(), "lastEditorFsid": r.id(),
|
||||||
"currentId": r.id(), "rootId": r.id(), "parentId": r.id(True),
|
"currentId": r.id(), "rootId": r.id(), "parentId": r.id(True),
|
||||||
"createdUnixSeconds": r.u64(), "modifiedUnixSeconds": r.u64(), "coverFrame": r.u32(),
|
"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),
|
"allowSpinoffs": bool(flags & 2), "bgmFollowsSpeed": version == 1 or bool(flags & 4),
|
||||||
"loopPlayback": True if version == 1 else not bool(flags & 8),
|
"loopPlayback": True if version == 1 else not bool(flags & 8),
|
||||||
"frameCount": frame_count, "audioTrackCount": audio_count, "formatVersion": version,
|
"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)
|
pcm.frombytes(raw_pcm)
|
||||||
if struct.pack("=H", 1) != b"\x01\x00":
|
if struct.pack("=H", 1) != b"\x01\x00":
|
||||||
pcm.byteswap()
|
pcm.byteswap()
|
||||||
tracks.append(AudioTrack(index, channels, fps if ti == 0 and version == 1 else reference_fps,
|
reference_speed = reference_fps if version >= 3 and reserved != 0 and ti == 0 else 0
|
||||||
sample_rate, name, cues, pcm))
|
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:
|
if r.at != body_end:
|
||||||
raise SppmError("unexpected trailing bytes")
|
raise SppmError("unexpected trailing bytes")
|
||||||
return SppmNote(meta, palette, frames, tracks, signature)
|
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
|
times = (base + np.arange(count, dtype=np.float64)) / sample_rate
|
||||||
for track, source in sources:
|
for track, source in sources:
|
||||||
if track.index == 0:
|
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
|
positions = times * rate
|
||||||
valid = np.ones(count, dtype=bool) if note.metadata["loopPlayback"] else positions < len(source)
|
valid = np.ones(count, dtype=bool) if note.metadata["loopPlayback"] else positions < len(source)
|
||||||
if note.metadata["loopPlayback"]:
|
if note.metadata["loopPlayback"]:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from pathlib import Path
|
|||||||
from sppm_tools import get_sppm_metadata, get_sppm_thumbnail, read_sppm, sppm_to_mp4
|
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()
|
payload = bytearray()
|
||||||
palette = bytearray(256 * 4)
|
palette = bytearray(256 * 4)
|
||||||
palette[4:8] = bytes((255, 0, 0, 255))
|
palette[4:8] = bytes((255, 0, 0, 255))
|
||||||
@@ -37,15 +37,15 @@ def fixture() -> bytes:
|
|||||||
payload.extend(encoded)
|
payload.extend(encoded)
|
||||||
|
|
||||||
pcm = struct.pack("<800h", *([1200, -1200] * 400))
|
pcm = struct.pack("<800h", *([1200, -1200] * 400))
|
||||||
payload.extend(struct.pack("<BBB I", 0, 1, 12, 8000))
|
payload.extend(struct.pack("<BBB I", 0, 1, (6 if version >= 3 and speed else 12), 8000))
|
||||||
text("BGM")
|
text("BGM")
|
||||||
payload.extend(struct.pack("<HII", 0, 800, zlib.crc32(pcm) & 0xffffffff))
|
payload.extend(struct.pack("<HII", 0, 800, zlib.crc32(pcm) & 0xffffffff))
|
||||||
payload.extend(pcm)
|
payload.extend(pcm)
|
||||||
|
|
||||||
header = bytearray(b"SPPM")
|
header = bytearray(b"SPPM")
|
||||||
header.extend(struct.pack("<HHQ I HH B H B B I B H", 2, 36, 36 + len(payload),
|
header.extend(struct.pack("<HHQ I HH B H B B I B H", version, 36, 36 + len(payload),
|
||||||
zlib.crc32(payload) & 0xffffffff, 640, 360, 4, 256,
|
zlib.crc32(payload) & 0xffffffff, 640, 360, 4, 256,
|
||||||
12, 2, 1, 1, 0))
|
12, 2, 1, 1, speed))
|
||||||
assert len(header) == 36
|
assert len(header) == 36
|
||||||
return bytes(header + payload)
|
return bytes(header + payload)
|
||||||
|
|
||||||
@@ -75,6 +75,12 @@ class SppmToolTests(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(ValueError, "CRC"):
|
with self.assertRaisesRegex(ValueError, "CRC"):
|
||||||
read_sppm(broken)
|
read_sppm(broken)
|
||||||
|
|
||||||
|
def test_reads_dsi_speed_indices_using_measured_rates(self):
|
||||||
|
note = read_sppm(fixture(version=3, speed=1))
|
||||||
|
self.assertEqual(note.metadata["speed"], 1)
|
||||||
|
self.assertEqual(note.metadata["fps"], 0.4985)
|
||||||
|
self.assertEqual(note.audio_tracks[0].reference_speed, 6)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
Reference in new issue
Block a user
