From e5db55b050963846b8ea36479de89c808cab7e47 Mon Sep 17 00:00:00 2001 From: Darxoon Date: Mon, 29 Dec 2025 18:48:50 +0100 Subject: [PATCH 1/5] strip segments in ron output of ids --- mamar-web/src/app/doc/Ruler.tsx | 20 ++++++++- pm64/src/bgm/de.rs | 12 +++--- pm64/src/bgm/mamar.rs | 2 +- pm64/src/bgm/midi.rs | 2 +- pm64/src/bgm/mod.rs | 73 +++++++++++++++++++++++++++++---- 5 files changed, 90 insertions(+), 19 deletions(-) diff --git a/mamar-web/src/app/doc/Ruler.tsx b/mamar-web/src/app/doc/Ruler.tsx index f602d3c..12c5bb4 100644 --- a/mamar-web/src/app/doc/Ruler.tsx +++ b/mamar-web/src/app/doc/Ruler.tsx @@ -18,7 +18,7 @@ interface Loop { } function getLoops(segments: Segment[]): Loop[] { - const loops = [] + const loops: Loop[] = [] for (let startIdx = 0; startIdx < segments.length; startIdx++) { const start = segments[startIdx] @@ -27,6 +27,11 @@ function getLoops(segments: Segment[]): Loop[] { for (let endIdx = 0; endIdx < segments.length; endIdx++) { const end = segments[endIdx] if (end.type === "EndLoop" && end.label_index === start.label_index) { + if (start.id == null) { + console.error("Segment", start, "does not have an ID") + continue + } + loops.push({ id: start.id, start: startIdx, @@ -139,6 +144,11 @@ export default function Ruler() { let totalTime = 0 for (let i = 0; i < segmentLengths.length; i++) { const segment = segments[i] + if (segment.id == null) { + console.error("Segment", segment, "does not have an ID") + continue + } + let length = segmentLengths[i] if (length === 0) { @@ -239,11 +249,17 @@ function LoopDialog({ loop, close }: { loop: Loop, close: () => void }) { function deleteLoop() { const start = variation?.segments[loop.start] - if (start) + if (start) { + if (start.id == null) { + console.error("Segment", start, "does not have an ID") + return + } + variationDispatch({ type: "toggle_segment_loop", id: start.id, }) + } } return diff --git a/pm64/src/bgm/de.rs b/pm64/src/bgm/de.rs index 205e44c..35918ec 100644 --- a/pm64/src/bgm/de.rs +++ b/pm64/src/bgm/de.rs @@ -306,35 +306,35 @@ impl Segment { }; Ok(Segment::Subseg { - id: gen_id(), + id: Some(gen_id()), track_list, }) } segment_commands::START_LOOP => { f.seek(SeekFrom::Current(-2))?; Ok(Segment::StartLoop { - id: gen_id(), + id: Some(gen_id()), label_index: f.read_u16_be()?, }) } - segment_commands::WAIT => Ok(Segment::Wait { id: gen_id() }), + segment_commands::WAIT => Ok(Segment::Wait { id: Some(gen_id()) }), segment_commands::END_LOOP => { Ok(Segment::EndLoop { - id: gen_id(), + id: Some(gen_id()), label_index: (data & 0x1F) as u8, // bits 0-4 iter_count: ((data >> 5) & 0x7F) as u8, // bits 5-11 }) } segment_commands::UNKNOWN_6 => { Ok(Segment::Unknown6 { - id: gen_id(), + id: Some(gen_id()), label_index: (data & 0x1F) as u8, // bits 0-4 iter_count: ((data >> 5) & 0x7F) as u8, // bits 5-11 }) } segment_commands::UNKNOWN_7 => { Ok(Segment::Unknown7 { - id: gen_id(), + id: Some(gen_id()), label_index: (data & 0x1F) as u8, // bits 0-4 iter_count: ((data >> 5) & 0x7F) as u8, // bits 5-11 }) diff --git a/pm64/src/bgm/mamar.rs b/pm64/src/bgm/mamar.rs index f50afad..2307bff 100644 --- a/pm64/src/bgm/mamar.rs +++ b/pm64/src/bgm/mamar.rs @@ -60,7 +60,7 @@ mod test { variations: [ Some(Variation { segments: vec![Segment::Subseg { - id: 0, + id: Some(0), track_list: 0x1234, }], }), diff --git a/pm64/src/bgm/midi.rs b/pm64/src/bgm/midi.rs index 17bbb56..b90c9d3 100644 --- a/pm64/src/bgm/midi.rs +++ b/pm64/src/bgm/midi.rs @@ -178,7 +178,7 @@ pub fn to_bgm(raw: &[u8]) -> Result> { let (_, variation) = bgm.add_variation().unwrap(); variation.segments = vec![Segment::Subseg { - id: gen_id(), + id: Some(gen_id()), track_list: track_list_id, }]; diff --git a/pm64/src/bgm/mod.rs b/pm64/src/bgm/mod.rs index 90f4de1..4cdaed2 100644 --- a/pm64/src/bgm/mod.rs +++ b/pm64/src/bgm/mod.rs @@ -149,7 +149,7 @@ impl Bgm { self.variations[variation].as_mut().unwrap().segments.insert( idx, Segment::Subseg { - id: gen_id(), + id: Some(gen_id()), track_list, }, ) @@ -157,6 +157,7 @@ impl Bgm { } pub fn from_ron_string(input_string: &str) -> Result { + // generate ids for commands let matches: Vec> = RON_COMMAND_REGEX.captures_iter(&input_string).collect(); let mut modified_input_string = input_string.to_string(); @@ -176,10 +177,34 @@ impl Bgm { ); } - Ok(ron::from_str::(&modified_input_string)?) + let mut bgm = ron::from_str::(&modified_input_string)?; + + // generate ids for segments + for variation in &mut bgm.variations { + let Some(variation) = variation else { + continue; + }; + + for segment in &mut variation.segments { + segment.add_new_id(); + } + } + + Ok(bgm) } - pub fn to_ron_string(&self) -> Result { + pub fn to_ron_string(mut self) -> Result { + // strip segments of id + for variation in &mut self.variations { + let Some(variation) = variation else { + continue; + }; + + for segment in &mut variation.segments { + segment.strip_id(); + } + } + let pretty_config = ron::ser::PrettyConfig::new().indentor(" ").depth_limit(5); let bgm_string = ron::ser::to_string_pretty(&self, pretty_config.clone())?.to_string(); @@ -228,33 +253,63 @@ pub struct Variation { pub enum Segment { #[serde(rename_all = "camelCase")] Subseg { - id: Id, + #[serde(skip_serializing_if = "Option::is_none")] + id: Option, track_list: TrackListId, }, StartLoop { - id: Id, + #[serde(skip_serializing_if = "Option::is_none")] + id: Option, label_index: u16, }, Wait { - id: Id, + #[serde(skip_serializing_if = "Option::is_none")] + id: Option, }, EndLoop { - id: Id, + #[serde(skip_serializing_if = "Option::is_none")] + id: Option, label_index: u8, iter_count: u8, }, Unknown6 { - id: Id, + #[serde(skip_serializing_if = "Option::is_none")] + id: Option, label_index: u8, iter_count: u8, }, Unknown7 { - id: Id, + #[serde(skip_serializing_if = "Option::is_none")] + id: Option, label_index: u8, iter_count: u8, }, } +impl Segment { + pub fn add_new_id(&mut self) { + match self { + Segment::Subseg { id, .. } => *id = Some(gen_id()), + Segment::StartLoop { id, .. } => *id = Some(gen_id()), + Segment::Wait { id } => *id = Some(gen_id()), + Segment::EndLoop { id, .. } => *id = Some(gen_id()), + Segment::Unknown6 { id, .. } => *id = Some(gen_id()), + Segment::Unknown7 { id, .. } => *id = Some(gen_id()), + } + } + + pub fn strip_id(&mut self) { + match self { + Segment::Subseg { id, .. } => *id = None, + Segment::StartLoop { id, .. } => *id = None, + Segment::Wait { id } => *id = None, + Segment::EndLoop { id, .. } => *id = None, + Segment::Unknown6 { id, .. } => *id = None, + Segment::Unknown7 { id, .. } => *id = None, + } + } +} + mod segment_commands { pub const END: u32 = 0; pub const SUBSEG: u32 = 1 << 16; From eff51ab729ad0d7fafff5a8f0f444a0deb18d21b Mon Sep 17 00:00:00 2001 From: Darxoon Date: Mon, 29 Dec 2025 20:03:17 +0100 Subject: [PATCH 2/5] remove type tag on segment enum for better ron output --- mamar-web/src/app/doc/Ruler.tsx | 52 +++++++++++++++++++--------- mamar-web/src/app/doc/SegmentMap.tsx | 12 ++++--- mamar-web/src/app/store/segment.ts | 26 +++++++++++--- mamar-web/src/app/store/variation.ts | 46 ++++++++++++++---------- pm64/src/bgm/mod.rs | 1 - 5 files changed, 92 insertions(+), 45 deletions(-) diff --git a/mamar-web/src/app/doc/Ruler.tsx b/mamar-web/src/app/doc/Ruler.tsx index 12c5bb4..5721c64 100644 --- a/mamar-web/src/app/doc/Ruler.tsx +++ b/mamar-web/src/app/doc/Ruler.tsx @@ -9,6 +9,7 @@ import styles from "./Ruler.module.scss" import { useTime } from "./timectx" import { useBgm, useSegment, useVariation } from "../store" +import { getSegmentId } from "../store/segment" interface Loop { id: number @@ -22,21 +23,21 @@ function getLoops(segments: Segment[]): Loop[] { for (let startIdx = 0; startIdx < segments.length; startIdx++) { const start = segments[startIdx] - if (start.type === "StartLoop") { + if ("StartLoop" in start) { // Look for EndLoop for (let endIdx = 0; endIdx < segments.length; endIdx++) { const end = segments[endIdx] - if (end.type === "EndLoop" && end.label_index === start.label_index) { - if (start.id == null) { + if ("EndLoop" in end && end.EndLoop.label_index === start.StartLoop.label_index) { + if (start.StartLoop.id == null) { console.error("Segment", start, "does not have an ID") continue } loops.push({ - id: start.id, + id: start.StartLoop.id, start: startIdx, end: endIdx, - iterCount: end.iter_count, + iterCount: end.EndLoop.iter_count, }) break } @@ -113,8 +114,8 @@ export function useSegmentLengths(): number[] { const segments = variation?.segments ?? [] return segments.map(segment => { - if (bgm && segment.type === "Subseg") { - const master = bgm.trackLists[segment.trackList].tracks[0] + if (bgm && "Subseg" in segment) { + const master = bgm.trackLists[segment.Subseg.trackList].tracks[0] return master.commands.reduce((totalDelay, event) => { if (event.type === "Delay") { return totalDelay + event.value @@ -144,7 +145,8 @@ export default function Ruler() { let totalTime = 0 for (let i = 0; i < segmentLengths.length; i++) { const segment = segments[i] - if (segment.id == null) { + const id = getSegmentId(segment) + if (id == null) { console.error("Segment", segment, "does not have an ID") continue } @@ -156,11 +158,11 @@ export default function Ruler() { for (const loop of loops) { if (i === loop.start) { currentLoop = loop - elements.push() + elements.push() } if (i === loop.end) { currentLoop = null - elements.push() + elements.push() } continue } @@ -169,18 +171,18 @@ export default function Ruler() { if (currentLoop !== null) { // Consume all segments that are part of this loop - while (segments[i + 1].type !== "EndLoop") { + while (!("EndLoop" in segments[i + 1])) { length += segmentLengths[++i] } const loop = Object.assign({}, currentLoop!) // Avoids stale currentLoop reference in dialog func below - elements.push( + elements.push( {close => } ) } else { - elements.push() + elements.push() } totalTime += length @@ -224,9 +226,15 @@ function RulerSegment({ segment, currentLoop, highlightedLoop, length, onPress } style={ticksToStyle(length)} title={currentLoop === null ? "Double-click to loop" : ""} onDoubleClick={() => { + const id = getSegmentId(segment) + if (id == null) { + console.error("Segment", segment, "does not have an ID") + return + } + dispatch({ type: "toggle_segment_loop", - id: segment.id, + id, }) }} > @@ -238,7 +246,12 @@ function RulerSegment({ segment, currentLoop, highlightedLoop, length, onPress } function LoopDialog({ loop, close }: { loop: Loop, close: () => void }) { const [variation, variationDispatch] = useVariation() - const [, endDispatch] = useSegment(variation?.segments[loop.end].id) + + const end = variation?.segments[loop.end] + const id = (end && "EndLoop" in end) ? end.EndLoop.id : undefined + console.assert(end && "EndLoop" in end, "Segment", end, "is not EndLoop") + + const [, endDispatch] = useSegment(id) function setIterCount(iterCount: number) { endDispatch({ @@ -250,14 +263,19 @@ function LoopDialog({ loop, close }: { loop: Loop, close: () => void }) { function deleteLoop() { const start = variation?.segments[loop.start] if (start) { - if (start.id == null) { + if (!("StartLoop" in start)) { + console.error("Segment", start, "is not StartLoop") + return + } + + if (start.StartLoop.id == null) { console.error("Segment", start, "does not have an ID") return } variationDispatch({ type: "toggle_segment_loop", - id: start.id, + id: start.StartLoop.id, }) } } diff --git a/mamar-web/src/app/doc/SegmentMap.tsx b/mamar-web/src/app/doc/SegmentMap.tsx index 59bffb6..80880ee 100644 --- a/mamar-web/src/app/doc/SegmentMap.tsx +++ b/mamar-web/src/app/doc/SegmentMap.tsx @@ -11,6 +11,7 @@ import TrackControls from "../emu/TrackControls" import { useBgm, useDoc, useVariation } from "../store" import useSelection, { SelectionProvider } from "../util/hooks/useSelection" import { Track } from "pm64-typegen" +import { getSegmentId } from "../store/segment" const TRACK_HEAD_WIDTH = 100 // Match with $trackHead-width @@ -177,16 +178,19 @@ function Container() { {i > 0 && } } {variation.segments.map((segment, segmentIndex) => { - if (segment.type === "Subseg") { + if ("Subseg" in segment) { return - + } else { - return
+ const id = getSegmentId(segment) + console.assert(id != null, "Segment", segment, "does not have an ID") + + return
} })}
)} diff --git a/mamar-web/src/app/store/segment.ts b/mamar-web/src/app/store/segment.ts index 302a3d9..0af902a 100644 --- a/mamar-web/src/app/store/segment.ts +++ b/mamar-web/src/app/store/segment.ts @@ -10,10 +10,12 @@ export type SegmentAction = { export function segmentReducer(segment: Segment, action: SegmentAction): Segment { switch (action.type) { case "set_loop_iter_count": - if (segment.type === "EndLoop") { + if ("EndLoop" in segment) { return { - ...segment, - iter_count: action.iter_count, + EndLoop: { + ...segment.EndLoop, + iter_count: action.iter_count, + }, } } else { console.warn("Tried to set loop iter count on non-end loop segment") @@ -22,10 +24,26 @@ export function segmentReducer(segment: Segment, action: SegmentAction): Segment } } +export function getSegmentId(segment: Segment): number | undefined { + if ("Subseg" in segment) { + return segment.Subseg.id + } else if ("StartLoop" in segment) { + return segment.StartLoop.id + } else if ("Wait" in segment) { + return segment.Wait.id + } else if ("EndLoop" in segment) { + return segment.EndLoop.id + } else if ("Unknown6" in segment) { + return segment.Unknown6.id + } else if ("Unknown7" in segment) { + return segment.Unknown7.id + } +} + export const useSegment = (id?: number, variationIndex?: number, docId?: string): [Segment | undefined, (action: SegmentAction) => void] => { const [variation, dispatch] = useVariation(variationIndex, docId) return [ - variation?.segments.find(s => s.id === id), + variation?.segments.find(s => getSegmentId(s) === id), action => { if (id) { dispatch({ diff --git a/mamar-web/src/app/store/variation.ts b/mamar-web/src/app/store/variation.ts index db8fbdc..3d25409 100644 --- a/mamar-web/src/app/store/variation.ts +++ b/mamar-web/src/app/store/variation.ts @@ -3,17 +3,17 @@ import { Segment, Variation } from "pm64-typegen" import { useBgm } from "./bgm" import { useDoc } from "./doc" -import { SegmentAction, segmentReducer } from "./segment" +import { getSegmentId, SegmentAction, segmentReducer } from "./segment" function cleanLoops(segments: (Segment | null)[]): Segment[] { return produce(segments, cleaned => { // Ensure StartLoop comes before EndLoop with the same label_index for (let i = 0; i < cleaned.length; i++) { const endLoop = cleaned[i] - if (endLoop?.type === "EndLoop") { + if (endLoop && "EndLoop" in endLoop) { for (let j = i + 1; j < cleaned.length; j++) { const startLoop = cleaned[j] - if (startLoop?.type === "StartLoop" && startLoop.label_index === endLoop.label_index) { + if (startLoop && "StartLoop" in startLoop && startLoop.StartLoop.label_index === endLoop.EndLoop.label_index) { const temp = cleaned[i] cleaned[i] = cleaned[j] cleaned[j] = temp @@ -27,9 +27,9 @@ function cleanLoops(segments: (Segment | null)[]): Segment[] { for (let i = 0; i < cleaned.length - 1; i++) { const a = cleaned[i] const b = cleaned[i + 1] - if (a?.type === "StartLoop" && - b?.type === "EndLoop" && - a.label_index === b.label_index + if (a && "StartLoop" in a && + b && "EndLoop" in b && + a.StartLoop.label_index === b.EndLoop.label_index ) { cleaned[i] = null cleaned[i + 1] = null @@ -61,7 +61,7 @@ export function variationReducer(variation: Variation, action: VariationAction): return { ...variation, segments: variation.segments.map(segment => { - if (segment.id === action.id) { + if (getSegmentId(segment) === action.id) { return segmentReducer(segment, action.action) } else { return segment @@ -70,7 +70,7 @@ export function variationReducer(variation: Variation, action: VariationAction): } case "move_segment": return produce(variation, draft => { - const fromIndex = draft.segments.findIndex(s => s.id === action.id) + const fromIndex = draft.segments.findIndex(s => getSegmentId(s)=== action.id) if (fromIndex === -1) return const segment = draft.segments[fromIndex] @@ -81,9 +81,10 @@ export function variationReducer(variation: Variation, action: VariationAction): }) case "add_segment": const newSeg: Segment = { - type: "Subseg", - id: action.id, - trackList: action.trackList, + Subseg: { + id: action.id, + trackList: action.trackList, + }, } return { ...variation, @@ -94,7 +95,7 @@ export function variationReducer(variation: Variation, action: VariationAction): } case "toggle_segment_loop": { return produce(variation, draft => { - const i = draft.segments.findIndex(s => s.id === action.id) + const i = draft.segments.findIndex(s => getSegmentId(s) === action.id) if (i === -1) return let inLoop = false @@ -104,12 +105,12 @@ export function variationReducer(variation: Variation, action: VariationAction): // Traverse backwards to find StartLoops for (let j = i - 1; j >= 0; j--) { const s = draft.segments[j] - if (s.type === "StartLoop" && s.label_index !== undefined) { - const startIndex = s.label_index + if ("StartLoop" in s && s.StartLoop.label_index !== undefined) { + const startIndex = s.StartLoop.label_index // Now, search for matching EndLoop after the StartLoop for (let k = i + 1; k < draft.segments.length; k++) { const segment = draft.segments[k] - if (segment.type === "EndLoop" && segment.label_index === startIndex) { + if ("EndLoop" in segment && segment.EndLoop.label_index === startIndex) { inLoop = true loopStartIndex = j loopEndIndex = k @@ -120,8 +121,15 @@ export function variationReducer(variation: Variation, action: VariationAction): } } - const nextId = Math.max(...draft.segments.map(s => s.id)) + 1 - const nextLabel = Math.max(0, ...draft.segments.map(s => ((s.type === "StartLoop" || s.type === "EndLoop") ? s.label_index ?? 0 : 0))) + 1 + const nextId = Math.max(...draft.segments.map(s => getSegmentId(s) ?? 0)) + 1 + const nextLabel = Math.max(0, ...draft.segments.map(s => { + if ("StartLoop" in s) + return s.StartLoop.label_index + else if ("EndLoop" in s) + return s.EndLoop.label_index + else + return 0 + })) + 1 if (inLoop) { // Remove the loop start/end @@ -129,8 +137,8 @@ export function variationReducer(variation: Variation, action: VariationAction): draft.segments.splice(loopEndIndex - 1, 1) } else { // Create a new loop around this segment - draft.segments.splice(i, 0, { type: "StartLoop", id: nextId, label_index: nextLabel }) - draft.segments.splice(i + 2, 0, { type: "EndLoop", id: nextId + 1, label_index: nextLabel, iter_count: 0 }) + draft.segments.splice(i, 0, { StartLoop: { id: nextId, label_index: nextLabel } }) + draft.segments.splice(i + 2, 0, { EndLoop: { id: nextId + 1, label_index: nextLabel, iter_count: 0 } }) } draft.segments = cleanLoops(draft.segments) diff --git a/pm64/src/bgm/mod.rs b/pm64/src/bgm/mod.rs index 4cdaed2..aec690d 100644 --- a/pm64/src/bgm/mod.rs +++ b/pm64/src/bgm/mod.rs @@ -249,7 +249,6 @@ pub struct Variation { } #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize, TypeDef)] -#[serde(tag = "type")] pub enum Segment { #[serde(rename_all = "camelCase")] Subseg { From bc1f1689debfcbb0b59a0f0b5efb142de614b9ac Mon Sep 17 00:00:00 2001 From: Darxoon Date: Mon, 29 Dec 2025 20:41:05 +0100 Subject: [PATCH 3/5] rename camel case serde fields back to snake case --- mamar-web/src/app/InstrumentInput.tsx | 4 ++-- mamar-web/src/app/doc/Ruler.tsx | 2 +- mamar-web/src/app/doc/SegmentMap.tsx | 8 ++++---- mamar-web/src/app/doc/SubsegDetails.tsx | 6 +++--- mamar-web/src/app/doc/Tracker.tsx | 2 +- mamar-web/src/app/store/bgm.ts | 12 ++++++------ mamar-web/src/app/store/variation.ts | 2 +- pm64/src/bgm/cmd.rs | 1 - pm64/src/bgm/mod.rs | 7 ------- 9 files changed, 18 insertions(+), 26 deletions(-) diff --git a/mamar-web/src/app/InstrumentInput.tsx b/mamar-web/src/app/InstrumentInput.tsx index 1945eac..7d6ddc9 100644 --- a/mamar-web/src/app/InstrumentInput.tsx +++ b/mamar-web/src/app/InstrumentInput.tsx @@ -84,8 +84,8 @@ export default function InstrumentInput({ index, onChange }: Props) { dispatch({ type: "update_instrument", index, partial: { reverb } })} /> - dispatch({ type: "update_instrument", index, partial: { coarseTune } })} /> - dispatch({ type: "update_instrument", index, partial: { fineTune } })} /> + dispatch({ type: "update_instrument", index, partial: { coarse_tune } })} /> + dispatch({ type: "update_instrument", index, partial: { fine_tune } })} /> } diff --git a/mamar-web/src/app/doc/Ruler.tsx b/mamar-web/src/app/doc/Ruler.tsx index 5721c64..2378578 100644 --- a/mamar-web/src/app/doc/Ruler.tsx +++ b/mamar-web/src/app/doc/Ruler.tsx @@ -115,7 +115,7 @@ export function useSegmentLengths(): number[] { return segments.map(segment => { if (bgm && "Subseg" in segment) { - const master = bgm.trackLists[segment.Subseg.trackList].tracks[0] + const master = bgm.track_lists[segment.Subseg.track_list].tracks[0] return master.commands.reduce((totalDelay, event) => { if (event.type === "Delay") { return totalDelay + event.value diff --git a/mamar-web/src/app/doc/SegmentMap.tsx b/mamar-web/src/app/doc/SegmentMap.tsx index 80880ee..962a4f6 100644 --- a/mamar-web/src/app/doc/SegmentMap.tsx +++ b/mamar-web/src/app/doc/SegmentMap.tsx @@ -22,7 +22,7 @@ function hasParentTrack({ polyphony }: Track): boolean { function PianoRollThumbnail({ trackIndex, trackListIndex }: { trackIndex: number, trackListIndex: number }) { const [doc, dispatch] = useDoc() const [bgm] = useBgm() - const track = bgm?.trackLists[trackListIndex]?.tracks[trackIndex] + const track = bgm?.track_lists[trackListIndex]?.tracks[trackIndex] const isSelected = doc?.panelContent.type === "tracker" && doc?.panelContent.trackList === trackListIndex && doc?.panelContent.track === trackIndex const nameId = useId() const commands = useDeferredValue(track?.commands) @@ -50,8 +50,8 @@ function PianoRollThumbnail({ trackIndex, trackListIndex }: { trackIndex: number aria-labelledby={nameId} className={classNames({ [styles.pianoRollThumbnail]: true, - [styles.drumRegion]: track.isDrumTrack, - [styles.disabledRegion]: track.isDisabled, + [styles.drumRegion]: track.is_drum_track, + [styles.disabledRegion]: track.is_disabled, [styles.hasInterestingParentTrack]: hasParentTrack(track), [styles.selected]: isSelected, })} @@ -184,7 +184,7 @@ function Container() { colorVersion={6} UNSAFE_style={ticksToStyle(segmentLengths[segmentIndex])} > - + } else { const id = getSegmentId(segment) diff --git a/mamar-web/src/app/doc/SubsegDetails.tsx b/mamar-web/src/app/doc/SubsegDetails.tsx index 6240bf1..21a0132 100644 --- a/mamar-web/src/app/doc/SubsegDetails.tsx +++ b/mamar-web/src/app/doc/SubsegDetails.tsx @@ -16,7 +16,7 @@ export interface Props { export default function SubsegDetails({ trackListId, trackIndex }: Props) { const hid = useId() const [bgm, dispatch] = useBgm() - const track = bgm?.trackLists[trackListId]?.tracks[trackIndex] + const track = bgm?.track_lists[trackListId]?.tracks[trackIndex] // Track name editing is debounced to prevent dispatch spam when typing const [name, setName] = useState(track?.name) @@ -42,9 +42,9 @@ export default function SubsegDetails({ trackListId, trackIndex }: Props) { value={name} onChange={setName} /> - dispatch({ type: "modify_track_settings", trackList: trackListId, track: trackIndex, isDisabled: !v })}>Enabled + dispatch({ type: "modify_track_settings", trackList: trackListId, track: trackIndex, isDisabled: !v })}>Enabled {trackIndex !== 0 ? <> - dispatch({ type: "modify_track_settings", trackList: trackListId, track: trackIndex, isDrumTrack })}>Percussion + dispatch({ type: "modify_track_settings", trackList: trackListId, track: trackIndex, isDrumTrack })}>Percussion { dispatch({ type: "modify_track_settings", trackList: trackListId, track: trackIndex, polyphony }) }} /> diff --git a/mamar-web/src/app/doc/Tracker.tsx b/mamar-web/src/app/doc/Tracker.tsx index 7108691..1703f5f 100644 --- a/mamar-web/src/app/doc/Tracker.tsx +++ b/mamar-web/src/app/doc/Tracker.tsx @@ -531,7 +531,7 @@ function CommandList({ width, height }: { }) { const [bgm] = useBgm() const { trackListId, trackIndex } = useContext(trackListCtx)! - const track = bgm?.trackLists[trackListId]?.tracks[trackIndex] + const track = bgm?.track_lists[trackListId]?.tracks[trackIndex] const commands: pm64.Event[] = track?.commands ?? [] return { - const track = draft.trackLists[action.trackList].tracks[action.track] + const track = draft.track_lists[action.trackList].tracks[action.track] track.commands = arrayMove(track.commands, action.oldIndex, action.newIndex) }) case "update_track_command": return produce(bgm, draft => { - const track = draft.trackLists[action.trackList].tracks[action.track] + const track = draft.track_lists[action.trackList].tracks[action.track] for (let i = 0; i < track.commands.length; i++) { if (track.commands[i].id === action.command.id) { track.commands[i] = action.command @@ -84,23 +84,23 @@ export function bgmReducer(bgm: Bgm, action: BgmAction): Bgm { }) case "delete_track_command": return produce(bgm, draft => { - const track = draft.trackLists[action.trackList].tracks[action.track] + const track = draft.track_lists[action.trackList].tracks[action.track] track.commands.splice(action.index, 1) }) case "modify_track_settings": return produce(bgm, draft => { - const track = draft.trackLists[action.trackList].tracks[action.track] + const track = draft.track_lists[action.trackList].tracks[action.track] if (action.name !== undefined) { track.name = action.name } if (action.isDisabled !== undefined) { - track.isDisabled = action.isDisabled + track.is_disabled = action.isDisabled } if (action.polyphony !== undefined) { track.polyphony = action.polyphony } if (action.isDrumTrack !== undefined) { - track.isDrumTrack = action.isDrumTrack + track.is_drum_track = action.isDrumTrack } }) case "update_instrument": diff --git a/mamar-web/src/app/store/variation.ts b/mamar-web/src/app/store/variation.ts index 3d25409..f689006 100644 --- a/mamar-web/src/app/store/variation.ts +++ b/mamar-web/src/app/store/variation.ts @@ -83,7 +83,7 @@ export function variationReducer(variation: Variation, action: VariationAction): const newSeg: Segment = { Subseg: { id: action.id, - trackList: action.trackList, + track_list: action.trackList, }, } return { diff --git a/pm64/src/bgm/cmd.rs b/pm64/src/bgm/cmd.rs index f74cee6..43b3083 100644 --- a/pm64/src/bgm/cmd.rs +++ b/pm64/src/bgm/cmd.rs @@ -565,7 +565,6 @@ enum DelayLookup { } #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, TypeDef)] -#[serde(rename_all = "camelCase")] pub struct Event { pub id: Id, #[serde(flatten)] diff --git a/pm64/src/bgm/mod.rs b/pm64/src/bgm/mod.rs index aec690d..0231b72 100644 --- a/pm64/src/bgm/mod.rs +++ b/pm64/src/bgm/mod.rs @@ -33,7 +33,6 @@ pub type TrackListId = u64; #[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize, TypeDef)] #[serde(default)] -#[serde(rename_all = "camelCase")] pub struct Bgm { pub name: String, @@ -243,14 +242,12 @@ impl Bgm { } #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize, TypeDef)] -#[serde(rename_all = "camelCase")] pub struct Variation { pub segments: Vec, } #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize, TypeDef)] pub enum Segment { - #[serde(rename_all = "camelCase")] Subseg { #[serde(skip_serializing_if = "Option::is_none")] id: Option, @@ -321,7 +318,6 @@ mod segment_commands { #[derive(Clone, Default, PartialEq, Eq, Debug, Serialize, Deserialize, TypeDef)] #[serde(default)] -#[serde(rename_all = "camelCase")] pub struct TrackList { /// Encode/decode file position. #[serde(skip_serializing_if = "Option::is_none")] @@ -351,7 +347,6 @@ impl TrackList { #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize, TypeDef)] #[serde(default)] -#[serde(rename_all = "camelCase")] pub struct Track { #[serde(default)] pub name: String, @@ -439,7 +434,6 @@ impl Polyphony { } #[derive(Clone, Default, PartialEq, Eq, Debug, Serialize, Deserialize, TypeDef)] -#[serde(rename_all = "camelCase")] #[serde(default)] pub struct Drum { pub bank: u8, @@ -464,7 +458,6 @@ pub struct Drum { } #[derive(Clone, Default, PartialEq, Eq, Debug, Serialize, Deserialize, TypeDef)] -#[serde(rename_all = "camelCase")] #[serde(default)] pub struct Instrument { /// Upper nibble = bank. (0..=6 are valid?) From 904b6cf84996f1b4bc53ffa231547962ff1ab977 Mon Sep 17 00:00:00 2001 From: Darxoon Date: Mon, 29 Dec 2025 23:34:04 +0100 Subject: [PATCH 4/5] remove type tag on command enum for better ron output --- mamar-web/src/app/doc/Ruler.tsx | 10 +- mamar-web/src/app/doc/SegmentMap.tsx | 18 +- mamar-web/src/app/doc/SubsegDetails.tsx | 5 +- mamar-web/src/app/doc/Tracker.tsx | 255 +++++++++++++----------- pm64/src/bgm/cmd.rs | 1 - 5 files changed, 160 insertions(+), 129 deletions(-) diff --git a/mamar-web/src/app/doc/Ruler.tsx b/mamar-web/src/app/doc/Ruler.tsx index 2378578..25bc789 100644 --- a/mamar-web/src/app/doc/Ruler.tsx +++ b/mamar-web/src/app/doc/Ruler.tsx @@ -1,6 +1,6 @@ import { Button, ButtonGroup, Content, Dialog, DialogTrigger, Divider, Form, Heading, NumberField, Switch } from "@adobe/react-spectrum" import classNames from "classnames" -import { Segment } from "pm64-typegen" +import { Event, Segment } from "pm64-typegen" import { useState } from "react" import { usePress } from "react-aria" @@ -116,9 +116,11 @@ export function useSegmentLengths(): number[] { return segments.map(segment => { if (bgm && "Subseg" in segment) { const master = bgm.track_lists[segment.Subseg.track_list].tracks[0] - return master.commands.reduce((totalDelay, event) => { - if (event.type === "Delay") { - return totalDelay + event.value + const commands = master.commands as unknown as Event[] + + return commands.reduce((totalDelay, event) => { + if ("Delay" in event) { + return totalDelay + event.Delay.value } else { return totalDelay } diff --git a/mamar-web/src/app/doc/SegmentMap.tsx b/mamar-web/src/app/doc/SegmentMap.tsx index 962a4f6..abb36f3 100644 --- a/mamar-web/src/app/doc/SegmentMap.tsx +++ b/mamar-web/src/app/doc/SegmentMap.tsx @@ -84,11 +84,11 @@ const Thumbnail = memo(({ commands }: { commands: Event[] }) => { let maxPitch = 0 let hasNoteInRange = false for (const command of commands) { - if (command.type === "Note") { - minPitch = Math.min(minPitch, command.pitch) - maxPitch = Math.max(maxPitch, command.pitch) + if ("Note" in command) { + minPitch = Math.min(minPitch, command.Note.pitch) + maxPitch = Math.max(maxPitch, command.Note.pitch) - if (command.pitch >= c2 && command.pitch <= c5) { + if (command.Note.pitch >= c2 && command.Note.pitch <= c5) { hasNoteInRange = true } } @@ -109,16 +109,16 @@ const Thumbnail = memo(({ commands }: { commands: Event[] }) => { const notes = [] let time = 0 for (const command of commands) { - if (command.type === "Note") { + if ("Note" in command) { notes.push() - } else if (command.type === "Delay") { - time += command.value + } else if ("Delay" in command) { + time += command.Delay.value } } diff --git a/mamar-web/src/app/doc/SubsegDetails.tsx b/mamar-web/src/app/doc/SubsegDetails.tsx index 21a0132..5f3d6cb 100644 --- a/mamar-web/src/app/doc/SubsegDetails.tsx +++ b/mamar-web/src/app/doc/SubsegDetails.tsx @@ -6,7 +6,8 @@ import styles from "./SubsegDetails.module.scss" import Tracker from "./Tracker" import { useBgm } from "../store" -import { Polyphony } from "pm64-typegen" +import { Bgm, Polyphony } from "pm64-typegen" +import { BgmAction } from "../store/bgm" export interface Props { trackListId: number @@ -15,7 +16,7 @@ export interface Props { export default function SubsegDetails({ trackListId, trackIndex }: Props) { const hid = useId() - const [bgm, dispatch] = useBgm() + const [bgm, dispatch]: [Bgm | undefined, (action: BgmAction) => void] = useBgm() const track = bgm?.track_lists[trackListId]?.tracks[trackIndex] // Track name editing is debounced to prevent dispatch spam when typing diff --git a/mamar-web/src/app/doc/Tracker.tsx b/mamar-web/src/app/doc/Tracker.tsx index 1703f5f..dc153b5 100644 --- a/mamar-web/src/app/doc/Tracker.tsx +++ b/mamar-web/src/app/doc/Tracker.tsx @@ -31,452 +31,477 @@ function InputBox({ children }: { children: ReactNode }) { } -function Command({ command }:{ command: pm64.Event }) { +/** + * Converts something like `"a" & {id: number} | {x: number, id: number}` + * into `{a: null, id: number} | {x: number, id: number}` + * to be more accurate to serde because the TS mappings are wrong + */ +type FixSerdeEnum = X extends {id: number} & infer Str ? Str extends string ? {id: number} & {[key in Str]: null} : X : X + +type DeepPartial = T extends object ? { + [P in keyof T]?: DeepPartial; +} : T; + +function Command({ command: rawCommand }:{ command: pm64.Event }) { + // fix rust typescript mappings + const command = rawCommand as FixSerdeEnum + + console.log(command) + const [, dispatch] = useBgm() const { trackListId, trackIndex } = useContext(trackListCtx)! - const mutate = (partial: Partial) => { + const mutate = (partial: DeepPartial) => { // TODO: debounce trailing + const newCommand: any = { ...command } + for (const [key, value] of Object.entries(partial)) { + if (typeof value === 'object') { + newCommand[key] = { ...newCommand[key], ...value } + } else { + newCommand[key] = value + } + } + dispatch({ type: "update_track_command", trackList: trackListId, track: trackIndex, - command: { ...command, ...partial } as pm64.Event, + command: newCommand as pm64.Event, }) } - if (command.type === "End") { + if ("End" in command) { return
end region
- } else if (command.type === "Delay") { + } else if ("Delay" in command) { return
wait - mutate({ value })} /> + mutate({ Delay: { value } })} /> ticks
- } else if (command.type === "Note") { + } else if ("Note" in command) { return
play note - mutate({ pitch })} /> + mutate({ Note: { pitch } })} /> at volume - mutate({ velocity })} /> + mutate({ Note: { velocity } })} /> for - mutate({ length })} /> + mutate({ Note: { length } })} /> ticks
- } else if (command.type === "MasterTempo") { + } else if ("MasterTempo" in command) { return
set tempo to mutate({ value })} + onChange={value => mutate({ MasterTempo: { value } })} />
- } else if (command.type === "MasterVolume"){ + } else if ("MasterVolume" in command){ return
set master volume to mutate({ value })} + onChange={value => mutate({ MasterVolume: { value } })} />
- } else if (command.type === "MasterPitchShift") { + } else if ("MasterPitchShift" in command) { return
pitch shift master mutate({ cent })} + onChange={cent => mutate({ MasterPitchShift: { cent } })} /> cents
- } else if (command.type === "UnkCmdE3") { + } else if ("UnkCmdE3" in command) { return
unk command E3 effect type mutate({ bank })} + onChange={effect_type => mutate({ UnkCmdE3: { effect_type } })} />
- } else if (command.type === "MasterTempoFade") { + } else if ("MasterTempoFade" in command) { return
fade tempo to mutate({ value })} + onChange={value => mutate({ MasterTempoFade: { value } })} /> over mutate({ time })} + onChange={time => mutate({ MasterTempoFade: { time } })} /> ticks
- } else if (command.type === "MasterVolumeFade") { + } else if ("MasterVolumeFade" in command) { return
fade master volume to mutate({ volume })} + onChange={volume => mutate({ MasterVolumeFade: { volume } })} /> over mutate({ time })} + onChange={time => mutate({ MasterVolumeFade: { time } })} /> ticks
- } else if (command.type === "MasterEffect") { + } else if ("MasterEffect" in command) { // TODO: effect combobox return
use room effect mutate({ index })} + onChange={index => mutate({ MasterEffect: { index } })} /> value mutate({ value })} + onChange={value => mutate({ MasterEffect: { value } })} />
- } else if (command.type === "TrackOverridePatch") { + } else if ("TrackOverridePatch" in command) { return
override instrument bank mutate({ bank })} + onChange={bank => mutate({ TrackOverridePatch: { bank } })} /> patch mutate({ patch })} + onChange={patch => mutate({ TrackOverridePatch: { patch } })} />
- } else if (command.type === "SubTrackVolume") { + } else if ("SubTrackVolume" in command) { return
set region volume to mutate({ value })} + onChange={value => mutate({ SubTrackVolume: { value } })} />
- } else if (command.type === "SubTrackPan") { + } else if ("SubTrackPan" in command) { // TODO: bespoke input for pan value return
set region pan to mutate({ value })} + onChange={value => mutate({ SubTrackPan: { value } })} />
- } else if (command.type === "SubTrackReverb") { + } else if ("SubTrackReverb" in command) { return
set region reverb to mutate({ value })} + onChange={value => mutate({ SubTrackReverb: { value } })} />
- } else if (command.type === "SegTrackVolume") { + } else if ("SegTrackVolume" in command) { return
set volume to mutate({ value })} + onChange={value => mutate({ SegTrackVolume: { value } })} />
- } else if (command.type === "SubTrackCoarseTune") { + } else if ("SubTrackCoarseTune" in command) { return
set region coarse tune to mutate({ value })} + onChange={value => mutate({ SubTrackCoarseTune: { value } })} />
- } else if (command.type === "SubTrackFineTune") { + } else if ("SubTrackFineTune" in command) { return
set region fine tune to mutate({ value })} + onChange={value => mutate({ SubTrackFineTune: { value } })} />
- } else if (command.type === "SegTrackTune") { + } else if ("SegTrackTune" in command) { return
set pitch bend to mutate({ bend })} + onChange={bend => mutate({ SegTrackTune: { bend } })} />
- } else if (command.type === "TrackTremolo") { + } else if ("TrackTremolo" in command) { return
tremolo for mutate({ time })} + onChange={time => mutate({ TrackTremolo: { time } })} /> ticks at speed mutate({ speed })} + onChange={speed => mutate({ TrackTremolo: { speed } })} /> with mutate({ amount })} + onChange={amount => mutate({ TrackTremolo: { amount } })} /> wobble
- } else if (command.type === "TrackTremoloSpeed") { + } else if ("TrackTremoloSpeed" in command) { return
set tremolo speed to mutate({ value })} + onChange={value => mutate({ TrackTremoloSpeed: { value } })} />
- } else if (command.type === "TrackTremoloTime") { + } else if ("TrackTremoloTime" in command) { return
set tremolo duration to mutate({ time })} + onChange={time => mutate({ TrackTremoloTime: { time } })} />
- } else if (command.type === "TrackTremoloStop") { + } else if ("TrackTremoloStop" in command) { return
stop tremolo
- } else if (command.type === "UnkCmdF4") { + } else if ("UnkCmdF4" in command) { return
unknown command F4
- } else if (command.type === "SetTrackVoice") { + } else if ("SetTrackVoice" in command) { return
use instrument mutate({ index })} + index={command.SetTrackVoice.index} + onChange={index => mutate({ SetTrackVoice: { index } })} />
- } else if (command.type === "TrackVolumeFade") { + } else if ("TrackVolumeFade" in command) { return
fade track volume to mutate({ value })} + onChange={value => mutate({ TrackVolumeFade: { value } })} /> over mutate({ time })} + onChange={time => mutate({ TrackVolumeFade: { time } })} /> ticks
- } else if (command.type === "SubTrackReverbType") { + } else if ("SubTrackReverbType" in command) { return
set region reverb type to mutate({ index })} + onChange={index => mutate({ SubTrackReverbType: { index } })} />
- } else if (command.type === "Jump") { + } else if ("Jump" in command) { return
jump - {!(command.unk_00 === 0 && command.unk_02 === 0) ? <> + {!(command.Jump.unk_00 === 0 && command.Jump.unk_02 === 0) ? <> mutate({ unk_00 })} + onChange={unk_00 => mutate({ Jump: { unk_00 } })} /> mutate({ unk_02 })} + onChange={unk_02 => mutate({ Jump: { unk_02 } })} /> : null}
- } else if (command.type === "EventTrigger") { + } else if ("EventTrigger" in command) { return
trigger event mutate({ event_info })} + onChange={event_info => mutate({ EventTrigger: { event_info } })} />
- } else if (command.type === "Detour") { + } else if ("Detour" in command) { // TODO: draggable jump arrow block like human resource machine return
detour mutate({ start_label })} + value={command.Detour.start_label} + onChange={start_label => mutate({ Detour: { start_label } })} /> to mutate({ end_label })} + value={command.Detour.end_label} + onChange={end_label => mutate({ Detour: { end_label } })} />
- } else if (command.type === "UnkCmdFF") { + } else if ("UnkCmdFF" in command) { return
unknown command FF 0 mutate({ unk_00 })} + onChange={unk_00 => mutate({ UnkCmdFF: { unk_00 } })} /> 1 mutate({ unk_01 })} + onChange={unk_01 => mutate({ UnkCmdFF: { unk_01 } })} /> 2 mutate({ unk_02 })} + onChange={unk_02 => mutate({ UnkCmdFF: { unk_02 } })} />
- } else if (command.type === "Marker") { + } else if ("Marker" in command) { return
jump target " mutate({ label })} + value={command.Marker.label} + onChange={label => mutate({ Marker: { label } })} /> " @@ -534,6 +559,8 @@ function CommandList({ width, height }: { const track = bgm?.track_lists[trackListId]?.tracks[trackIndex] const commands: pm64.Event[] = track?.commands ?? [] + console.log("command list", trackListId, trackIndex) + return () + console.log("tracker") + return
Date: Tue, 30 Dec 2025 00:21:18 +0100 Subject: [PATCH 5/5] turn commands with just "value" field into tuple variants and turn Bgm.track_lists field into BTreeMap --- mamar-web/src/app/doc/Ruler.tsx | 2 +- mamar-web/src/app/doc/SegmentMap.tsx | 2 +- mamar-web/src/app/doc/Tracker.tsx | 38 ++++++------ pm64/src/bgm/cmd.rs | 88 +++++++++------------------- pm64/src/bgm/de.rs | 28 ++++----- pm64/src/bgm/en.rs | 20 +++---- pm64/src/bgm/mamar.rs | 4 +- pm64/src/bgm/midi.rs | 38 ++++++------ pm64/src/bgm/mod.rs | 5 +- 9 files changed, 94 insertions(+), 131 deletions(-) diff --git a/mamar-web/src/app/doc/Ruler.tsx b/mamar-web/src/app/doc/Ruler.tsx index 25bc789..49826e2 100644 --- a/mamar-web/src/app/doc/Ruler.tsx +++ b/mamar-web/src/app/doc/Ruler.tsx @@ -120,7 +120,7 @@ export function useSegmentLengths(): number[] { return commands.reduce((totalDelay, event) => { if ("Delay" in event) { - return totalDelay + event.Delay.value + return totalDelay + event.Delay } else { return totalDelay } diff --git a/mamar-web/src/app/doc/SegmentMap.tsx b/mamar-web/src/app/doc/SegmentMap.tsx index abb36f3..b6d1fdb 100644 --- a/mamar-web/src/app/doc/SegmentMap.tsx +++ b/mamar-web/src/app/doc/SegmentMap.tsx @@ -118,7 +118,7 @@ const Thumbnail = memo(({ commands }: { commands: Event[] }) => { height={1} />) } else if ("Delay" in command) { - time += command.Delay.value + time += command.Delay } } diff --git a/mamar-web/src/app/doc/Tracker.tsx b/mamar-web/src/app/doc/Tracker.tsx index dc153b5..dd70920 100644 --- a/mamar-web/src/app/doc/Tracker.tsx +++ b/mamar-web/src/app/doc/Tracker.tsx @@ -77,7 +77,7 @@ function Command({ command: rawCommand }:{ command: pm64.Event }) { } else if ("Delay" in command) { return
wait - mutate({ Delay: { value } })} /> + mutate({ Delay: value })} /> ticks
} else if ("Note" in command) { @@ -95,10 +95,10 @@ function Command({ command: rawCommand }:{ command: pm64.Event }) { set tempo to mutate({ MasterTempo: { value } })} + onChange={value => mutate({ MasterTempo: value })} />
@@ -107,10 +107,10 @@ function Command({ command: rawCommand }:{ command: pm64.Event }) { set master volume to mutate({ MasterVolume: { value } })} + onChange={value => mutate({ MasterVolume: value })} />
@@ -231,10 +231,10 @@ function Command({ command: rawCommand }:{ command: pm64.Event }) { set region volume to mutate({ SubTrackVolume: { value } })} + onChange={value => mutate({ SubTrackVolume: value })} />
@@ -244,10 +244,10 @@ function Command({ command: rawCommand }:{ command: pm64.Event }) { set region pan to mutate({ SubTrackPan: { value } })} + onChange={value => mutate({ SubTrackPan: value })} /> @@ -256,10 +256,10 @@ function Command({ command: rawCommand }:{ command: pm64.Event }) { set region reverb to mutate({ SubTrackReverb: { value } })} + onChange={value => mutate({ SubTrackReverb: value })} /> @@ -268,10 +268,10 @@ function Command({ command: rawCommand }:{ command: pm64.Event }) { set volume to mutate({ SegTrackVolume: { value } })} + onChange={value => mutate({ SegTrackVolume: value })} /> @@ -280,10 +280,10 @@ function Command({ command: rawCommand }:{ command: pm64.Event }) { set region coarse tune to mutate({ SubTrackCoarseTune: { value } })} + onChange={value => mutate({ SubTrackCoarseTune: value })} /> @@ -292,10 +292,10 @@ function Command({ command: rawCommand }:{ command: pm64.Event }) { set region fine tune to mutate({ SubTrackFineTune: { value } })} + onChange={value => mutate({ SubTrackFineTune: value })} /> @@ -346,10 +346,10 @@ function Command({ command: rawCommand }:{ command: pm64.Event }) { return
set tremolo speed to mutate({ TrackTremoloSpeed: { value } })} + onChange={value => mutate({ TrackTremoloSpeed: value })} />
} else if ("TrackTremoloTime" in command) { diff --git a/pm64/src/bgm/cmd.rs b/pm64/src/bgm/cmd.rs index 2d486a2..5709d8b 100644 --- a/pm64/src/bgm/cmd.rs +++ b/pm64/src/bgm/cmd.rs @@ -95,11 +95,7 @@ impl CommandSeq { let mut old_delay_range = index..index; let mut delta_time: usize = 0; - while let Some(Event { - command: Delay { value: t }, - .. - }) = self.vec.get(old_delay_range.end) - { + while let Some(Event { command: Delay(t), .. }) = self.vec.get(old_delay_range.end) { old_delay_range.end += 1; delta_time += *t; } @@ -112,7 +108,7 @@ impl CommandSeq { fn delay(time: usize) -> Box> { if time > 0 { - Box::new(iter::once(Command::Delay { value: time }.into())) + Box::new(iter::once(Command::Delay(time).into())) } else { Box::new(iter::empty()) } @@ -181,11 +177,7 @@ impl CommandSeq { } let mut delta_time: usize = 0; - while let Some(Event { - command: Delay { value: t }, - .. - }) = self.vec.get(old_delay_range.end) - { + while let Some(Event { command: Delay(t), .. }) = self.vec.get(old_delay_range.end) { old_delay_range.end += 1; delta_time += *t; } @@ -198,7 +190,7 @@ impl CommandSeq { fn delay(time: usize) -> Box> { if time > 0 { - Box::new(iter::once(Command::Delay { value: time }.into())) + Box::new(iter::once(Command::Delay(time).into())) } else { Box::new(iter::empty()) } @@ -251,7 +243,7 @@ impl CommandSeq { for command in self.vec.iter() { if let Event { - command: Delay { value: delta_time }, + command: Delay(delta_time), .. } = command { @@ -268,7 +260,7 @@ impl CommandSeq { /// Equivalent to [CommandSeq::len_time] for a sequence with no [Command::Note]s. pub fn playback_time(&self) -> usize { self.iter_time().last().map_or(0, |(time, event)| match event.command { - Command::Delay { value: delta } => time + delta, + Command::Delay(delta) => time + delta, Command::Note { length, .. } => time + length as usize, _ => time, }) @@ -285,12 +277,8 @@ impl CommandSeq { /// sounds the same). pub fn shrink(&mut self) { // Remove useless commands - self.vec.retain(|event| { - !matches!( - event.command, - Command::Delay { value: 0 } | Command::Note { length: 0, .. } - ) - }); + self.vec + .retain(|event| !matches!(event.command, Command::Delay(0) | Command::Note { length: 0, .. })); // TODO: combine redundant subsequences (e.g. multiple Delay, multiple MasterTempo without a delay between) @@ -336,17 +324,17 @@ impl CommandSeq { } pub fn clear_command(&mut self, idx: usize) { - self.vec[idx] = Command::Delay { value: 0 }.into(); + self.vec[idx] = Command::Delay(0).into(); } pub fn zero_all_delays(&mut self) { for cmd in &mut self.vec { if let Event { - command: Command::Delay { value: _ }, + command: Command::Delay(_), .. } = cmd { - *cmd = Command::Delay { value: 0 }.into(); + *cmd = Command::Delay(0).into(); } } } @@ -432,7 +420,7 @@ impl CommandSeq { for (index, command) in self.vec.iter().enumerate() { if let Event { - command: Delay { value: delta_time }, + command: Delay(delta_time), .. } = command { @@ -580,9 +568,7 @@ pub enum Command { End, /// Sleeps for however many ticks before continuing playback on this track. - Delay { - value: usize, - }, + Delay(usize), /// Plays a note or drum sound. Note { @@ -592,14 +578,10 @@ pub enum Command { }, /// Sets the beats-per-minute of the composition. - MasterTempo { - value: u16, - }, + MasterTempo(u16), /// Sets the composition volume. - MasterVolume { - value: u8, - }, + MasterVolume(u8), /// Sets the composition transpose value. MasterPitchShift { @@ -636,33 +618,21 @@ pub enum Command { }, /// Sets the volume for this track only. Resets at the end of the [super::Subsegment]. - SubTrackVolume { - value: u8, - }, + SubTrackVolume(u8), /// Left = (+/-)0. /// Middle = (+/-)64. /// Right = (+/-)127. - SubTrackPan { - value: i8, - }, + SubTrackPan(i8), - SubTrackReverb { - value: u8, - }, + SubTrackReverb(u8), /// Sets the volume for this track only. Resets at the end of the [super::Segment]. - SegTrackVolume { - value: u8, - }, + SegTrackVolume(u8), - SubTrackCoarseTune { - value: u8, - }, + SubTrackCoarseTune(u8), - SubTrackFineTune { - value: u8, - }, + SubTrackFineTune(u8), SegTrackTune { bend: i16, @@ -675,9 +645,7 @@ pub enum Command { time: u8, }, - TrackTremoloSpeed { - value: u8, - }, + TrackTremoloSpeed(u8), TrackTremoloTime { time: u8, @@ -750,7 +718,7 @@ pub const DELAY_MAX: u8 = 0x78; impl Default for Command { /// Returns a no-op command. Cannot be encoded. fn default() -> Self { - Delay { value: 0 } + Delay(0) } } @@ -819,7 +787,7 @@ impl<'a> Iterator for TimeIter<'a> { let ret = (self.current_time, command); if let Event { - command: Delay { value: delta_time }, + command: Delay(delta_time), .. } = command { @@ -917,7 +885,7 @@ mod test { velocity: 100, length: 10, }, - Command::Delay { value: 15 }, + Command::Delay(15), Command::Note { pitch: 100, // same pitch velocity: 100, @@ -948,7 +916,7 @@ mod test { velocity: 100, length: 10, }, - Command::Delay { value: 10 }, // note should finish + Command::Delay(10), // note should finish Command::Note { pitch: 200, velocity: 100, @@ -962,9 +930,9 @@ mod test { fn split_at() { let mut seq = CommandSeq::from(vec![ Command::Marker { label: "A".to_string() }, - Command::Delay { value: 5 }, + Command::Delay(5), Command::Marker { label: "B".to_string() }, - Command::Delay { value: 5 }, + Command::Delay(5), Command::End, ]); let split = seq.split_at(4); diff --git a/pm64/src/bgm/de.rs b/pm64/src/bgm/de.rs index 35918ec..b5e6919 100644 --- a/pm64/src/bgm/de.rs +++ b/pm64/src/bgm/de.rs @@ -413,9 +413,7 @@ impl CommandSeq { } // Delay - 0x01..=0x77 => Command::Delay { - value: cmd_byte as usize, - }, + 0x01..=0x77 => Command::Delay(cmd_byte as usize), // Long delay 0x78..=0x7F => { @@ -425,9 +423,7 @@ impl CommandSeq { let num_256s = (cmd_byte - 0x78) as usize; let extend = f.read_u8()? as usize; - Command::Delay { - value: 0x78 + num_256s * 256 + extend, - } + Command::Delay(0x78 + num_256s * 256 + extend) // This logic taken from N64MidiTool //Command::Delay(0x78 + (cmd_byte as usize) + ((f.read_u8()? & 7) as usize) << 8) @@ -460,10 +456,8 @@ impl CommandSeq { } } - 0xE0 => Command::MasterTempo { - value: f.read_u16_be()?, - }, - 0xE1 => Command::MasterVolume { value: f.read_u8()? }, + 0xE0 => Command::MasterTempo(f.read_u16_be()?), + 0xE1 => Command::MasterVolume(f.read_u8()?), 0xE2 => Command::MasterPitchShift { cent: f.read_u8()? }, 0xE3 => Command::UnkCmdE3 { effect_type: f.read_u8()?, @@ -485,19 +479,19 @@ impl CommandSeq { bank: f.read_u8()?, patch: f.read_u8()?, }, - 0xE9 => Command::SubTrackVolume { value: f.read_u8()? }, - 0xEA => Command::SubTrackPan { value: f.read_i8()? }, - 0xEB => Command::SubTrackReverb { value: f.read_u8()? }, - 0xEC => Command::SegTrackVolume { value: f.read_u8()? }, - 0xED => Command::SubTrackCoarseTune { value: f.read_u8()? }, - 0xEE => Command::SubTrackFineTune { value: f.read_u8()? }, + 0xE9 => Command::SubTrackVolume(f.read_u8()?), + 0xEA => Command::SubTrackPan(f.read_i8()?), + 0xEB => Command::SubTrackReverb(f.read_u8()?), + 0xEC => Command::SegTrackVolume(f.read_u8()?), + 0xED => Command::SubTrackCoarseTune(f.read_u8()?), + 0xEE => Command::SubTrackFineTune(f.read_u8()?), 0xEF => Command::SegTrackTune { bend: f.read_i16_be()? }, 0xF0 => Command::TrackTremolo { amount: f.read_u8()?, speed: f.read_u8()?, time: f.read_u8()?, }, - 0xF1 => Command::TrackTremoloSpeed { value: f.read_u8()? }, + 0xF1 => Command::TrackTremoloSpeed(f.read_u8()?), 0xF2 => Command::TrackTremoloTime { time: f.read_u8()? }, 0xF3 => Command::TrackTremoloStop, 0xF4 => Command::UnkCmdF4 { diff --git a/pm64/src/bgm/en.rs b/pm64/src/bgm/en.rs index 10a63fb..8f2a923 100644 --- a/pm64/src/bgm/en.rs +++ b/pm64/src/bgm/en.rs @@ -413,7 +413,7 @@ impl CommandSeq { for Event { command, .. } in self.iter() { match command { - Command::Delay { value: mut delay } => { + Command::Delay(mut delay) => { // https://github.com/KernelEquinox/midi2bgm/blob/master/midi2bgm.cpp#L202 while delay > 0 { if delay < 0x78 { @@ -456,11 +456,11 @@ impl CommandSeq { f.write_all(&[first_byte | 0xC0, second_byte])?; } } - Command::MasterTempo { value: bpm } => { + Command::MasterTempo(bpm) => { f.write_u8(0xE0)?; f.write_u16_be(*bpm)?; } - Command::MasterVolume { value: volume } => { + Command::MasterVolume(volume) => { f.write_u8(0xE1)?; f.write_u8(*volume)?; } @@ -488,27 +488,27 @@ impl CommandSeq { f.write_u8(*bank)?; f.write_u8(*patch)?; } - Command::SubTrackVolume { value: a } => { + Command::SubTrackVolume(a) => { f.write_u8(0xE9)?; f.write_u8(*a)?; } - Command::SubTrackPan { value: a } => { + Command::SubTrackPan(a) => { f.write_u8(0xEA)?; f.write_i8(*a)?; } - Command::SubTrackReverb { value: a } => { + Command::SubTrackReverb(a) => { f.write_u8(0xEB)?; f.write_u8(*a)?; } - Command::SegTrackVolume { value: a } => { + Command::SegTrackVolume(a) => { f.write_u8(0xEC)?; f.write_u8(*a)?; } - Command::SubTrackCoarseTune { value: a } => { + Command::SubTrackCoarseTune(a) => { f.write_u8(0xED)?; f.write_u8(*a)?; } - Command::SubTrackFineTune { value: a } => { + Command::SubTrackFineTune(a) => { f.write_u8(0xEE)?; f.write_u8(*a)?; } @@ -559,7 +559,7 @@ impl CommandSeq { f.write_u8(0xE3)?; f.write_u8(*effect_type)?; } - Command::TrackTremoloSpeed { value } => { + Command::TrackTremoloSpeed(value) => { f.write_u8(0xF1)?; f.write_u8(*value)?; } diff --git a/pm64/src/bgm/mamar.rs b/pm64/src/bgm/mamar.rs index 2307bff..97fcc8d 100644 --- a/pm64/src/bgm/mamar.rs +++ b/pm64/src/bgm/mamar.rs @@ -49,14 +49,14 @@ mod test { #[test] fn encode_decode_metadata_preserves_track_names() { - use std::collections::HashMap; + use std::collections::BTreeMap; let track_list = TrackList { pos: Some(0x1234), tracks: core::array::from_fn(|_| Track::default()), }; let mut bgm = Bgm { - track_lists: HashMap::from([(0x1234, track_list)]), + track_lists: BTreeMap::from([(0x1234, track_list)]), variations: [ Some(Variation { segments: vec![Segment::Subseg { diff --git a/pm64/src/bgm/midi.rs b/pm64/src/bgm/midi.rs index b90c9d3..71c4eb5 100644 --- a/pm64/src/bgm/midi.rs +++ b/pm64/src/bgm/midi.rs @@ -195,6 +195,7 @@ fn midi_track_to_bgm_track( use midly::{MidiMessage, TrackEventKind}; /// NoteOn data + #[derive(Clone, Copy)] struct Note { time: usize, vel: u8, @@ -222,7 +223,7 @@ fn midi_track_to_bgm_track( let mut set_bank_patch = false; let mut time = 0; - let mut started_notes: HashMap = HashMap::new(); // Maps key to notes that have not finished yet + let mut started_notes: BTreeMap = BTreeMap::new(); // Maps key to notes that have not finished yet let mut instrument_name = None; let mut track_name = None; @@ -305,7 +306,7 @@ fn midi_track_to_bgm_track( MidiMessage::Aftertouch { key: _, vel } | MidiMessage::ChannelAftertouch { vel } => { track .commands - .insert_end(time_cvt, Command::SubTrackVolume { value: vel.as_int() }); + .insert_end(time_cvt, Command::SubTrackVolume(vel.as_int())); } MidiMessage::ProgramChange { program } => { if !set_bank_patch { @@ -351,13 +352,13 @@ fn midi_track_to_bgm_track( pitch_range_cmd_state = PitchRangeCommandState::None; } // Channel Volume - 7 | 39 => track.commands.insert_end(time_cvt, Command::SubTrackVolume { value }), + 7 | 39 => track.commands.insert_end(time_cvt, Command::SubTrackVolume(value)), // Pan - 10 | 42 | 8 | 40 => track - .commands - .insert_end(time_cvt, Command::SubTrackPan { value: value as i8 }), + 10 | 42 | 8 | 40 => { + track.commands.insert_end(time_cvt, Command::SubTrackPan(value as i8)) + } // Effect control 1 - 12 | 44 => track.commands.insert_end(time_cvt, Command::SubTrackReverb { value }), + 12 | 44 => track.commands.insert_end(time_cvt, Command::SubTrackReverb(value)), // Damper pedal on/off (sustain) 64 => { let sustain = if value >= 64 { @@ -408,7 +409,7 @@ fn midi_track_to_bgm_track( } // All notes off / All sound off 123 | 120 => { - for (key, start) in started_notes.drain() { + for (&key, &start) in &started_notes { let length = time - start.time; track.commands.insert_end( convert_time(start.time, time_divisor), @@ -419,6 +420,8 @@ fn midi_track_to_bgm_track( }, ); } + + started_notes.clear(); } // Poly[phonic] mode on/off 126 => { @@ -443,12 +446,9 @@ fn midi_track_to_bgm_track( if track_number == 0 { let microseconds_per_beat = tempo.as_int() as f32; let beats_per_minute = (60_000_000.0 / microseconds_per_beat).round() as u16; - track.commands.insert_end( - time_cvt, - Command::MasterTempo { - value: beats_per_minute, - }, - ); + track + .commands + .insert_end(time_cvt, Command::MasterTempo(beats_per_minute)); log::debug!("bpm: {}", beats_per_minute); } else { log::warn!("ignoring non-master tempo change"); @@ -477,8 +477,8 @@ fn midi_track_to_bgm_track( track.commands.insert_many_start( 0, vec![ - Command::MasterTempo { value: 120 }, - Command::MasterVolume { value: 100 }, + Command::MasterTempo(120), + Command::MasterVolume(100), Command::MasterEffect { index: 0, value: 1 }, ], ); @@ -493,9 +493,9 @@ fn midi_track_to_bgm_track( track.commands.insert_many_start( 0, vec![ - Command::SubTrackReverb { value: 0 }, - Command::SubTrackVolume { value: 100 }, - Command::SubTrackPan { value: 64 }, + Command::SubTrackReverb(0), + Command::SubTrackVolume(100), + Command::SubTrackPan(64), Command::SetTrackVoice { index: voice_idx as u8 }, ], ); diff --git a/pm64/src/bgm/mod.rs b/pm64/src/bgm/mod.rs index 0231b72..cac32b2 100644 --- a/pm64/src/bgm/mod.rs +++ b/pm64/src/bgm/mod.rs @@ -10,8 +10,9 @@ pub mod mamar; #[cfg(feature = "midly")] pub mod midi; +use std::collections::BTreeMap; use std::ops::Range; -use std::{collections::HashMap, sync::LazyLock}; +use std::sync::LazyLock; use regex::Regex; use serde_derive::{Deserialize, Serialize}; @@ -41,7 +42,7 @@ pub struct Bgm { pub drums: Vec, pub instruments: Vec, - pub track_lists: HashMap, + pub track_lists: BTreeMap, #[serde(skip)] pub unknowns: Vec,