From f09a8e8f8eacca88d82ddaf66b0375dccfcdcf0e Mon Sep 17 00:00:00 2001 From: uvcat Date: Wed, 9 Jul 2025 23:40:23 -0700 Subject: [PATCH 01/21] Fix #100 and bug where 3rd snaps were not saved to the simfile. --- src/Dialogs/AdjustSync.cpp | 2 +- src/Dialogs/AdjustTempoSM5.cpp | 12 ++++++------ src/Dialogs/CustomSnap.cpp | 5 ++--- src/Editor/View.cpp | 6 +++--- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Dialogs/AdjustSync.cpp b/src/Dialogs/AdjustSync.cpp index 154df092..a3900d78 100644 --- a/src/Dialogs/AdjustSync.cpp +++ b/src/Dialogs/AdjustSync.cpp @@ -50,7 +50,7 @@ WgSpinner* DialogAdjustSync::myCreateWidgetRow(StringRef label, double& val, int { WgSpinner* spinner = myLayout.add(label); spinner->value.bind(&val); - spinner->setPrecision(3, 3); + spinner->setPrecision(3, 6); spinner->onChange.bind(this, &DialogAdjustSync::onAction, setAction); spinner->setTooltip(tooltip1); diff --git a/src/Dialogs/AdjustTempoSM5.cpp b/src/Dialogs/AdjustTempoSM5.cpp index c3c65293..3b6c9f1d 100644 --- a/src/Dialogs/AdjustTempoSM5.cpp +++ b/src/Dialogs/AdjustTempoSM5.cpp @@ -61,7 +61,7 @@ void DialogAdjustTempoSM5::myCreateWidgets() WgSpinner* spinner = myLayout.add("Delay"); spinner->value.bind(&myDelay); - spinner->setPrecision(3, 3); + spinner->setPrecision(3, 6); spinner->setStep(0.001); spinner->setRange(0, 1000); spinner->onChange.bind(this, &DialogAdjustTempoSM5::onAction, (int)ACT_DELAY_SET); @@ -70,7 +70,7 @@ void DialogAdjustTempoSM5::myCreateWidgets() myLayout.row().col(84).col(154); spinner = myLayout.add("Warp"); spinner->value.bind(&myWarp); - spinner->setPrecision(3, 3); + spinner->setPrecision(3, 6); spinner->setRange(0, 1000); spinner->onChange.bind(this, &DialogAdjustTempoSM5::onAction, (int)ACT_WARP_SET); spinner->setTooltip("Warp length at the current beat, in beats"); @@ -120,7 +120,7 @@ void DialogAdjustTempoSM5::myCreateWidgets() spinner = myLayout.add("Speed"); spinner->value.bind(&mySpeedRatio); - spinner->setPrecision(2, 2); + spinner->setPrecision(2, 6); spinner->setStep(0.1); spinner->setRange(0, 1000); spinner->onChange.bind(this, &DialogAdjustTempoSM5::onAction, (int)ACT_SPEED_SET); @@ -128,7 +128,7 @@ void DialogAdjustTempoSM5::myCreateWidgets() spinner = myLayout.add(); spinner->value.bind(&mySpeedDelay); - spinner->setPrecision(2, 2); + spinner->setPrecision(2, 6); spinner->setStep(0.1); spinner->setRange(0, 1000); spinner->onChange.bind(this, &DialogAdjustTempoSM5::onAction, (int)ACT_SPEED_SET); @@ -145,7 +145,7 @@ void DialogAdjustTempoSM5::myCreateWidgets() spinner = myLayout.add("Scroll"); spinner->value.bind(&myScrollRatio); - spinner->setPrecision(2, 2); + spinner->setPrecision(2, 6); spinner->setStep(0.1); spinner->setRange(0, 1000); spinner->onChange.bind(this, &DialogAdjustTempoSM5::onAction, (int)ACT_SCROLL_SET); @@ -153,7 +153,7 @@ void DialogAdjustTempoSM5::myCreateWidgets() spinner = myLayout.add("Fakes"); spinner->value.bind(&myFakeBeats); - spinner->setPrecision(3, 3); + spinner->setPrecision(3, 6); spinner->setRange(0, 1000); spinner->onChange.bind(this, &DialogAdjustTempoSM5::onAction, (int)ACT_FAKE_SET); spinner->setTooltip("Fake region, in beats"); diff --git a/src/Dialogs/CustomSnap.cpp b/src/Dialogs/CustomSnap.cpp index e7808d89..18212c0d 100644 --- a/src/Dialogs/CustomSnap.cpp +++ b/src/Dialogs/CustomSnap.cpp @@ -26,17 +26,16 @@ namespace Vortex { WgSpinner* scol = myLayout.add("Snapping"); scol->value.bind(&myCustomSnap); scol->onChange.bind(this, &DialogCustomSnap::onChange); - scol->setRange(1.0, 192.0); + scol->setRange(4.0, 192.0); scol->setPrecision(0, 0); scol->startCapturingText(); } void DialogCustomSnap::onChange() { - if (myCustomSnap > 0 && myCustomSnap <= 192) + if (myCustomSnap >= 4 && myCustomSnap <= 192) { gView->setCustomSnap(myCustomSnap); - //requestClose(); } } }; // namespace Vortex \ No newline at end of file diff --git a/src/Editor/View.cpp b/src/Editor/View.cpp index cd8085b8..4061ebd6 100644 --- a/src/Editor/View.cpp +++ b/src/Editor/View.cpp @@ -71,7 +71,7 @@ ViewImpl() , myZoomLevel(8) , myScaleLevel(4) , mySnapType(ST_NONE) - , myCustomSnap(1) + , myCustomSnap(20) , myUseTimeBasedView(true) , myUseReverseScroll(false) , myUseChartPreview(false) @@ -100,7 +100,7 @@ void loadSettings(XmrNode& settings) view->get("receptorX", &myReceptorX); view->get("receptorY", &myReceptorY); - myCustomSnap = min(max(myCustomSnap, 1), 192); + myCustomSnap = min(max(myCustomSnap, 5), 191); myZoomLevel = min(max(myZoomLevel, -2.0), 16.0); myScaleLevel = min(max(myScaleLevel, 1.0), 10.0); } @@ -441,7 +441,7 @@ void setSnapType(int type) void setCustomSnap(int size) { - if (size < 1) size = 1; + if (size < 4) size = 4; if (size > 192) size = 192; // If the custom snap is a non-custom value, set the snap to that value instead for (int i = 0; i < ST_CUSTOM; i++) From 450cb6b43ca766901e62809995023a25e2e8c9de Mon Sep 17 00:00:00 2001 From: uvcat Date: Thu, 10 Jul 2025 01:36:13 -0700 Subject: [PATCH 02/21] Fix hold quantizations being incorrectly saved. --- src/Simfile/SaveSm.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Simfile/SaveSm.cpp b/src/Simfile/SaveSm.cpp index 0b179962..8050ed9f 100644 --- a/src/Simfile/SaveSm.cpp +++ b/src/Simfile/SaveSm.cpp @@ -470,9 +470,10 @@ static int gcd(int a, int b) } } -static void GetSectionCompression(const char* section, int width, std::list quantVec, int& count, int& pitch) +static bool GetSectionCompression(const char* section, int width, std::list quantVec, int& count, int& pitch) { // Determines the best compression for the given section. + bool error = false; int best = ROWS_PER_NOTE_SECTION; String zeroline(width, '0'); std::list::iterator it; @@ -481,7 +482,7 @@ static void GetSectionCompression(const char* section, int width, std::listendrow - startRow) * numCols + (int)hold->col; section[pos] = '3'; - quantVec.push_front(it->quant); + quantVec.push_front(holds[it->col]->quant); --remainingHolds; } } @@ -623,8 +625,8 @@ static void WriteSections(ExportData& data) { int pos = (hold->endrow - startRow) * numCols + hold->col; section[pos] = '3'; + quantVec.push_front(holds[col]->quant); holds[col] = nullptr; - quantVec.push_front(it->quant); --remainingHolds; } } @@ -635,7 +637,10 @@ static void WriteSections(ExportData& data) int count, pitch; const char* m = section; quantVec.unique(); - GetSectionCompression(m, numCols, quantVec, count, pitch); + if (GetSectionCompression(m, numCols, quantVec, count, pitch)) + { + HudError("Bug: zero or negative quantization recorded in chart in measure starting at row %d, quantization %d", startRow, it->quant); + } quantVec.clear(); if (ROWS_PER_NOTE_SECTION % count != 0) { From 8ec05f9752fafa50902a64fa17b2628b43d5c0c3 Mon Sep 17 00:00:00 2001 From: sukibaby <163092272+sukibaby@users.noreply.github.com> Date: Thu, 10 Jul 2025 02:46:18 -0700 Subject: [PATCH 03/21] Improve stability of MP3 loading Switching to unique_ptr / make_unique manages the lifetime of the object automatically and also handles cleanup automatically. Addresses a common crash when loading MP3 via drag and drop. --- src/Editor/LoadMp3.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Editor/LoadMp3.cpp b/src/Editor/LoadMp3.cpp index 8cda7ae9..8298baba 100644 --- a/src/Editor/LoadMp3.cpp +++ b/src/Editor/LoadMp3.cpp @@ -4,6 +4,7 @@ #include #include +#include #include @@ -431,19 +432,18 @@ int MP3Loader::readFrames(int frames, short* buffer) SoundSource* LoadMP3(FileReader* file, String& title, String& artist) { - MP3Loader* loader = new MP3Loader; + std::unique_ptr loader = std::make_unique(); loader->file = file; // Decode and synth the first frame to check if the file is valid. if(!loader->decodeFirstFrame()) { loader->file = nullptr; - delete loader; return nullptr; } // The file is valid, return the MP3 loader. - return loader; + return loader.release(); } }; // namespace Vortex From b3e14e149485cb8941feb61f06b0ff994583c6d7 Mon Sep 17 00:00:00 2001 From: uvcat Date: Thu, 10 Jul 2025 09:52:32 -0700 Subject: [PATCH 04/21] More sanitization on the saving compression logic. --- src/Simfile/SaveSm.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Simfile/SaveSm.cpp b/src/Simfile/SaveSm.cpp index 8050ed9f..41447fa0 100644 --- a/src/Simfile/SaveSm.cpp +++ b/src/Simfile/SaveSm.cpp @@ -480,10 +480,12 @@ static bool GetSectionCompression(const char* section, int width, std::list 192) { + // If there's a quantization error assume nothing error = true; - continue; + lcm = ROWS_PER_NOTE_SECTION; + break; } lcm = lcm * *it / gcd(lcm, *it); if (lcm > ROWS_PER_NOTE_SECTION) From d14a13b567548d3b448d143681eaa4f5e5bf3a67 Mon Sep 17 00:00:00 2001 From: uvcat Date: Thu, 10 Jul 2025 12:02:16 -0700 Subject: [PATCH 05/21] Fix waveform rendering crash on loading mp3 audio --- src/Editor/Waveform.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Editor/Waveform.cpp b/src/Editor/Waveform.cpp index 92bf40ef..29217d94 100644 --- a/src/Editor/Waveform.cpp +++ b/src/Editor/Waveform.cpp @@ -457,6 +457,12 @@ void sampleEdges(WaveEdge* edges, int w, int h, int channel, int blockId, bool f return; } + // A crash can occur if another thread is loading the audio. Just do nothing if it is. + if (!music.isAllocated()) + { + return; + } + double sampleSkip = max(0.001, (samplesPerPixel / 200.0)); int wh = w / 2 - 1; From 353ef173fc7a70e574cf657ac097108ec343e430 Mon Sep 17 00:00:00 2001 From: uvcat Date: Thu, 10 Jul 2025 13:45:14 -0700 Subject: [PATCH 06/21] Fix quantization loading of holds across measures. --- src/Dialogs/AdjustSync.cpp | 2 ++ src/Dialogs/AdjustTempo.cpp | 2 ++ src/Simfile/LoadSm.cpp | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/src/Dialogs/AdjustSync.cpp b/src/Dialogs/AdjustSync.cpp index a3900d78..9134bca3 100644 --- a/src/Dialogs/AdjustSync.cpp +++ b/src/Dialogs/AdjustSync.cpp @@ -69,10 +69,12 @@ void DialogAdjustSync::myCreateWidgets() WgSpinner* offset = myCreateWidgetRow("Music offset", myOffset, ACT_SET_OFS, ACT_TWEAK_OFS, "Music start time relative to the first beat, in seconds", "Tweak the music offset"); offset->setRange(-100.0, 100.0); + offset->setPrecision(3, 6); offset->setStep(0.001); WgSpinner* bpm = myCreateWidgetRow("Initial BPM", myInitialBPM, ACT_SET_BPM, ACT_TWEAK_BPM, "Music tempo at the first beat, in beats per minute", "Tweak the initial BPM"); + bpm->setPrecision(3, 6); bpm->setRange(VC_MIN_BPM, VC_MAX_BPM); bpm->setStep(1.0); diff --git a/src/Dialogs/AdjustTempo.cpp b/src/Dialogs/AdjustTempo.cpp index a7423a1c..5331a4be 100644 --- a/src/Dialogs/AdjustTempo.cpp +++ b/src/Dialogs/AdjustTempo.cpp @@ -91,10 +91,12 @@ void DialogAdjustTempo::myCreateWidgets() WgSpinner* bpm = myCreateWidgetRow("BPM", 0, myBPM, ACT_BPM_SET); bpm->setRange(VC_MIN_BPM, VC_MAX_BPM); + bpm->setPrecision(3, 6); bpm->setStep(1.0); WgSpinner* stop = myCreateWidgetRow("Stop", 28, myStop, ACT_STOP_SET); stop->setRange(VC_MIN_STOP, VC_MAX_STOP); + stop->setPrecision(3, 6); stop->setStep(0.001); myLayout.row().col(242); diff --git a/src/Simfile/LoadSm.cpp b/src/Simfile/LoadSm.cpp index b9a40f06..ad0a11b7 100644 --- a/src/Simfile/LoadSm.cpp +++ b/src/Simfile/LoadSm.cpp @@ -2,6 +2,7 @@ #include #include +#include #include @@ -372,8 +373,29 @@ struct ReadNoteData NoteList* notes; Vector holdPos; Vector holdType; + Vector quants; }; +static int gcd(int a, int b) +{ + if (a == 0) + { + return b; + } + if (b == 0) + { + return a; + } + if (a > b) + { + return gcd(a - b, b); + } + else + { + return gcd(a, b - a); + } +} + static void ReadNoteRow(ReadNoteData& data, int row, char* p, int quantization) { for(int col = 0; col < data.numCols; ++col, ++p) @@ -391,6 +413,7 @@ static void ReadNoteRow(ReadNoteData& data, int row, char* p, int quantization) data.notes->append({row, row, (uint)col, (uint)data.player, NOTE_STEP_OR_HOLD, (uint) quantization}); data.holdType[col] = (*p == '2') ? NOTE_STEP_OR_HOLD : NOTE_ROLL; data.holdPos[col] = data.notes->size(); + data.quants[col] = quantization; } else if(*p == '3') { @@ -400,7 +423,18 @@ static void ReadNoteRow(ReadNoteData& data, int row, char* p, int quantization) auto* hold = data.notes->begin() + holdPos - 1; hold->endrow = row; hold->type = data.holdType[col]; + // Make sure we set the note to its largest quantization to avoid data loss + if (data.quants[col] > 0 && hold->quant > 0) + { + hold->quant = quantization * hold->quant / gcd(quantization, hold->quant); + } + else // There was some error, so always play safe and use 192 + { + HudError("Bug: couldn't get hold quantization in row %d", row); + hold->quant = 192; + } data.holdPos[col] = 0; + data.quants[col] = 0; } } else if(*p == 'M') @@ -448,6 +482,7 @@ static void ParseNotes(ParseData& data, Chart* chart, StringRef style, char* not readNoteData.numCols = numCols; readNoteData.notes = &chart->notes; readNoteData.holdPos.resize(numCols, 0); + readNoteData.quants.resize(numCols, 0); readNoteData.holdType.resize(numCols, NOTE_STEP_OR_HOLD); int numSections = 0; From 1471f05f0b0076b5d322f7aa5d3a8a5073329c21 Mon Sep 17 00:00:00 2001 From: uvcat Date: Thu, 10 Jul 2025 14:31:21 -0700 Subject: [PATCH 07/21] Handle changing the quantization while stepping. Refactored saving, and added failsafe mechanisms to prevent data loss. --- src/Core/Utils.h | 19 ++++++++++++ src/Editor/Editing.cpp | 28 +++++++++++++++++ src/Simfile/LoadSm.cpp | 23 ++------------ src/Simfile/SaveSm.cpp | 69 ++++++++++++++++++++---------------------- 4 files changed, 82 insertions(+), 57 deletions(-) diff --git a/src/Core/Utils.h b/src/Core/Utils.h index da7994a5..f9e32205 100644 --- a/src/Core/Utils.h +++ b/src/Core/Utils.h @@ -326,6 +326,25 @@ inline color32 ToColor32(float r, float g, float b, float a) return u32; } +static int gcd(int a, int b) +{ + if (a == 0) + { + return b; + } + if (b == 0) + { + return a; + } + if (a > b) + { + return gcd(a - b, b); + } + else + { + return gcd(a, b - a); + } +} }; // namespace Vortex #undef TT \ No newline at end of file diff --git a/src/Editor/Editing.cpp b/src/Editor/Editing.cpp index b8ef1b08..878c5636 100644 --- a/src/Editor/Editing.cpp +++ b/src/Editor/Editing.cpp @@ -327,6 +327,26 @@ void onChanges(int changes) // ================================================================================================ // EditingImpl :: member functions. +static int gcd(int a, int b) +{ + if (a == 0) + { + return b; + } + if (b == 0) + { + return a; + } + if (a > b) + { + return gcd(a - b, b); + } + else + { + return gcd(a, b - a); + } +} + void finishNotePlacement(int col) { auto& pnote = myPlacingNotes[col]; @@ -348,6 +368,14 @@ void finishNotePlacement(int col) } } + if (note.quant > 0 && note.quant <= 192) + { + note.quant = min(192u, note.quant * gView->getSnapQuant() / gcd(note.quant, gView->getSnapQuant())); + } + else + { + note.quant = 192; + } NoteEdit edit; edit.add.append(note); gNotes->modify(edit, false); diff --git a/src/Simfile/LoadSm.cpp b/src/Simfile/LoadSm.cpp index ad0a11b7..9b4a2dee 100644 --- a/src/Simfile/LoadSm.cpp +++ b/src/Simfile/LoadSm.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -376,26 +377,6 @@ struct ReadNoteData Vector quants; }; -static int gcd(int a, int b) -{ - if (a == 0) - { - return b; - } - if (b == 0) - { - return a; - } - if (a > b) - { - return gcd(a - b, b); - } - else - { - return gcd(a, b - a); - } -} - static void ReadNoteRow(ReadNoteData& data, int row, char* p, int quantization) { for(int col = 0; col < data.numCols; ++col, ++p) @@ -426,7 +407,7 @@ static void ReadNoteRow(ReadNoteData& data, int row, char* p, int quantization) // Make sure we set the note to its largest quantization to avoid data loss if (data.quants[col] > 0 && hold->quant > 0) { - hold->quant = quantization * hold->quant / gcd(quantization, hold->quant); + hold->quant = min(192u, quantization * hold->quant / gcd(quantization, hold->quant)); } else // There was some error, so always play safe and use 192 { diff --git a/src/Simfile/SaveSm.cpp b/src/Simfile/SaveSm.cpp index 41447fa0..4b6dc964 100644 --- a/src/Simfile/SaveSm.cpp +++ b/src/Simfile/SaveSm.cpp @@ -1,4 +1,5 @@ #include +#include #include @@ -450,24 +451,21 @@ static const char* GetDifficultyString(Difficulty difficulty) return "Edit"; } -static int gcd(int a, int b) +static inline bool TestSectionCompression(const char* section, int width, int quant) { - if (a == 0) - { - return b; - } - if (b == 0) - { - return a; - } - if (a > b) - { - return gcd(a - b, b); - } - else + String zeroline(width, '0'); + float mod = (float)ROWS_PER_NOTE_SECTION / quant; + for (int j = 0; j < ROWS_PER_NOTE_SECTION; ++j) { - return gcd(a, b - a); + float rem = round(fmod(j, mod)); + // Check all the compressed rows and make sure they are empty + if (rem > 0 && rem < static_cast(mod) + && memcmp(section + j * width, zeroline.str(), width)) + { + return false; + } } + return quant >= MIN_SECTIONS_PER_MEASURE; } static bool GetSectionCompression(const char* section, int width, std::list quantVec, int& count, int& pitch) @@ -505,35 +503,34 @@ static bool GetSectionCompression(const char* section, int width, std::list= 2; i--) + bool valid = false; + //The factor list is small, just check them all by hand, but only up to 96 at most since there won't be factors otherwise + for (int i = 4; i <= lcm / 2; i++) { // Skip anything that isn't a lcm factor if (lcm % i > 0) continue; - bool valid = true; - float mod = (float) ROWS_PER_NOTE_SECTION / i; - for (int j = 0; valid && j < ROWS_PER_NOTE_SECTION; ++j) - { - float rem = round(fmod(j, mod)); - // Check all the compressed rows and make sure they are empty - if (rem > 0 && rem < static_cast(mod) - && memcmp(section + j * width, zeroline.str(), width)) - { - valid = false; - break; - } - } - - // The first (largest) match is always the best - if (valid && i >= MIN_SECTIONS_PER_MEASURE) + // The first (smallest) match is always the best + if (TestSectionCompression(section, width, i)) { + valid = true; count = i; break; } } + + // If no factor was found, double-check we won't have any data loss from our lcm guess + // Why not check all factors? Saving files would be several times slower otherwise + if (!valid) + { + valid = TestSectionCompression(section, width, lcm); + } + if (!valid) + { + // Okay, we WOULD have had data loss, so set the rows to 192 and error + error = true; + count = ROWS_PER_NOTE_SECTION; + } } // Is our factor a standard snap? If so, use it. // If not, save the measure as 192nds for SM5 Editor compatibility. @@ -641,7 +638,7 @@ static void WriteSections(ExportData& data) quantVec.unique(); if (GetSectionCompression(m, numCols, quantVec, count, pitch)) { - HudError("Bug: zero or negative quantization recorded in chart in measure starting at row %d, quantization %d", startRow, it->quant); + HudError("Bug: invalid quantization recorded in chart in measure starting at row %d", startRow); } quantVec.clear(); if (ROWS_PER_NOTE_SECTION % count != 0) From a45a52a9534108b16800d3d74e8bb5899c32f620 Mon Sep 17 00:00:00 2001 From: uvcat Date: Thu, 10 Jul 2025 15:53:48 -0700 Subject: [PATCH 08/21] Don't save 6 rows. --- src/Simfile/SaveSm.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Simfile/SaveSm.cpp b/src/Simfile/SaveSm.cpp index 4b6dc964..3182e056 100644 --- a/src/Simfile/SaveSm.cpp +++ b/src/Simfile/SaveSm.cpp @@ -508,7 +508,8 @@ static bool GetSectionCompression(const char* section, int width, std::list 0) continue; + // Skipping 6 is a special case, since it's a factor of 192 but not a standard snap + if (lcm % i > 0 || i == 6) continue; // The first (smallest) match is always the best if (TestSectionCompression(section, width, i)) From 41aee2f9ef540178e0910984611e59fc8a2eb52e Mon Sep 17 00:00:00 2001 From: uvcat Date: Thu, 10 Jul 2025 16:10:45 -0700 Subject: [PATCH 09/21] Ditto, but better. --- src/Simfile/SaveSm.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Simfile/SaveSm.cpp b/src/Simfile/SaveSm.cpp index 3182e056..9c205108 100644 --- a/src/Simfile/SaveSm.cpp +++ b/src/Simfile/SaveSm.cpp @@ -508,8 +508,7 @@ static bool GetSectionCompression(const char* section, int width, std::list 0 || i == 6) continue; + if (lcm % i > 0) continue; // The first (smallest) match is always the best if (TestSectionCompression(section, width, i)) @@ -539,6 +538,11 @@ static bool GetSectionCompression(const char* section, int width, std::list Date: Thu, 10 Jul 2025 17:10:20 -0700 Subject: [PATCH 10/21] Massively simplify. --- src/Simfile/SaveSm.cpp | 84 ++++-------------------------------------- 1 file changed, 7 insertions(+), 77 deletions(-) diff --git a/src/Simfile/SaveSm.cpp b/src/Simfile/SaveSm.cpp index 9c205108..27ea5d76 100644 --- a/src/Simfile/SaveSm.cpp +++ b/src/Simfile/SaveSm.cpp @@ -468,83 +468,20 @@ static inline bool TestSectionCompression(const char* section, int width, int qu return quant >= MIN_SECTIONS_PER_MEASURE; } -static bool GetSectionCompression(const char* section, int width, std::list quantVec, int& count, int& pitch) +static void GetSectionCompression(const char* section, int width, int& count, int& pitch) { + count = ROWS_PER_NOTE_SECTION; // Determines the best compression for the given section. - bool error = false; - int best = ROWS_PER_NOTE_SECTION; - String zeroline(width, '0'); - std::list::iterator it; - int lcm = 1; - for (it = quantVec.begin(); it != quantVec.end(); it++) + // We actually don't care about custom snaps for this at all, since we want to save 192nds for non-standard-compressible snaps. + for (int i = 1; i < NUM_MEASURE_SUBDIV - 1; i++) { - if (*it <= 0 || *it > 192) - { - // If there's a quantization error assume nothing - error = true; - lcm = ROWS_PER_NOTE_SECTION; - break; - } - lcm = lcm * *it / gcd(lcm, *it); - if (lcm > ROWS_PER_NOTE_SECTION) + if (TestSectionCompression(section, width, MEASURE_SUBDIV[i])) { - lcm = ROWS_PER_NOTE_SECTION; + count = MEASURE_SUBDIV[i]; break; } } - - // Set whole and half step measures to be quarter notes by default - if (lcm <= MIN_SECTIONS_PER_MEASURE) - { - count = MIN_SECTIONS_PER_MEASURE; - } - else - { - // Determines the best compression for the given section. - // Maybe lcm is the best factor, so just keep that. - count = lcm; - bool valid = false; - //The factor list is small, just check them all by hand, but only up to 96 at most since there won't be factors otherwise - for (int i = 4; i <= lcm / 2; i++) - { - // Skip anything that isn't a lcm factor - if (lcm % i > 0) continue; - - // The first (smallest) match is always the best - if (TestSectionCompression(section, width, i)) - { - valid = true; - count = i; - break; - } - } - - // If no factor was found, double-check we won't have any data loss from our lcm guess - // Why not check all factors? Saving files would be several times slower otherwise - if (!valid) - { - valid = TestSectionCompression(section, width, lcm); - } - if (!valid) - { - // Okay, we WOULD have had data loss, so set the rows to 192 and error - error = true; - count = ROWS_PER_NOTE_SECTION; - } - } - // Is our factor a standard snap? If so, use it. - // If not, save the measure as 192nds for SM5 Editor compatibility. - if (ROWS_PER_NOTE_SECTION % count != 0) - { - count = ROWS_PER_NOTE_SECTION; - } - // Yes it is weird... but we don't save 6ths even though they factor into 192 evenly. - if (count == 6) - { - count = 12; - } pitch = (ROWS_PER_NOTE_SECTION * width) / count; - return error; } static void WriteSections(ExportData& data) @@ -594,12 +531,10 @@ static void WriteSections(ExportData& data) if(it->row == it->endrow) { section[pos] = GetNoteChar(it->type); - quantVec.push_front(it->quant); } else { section[pos] = GetHoldChar(it->type); - quantVec.push_front(it->quant); auto hold = holds[it->col]; if(hold) { @@ -607,7 +542,6 @@ static void WriteSections(ExportData& data) { int pos = ((int)hold->endrow - startRow) * numCols + (int)hold->col; section[pos] = '3'; - quantVec.push_front(holds[it->col]->quant); --remainingHolds; } } @@ -629,7 +563,6 @@ static void WriteSections(ExportData& data) { int pos = (hold->endrow - startRow) * numCols + hold->col; section[pos] = '3'; - quantVec.push_front(holds[col]->quant); holds[col] = nullptr; --remainingHolds; } @@ -641,10 +574,7 @@ static void WriteSections(ExportData& data) int count, pitch; const char* m = section; quantVec.unique(); - if (GetSectionCompression(m, numCols, quantVec, count, pitch)) - { - HudError("Bug: invalid quantization recorded in chart in measure starting at row %d", startRow); - } + GetSectionCompression(m, numCols, count, pitch); quantVec.clear(); if (ROWS_PER_NOTE_SECTION % count != 0) { From 247be31d7451b71663d06bf812d06b64712c497b Mon Sep 17 00:00:00 2001 From: uvcat Date: Thu, 10 Jul 2025 17:59:51 -0700 Subject: [PATCH 11/21] .sm files now save decimals to 6 places. Closes #110. --- src/Dialogs/AdjustSync.cpp | 2 -- src/Managers/TempoMan.cpp | 2 +- src/Simfile/SaveSm.cpp | 32 ++++++++++++++++---------------- src/Simfile/Segments.cpp | 2 +- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/Dialogs/AdjustSync.cpp b/src/Dialogs/AdjustSync.cpp index 9134bca3..a3900d78 100644 --- a/src/Dialogs/AdjustSync.cpp +++ b/src/Dialogs/AdjustSync.cpp @@ -69,12 +69,10 @@ void DialogAdjustSync::myCreateWidgets() WgSpinner* offset = myCreateWidgetRow("Music offset", myOffset, ACT_SET_OFS, ACT_TWEAK_OFS, "Music start time relative to the first beat, in seconds", "Tweak the music offset"); offset->setRange(-100.0, 100.0); - offset->setPrecision(3, 6); offset->setStep(0.001); WgSpinner* bpm = myCreateWidgetRow("Initial BPM", myInitialBPM, ACT_SET_BPM, ACT_TWEAK_BPM, "Music tempo at the first beat, in beats per minute", "Tweak the initial BPM"); - bpm->setPrecision(3, 6); bpm->setRange(VC_MIN_BPM, VC_MAX_BPM); bpm->setStep(1.0); diff --git a/src/Managers/TempoMan.cpp b/src/Managers/TempoMan.cpp index 74995443..0eb4b2bb 100644 --- a/src/Managers/TempoMan.cpp +++ b/src/Managers/TempoMan.cpp @@ -470,7 +470,7 @@ static bool Differs(const BpmRange& a, const BpmRange& b) static double ClampAndRound(double val, double min, double max) { - return round(clamp(val, min, max) * 1000.0) / 1000.0; + return round(clamp(val, min, max) * 1000000.0) / 1000000.0; } void modify(const SegmentEdit& edit) diff --git a/src/Simfile/SaveSm.cpp b/src/Simfile/SaveSm.cpp index 27ea5d76..28189d42 100644 --- a/src/Simfile/SaveSm.cpp +++ b/src/Simfile/SaveSm.cpp @@ -81,7 +81,7 @@ static void WriteTag(ExportData& data, const char* tag, double value, ForceWrite { if(ShouldWrite(data, when, value != 0, sscOnly)) { - data.file.printf("#%s:%.3f;\n", tag, value); + data.file.printf("#%s:%.6f;\n", tag, value); } } @@ -182,7 +182,7 @@ static void WriteBpms(ExportData& data, const Tempo* tempo) WriteSegments(data, "BPMS", tempo, START_OF_LINE, ',', SONG_ONLY, false, [&](const BpmChange& change) { - data.file.printf("%.3f=%.6f", + data.file.printf("%.6f=%.6f", ToBeat(change.row), change.bpm); }); } @@ -192,7 +192,7 @@ static void WriteStops(ExportData& data, const Tempo* tempo) WriteSegments(data, "STOPS", tempo, START_OF_LINE, ',', SONG_ONLY, false, [&](const Stop& stop) { - data.file.printf("%.3f=%.3f", + data.file.printf("%.6f=%.6f", ToBeat(stop.row), stop.seconds); }); } @@ -202,7 +202,7 @@ static void WriteDelays(ExportData& data, const Tempo* tempo) WriteSegments(data, "DELAYS", tempo, END_OF_LINE, ',', NEVER, true, [&](const Delay& delay) { - data.file.printf("%.3f=%.3f", + data.file.printf("%.6f=%.6f", ToBeat(delay.row), delay.seconds); }); } @@ -212,7 +212,7 @@ static void WriteWarps(ExportData& data, const Tempo* tempo) WriteSegments(data, "WARPS", tempo, END_OF_LINE, ',', NEVER, true, [&](const Warp& warp) { - data.file.printf("%.3f=%.3f", + data.file.printf("%.6f=%.6f", ToBeat(warp.row), ToBeat(warp.numRows)); }); } @@ -222,7 +222,7 @@ static void WriteSpeeds(ExportData& data, const Tempo* tempo) WriteSegments(data, "SPEEDS", tempo, END_OF_LINE, ',', NEVER, true, [&](const Speed& speed) { - data.file.printf("%.3f=%.3f=%.3f=%i", + data.file.printf("%.6f=%.6f=%.6f=%i", ToBeat(speed.row), speed.ratio, speed.delay, speed.unit); }); } @@ -232,7 +232,7 @@ static void WriteScrolls(ExportData& data, const Tempo* tempo) WriteSegments(data, "SCROLLS", tempo, END_OF_LINE, ',', NEVER, true, [&](const Scroll& scroll) { - data.file.printf("%.3f=%.3f", + data.file.printf("%.6f=%.6f", ToBeat(scroll.row), scroll.ratio); }); } @@ -242,7 +242,7 @@ static void WriteTickCounts(ExportData& data, const Tempo* tempo) WriteSegments(data, "TICKCOUNTS", tempo, END_OF_LINE, ',', NEVER, true, [&](const TickCount& tick) { - data.file.printf("%.3f=%i", + data.file.printf("%.6f=%i", ToBeat(tick.row), tick.ticks); }); } @@ -252,7 +252,7 @@ static void WriteTimeSignatures(ExportData& data, const Tempo* tempo) WriteSegments(data, "TIMESIGNATURES", tempo, END_OF_LINE, ',', NEVER, true, [&](const TimeSignature& sig) { - data.file.printf("%.3f=%i=%i", + data.file.printf("%.6f=%i=%i", ToBeat(sig.row), (int)ToBeat(sig.rowsPerMeasure), sig.beatNote); }); } @@ -262,7 +262,7 @@ static void WriteLabels(ExportData& data, const Tempo* tempo) WriteSegments