1
0

10 Commitit b34d6fbdb7 ... 58d5748de9

Tekijä SHA1 Viesti Päivämäärä
  kiwec 58d5748de9 Resume music after restarting SoundEngine 4 kuukautta sitten
  kiwec dac1bc5ff3 Fix master volume control on exclusive WASAPI 4 kuukautta sitten
  kiwec b2472567a3 Ignore empty samples 4 kuukautta sitten
  kiwec ed9d3123ef Use BASS_ASYNCFILE flag (allegedly good for low latency output) 4 kuukautta sitten
  kiwec 946342102e Fix skins with UTF-8 characters failing to open on Windows 4 kuukautta sitten
  kiwec b1cb6f1fdb Fix screenshots 4 kuukautta sitten
  kiwec a5b84087b5 Re-add strain graphs 4 kuukautta sitten
  kiwec c9a90001ee Open skins folder -> open skin folder 4 kuukautta sitten
  kiwec eebd98dc5d Don't submit scores of less than 7 seconds 4 kuukautta sitten
  kiwec c84fad78c7 Remove sliderhead fadeout animation 4 kuukautta sitten

BIN
libraries/rosu-pp-c/Win32/rosu_pp_c.lib


BIN
libraries/rosu-pp-c/x64/rosu_pp_c.lib


+ 66 - 5
rosu-pp-c/src/lib.rs

@@ -6,18 +6,66 @@ use libc;
 use rosu_pp;
 
 #[repr(C)]
-#[derive(Default)]
 pub struct pp_info {
     pub total_stars: f64,
     pub aim_stars: f64,
     pub speed_stars: f64,
+
     pub pp: f64,
+
     pub num_objects: u32,
     pub num_circles: u32,
     pub num_spinners: u32,
+
+    pub aim_strains: *mut f64,
+    pub aim_strains_len: usize,
+    pub aim_strains_capacity: usize,
+    pub speed_strains: *mut f64,
+    pub speed_strains_len: usize,
+    pub speed_strains_capacity: usize,
+
     pub ok: bool,
 }
 
