diff --git a/mm/2s2h/Enhancements/Audio/AudioCollection.h b/mm/2s2h/Enhancements/Audio/AudioCollection.h index b596c65b7e..3f39034bf2 100644 --- a/mm/2s2h/Enhancements/Audio/AudioCollection.h +++ b/mm/2s2h/Enhancements/Audio/AudioCollection.h @@ -52,7 +52,7 @@ class AudioCollection { public: static AudioCollection* Instance; AudioCollection(); - std::map GetAllSequences() const { + const std::map& GetAllSequences() const { return mSequenceMap; } std::set GetIncludedSequences() const { diff --git a/mm/2s2h/Enhancements/Audio/AudioEditor.cpp b/mm/2s2h/Enhancements/Audio/AudioEditor.cpp index b215ea8967..0be4a51efd 100644 --- a/mm/2s2h/Enhancements/Audio/AudioEditor.cpp +++ b/mm/2s2h/Enhancements/Audio/AudioEditor.cpp @@ -6,7 +6,6 @@ #include #include #include -// #include "../randomizer/3drando/random.hpp" #include "../../BenPort.h" #include #include "../../BenGui/UIWidgets.hpp" @@ -22,6 +21,9 @@ extern "C" s8 gSfxDefaultReverb; using namespace UIWidgets; +#define CVAR_PREFIX_AUDIO "gAudioEditor" +#define CVAR_AUDIO(var) CVAR_PREFIX_AUDIO "." var + static WidgetInfo lowHpAlarm; static WidgetInfo muteCarpenterSfx; static WidgetInfo childGoronCry; @@ -34,6 +36,113 @@ static WidgetInfo voicePitchEnable; static WidgetInfo randoMusicOnSceneChange; static WidgetInfo randomAudioOnSeedGen; +namespace AudioPreview { + +// Prevents race conditions with async audio engine updates. +#define AUDIO_PREVIEW_GRACE_FRAMES 30 + +extern "C" { +extern u8 sSeqFlags[]; +#define SEQ_FLAG_FANFARE (1 << 1) +#define SEQ_FLAG_FANFARE_KAMARO (1 << 2) +} + +static u16 sPreviewRestoreSeqId = NA_BGM_DISABLED; +static bool sIsBgmPreviewing = false; +static uint8_t sGraceFrames = 0; + +void Update() { + if (sGraceFrames > 0) { + sGraceFrames--; + return; + } + + u16 playingSeqId = CVarGetInteger(CVAR_AUDIO("Playing"), 0); + if (playingSeqId == 0) { + return; + } + + // Previews may run on players mismatched to their flags (e.g. fanfare songs on main BGM player). + // Manually check both to avoid polluting engine code. + u16 activeBgmId = AudioSeq_GetActiveSeqId(SEQ_PLAYER_BGM_MAIN); + u16 activeFanfareId = AudioSeq_GetActiveSeqId(SEQ_PLAYER_FANFARE); + + // Sequences < MAX_AUTHENTIC_SEQID use 8-bit IDs (masked with 0xFF), + // custom sequences >= MAX_AUTHENTIC_SEQID use full 16-bit IDs + u16 mask = (playingSeqId < MAX_AUTHENTIC_SEQID) ? 0xFF : 0xFFFF; + u16 matchId = playingSeqId & mask; + + bool isPlaying = (activeBgmId != NA_BGM_DISABLED && (activeBgmId & mask) == matchId) || + (activeFanfareId != NA_BGM_DISABLED && (activeFanfareId & mask) == matchId); + + if (!isPlaying) { + CVarSetInteger(CVAR_AUDIO("Playing"), 0); + sIsBgmPreviewing = false; + } +} + +void Stop() { + u16 playingSeqId = CVarGetInteger(CVAR_AUDIO("Playing"), 0); + if (playingSeqId == 0) { + return; + } + + if (sPreviewRestoreSeqId != NA_BGM_DISABLED) { + SEQCMD_PLAY_SEQUENCE(SEQ_PLAYER_BGM_MAIN, 0, sPreviewRestoreSeqId + SEQ_FLAG_ASYNC); + sPreviewRestoreSeqId = NA_BGM_DISABLED; + } else if (sIsBgmPreviewing) { + SEQCMD_STOP_SEQUENCE(SEQ_PLAYER_BGM_MAIN, 0); + } + sIsBgmPreviewing = false; + + // Ensure fanfares/ocarina sequences stop when ending a preview + SEQCMD_STOP_SEQUENCE(SEQ_PLAYER_FANFARE, 10); + + CVarSetInteger(CVAR_AUDIO("Playing"), 0); +} + +void Play(u16 sequenceId, SeqType sequenceType) { + + // Restore original BGM before switching to non-BGM preview (songs/fanfares/ocarina). + // Prevents the preview BGM from becoming orphaned without a stop button. + if (sPreviewRestoreSeqId != NA_BGM_DISABLED && !(sequenceType & (SEQ_BGM_WORLD | SEQ_BGM_EVENT | SEQ_BGM_BATTLE))) { + SEQCMD_PLAY_SEQUENCE(SEQ_PLAYER_BGM_MAIN, 0, sPreviewRestoreSeqId + SEQ_FLAG_ASYNC); + sPreviewRestoreSeqId = NA_BGM_DISABLED; + sIsBgmPreviewing = false; + } + + if (sequenceType & (SEQ_SFX | SEQ_VOICE)) { + AudioSfx_PlaySfx(sequenceId, &gZeroVec3f, 4, &gSfxDefaultFreqAndVolScale, &gSfxDefaultFreqAndVolScale, + &gSfxDefaultReverb); + } else if (sequenceType & SEQ_INSTRUMENT) { + AudioOcarina_SetInstrument(sequenceId - INSTRUMENT_OFFSET); + AudioOcarina_SetPlaybackSong(9, 1); + } else if (sequenceType & (SEQ_FANFARE | SEQ_OCARINA | SEQ_BGM_SONGS) || + (sequenceId < MAX_AUTHENTIC_SEQID && + (sSeqFlags[sequenceId] & (SEQ_FLAG_FANFARE | SEQ_FLAG_FANFARE_KAMARO)))) { + Audio_PlayFanfare(sequenceId); + sGraceFrames = AUDIO_PREVIEW_GRACE_FRAMES; + CVarSetInteger(CVAR_AUDIO("Playing"), sequenceId); + } else { + // Stop active fanfares to prevent BGM suppression + SEQCMD_STOP_SEQUENCE(SEQ_PLAYER_FANFARE, 1); + + u16 curSeqId = AudioSeq_GetActiveSeqId(SEQ_PLAYER_BGM_MAIN); + // Only capture restore point if not already previewing + if (sPreviewRestoreSeqId == NA_BGM_DISABLED && curSeqId != NA_BGM_DISABLED) { + sPreviewRestoreSeqId = curSeqId; + } + + Audio_SetSequenceMode(SEQ_MODE_IGNORE); + SEQCMD_PLAY_SEQUENCE(SEQ_PLAYER_BGM_MAIN, 1, sequenceId); + sIsBgmPreviewing = true; + + sGraceFrames = AUDIO_PREVIEW_GRACE_FRAMES; + CVarSetInteger(CVAR_AUDIO("Playing"), sequenceId); + } +} +} // namespace AudioPreview + namespace BenGui { extern std::shared_ptr mBenMenu; } @@ -54,8 +163,6 @@ size_t AuthenticCountBySequenceType(SeqType type) { return AudioCollection::Instance->CountSequencesByType(type); } -#define CVAR_AUDIO(var) CVAR_PREFIX_AUDIO "." var - // Grabs the current BGM sequence ID and replays it // which will lookup the proper override, or reset back to vanilla void ReplayCurrentBGM() { @@ -172,51 +279,38 @@ void UnlockGroup(const std::map& map, SeqType type) { } } } -extern "C" void Audio_ForceRestorePreviousBgm(void); -extern "C" void PreviewSequence(u16 seqId); void DrawPreviewButton(uint16_t sequenceId, std::string sfxKey, SeqType sequenceType) { const std::string cvarKey = AudioCollection::Instance->GetCvarKey(sfxKey); const std::string hiddenKey = "##" + cvarKey; const std::string stopButton = ICON_FA_STOP + hiddenKey; const std::string previewButton = ICON_FA_PLAY + hiddenKey; + UIWidgets::PushStyleButton(THEME_COLOR, ImVec2(10.0f, 6.0f)); if (CVarGetInteger(CVAR_AUDIO("Playing"), 0) == sequenceId) { if (ImGui::Button(stopButton.c_str())) { - Audio_ForceRestorePreviousBgm(); - CVarSetInteger(CVAR_AUDIO("Playing"), 0); + AudioPreview::Stop(); } UIWidgets::Tooltip("Stop Preview"); } else { if (ImGui::Button(previewButton.c_str())) { - if (CVarGetInteger(CVAR_AUDIO("Playing"), 0) != 0) { - Audio_ForceRestorePreviousBgm(); - CVarSetInteger(CVAR_AUDIO("Playing"), 0); - } else { - if (sequenceType == SEQ_SFX || sequenceType == SEQ_VOICE) { - AudioSfx_PlaySfx(sequenceId, &gZeroVec3f, 4, &gSfxDefaultFreqAndVolScale, - &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb); - } else if (sequenceType == SEQ_INSTRUMENT) { - AudioOcarina_SetInstrument(sequenceId - INSTRUMENT_OFFSET); - AudioOcarina_SetPlaybackSong(9, 1); - } else { - // TODO: Cant do both here, so have to click preview button twice - PreviewSequence(sequenceId); - CVarSetInteger(CVAR_AUDIO("Playing"), sequenceId); - } - } + AudioPreview::Play(sequenceId, sequenceType); } UIWidgets::Tooltip("Play Preview"); } + UIWidgets::PopStyleButton(); } void Draw_SfxTab(const std::string& tabId, SeqType type) { const std::map& map = AudioCollection::Instance->GetAllSequences(); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8.0f, 8.0f)); + const std::string hiddenTabId = "##" + tabId; const std::string resetAllButton = "Reset All" + hiddenTabId; const std::string randomizeAllButton = "Randomize All" + hiddenTabId; const std::string lockAllButton = "Lock All" + hiddenTabId; const std::string unlockAllButton = "Unlock All" + hiddenTabId; + UIWidgets::PushStyleButton(THEME_COLOR); if (ImGui::Button(resetAllButton.c_str())) { auto currentBGM = AudioSeq_GetActiveSeqId(SEQ_PLAYER_BGM_MAIN); auto prevReplacement = AudioCollection::Instance->GetReplacementSequence(currentBGM); @@ -260,11 +354,20 @@ void Draw_SfxTab(const std::string& tabId, SeqType type) { ReplayCurrentBGM(); } } + UIWidgets::PopStyleButton(); ImGui::BeginTable(tabId.c_str(), 3, ImGuiTableFlags_SizingFixedFit); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 100.0f); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed); + // Cache valid replacements for this group + std::unordered_map baseFilteredMap; + for (const auto& [value, seqData] : map) { + if ((seqData.category & type) && seqData.canBeUsedAsReplacement) { + baseFilteredMap[value] = seqData.label.c_str(); + } + } + for (const auto& [defaultValue, seqData] : map) { if (~(seqData.category) & type) { continue; @@ -288,32 +391,35 @@ void Draw_SfxTab(const std::string& tabId, SeqType type) { ImGui::TableNextRow(); ImGui::TableNextColumn(); + ImGui::AlignTextToFramePadding(); ImGui::Text("%s", seqData.label.c_str()); ImGui::TableNextColumn(); ImGui::PushItemWidth(-FLT_MIN); const int initialValue = map.contains(currentValue) ? currentValue : defaultValue; - if (ImGui::BeginCombo(hiddenKey.c_str(), map.at(initialValue).label.c_str())) { - for (const auto& [value, seqData] : map) { - // If excluded as a replacement sequence, don't show in other dropdowns except the effect's own - // dropdown. - if (~(seqData.category) & type || - (!seqData.canBeUsedAsReplacement && initialSfxKey != seqData.sfxKey)) { - continue; - } - - if (ImGui::Selectable(seqData.label.c_str())) { - CVarSetInteger(cvarKey.c_str(), value); - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); - UpdateCurrentBGM(defaultValue, type); - } - if (currentValue == value) { - ImGui::SetItemDefaultFocus(); - } + // Use cached map unless current value is hidden/invalid. + const std::unordered_map* mapToUse = &baseFilteredMap; + std::unordered_map tempMap; + + // Ensures they remain visible in the dropdown. + if (!baseFilteredMap.contains(initialValue) && map.contains(initialValue)) { + const auto& currentSeqData = map.at(initialValue); + if (currentSeqData.category & type) { + tempMap = baseFilteredMap; + tempMap[initialValue] = currentSeqData.label.c_str(); + mapToUse = &tempMap; } + } - ImGui::EndCombo(); + u16 tempValue = static_cast(currentValue); + if (UIWidgets::ComboboxWithSearch( + hiddenKey.c_str(), &tempValue, mapToUse, + { .labelPosition = UIWidgets::LabelPosition::None, .color = THEME_COLOR, .width = -FLT_MIN })) { + CVarSetInteger(cvarKey.c_str(), tempValue); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + UpdateCurrentBGM(defaultValue, type); } + ImGui::TableNextColumn(); ImGui::PushItemWidth(-FLT_MIN); // BENTODO ship checks for some values and passes either the replaced value or the default value. For some @@ -323,6 +429,7 @@ void Draw_SfxTab(const std::string& tabId, SeqType type) { auto locked = CVarGetInteger(cvarLockKey.c_str(), 0) == 1; ImGui::SameLine(); ImGui::PushItemWidth(-FLT_MIN); + UIWidgets::PushStyleButton(THEME_COLOR, ImVec2(10.0f, 6.0f)); if (ImGui::Button(resetButton.c_str())) { CVarClear(cvarKey.c_str()); CVarClear(cvarLockKey.c_str()); @@ -363,8 +470,10 @@ void Draw_SfxTab(const std::string& tabId, SeqType type) { Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); } UIWidgets::Tooltip(locked ? "Sound locked" : "Sound unlocked"); + UIWidgets::PopStyleButton(); } ImGui::EndTable(); + ImGui::PopStyleVar(1); } extern "C" u16 AudioEditor_GetReplacementSeq(u16 seqId) { @@ -448,32 +557,40 @@ void AudioEditor::InitElement() { AudioEditor_RandomizeAll(); } }); + + // This prevents preview state from getting out of sync when the menu is closed + GameInteractor::Instance->RegisterGameHook(AudioPreview::Update); } void AudioEditor::DrawElement() { AudioCollection::Instance->InitializeShufflePool(); - float buttonSegments = ImGui::GetContentRegionAvail().x / 4; - if (ImGui::Button("Randomize All Groups", ImVec2(buttonSegments, 30.0f))) { + ImGui::BeginTable("##TopButtons", 4, ImGuiTableFlags_SizingStretchSame); + ImGui::TableNextColumn(); + UIWidgets::PushStyleButton(THEME_COLOR); + if (ImGui::Button("Randomize All Groups", ImVec2(-FLT_MIN, 0))) { AudioEditor_RandomizeAll(); } UIWidgets::Tooltip("Randomizes all unlocked music and sound effects across tab groups"); - ImGui::SameLine(); - if (ImGui::Button("Reset All Groups", ImVec2(buttonSegments, 30.0f))) { + ImGui::TableNextColumn(); + if (ImGui::Button("Reset All Groups", ImVec2(-FLT_MIN, 0))) { AudioEditor_ResetAll(); } UIWidgets::Tooltip("Resets all unlocked music and sound effects across tab groups"); - ImGui::SameLine(); - if (ImGui::Button("Lock All Groups", ImVec2(buttonSegments, 30.0f))) { + ImGui::TableNextColumn(); + if (ImGui::Button("Lock All Groups", ImVec2(-FLT_MIN, 0))) { AudioEditor_LockAll(); } UIWidgets::Tooltip("Locks all music and sound effects across tab groups"); - ImGui::SameLine(); - if (ImGui::Button("Unlock All Groups", ImVec2(buttonSegments, 30.0f))) { + ImGui::TableNextColumn(); + if (ImGui::Button("Unlock All Groups", ImVec2(-FLT_MIN, 0))) { AudioEditor_UnlockAll(); } UIWidgets::Tooltip("Unlocks all music and sound effects across tab groups"); + UIWidgets::PopStyleButton(); + ImGui::EndTable(); + UIWidgets::PushStyleTabs(THEME_COLOR); if (ImGui::BeginTabBar("SfxContextTabBar", ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)) { static ImVec2 cellPadding(8.0f, 8.0f); if (ImGui::BeginTabItem("Audio Options")) { @@ -557,8 +674,13 @@ void AudioEditor::DrawElement() { std::set seqsToExclude = {}; static ImGuiTextFilter sequenceSearch; + UIWidgets::PushStyleInput(THEME_COLOR); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 8.0f)); sequenceSearch.Draw("Filter (inc,-exc)", 490.0f); + ImGui::PopStyleVar(); + UIWidgets::PopStyleInput(); ImGui::SameLine(); + UIWidgets::PushStyleButton(THEME_COLOR); if (ImGui::Button("Exclude All")) { for (auto seqInfo : AudioCollection::Instance->GetIncludedSequences()) { if (sequenceSearch.PassFilter(seqInfo->label.c_str()) && showType[seqInfo->category]) { @@ -574,6 +696,7 @@ void AudioEditor::DrawElement() { } } } + UIWidgets::PopStyleButton(); ImGui::BeginTable("sequenceTypes", 9, ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_Borders); @@ -635,8 +758,10 @@ void AudioEditor::DrawElement() { ImGui::TableNextColumn(); ImGui::BeginChild("ChildIncludedSequences", ImVec2(0, -8)); + UIWidgets::PushStyleButton(THEME_COLOR, ImVec2(10.0f, 6.0f)); for (auto seqInfo : AudioCollection::Instance->GetIncludedSequences()) { if (sequenceSearch.PassFilter(seqInfo->label.c_str()) && showType[seqInfo->category]) { + ImGui::PushID(seqInfo->sfxKey.c_str()); if (ImGui::Button(std::string(ICON_FA_TIMES "##" + seqInfo->sfxKey).c_str())) { seqsToExclude.insert(seqInfo); } @@ -646,8 +771,10 @@ void AudioEditor::DrawElement() { DrawTypeChip(seqInfo->category); ImGui::SameLine(); ImGui::Text("%s", seqInfo->label.c_str()); + ImGui::PopID(); } } + UIWidgets::PopStyleButton(); ImGui::EndChild(); // remove the sequences we added to the temp set @@ -659,8 +786,10 @@ void AudioEditor::DrawElement() { ImGui::TableNextColumn(); ImGui::BeginChild("ChildExcludedSequences", ImVec2(0, -8)); + UIWidgets::PushStyleButton(THEME_COLOR, ImVec2(10.0f, 6.0f)); for (auto seqInfo : AudioCollection::Instance->GetExcludedSequences()) { if (sequenceSearch.PassFilter(seqInfo->label.c_str()) && showType[seqInfo->category]) { + ImGui::PushID(seqInfo->sfxKey.c_str()); if (ImGui::Button(std::string(ICON_FA_PLUS "##" + seqInfo->sfxKey).c_str())) { seqsToInclude.insert(seqInfo); } @@ -670,8 +799,10 @@ void AudioEditor::DrawElement() { DrawTypeChip(seqInfo->category); ImGui::SameLine(); ImGui::Text("%s", seqInfo->label.c_str()); + ImGui::PopID(); } } + UIWidgets::PopStyleButton(); ImGui::EndChild(); // add the sequences we added to the temp set @@ -689,6 +820,8 @@ void AudioEditor::DrawElement() { ImGui::EndTabBar(); } + + UIWidgets::PopStyleTabs(); } static std::array allTypes = { SEQ_BGM_WORLD, SEQ_BGM_EVENT, SEQ_BGM_BATTLE, SEQ_OCARINA, @@ -809,7 +942,7 @@ void RegisterAudioWidgets() { ovlDuration = { .name = "Overlay Duration: %d seconds", .type = WidgetType::WIDGET_CVAR_SLIDER_INT }; ovlDuration.CVar(CVAR_AUDIO("SeqNameNotificationDuration")) - .Options(IntSliderOptions().Color(THEME_COLOR).Min(1).Max(10).DefaultValue(5).Size(ImVec2(300.0f, 0.0f))); + .Options(IntSliderOptions().Color(THEME_COLOR).Min(1).Max(10).DefaultValue(5).Size(ImVec2(600.0f, 0.0f))); AddAudioSearchWidget(ovlDuration); voicePitchEnable = { .name = "Enable Link's Voice Pitch Multiplier", .type = WidgetType::WIDGET_CVAR_CHECKBOX }; @@ -830,7 +963,7 @@ void RegisterAudioWidgets() { .Min(0.4f) .Max(2.5f) .DefaultValue(1.0f) - .Size(ImVec2(300.0f, 0.0f)) + .Size(ImVec2(600.0f, 0.0f)) .Tooltip("Adjust Link's Voice Pitch Multiplier.Limits are 40% to 250%")); AddAudioSearchWidget(voicePitch); } diff --git a/mm/src/audio/code_8019AF00.c b/mm/src/audio/code_8019AF00.c index 8bc60008bc..eac2c85eaf 100644 --- a/mm/src/audio/code_8019AF00.c +++ b/mm/src/audio/code_8019AF00.c @@ -2073,24 +2073,6 @@ const char sAudioOcarinaUnusedText5[] = "last key is bad !!! %d %d %02X %02X\n"; const char sAudioOcarinaUnusedText6[] = "last key step is too short !!! %d:%d %d<%d\n"; const char sAudioOcarinaUnusedText7[] = "check is over!!! %d %d %d\n"; -// BENTODO find a final place for this function -// 2S2H [Port] Part of the audio editor -void PreviewSequence(u16 seqId) { - u16 curSeqId = AudioSeq_GetActiveSeqId(SEQ_PLAYER_BGM_MAIN); - - // if ((curSeqId & 0xFF) != NA_BGM_GANON_TOWER && (curSeqId & 0xFF) != NA_BGM_ESCAPE && curSeqId != seqId) { - Audio_SetSequenceMode(SEQ_MODE_IGNORE); - if (curSeqId != NA_BGM_DISABLED) { - sPrevMainBgmSeqId = curSeqId; - } else { - osSyncPrintf("Middle Boss BGM Start not stack \n"); - } - - SEQCMD_PLAY_SEQUENCE(SEQ_PLAYER_BGM_MAIN, 1, seqId); - - // } -} - void AudioOcarina_ReadControllerInput(void) { Input inputs[MAXCONTROLLERS]; Input* input = &inputs[0];