From 0b9e3a642aa0ca279a0e932abd2cd25d251a3b7c Mon Sep 17 00:00:00 2001 From: Alex Bates Date: Sat, 24 Sep 2022 00:55:08 +0100 Subject: [PATCH] wip bgm encoding --- mamar-wasm-bridge/src/lib.rs | 17 + mamar-web/package.json | 2 + mamar-web/src/app/emu/PlaybackControls.tsx | 193 +- mamar-web/src/app/index.html | 2 +- mamar-web/src/app/util/hooks/useMupen.ts | 19 +- mamar-web/src/index.scss | 2 +- mamar-web/tsconfig.json | 3 +- patches/build.py | 74 +- patches/build/patches.d.ts | 28022 +++++++++++++++++- patches/build/patches.js | 28173 ++++++++++++++++++- patches/package.json | 2 +- patches/src/patches.c | 98 +- patches/src/patches.ld | 2 +- yarn.lock | 10 + 14 files changed, 56547 insertions(+), 72 deletions(-) diff --git a/mamar-wasm-bridge/src/lib.rs b/mamar-wasm-bridge/src/lib.rs index 0ed42b0..d2d95d4 100644 --- a/mamar-wasm-bridge/src/lib.rs +++ b/mamar-wasm-bridge/src/lib.rs @@ -38,6 +38,23 @@ pub fn bgm_decode(data: &[u8]) -> JsValue { } } +#[wasm_bindgen] +pub fn bgm_encode(bgm: &JsValue) -> JsValue { + let bgm: Bgm = from_js(bgm); + let mut f = Cursor::new(Vec::new()); + match bgm.encode(&mut f) { + Ok(_) => { + let data: Vec = f.into_inner(); + let arr = js_sys::Uint8Array::new_with_length(data.len() as u32); + for (i, v) in data.into_iter().enumerate() { + arr.set_index(i as u32, v); + } + arr.into() + }, + Err(e) => e.to_string().into(), + } +} + #[wasm_bindgen] pub fn sbn_decode(rom: &[u8]) -> JsValue { const SBN_START: usize = 0xF00000; diff --git a/mamar-web/package.json b/mamar-web/package.json index 63238fe..dc90315 100644 --- a/mamar-web/package.json +++ b/mamar-web/package.json @@ -13,6 +13,7 @@ "dependencies": { "@adobe/react-spectrum": "^3.21.2", "@spectrum-icons/workflow": "^4.0.2", + "@types/stats.js": "^0.17.0", "browser-fs-access": "^0.31.0", "idb-keyval": "^6.2.0", "mamar-wasm-bridge": "*", @@ -24,6 +25,7 @@ "react-dom": "^18.2.0", "react-stately": "^3.17.0", "react-tracked": "^1.7.10", + "stats.js": "^0.17.0", "use-undoable": "^3.3.11" }, "devDependencies": { diff --git a/mamar-web/src/app/emu/PlaybackControls.tsx b/mamar-web/src/app/emu/PlaybackControls.tsx index 39ed752..dabddea 100644 --- a/mamar-web/src/app/emu/PlaybackControls.tsx +++ b/mamar-web/src/app/emu/PlaybackControls.tsx @@ -1,68 +1,215 @@ import { ActionButton, Flex, ToggleButton, View } from "@adobe/react-spectrum" import Play from "@spectrum-icons/workflow/Play" +import { bgm_encode } from "mamar-wasm-bridge" +import { EmulatorControls } from "mupen64plus-web" import * as patches from "patches" import { Bgm } from "pm64-typegen" -import { useEffect, useMemo, useState } from "react" +import { useEffect, useRef, useState } from "react" import { useBgm } from "../store" import useMupen from "../util/hooks/useMupen" import useRomData from "../util/hooks/useRomData" -import Patcher from "../util/Patcher" import "./PlaybackControls.scss" -function usePatchedRom(bgm: Bgm | undefined): ArrayBuffer | undefined { - const cleanRom = useRomData() +class DramView { + u8: Uint8Array - return useMemo(() => { - if (!bgm) { - return undefined + constructor(mupen: EmulatorControls) { + this.u8 = mupen.getDram() + } + + readU8(address: number) { + address = address & 0x00FFFFFF + return this.u8[address] + } + + writeU8(address: number, data: Uint8Array | number) { + address = address & 0x00FFFFFF + if (typeof data === "number") { + this.u8[address] = data + } else { + for (let i = 0; i < data.length; i++) { + this.u8[address + i] = data[i] + } + } + } + + readU32(address: number) { + address = address & 0x00FFFFFF + return this.u8[address] | (this.u8[address + 1] << 8) | (this.u8[address + 2] << 16) | (this.u8[address + 3] << 24) + } + + readU32Byteswapped(address: number) { + address = address & 0x00FFFFFF + return (this.u8[address] << 24) | (this.u8[address + 1] << 16) | (this.u8[address + 2] << 8) | this.u8[address + 3] + } + + writeU32Byteswapped(address: number, data: Uint8Array | number) { + address = address & 0x00FFFFFF + + if (typeof data === "number") { + this.u8[address] = (data >> 24) & 0xFF + this.u8[address + 1] = (data >> 16) & 0xFF + this.u8[address + 2] = (data >> 8) & 0xFF + this.u8[address + 3] = data & 0xFF + } else { + // Convert to Uint32Array + if (data.length % 4 !== 0) { + throw new Error("data length must be a multiple of 4") + } + const u32 = new Uint32Array(data.length / 4) + + for (let i = 0; i < u32.length; i++) { + u32[i] = (data[i * 4] << 24) + | (data[i * 4 + 1] << 16) + | (data[i * 4 + 2] << 8) + | data[i * 4 + 3] + } + + // Byte swap + for (let i = 0; i < u32.length; i++) { + u32[i] = ((u32[i] & 0xff000000) >> 24) | ((u32[i] & 0x00ff0000) >> 8) | ((u32[i] & 0x0000ff00) << 8) | ((u32[i] & 0x000000ff) << 24) + } + + // Write u32 + for (let i = 0; i < u32.length; i++) { + this.u8[address + i * 4] = (u32[i] >> 24) & 0xFF + this.u8[address + i * 4 + 1] = (u32[i] >> 16) & 0xFF + this.u8[address + i * 4 + 2] = (u32[i] >> 8) & 0xFF + this.u8[address + i * 4 + 3] = u32[i] & 0xFF + } + } + } + + writeU32(address: number, data: Uint32Array | number) { + address = address & 0x00FFFFFF + if (typeof data === "number") { + this.u8[address] = data & 0xFF + this.u8[address + 1] = (data >> 8) & 0xFF + this.u8[address + 2] = (data >> 16) & 0xFF + this.u8[address + 3] = (data >> 24) & 0xFF + } else { + for (let i = 0; i < data.length; i++) { + this.u8[address + i * 4] = data[i] & 0xFF + this.u8[address + i * 4 + 1] = (data[i] >> 8) & 0xFF + this.u8[address + i * 4 + 2] = (data[i] >> 16) & 0xFF + this.u8[address + i * 4 + 3] = (data[i] >> 24) & 0xFF + } } + } +} + +function writePatches(mupen: EmulatorControls) { + const dram = new DramView(mupen) + + dram.writeU32(patches.RAM_state_step_logos, patches.ASM_PATCH_state_step_logos) + dram.writeU32(patches.RAM_PATCH_state_step_logos, patches.ASM_PATCH_state_step_logos) + + dram.writeU32(patches.RAM_state_step_title_screen, patches.ASM_PATCH_state_step_title_screen) + dram.writeU32(patches.RAM_PATCH_state_step_title_screen, patches.ASM_PATCH_state_step_title_screen) + + dram.writeU32(patches.RAM_appendGfx_title_screen, patches.ASM_PATCH_appendGfx_title_screen) + dram.writeU32(patches.RAM_PATCH_appendGfx_title_screen, patches.ASM_PATCH_appendGfx_title_screen) + + dram.writeU32(patches.RAM_au_load_song_files, patches.ASM_PATCH_au_load_song_files) + dram.writeU32(patches.RAM_PATCH_au_load_song_files, patches.ASM_PATCH_au_load_song_files) + + dram.writeU32(patches.RAM_MAMAR_au_load_song_files, patches.ASM_MAMAR_au_load_song_files) +} - const rom = cleanRom.slice(0) - //const patcher = new Patcher(rom) - //patcher.overwriteFunction(0xF4A4, patches.skipIntroLogos) - //patcher.overwriteFunction(0x10, [0x1B99678D, 0xE109577C]) +let songId = 0 - // TODO: encode bgm, write over Toad Town in SBN +function writeBgm(mupen: EmulatorControls, bgm: Bgm) { + const bgmBin: Uint8Array | string = bgm_encode(bgm) - return rom - }, [cleanRom, bgm]) + if (bgmBin instanceof Uint8Array) { + const dram = new DramView(mupen) + + if (bgmBin.length > 0x20000) { + throw new Error(`Encoded BGM too large, ${bgmBin.length} > 0x20000 bytes`) + } + + console.log(`Writing BGM to ${patches.RAM_MAMAR_bgm.toString(16)}`) + dram.writeU32Byteswapped(patches.RAM_MAMAR_bgm, bgmBin) + dram.writeU32(patches.RAM_MAMAR_bgm_size, bgmBin.length) + dram.writeU32(patches.RAM_MAMAR_bk_files, new Uint32Array([0, 0, 0])) + dram.writeU32(patches.RAM_MAMAR_song_id, songId++) + dram.writeU32(patches.RAM_MAMAR_song_variation, 0) + dram.writeU32(patches.RAM_MAMAR_ambient_sounds, 6) // AMBIENT_SILENCE + } else { + throw new Error(bgmBin) + } } export default function PlaybackControls() { const [bgm] = useBgm() const [isPlaying, setIsPlaying] = useState(false) - const romData = usePatchedRom(isPlaying ? bgm : undefined) - const mupen = useMupen(romData) + const romData = useRomData() + const bpmRef = useRef(null) + const mupen = useMupen(bgm ? romData : undefined, () => { + if (!mupen || !bgm) + return + + const ram = new DramView(mupen) + + if (bpmRef.current) { + const bpm = ram.readU32(patches.RAM_MAMAR_out_masterTempo) / 100 + bpmRef.current.innerText = `${bpm} BPM` + } + }) useEffect(() => { + if (!mupen || !bgm) + return + if (isPlaying) { - mupen?.resume?.() + writePatches(mupen) + //writeBgm(mupen, bgm) + mupen.resume() } else { - mupen?.pause?.() + mupen.pause() } + }, [mupen, bgm, isPlaying]) + useEffect(() => { const onKeydown = (event: KeyboardEvent) => { if (event.key === " ") { - setIsPlaying(!isPlaying) + setIsPlaying(p => !p) } } document.addEventListener("keydown", onKeydown) return () => document.removeEventListener("keydown", onKeydown) - }, [mupen, isPlaying]) + }, []) + + /*useEffect(() => { + if (!mupen || !bgm) + return + + const i = setInterval(() => { + writeBgm(mupen, bgm) + }, 4000) + return () => clearInterval(i) + }, [mupen, bgm])*/ if (!bgm) { return } - return + return + + { - mupen?.softReset() + if (mupen) { + mupen.pause() + writePatches(mupen) + writeBgm(mupen, bgm) + mupen.resume() + } }} > - {"<-" /* TODO: icon */} + {"Compile"}