+impl Default for pp_info {
+    fn default() -> Self {
+        pp_info {
+            total_stars: 0.0,
+            aim_stars: 0.0,
+            speed_stars: 0.0,
+
+            pp: 0.0,
+
+            num_objects: 0,
+            num_circles: 0,
+            num_spinners: 0,
+
+            aim_strains: std::ptr::null_mut(),
+            aim_strains_len: 0,
+            aim_strains_capacity: 0,
+            speed_strains: std::ptr::null_mut(),
+            speed_strains_len: 0,
+            speed_strains_capacity: 0,
+
+            ok: false,
+        }
+    }
+}
+
+#[no_mangle]
+pub extern "C" fn free_pp_info(info: pp_info) {
+    if !info.aim_strains.is_null() {
+        unsafe {
+            Vec::from_raw_parts(info.aim_strains, info.aim_strains_len, info.aim_strains_capacity);
+        }
+    }
+    if !info.speed_strains.is_null() {
+        unsafe {
+            Vec::from_raw_parts(info.speed_strains, info.speed_strains_len, info.speed_strains_capacity);
+        }
+    }
+}
+
 #[no_mangle]
 pub extern "C" fn calculate_full_pp(path: *const libc::c_char, mod_flags: u32, ar: f32, cs: f32, od: f32, speed_multiplier: f64) -> pp_info {
     let c_path = unsafe { std::ffi::CStr::from_ptr(path) };
@@ -33,23 +81,36 @@ pub extern "C" fn calculate_full_pp(path: *const libc::c_char, mod_flags: u32, a
         Err(_) => return pp_info::default()
     };
 
-    let diff_attrs = rosu_pp::Difficulty::new()
+    let diff = rosu_pp::Difficulty::new()
         .mods(mod_flags)
         .ar(ar, false)
         .cs(cs, false)
         .od(od, false)
-        .clock_rate(speed_multiplier)
-        .with_mode()
-        .calculate(&osu_map);
+        .clock_rate(speed_multiplier);
+
+    let diff_attrs = diff.with_mode().calculate(&osu_map);
+    let diff_strains = diff.with_mode().strains(&osu_map);
+    let mut aim_strains = std::mem::ManuallyDrop::new(diff_strains.aim);
+    let mut speed_strains = std::mem::ManuallyDrop::new(diff_strains.speed);
 
     let mut result = pp_info {
         total_stars: diff_attrs.stars,
         aim_stars: diff_attrs.aim,
         speed_stars: diff_attrs.speed,
+
         pp: 0.0,
+
         num_objects: diff_attrs.n_circles + diff_attrs.n_sliders + diff_attrs.n_spinners,
         num_circles: diff_attrs.n_circles,
         num_spinners: diff_attrs.n_spinners,
+
+        aim_strains: aim_strains.as_mut_ptr(),
+        aim_strains_len: aim_strains.len(),
+        aim_strains_capacity: aim_strains.capacity(),
+        speed_strains: speed_strains.as_mut_ptr(),
+        speed_strains_len: speed_strains.len(),
+        speed_strains_capacity: speed_strains.capacity(),
+
         ok: true,
     };
 

+ 22 - 8
src/App/Osu/Beatmap.cpp

@@ -681,6 +681,8 @@ void Beatmap::select() {
 }
 
 void Beatmap::selectDifficulty2(DatabaseBeatmap *difficulty2) {
+    auto previous_diff = m_selectedDifficulty2;
+
     if(difficulty2 != NULL) {
         m_selectedDifficulty2 = difficulty2;
 
@@ -691,12 +693,21 @@ void Beatmap::selectDifficulty2(DatabaseBeatmap *difficulty2) {
 
     if(osu_beatmap_preview_mods_live.getBool()) onModUpdate();
 
-    // Request full pp recomputation
-    if(m_selectedDifficulty2 && !m_selectedDifficulty2->do_not_store) {
-        m_selectedDifficulty2->m_calculate_full_pp =
-            std::async(std::launch::async, calculate_full_pp, m_selectedDifficulty2->m_sFilePath.c_str(),
-                       osu->getScore()->getModsLegacy(), getAR(), getCS(), getOD(), osu->getSpeedMultiplier());
+    if(previous_diff != difficulty2) {
+        // TODO @kiwec: commented out because this still gets triggered when
+        //              starting a map, thus erasing strains from schrubbing timeline
+        // m_aimStrains.clear();
+        // m_speedStrains.clear();
+
+        // Request full pp recomputation
+        if(m_selectedDifficulty2 && !m_selectedDifficulty2->do_not_store) {
+            // TODO @kiwec: free pp_info of previous m_calculate_full_pp, if applicable
+            m_selectedDifficulty2->m_calculate_full_pp =
+                std::async(std::launch::async, calculate_full_pp, m_selectedDifficulty2->m_sFilePath.c_str(),
+                           osu->getScore()->getModsLegacy(), getAR(), getCS(), getOD(), osu->getSpeedMultiplier());
+        }
     }
+
 }
 
 void Beatmap::deselect() {
@@ -3716,9 +3727,9 @@ void Beatmap::saveAndSubmitScore(bool quit) {
     osu->getScore()->setPPv2(pp);
 
     // save local score, but only under certain conditions
-    const bool isComplete = (num300s + num100s + num50s + numMisses >= numHitObjects);
-    const bool isZero = (osu->getScore()->getScore() < 1);
-    const bool isCheated = (osu->getModAuto() || (osu->getModAutopilot() && osu->getModRelax())) ||
+    bool isComplete = (num300s + num100s + num50s + numMisses >= numHitObjects);
+    bool isZero = (osu->getScore()->getScore() < 1);
+    bool isCheated = (osu->getModAuto() || (osu->getModAutopilot() && osu->getModRelax())) ||
                            osu->getScore()->isUnranked() || is_watching || is_spectating;
 
     FinishedScore score;
@@ -3741,6 +3752,9 @@ void Beatmap::saveAndSubmitScore(bool quit) {
     score.ragequit = quit;
     score.play_time_ms = m_iCurMusicPos / osu->getSpeedMultiplier();
 
+    // osu!stable doesn't submit scores of less than 7 seconds
+    isZero |= (score.play_time_ms < 7000);
+
     score.num300s = osu->getScore()->getNum300s();
     score.num100s = osu->getScore()->getNum100s();
     score.num50s = osu->getScore()->getNum50s();

+ 3 - 0
src/App/Osu/Beatmap.h

@@ -178,6 +178,9 @@ class Beatmap {
                                                               m_speedNotesForNumHitObjects.size() - 1)]
                     : 0);
     }
+    
+    std::vector<double> m_aimStrains;
+    std::vector<double> m_speedStrains;
 
     // set to false when using non-vanilla mods (disables score submission)
     bool vanilla = true;

+ 6 - 0
src/App/Osu/Changelog.cpp

@@ -29,8 +29,14 @@ Changelog::Changelog() : ScreenBackable() {
     CHANGELOG latest;
     latest.title =
         UString::format("%.2f (%s, %s)", convar->getConVarByName("osu_version")->getFloat(), __DATE__, __TIME__);
+    latest.changes.push_back("- Changed \"Open Skins folder\" button to open the currently selected skin's folder");
+    latest.changes.push_back("- Fixed master volume control not working on exclusive WASAPI");
+    latest.changes.push_back("- Fixed screenshots failing to save");
+    latest.changes.push_back("- Fixed skins with non-ANSI folder names failing to open on Windows");
     latest.changes.push_back("- Fixed sliderslide and spinnerspin sounds not looping");
     latest.changes.push_back("- Improved sound engine reliability");
+    latest.changes.push_back("- Re-added strain graphs");
+    latest.changes.push_back("- Removed sliderhead fadeout animation (set osu_slider_sliderhead_fadeout to 1 for old behavior)");
     changelogs.push_back(latest);
 
     CHANGELOG v35_03;

+ 1 - 0
src/App/Osu/DatabaseBeatmap.cpp

@@ -178,6 +178,7 @@ DatabaseBeatmap::~DatabaseBeatmap() {
     if(m_calculate_full_pp.valid()) {
         m_calculate_full_pp.wait();
         m_calculate_full_pp = std::future<pp_info>();
+        // TODO @kiwec: free pp_info
     }
 
     if(m_difficulties != NULL) {

+ 94 - 0
src/App/Osu/HUD.cpp

@@ -214,6 +214,7 @@ ConVar osu_draw_accuracy("osu_draw_accuracy", true, FCVAR_DEFAULT);
 ConVar osu_draw_target_heatmap("osu_draw_target_heatmap", true, FCVAR_DEFAULT);
 ConVar osu_draw_scrubbing_timeline("osu_draw_scrubbing_timeline", true, FCVAR_DEFAULT);
 ConVar osu_draw_scrubbing_timeline_breaks("osu_draw_scrubbing_timeline_breaks", true, FCVAR_DEFAULT);
+ConVar osu_draw_scrubbing_timeline_strain_graph("osu_draw_scrubbing_timeline_strain_graph", false, FCVAR_DEFAULT);
 ConVar osu_draw_continue("osu_draw_continue", true, FCVAR_DEFAULT);
 ConVar osu_draw_scoreboard("osu_draw_scoreboard", true, FCVAR_DEFAULT);
 ConVar osu_draw_scoreboard_mp("osu_draw_scoreboard_mp", true, FCVAR_DEFAULT);
@@ -2260,6 +2261,99 @@ void HUD::drawScrubbingTimeline(Graphics *g, unsigned long beatmapTime, unsigned
     const unsigned long endTimeMS = startTimeMS + lengthMS;
     const unsigned long currentTimeMS = beatmapTime;
 
+    // draw strain graph
+    if(osu_draw_scrubbing_timeline_strain_graph.getBool()) {
+        const std::vector<double> &aimStrains = osu->getSelectedBeatmap()->m_aimStrains;
+        const std::vector<double> &speedStrains = osu->getSelectedBeatmap()->m_speedStrains;
+        const float speedMultiplier = osu->getSpeedMultiplier();
+
+        if(aimStrains.size() > 0 && aimStrains.size() == speedStrains.size()) {
+            const float strainStepMS = 400.0f * speedMultiplier;
+
+            // get highest strain values for normalization
+            double highestAimStrain = 0.0;
+            double highestSpeedStrain = 0.0;
+            double highestStrain = 0.0;
+            int highestStrainIndex = -1;
+            for(int i = 0; i < aimStrains.size(); i++) {
+                const double aimStrain = aimStrains[i];
+                const double speedStrain = speedStrains[i];
+                const double strain = aimStrain + speedStrain;
+
+                if(strain > highestStrain) {
+                    highestStrain = strain;
+                    highestStrainIndex = i;
+                }
+                if(aimStrain > highestAimStrain) highestAimStrain = aimStrain;
+                if(speedStrain > highestSpeedStrain) highestSpeedStrain = speedStrain;
+            }
+
+            // draw strain bar graph
+            if(highestAimStrain > 0.0 && highestSpeedStrain > 0.0 && highestStrain > 0.0) {
+                const float msPerPixel =
+                    (float)lengthMS /
+                    (float)(osu->getScreenWidth() - (((float)startTimeMS / (float)endTimeMS)) * osu->getScreenWidth());
+                const float strainWidth = strainStepMS / msPerPixel;
+                const float strainHeightMultiplier = osu_hud_scrubbing_timeline_strains_height.getFloat() * dpiScale;
+
+                const float offsetX =
+                    ((float)startTimeMS / (float)strainStepMS) * strainWidth;  // compensate for startTimeMS
+
+                const float alpha = osu_hud_scrubbing_timeline_strains_alpha.getFloat() * galpha;
+                const Color aimStrainColor =
+                    COLORf(alpha, osu_hud_scrubbing_timeline_strains_aim_color_r.getInt() / 255.0f,
+                           osu_hud_scrubbing_timeline_strains_aim_color_g.getInt() / 255.0f,
+                           osu_hud_scrubbing_timeline_strains_aim_color_b.getInt() / 255.0f);
+                const Color speedStrainColor =
+                    COLORf(alpha, osu_hud_scrubbing_timeline_strains_speed_color_r.getInt() / 255.0f,
+                           osu_hud_scrubbing_timeline_strains_speed_color_g.getInt() / 255.0f,
+                           osu_hud_scrubbing_timeline_strains_speed_color_b.getInt() / 255.0f);
+
+                g->setDepthBuffer(true);
+                for(int i = 0; i < aimStrains.size(); i++) {
+                    const double aimStrain = (aimStrains[i]) / highestStrain;
+                    const double speedStrain = (speedStrains[i]) / highestStrain;
+                    // const double strain = (aimStrains[i] + speedStrains[i]) / highestStrain;
+
+                    const double aimStrainHeight = aimStrain * strainHeightMultiplier;
+                    const double speedStrainHeight = speedStrain * strainHeightMultiplier;
+                    // const double strainHeight = strain * strainHeightMultiplier;
+
+                    g->setColor(aimStrainColor);
+                    g->fillRect(i * strainWidth + offsetX, cursorPos.y - aimStrainHeight,
+                                max(1.0f, std::round(strainWidth + 0.5f)), aimStrainHeight);
+
+                    g->setColor(speedStrainColor);
+                    g->fillRect(i * strainWidth + offsetX, cursorPos.y - aimStrainHeight - speedStrainHeight,
+                                max(1.0f, std::round(strainWidth + 0.5f)), speedStrainHeight + 1);
+                }
+                g->setDepthBuffer(false);
+
+                // highlight highest total strain value (+- section block)
+                if(highestStrainIndex > -1) {
+                    const double aimStrain = (aimStrains[highestStrainIndex]) / highestStrain;
+                    const double speedStrain = (speedStrains[highestStrainIndex]) / highestStrain;
+                    // const double strain = (aimStrains[i] + speedStrains[i]) / highestStrain;
+
+                    const double aimStrainHeight = aimStrain * strainHeightMultiplier;
+                    const double speedStrainHeight = speedStrain * strainHeightMultiplier;
+                    // const double strainHeight = strain * strainHeightMultiplier;
+
+                    Vector2 topLeftCenter = Vector2(highestStrainIndex * strainWidth + offsetX + strainWidth / 2.0f,
+                                                    cursorPos.y - aimStrainHeight - speedStrainHeight);
+
+                    const float margin = 5.0f * dpiScale;
+
+                    g->setColor(0xffffffff);
+                    g->setAlpha(alpha);
+                    g->drawRect(topLeftCenter.x - margin * strainWidth, topLeftCenter.y - margin * strainWidth,
+                                strainWidth * 2 * margin,
+                                aimStrainHeight + speedStrainHeight + 2 * margin * strainWidth);
+                }
+            }
+        }
+    }
+
     // breaks
     g->setColor(greyTransparent);
     g->setAlpha(galpha);

+ 30 - 15
src/App/Osu/OptionsMenu.cpp

@@ -578,6 +578,15 @@ OptionsMenu::OptionsMenu() : ScreenBackable() {
                 "from ALL scores?\n(Even from ones you got in your database because you watched a replay?)",
                 convar->getConVarByName("osu_user_switcher_include_legacy_scores_for_names"));
 
+    addSubSection("Songbrowser");
+    addCheckbox("Draw Strain Graph in Songbrowser",
+                "Hold either SHIFT/CTRL to show only speed/aim strains.\nSpeed strain is red, aim strain is "
+                "green.\n(See osu_hud_scrubbing_timeline_strains_*)",
+                convar->getConVarByName("osu_draw_songbrowser_strain_graph"));
+    addCheckbox("Draw Strain Graph in Scrubbing Timeline",
+                "Speed strain is red, aim strain is green.\n(See osu_hud_scrubbing_timeline_strains_*)",
+                convar->getConVarByName("osu_draw_scrubbing_timeline_strain_graph"));
+
     addSubSection("Window");
     addCheckbox("Pause on Focus Loss", "Should the game pause when you switch to another application?",
                 convar->getConVarByName("osu_pause_on_focus_loss"));
@@ -807,19 +816,18 @@ OptionsMenu::OptionsMenu() : ScreenBackable() {
     addSubSection("Skin");
     addSkinPreview();
     {
-        addButton("Open Skin folder")
-            ->setClickCallback(fastdelegate::MakeDelegate(this, &OptionsMenu::openSkinsFolder));
-
         OPTIONS_ELEMENT skinSelect;
         {
             skinSelect = addButton("Select Skin", "default");
             m_skinSelectLocalButton = skinSelect.elements[0];
             m_skinLabel = (CBaseUILabel *)skinSelect.elements[1];
         }
-
         ((CBaseUIButton *)m_skinSelectLocalButton)
             ->setClickCallback(fastdelegate::MakeDelegate(this, &OptionsMenu::onSkinSelect));
 
+        addButton("Open current Skin folder")
+            ->setClickCallback(fastdelegate::MakeDelegate(this, &OptionsMenu::openCurrentSkinFolder));
+
         OPTIONS_ELEMENT skinReload = addButtonButton("Reload Skin", "Random Skin");
         ((UIButton *)skinReload.elements[0])
             ->setClickCallback(fastdelegate::MakeDelegate(this, &OptionsMenu::onSkinReload));
@@ -2376,12 +2384,22 @@ void OptionsMenu::onRawInputToAbsoluteWindowChange(CBaseUICheckbox *checkbox) {
     }
 }
 
-void OptionsMenu::openSkinsFolder() {
-    UString skinFolder = convar->getConVarByName("osu_folder")->getString();
-    skinFolder.append(convar->getConVarByName("osu_folder_sub_skins")->getString());
-
-    std::string skin_folder_str(skinFolder.toUtf8());
-    env->openDirectory(skin_folder_str);
+void OptionsMenu::openCurrentSkinFolder() {
+    auto current_skin = convar->getConVarByName("osu_skin")->getString();
+    if(current_skin == UString("default")) {
+#ifdef _WIN32
+        // ................yeah
+        env->openDirectory(MCENGINE_DATA_DIR "materials\\default");
+#else
+        env->openDirectory(MCENGINE_DATA_DIR "materials/default");
+#endif
+    } else {
+        UString skinFolder = convar->getConVarByName("osu_folder")->getString();
+        skinFolder.append(convar->getConVarByName("osu_folder_sub_skins")->getString());
+        skinFolder.append(current_skin);
+        std::string skin_folder_str(skinFolder.toUtf8());
+        env->openDirectory(skinFolder.toUtf8());
+    }
 }
 
 void OptionsMenu::onSkinSelect() {
@@ -2403,12 +2421,9 @@ void OptionsMenu::onSkinSelect() {
         if(defaultText == m_osu_skin_ref->getString()) buttonDefault->setTextBrightColor(0xff00ff00);
 
         for(int i = 0; i < skinFolders.size(); i++) {
-            if(skinFolders[i].compare(".") == 0 ||
-               skinFolders[i].compare("..") == 0)  // is this universal in every file system? too lazy to check. should
-                                                   // probably fix this in the engine and not here
-                continue;
+            if(skinFolders[i].compare(".") == 0 || skinFolders[i].compare("..") == 0) continue;
 
-            CBaseUIButton *button = m_contextMenu->addButton(UString(skinFolders[i].c_str()));
+            CBaseUIButton *button = m_contextMenu->addButton(skinFolders[i].c_str());
             auto osu_skin = m_osu_skin_ref->getString();
             if(skinFolders[i].compare(osu_skin.toUtf8()) == 0) button->setTextBrightColor(0xff00ff00);
         }

+ 1 - 1
src/App/Osu/OptionsMenu.h

@@ -111,7 +111,7 @@ class OptionsMenu : public ScreenBackable, public NotificationOverlayKeyListener
     void onBorderlessWindowedChange(CBaseUICheckbox *checkbox);
     void onDPIScalingChange(CBaseUICheckbox *checkbox);
     void onRawInputToAbsoluteWindowChange(CBaseUICheckbox *checkbox);
-    void openSkinsFolder();
+    void openCurrentSkinFolder();
     void onSkinSelect();
     void onSkinSelect2(UString skinName, int id = -1);
     void onSkinReload();

+ 8 - 1
src/App/Osu/Osu.cpp

@@ -721,6 +721,13 @@ void Osu::update() {
         m_screens[i]->mouse_update(&propagate_clicks);
     }
 
+    if(music_unpause_scheduled && engine->getSound()->isReady()) {
+        if(getSelectedBeatmap()->getMusic() != NULL) {
+            engine->getSound()->play(getSelectedBeatmap()->getMusic());
+        }
+        music_unpause_scheduled = false;
+    }
+
     // main beatmap update
     m_bSeeking = false;
     if(isInPlayMode()) {
@@ -1619,7 +1626,7 @@ void Osu::saveScreenshot() {
     int screenshotNumber = 0;
     UString screenshot_path;
     do {
-        screenshot_path = UString("screenshots/screenshot%d.png", screenshotNumber);
+        screenshot_path = UString::format("screenshots/screenshot%d.png", screenshotNumber);
         screenshotNumber++;
     } while(env->fileExists(screenshot_path.toUtf8()));
 

+ 1 - 0
src/App/Osu/Osu.h

@@ -347,6 +347,7 @@ class Osu : public App, public MouseListener {
     u32 watched_user_id = 0;
 
     // custom
+    bool music_unpause_scheduled = false;
     bool m_bScheduleEndlessModNextBeatmap;
     int m_iMultiplayerClientNumEscPresses;
     bool m_bWasBossKeyPaused;

+ 4 - 1
src/App/Osu/Slider.cpp

@@ -56,6 +56,9 @@ ConVar osu_slider_reverse_arrow_animated("osu_slider_reverse_arrow_animated", tr
                                          "pulse animation on reverse arrows");
 ConVar osu_slider_reverse_arrow_alpha_multiplier("osu_slider_reverse_arrow_alpha_multiplier", 1.0f, FCVAR_DEFAULT);
 
+// osu!stable doesn't display a fadeout animation for sliderheads
+ConVar osu_slider_sliderhead_fadeout("osu_slider_sliderhead_fadeout", false, FCVAR_DEFAULT);
+
 ConVar *Slider::m_osu_playfield_mirror_horizontal_ref = NULL;
 ConVar *Slider::m_osu_playfield_mirror_vertical_ref = NULL;
 ConVar *Slider::m_osu_playfield_rotation_ref = NULL;
@@ -376,7 +379,7 @@ void Slider::draw(Graphics *g) {
                                  1.0f - m_fEndSliderBodyFadeAnimation, getTime());
     }
 
-    if(m_fStartHitAnimation > 0.0f && m_fStartHitAnimation != 1.0f && !osu->getModHD()) {
+    if(osu_slider_sliderhead_fadeout.getBool() && m_fStartHitAnimation > 0.0f && m_fStartHitAnimation != 1.0f && !osu->getModHD()) {
         float alpha = 1.0f - m_fStartHitAnimation;
 
         float scale = m_fStartHitAnimation;

+ 148 - 0
src/App/Osu/SongBrowser/SongBrowser.cpp

@@ -67,6 +67,7 @@ ConVar osu_songbrowser_bottombar_percent("osu_songbrowser_bottombar_percent", 0.
 
 ConVar osu_draw_songbrowser_background_image("osu_draw_songbrowser_background_image", true, FCVAR_DEFAULT);
 ConVar osu_draw_songbrowser_menu_background_image("osu_draw_songbrowser_menu_background_image", true, FCVAR_DEFAULT);
+ConVar osu_draw_songbrowser_strain_graph("osu_draw_songbrowser_strain_graph", false, FCVAR_DEFAULT);
 ConVar osu_songbrowser_scorebrowser_enabled("osu_songbrowser_scorebrowser_enabled", true, FCVAR_DEFAULT);
 ConVar osu_songbrowser_background_fade_in_duration("osu_songbrowser_background_fade_in_duration", 0.1f, FCVAR_DEFAULT);
 
@@ -345,6 +346,25 @@ SongBrowser::SongBrowser() : ScreenBackable() {
     m_osu_scores_enabled = convar->getConVarByName("osu_scores_enabled");
     m_name_ref = convar->getConVarByName("name");
 
+    m_osu_draw_scrubbing_timeline_strain_graph_ref =
+        convar->getConVarByName("osu_draw_scrubbing_timeline_strain_graph");
+    m_osu_hud_scrubbing_timeline_strains_height_ref =
+        convar->getConVarByName("osu_hud_scrubbing_timeline_strains_height");
+    m_osu_hud_scrubbing_timeline_strains_alpha_ref =
+        convar->getConVarByName("osu_hud_scrubbing_timeline_strains_alpha");
+    m_osu_hud_scrubbing_timeline_strains_aim_color_r_ref =
+        convar->getConVarByName("osu_hud_scrubbing_timeline_strains_aim_color_r");
+    m_osu_hud_scrubbing_timeline_strains_aim_color_g_ref =
+        convar->getConVarByName("osu_hud_scrubbing_timeline_strains_aim_color_g");
+    m_osu_hud_scrubbing_timeline_strains_aim_color_b_ref =
+        convar->getConVarByName("osu_hud_scrubbing_timeline_strains_aim_color_b");
+    m_osu_hud_scrubbing_timeline_strains_speed_color_r_ref =
+        convar->getConVarByName("osu_hud_scrubbing_timeline_strains_speed_color_r");
+    m_osu_hud_scrubbing_timeline_strains_speed_color_g_ref =
+        convar->getConVarByName("osu_hud_scrubbing_timeline_strains_speed_color_g");
+    m_osu_hud_scrubbing_timeline_strains_speed_color_b_ref =
+        convar->getConVarByName("osu_hud_scrubbing_timeline_strains_speed_color_b");
+
     m_osu_draw_statistics_perfectpp_ref = convar->getConVarByName("osu_draw_statistics_perfectpp");
     m_osu_draw_statistics_totalstars_ref = convar->getConVarByName("osu_draw_statistics_totalstars");
 
@@ -629,6 +649,122 @@ void SongBrowser::draw(Graphics *g) {
     // draw score browser
     m_scoreBrowser->draw(g);
 
+    // draw strain graph of currently selected beatmap
+    if(osu_draw_songbrowser_strain_graph.getBool()) {
+        const std::vector<double> &aimStrains = getSelectedBeatmap()->m_aimStrains;
+        const std::vector<double> &speedStrains = getSelectedBeatmap()->m_speedStrains;
+        const float speedMultiplier = osu->getSpeedMultiplier();
+
+        if(aimStrains.size() > 0 && aimStrains.size() == speedStrains.size()) {
+            const float strainStepMS = 400.0f * speedMultiplier;
+
+            const unsigned long lengthMS = strainStepMS * aimStrains.size();
+
+            // get highest strain values for normalization
+            double highestAimStrain = 0.0;
+            double highestSpeedStrain = 0.0;
+            double highestStrain = 0.0;
+            int highestStrainIndex = -1;
+            for(int i = 0; i < aimStrains.size(); i++) {
+                const double aimStrain = aimStrains[i];
+                const double speedStrain = speedStrains[i];
+                const double strain = aimStrain + speedStrain;
+
+                if(strain > highestStrain) {
+                    highestStrain = strain;
+                    highestStrainIndex = i;
+                }
+                if(aimStrain > highestAimStrain) highestAimStrain = aimStrain;
+                if(speedStrain > highestSpeedStrain) highestSpeedStrain = speedStrain;
+            }
+
+            // draw strain bar graph
+            if(highestAimStrain > 0.0 && highestSpeedStrain > 0.0 && highestStrain > 0.0) {
+                const float dpiScale = Osu::getUIScale();
+
+                const float graphWidth = m_scoreBrowser->getSize().x;
+
+                const float msPerPixel = (float)lengthMS / graphWidth;
+                const float strainWidth = strainStepMS / msPerPixel;
+                const float strainHeightMultiplier =
+                    m_osu_hud_scrubbing_timeline_strains_height_ref->getFloat() * dpiScale;
+
+                McRect graphRect(0, m_bottombar->getPos().y - strainHeightMultiplier, graphWidth,
+                                 strainHeightMultiplier);
+
+                const float alpha = (graphRect.contains(engine->getMouse()->getPos())
+                                         ? 1.0f
+                                         : m_osu_hud_scrubbing_timeline_strains_alpha_ref->getFloat());
+
+                const Color aimStrainColor =
+                    COLORf(alpha, m_osu_hud_scrubbing_timeline_strains_aim_color_r_ref->getInt() / 255.0f,
+                           m_osu_hud_scrubbing_timeline_strains_aim_color_g_ref->getInt() / 255.0f,
+                           m_osu_hud_scrubbing_timeline_strains_aim_color_b_ref->getInt() / 255.0f);
+                const Color speedStrainColor =
+                    COLORf(alpha, m_osu_hud_scrubbing_timeline_strains_speed_color_r_ref->getInt() / 255.0f,
+                           m_osu_hud_scrubbing_timeline_strains_speed_color_g_ref->getInt() / 255.0f,
+                           m_osu_hud_scrubbing_timeline_strains_speed_color_b_ref->getInt() / 255.0f);
+
+                g->setDepthBuffer(true);
+                for(int i = 0; i < aimStrains.size(); i++) {
+                    const double aimStrain = (aimStrains[i]) / highestStrain;
+                    const double speedStrain = (speedStrains[i]) / highestStrain;
+                    // const double strain = (aimStrains[i] + speedStrains[i]) / highestStrain;
+
+                    const double aimStrainHeight = aimStrain * strainHeightMultiplier;
+                    const double speedStrainHeight = speedStrain * strainHeightMultiplier;
+                    // const double strainHeight = strain * strainHeightMultiplier;
+
+                    if(!engine->getKeyboard()->isShiftDown()) {
+                        g->setColor(aimStrainColor);
+                        g->fillRect(i * strainWidth, m_bottombar->getPos().y - aimStrainHeight,
+                                    max(1.0f, std::round(strainWidth + 0.5f)), aimStrainHeight);
+                    }
+
+                    if(!engine->getKeyboard()->isControlDown()) {
+                        g->setColor(speedStrainColor);
+                        g->fillRect(i * strainWidth,
+                                    m_bottombar->getPos().y -
+                                        (engine->getKeyboard()->isShiftDown() ? 0 : aimStrainHeight) -
+                                        speedStrainHeight,
+                                    max(1.0f, std::round(strainWidth + 0.5f)), speedStrainHeight + 1);
+                    }
+                }
+                g->setDepthBuffer(false);
+
+                // highlight highest total strain value (+- section block)
+                if(highestStrainIndex > -1) {
+                    const double aimStrain = (aimStrains[highestStrainIndex]) / highestStrain;
+                    const double speedStrain = (speedStrains[highestStrainIndex]) / highestStrain;
+                    // const double strain = (aimStrains[i] + speedStrains[i]) / highestStrain;
+
+                    const double aimStrainHeight = aimStrain * strainHeightMultiplier;
+                    const double speedStrainHeight = speedStrain * strainHeightMultiplier;
+                    // const double strainHeight = strain * strainHeightMultiplier;
+
+                    Vector2 topLeftCenter = Vector2(highestStrainIndex * strainWidth + strainWidth / 2.0f,
+                                                    m_bottombar->getPos().y - aimStrainHeight - speedStrainHeight);
+
+                    const float margin = 5.0f * dpiScale;
+
+                    g->setColor(0xffffffff);
+                    g->setAlpha(alpha);
+                    g->drawRect(topLeftCenter.x - margin * strainWidth, topLeftCenter.y - margin * strainWidth,
+                                strainWidth * 2 * margin,
+                                aimStrainHeight + speedStrainHeight + 2 * margin * strainWidth);
+                    g->setAlpha(alpha * 0.5f);
+                    g->drawRect(topLeftCenter.x - margin * strainWidth - 2, topLeftCenter.y - margin * strainWidth - 2,
+                                strainWidth * 2 * margin + 4,
+                                aimStrainHeight + speedStrainHeight + 2 * margin * strainWidth + 4);
+                    g->setAlpha(alpha * 0.25f);
+                    g->drawRect(topLeftCenter.x - margin * strainWidth - 4, topLeftCenter.y - margin * strainWidth - 4,
+                                strainWidth * 2 * margin + 8,
+                                aimStrainHeight + speedStrainHeight + 2 * margin * strainWidth + 8);
+                }
+            }
+        }
+    }
+
     // draw song browser
     m_songBrowser->draw(g);
 
@@ -765,6 +901,18 @@ void SongBrowser::mouse_update(bool *propagate_clicks) {
         if(db_diff->m_calculate_full_pp.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
             db_diff->m_pp_info = db_diff->m_calculate_full_pp.get();
             db_diff->m_calculate_full_pp = std::future<pp_info>();
+
+            m_selectedBeatmap->m_aimStrains = std::vector<f64>(
+                db_diff->m_pp_info.aim_strains,
+                &db_diff->m_pp_info.aim_strains[db_diff->m_pp_info.aim_strains_len]
+            );
+            m_selectedBeatmap->m_speedStrains = std::vector<f64>(
+                db_diff->m_pp_info.speed_strains,
+                &db_diff->m_pp_info.speed_strains[db_diff->m_pp_info.speed_strains_len]
+            );
+
+            // Free the strains, which we already copied into vectors
+            free_pp_info(db_diff->m_pp_info);
         }
     }
 

+ 10 - 0
src/App/Osu/SongBrowser/SongBrowser.h

@@ -240,6 +240,16 @@ class SongBrowser : public ScreenBackable {
     ConVar *m_osu_scores_enabled;
     ConVar *m_name_ref;
 
+    ConVar *m_osu_draw_scrubbing_timeline_strain_graph_ref;
+    ConVar *m_osu_hud_scrubbing_timeline_strains_height_ref;
+    ConVar *m_osu_hud_scrubbing_timeline_strains_alpha_ref;
+    ConVar *m_osu_hud_scrubbing_timeline_strains_aim_color_r_ref;
+    ConVar *m_osu_hud_scrubbing_timeline_strains_aim_color_g_ref;
+    ConVar *m_osu_hud_scrubbing_timeline_strains_aim_color_b_ref;
+    ConVar *m_osu_hud_scrubbing_timeline_strains_speed_color_r_ref;
+    ConVar *m_osu_hud_scrubbing_timeline_strains_speed_color_g_ref;
+    ConVar *m_osu_hud_scrubbing_timeline_strains_speed_color_b_ref;
+
     ConVar *m_osu_draw_statistics_perfectpp_ref;
     ConVar *m_osu_draw_statistics_totalstars_ref;
 

+ 11 - 0
src/App/Osu/pp.h

@@ -9,14 +9,25 @@ typedef struct {
     f64 total_stars;
     f64 aim_stars;
     f64 speed_stars;
+
     f64 pp;
+
     u32 num_objects;
     u32 num_circles;
     u32 num_spinners;
+
+    f64 *aim_strains;
+    size_t aim_strains_len;
+    size_t aim_strains_capacity;
+    f64 *speed_strains;
+    size_t speed_strains_len;
+    size_t speed_strains_capacity;
+
     bool ok;
 } pp_info;
 
 pp_info calculate_full_pp(const char* path, u32 mod_flags, f32 ar, f32 cs, f32 od, f64 speed_multiplier);
+void free_pp_info(pp_info info);
 
 typedef struct gradual_pp gradual_pp;
 typedef struct {

+ 6 - 0
src/Engine/Main/EngineFeatures.h

@@ -1,9 +1,15 @@
 #pragma once
 
 #ifndef MCENGINE_DATA_DIR
+
+#ifdef _WIN32
+#define MCENGINE_DATA_DIR ".\\"
+#else
 #define MCENGINE_DATA_DIR "./"
 #endif
 
+#endif
+
 /*
  * OpenGL graphics (Desktop, legacy + modern)
  */

+ 4 - 10
src/Engine/Platform/File.cpp

@@ -1,9 +1,4 @@
-//================ Copyright (c) 2016, PG, All rights reserved. =================//
-//
-// Purpose:		file wrapper, for cross-platform unicode path support
-//
-// $NoKeywords: $file
-//===============================================================================//
+#include <filesystem>
 
 #include "File.h"
 
@@ -49,7 +44,7 @@ StdFile::StdFile(std::string filePath, File::TYPE type) {
     m_iFileSize = 0;
 
     if(m_bRead) {
-        m_ifstream.open(filePath.c_str(), std::ios::in | std::ios::binary);
+        m_ifstream.open(std::filesystem::u8path(filePath.c_str()), std::ios::in | std::ios::binary);
 
         // check if we could open it at all
         if(!m_ifstream.good()) {
@@ -82,9 +77,8 @@ StdFile::StdFile(std::string filePath, File::TYPE type) {
         }
         m_ifstream.clear();  // clear potential error state due to the check above
         m_ifstream.seekg(0, std::ios::beg);
-    } else  // WRITE
-    {
-        m_ofstream.open(filePath.c_str(), std::ios::out | std::ios::trunc | std::ios::binary);
+    } else {  // WRITE
+        m_ofstream.open(std::filesystem::u8path(filePath.c_str()), std::ios::out | std::ios::trunc | std::ios::binary);
 
         // check if we could open it at all
         if(!m_ofstream.good()) {

+ 58 - 41
src/Engine/Platform/WinEnvironment.cpp

@@ -10,6 +10,7 @@
 #include <Commdlg.h>
 #include <shellapi.h>
 
+#include <filesystem>
 #include <string>
 
 #include "ConVar.h"
@@ -116,7 +117,7 @@ bool WinEnvironment::fileExists(std::string filename) {
     WIN32_FIND_DATA FindFileData;
     HANDLE handle = FindFirstFile(filename.c_str(), &FindFileData);
     if(handle == INVALID_HANDLE_VALUE)
-        return std::ifstream(filename.c_str()).good();
+        return std::ifstream(std::filesystem::u8path(filename.c_str())).good();
     else {
         FindClose(handle);
         return true;
@@ -246,33 +247,41 @@ UString WinEnvironment::openFolderWindow(UString title, UString initialpath) {
 }
 
 std::vector<std::string> WinEnvironment::getFilesInFolder(std::string folder) {
+    // Since we want to avoid wide strings in the codebase as much as possible,
+    // we convert wide paths to UTF-8 (as they fucking should be).
+    // We can't just use FindFirstFileA, because then any path with unicode
+    // characters will fail to open!
+    // Keep in mind that windows can't handle the way too modern 1993 UTF-8, so
+    // you have to use std::filesystem::u8path() or convert it back to a wstring
+    // before using the windows API.
+
     folder.append("*.*");
-    WIN32_FIND_DATA data;
-    std::string buffer;
+    WIN32_FIND_DATAW data;
+    std::wstring buffer;
     std::vector<std::string> files;
 
-    HANDLE handle = FindFirstFile(folder.c_str(), &data);
+    int size = MultiByteToWideChar(CP_UTF8, 0, folder.c_str(), folder.length(), NULL, 0);
+    std::wstring wfile(size, 0);
+    MultiByteToWideChar(CP_UTF8, 0, folder.c_str(), folder.length(), (LPWSTR)wfile.c_str(), wfile.length());
+
+    HANDLE handle = FindFirstFileW(wfile.c_str(), &data);
 
     while(true) {
-        std::string filename(data.cFileName);
-
-        if(filename != buffer) {
-            buffer = filename;
-
-            if(filename.length() > 0) {
-                if((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)  // directory
-                {
-                    /// if (filename.length() > 0)
-                    ///	folders.push_back(filename.c_str());
-                } else  // file
-                {
-                    if(filename.length() > 0) files.push_back(filename.c_str());
-                }
+        std::wstring filename(data.cFileName);
+        if(filename == buffer) break;
+
+        buffer = filename;
+
+        if(filename.length() > 0) {
+            if((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) {
+                int size = WideCharToMultiByte(CP_UTF8, 0, filename.c_str(), filename.length(), NULL, 0, NULL, NULL);
+                std::string utf8filename(size, 0);
+                WideCharToMultiByte(CP_UTF8, 0, filename.c_str(), size, (LPSTR)utf8filename.c_str(), size, NULL, NULL);
+                files.push_back(utf8filename);
             }
+        }
 
-            FindNextFile(handle, &data);
-        } else
-            break;
+        FindNextFileW(handle, &data);
     }
 
     FindClose(handle);
@@ -280,33 +289,41 @@ std::vector<std::string> WinEnvironment::getFilesInFolder(std::string folder) {
 }
 
 std::vector<std::string> WinEnvironment::getFoldersInFolder(std::string folder) {
+    // Since we want to avoid wide strings in the codebase as much as possible,
+    // we convert wide paths to UTF-8 (as they fucking should be).
+    // We can't just use FindFirstFileA, because then any path with unicode
+    // characters will fail to open!
+    // Keep in mind that windows can't handle the way too modern 1993 UTF-8, so
+    // you have to use std::filesystem::u8path() or convert it back to a wstring
+    // before using the windows API.
+
     folder.append("*.*");
-    WIN32_FIND_DATA data;
-    std::string buffer;
+    WIN32_FIND_DATAW data;
+    std::wstring buffer;
     std::vector<std::string> folders;
 
-    HANDLE handle = FindFirstFile(folder.c_str(), &data);
+	int size = MultiByteToWideChar(CP_UTF8, 0, folder.c_str(), folder.length(), NULL, 0);
+    std::wstring wfolder(size, 0);
+    MultiByteToWideChar(CP_UTF8, 0, folder.c_str(), folder.length(), (LPWSTR)wfolder.c_str(), wfolder.length());
+
+    HANDLE handle = FindFirstFileW(wfolder.c_str(), &data);
 
     while(true) {
-        std::string filename(data.cFileName);
-
-        if(filename != buffer) {
-            buffer = filename;
-
-            if(filename.length() > 0) {
-                if((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)  // directory
-                {
-                    if(filename.length() > 0) folders.push_back(filename.c_str());
-                } else  // file
-                {
-                    /// if (filename.length() > 0)
-                    ///	files.push_back(filename.c_str());
-                }
+        std::wstring filename(data.cFileName);
+        if(filename == buffer) break;
+
+        buffer = filename;
+
+        if(filename.length() > 0) {
+            if((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
+                int size = WideCharToMultiByte(CP_UTF8, 0, filename.c_str(), filename.length(), NULL, 0, NULL, NULL);
+                std::string utf8filename(size, 0);
+                WideCharToMultiByte(CP_UTF8, 0, filename.c_str(), size, (LPSTR)utf8filename.c_str(), size, NULL, NULL);
+                folders.push_back(utf8filename);
             }
+        }
 
-            FindNextFile(handle, &data);
-        } else
-            break;
+        FindNextFileW(handle, &data);
     }
 
     FindClose(handle);

+ 24 - 4
src/Engine/Sound.cpp

@@ -105,11 +105,22 @@ void Sound::initAsync() {
         }
     }
 
+#ifdef _WIN32
+    // On Windows, we need to convert the UTF-8 path to UTF-16, or paths with unicode characters will fail to open
+    int size = MultiByteToWideChar(CP_UTF8, 0, m_sFilePath.c_str(), m_sFilePath.length(), NULL, 0);
+    std::wstring file_path(size, 0);
+    MultiByteToWideChar(CP_UTF8, 0, m_sFilePath.c_str(), m_sFilePath.length(), (LPWSTR)file_path.c_str(), file_path.length());
+#else
+    std::string file_path = m_sFilePath;
+#endif
+
     if(m_bStream) {
         auto flags = BASS_STREAM_DECODE | BASS_SAMPLE_FLOAT;
         if(m_bPrescan) flags |= BASS_STREAM_PRESCAN;
+        if(convar->getConVarByName("snd_async_buffer")->getInt() > 0) flags |= BASS_ASYNCFILE;
+        if(env->getOS() == Environment::OS::WINDOWS) flags |= BASS_UNICODE;
 
-        m_stream = BASS_StreamCreateFile(false, m_sFilePath.c_str(), 0, 0, flags);
+        m_stream = BASS_StreamCreateFile(false, file_path.c_str(), 0, 0, flags);
         if(!m_stream) {
             debugLog("BASS_StreamCreateFile() returned error %d on file %s\n", BASS_ErrorGetCode(),
                      m_sFilePath.c_str());
@@ -128,10 +139,19 @@ void Sound::initAsync() {
         f64 lengthInMilliSeconds = lengthInSeconds * 1000.0;
         m_length = (u32)lengthInMilliSeconds;
     } else {
-        m_sample = BASS_SampleLoad(false, m_sFilePath.c_str(), 0, 0, 1, BASS_SAMPLE_FLOAT);
+        auto flags = BASS_SAMPLE_FLOAT;
+        if(env->getOS() == Environment::OS::WINDOWS) flags |= BASS_UNICODE;
+
+        m_sample = BASS_SampleLoad(false, file_path.c_str(), 0, 0, 1, flags);
         if(!m_sample) {
-            debugLog("BASS_SampleLoad() returned error %d on file %s\n", BASS_ErrorGetCode(), m_sFilePath.c_str());
-            return;
+            auto code = BASS_ErrorGetCode();
+            if(code == BASS_ERROR_EMPTY) {
+                debugLog("Sound: Ignoring empty file %s\n", m_sFilePath.c_str());
+                return;
+            } else {
+                debugLog("BASS_SampleLoad() returned error %d on file %s\n", code, m_sFilePath.c_str());
+                return;
+            }
         }
 
         // Only compute the length once

+ 12 - 10
src/Engine/SoundEngine.cpp

@@ -35,6 +35,7 @@ ConVar snd_updateperiod("snd_updateperiod", 10, FCVAR_DEFAULT | FCVAR_PRIVATE, "
 ConVar snd_dev_period("snd_dev_period", 10, FCVAR_DEFAULT | FCVAR_PRIVATE,
                       "BASS_CONFIG_DEV_PERIOD length in milliseconds, or if negative then in samples");
 ConVar snd_dev_buffer("snd_dev_buffer", 30, FCVAR_DEFAULT | FCVAR_PRIVATE, "BASS_CONFIG_DEV_BUFFER length in milliseconds");
+ConVar snd_async_buffer("snd_async_buffer", 65536, FCVAR_DEFAULT | FCVAR_PRIVATE, "BASS_CONFIG_ASYNCFILE_BUFFER length in bytes. Set to 0 to disable.");
 
 ConVar snd_restrict_play_frame(
     "snd_restrict_play_frame", true, FCVAR_DEFAULT | FCVAR_PRIVATE,
@@ -783,11 +784,6 @@ void SoundEngine::setOutputDevice(OUTPUT_DEVICE device) {
     if(osu->getSelectedBeatmap()->getMusic() != NULL) {
         was_playing = osu->getSelectedBeatmap()->getMusic()->isPlaying();
         prevMusicPositionMS = osu->getSelectedBeatmap()->getMusic()->getPositionMS();
-
-        if(osu->isInPlayMode() && was_playing) {
-            osu->getSelectedBeatmap()->pause(false);
-            osu->m_pauseMenu->setVisible(osu->getSelectedBeatmap()->isPaused());
-        }
     }
 
     // TODO: This is blocking main thread, can freeze for a long time on some sound cards
@@ -815,16 +811,17 @@ void SoundEngine::setOutputDevice(OUTPUT_DEVICE device) {
             osu->getSelectedBeatmap()->unloadMusic();
             osu->getSelectedBeatmap()->loadMusic(false, osu->getSelectedBeatmap()->m_bForceStreamPlayback);
             osu->getSelectedBeatmap()->getMusic()->setLoop(false);
-            if(was_playing) {
-                play(osu->getSelectedBeatmap()->getMusic());
-            }
             osu->getSelectedBeatmap()->getMusic()->setPositionMS(prevMusicPositionMS);
         } else {
             osu->getSelectedBeatmap()->unloadMusic();
-            osu->getSelectedBeatmap()->select();  // (triggers preview music play)
+            osu->getSelectedBeatmap()->select();
             osu->getSelectedBeatmap()->getMusic()->setPositionMS(prevMusicPositionMS);
         }
     }
+
+    if(was_playing) {
+        osu->music_unpause_scheduled = true;
+    }
 }
 
 void SoundEngine::setVolume(float volume) {
@@ -838,7 +835,12 @@ void SoundEngine::setVolume(float volume) {
 #endif
     } else if(m_currentOutputDevice.driver == OutputDriver::BASS_WASAPI) {
 #ifdef _WIN32
-        BASS_WASAPI_SetVolume(BASS_WASAPI_CURVE_WINDOWS | BASS_WASAPI_VOL_SESSION, m_fVolume);
+        if(hasExclusiveOutput()) {
+            // Device volume doesn't seem to work, so we'll use DSP instead
+            BASS_ChannelSetAttribute(g_bassOutputMixer, BASS_ATTRIB_VOLDSP, m_fVolume);
+        } else {
+            BASS_WASAPI_SetVolume(BASS_WASAPI_CURVE_WINDOWS | BASS_WASAPI_VOL_SESSION, m_fVolume);
+        }
 #endif
     } else {
         // 0 (silent) - 10000 (full).