diff --git a/CREDITS.md b/CREDITS.md index ac8eab24..94d8aca3 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -23,4 +23,5 @@ Projects The following projects were referenced in creating opsu!: * "Wojtkosu" - Wojtek Kowaluk (https://osu.ppy.sh/forum/t/97260) +* "osu-parser" - nojhamster (https://github.com/nojhamster/osu-parser) * "osu! web" - pictuga (https://github.com/pictuga/osu-web) \ No newline at end of file diff --git a/res/applause.wav b/res/applause.wav new file mode 100644 index 00000000..bd0739f9 Binary files /dev/null and b/res/applause.wav differ diff --git a/res/combobreak.wav b/res/combobreak.wav new file mode 100644 index 00000000..af985e73 Binary files /dev/null and b/res/combobreak.wav differ diff --git a/res/count1s.wav b/res/count1s.wav new file mode 100644 index 00000000..9ee7f80a Binary files /dev/null and b/res/count1s.wav differ diff --git a/res/count2s.wav b/res/count2s.wav new file mode 100644 index 00000000..9ee7f80a Binary files /dev/null and b/res/count2s.wav differ diff --git a/res/count3s.wav b/res/count3s.wav new file mode 100644 index 00000000..9ee7f80a Binary files /dev/null and b/res/count3s.wav differ diff --git a/res/drum-hitclap.wav b/res/drum-hitclap.wav new file mode 100644 index 00000000..50b33d1f Binary files /dev/null and b/res/drum-hitclap.wav differ diff --git a/res/drum-hitfinish.wav b/res/drum-hitfinish.wav new file mode 100644 index 00000000..dfe98b6d Binary files /dev/null and b/res/drum-hitfinish.wav differ diff --git a/res/drum-hitnormal.wav b/res/drum-hitnormal.wav new file mode 100644 index 00000000..94920040 Binary files /dev/null and b/res/drum-hitnormal.wav differ diff --git a/res/drum-hitwhistle.wav b/res/drum-hitwhistle.wav new file mode 100644 index 00000000..b67d5712 Binary files /dev/null and b/res/drum-hitwhistle.wav differ diff --git a/res/drum-sliderslide.wav b/res/drum-sliderslide.wav new file mode 100644 index 00000000..4a6dd596 Binary files /dev/null and b/res/drum-sliderslide.wav differ diff --git a/res/drum-slidertick.wav b/res/drum-slidertick.wav new file mode 100644 index 00000000..4a6dd596 Binary files /dev/null and b/res/drum-slidertick.wav differ diff --git a/res/drum-sliderwhistle.wav b/res/drum-sliderwhistle.wav new file mode 100644 index 00000000..4a6dd596 Binary files /dev/null and b/res/drum-sliderwhistle.wav differ diff --git a/res/failsound.wav b/res/failsound.wav new file mode 100644 index 00000000..ed2ceba2 Binary files /dev/null and b/res/failsound.wav differ diff --git a/res/gos.wav b/res/gos.wav new file mode 100644 index 00000000..b208fca8 Binary files /dev/null and b/res/gos.wav differ diff --git a/res/menuback.wav b/res/menuback.wav new file mode 100644 index 00000000..e2adab0f Binary files /dev/null and b/res/menuback.wav differ diff --git a/res/menuclick.wav b/res/menuclick.wav new file mode 100644 index 00000000..dc282b40 Binary files /dev/null and b/res/menuclick.wav differ diff --git a/res/menuhit.wav b/res/menuhit.wav new file mode 100644 index 00000000..b09cab5a Binary files /dev/null and b/res/menuhit.wav differ diff --git a/res/normal-hitclap.wav b/res/normal-hitclap.wav new file mode 100644 index 00000000..ce7812f9 Binary files /dev/null and b/res/normal-hitclap.wav differ diff --git a/res/normal-hitfinish.wav b/res/normal-hitfinish.wav new file mode 100644 index 00000000..16c855f8 Binary files /dev/null and b/res/normal-hitfinish.wav differ diff --git a/res/normal-hitnormal.wav b/res/normal-hitnormal.wav new file mode 100644 index 00000000..9de4c17f Binary files /dev/null and b/res/normal-hitnormal.wav differ diff --git a/res/normal-hitwhistle.wav b/res/normal-hitwhistle.wav new file mode 100644 index 00000000..b67d5712 Binary files /dev/null and b/res/normal-hitwhistle.wav differ diff --git a/res/normal-sliderslide.wav b/res/normal-sliderslide.wav new file mode 100644 index 00000000..878bf23c Binary files /dev/null and b/res/normal-sliderslide.wav differ diff --git a/res/normal-slidertick.wav b/res/normal-slidertick.wav new file mode 100644 index 00000000..fc51c46a Binary files /dev/null and b/res/normal-slidertick.wav differ diff --git a/res/normal-sliderwhistle.wav b/res/normal-sliderwhistle.wav new file mode 100644 index 00000000..878bf23c Binary files /dev/null and b/res/normal-sliderwhistle.wav differ diff --git a/res/readys.wav b/res/readys.wav new file mode 100644 index 00000000..28f2ec2a Binary files /dev/null and b/res/readys.wav differ diff --git a/res/sectionfail.wav b/res/sectionfail.wav new file mode 100644 index 00000000..2c30344f Binary files /dev/null and b/res/sectionfail.wav differ diff --git a/res/sectionpass.wav b/res/sectionpass.wav new file mode 100644 index 00000000..27881c48 Binary files /dev/null and b/res/sectionpass.wav differ diff --git a/res/shutter.wav b/res/shutter.wav new file mode 100644 index 00000000..5b4502db Binary files /dev/null and b/res/shutter.wav differ diff --git a/res/soft-hitclap.wav b/res/soft-hitclap.wav new file mode 100644 index 00000000..9ee7f80a Binary files /dev/null and b/res/soft-hitclap.wav differ diff --git a/res/soft-hitfinish.wav b/res/soft-hitfinish.wav new file mode 100644 index 00000000..fa1f6022 Binary files /dev/null and b/res/soft-hitfinish.wav differ diff --git a/res/soft-hitnormal.wav b/res/soft-hitnormal.wav new file mode 100644 index 00000000..9de4c17f Binary files /dev/null and b/res/soft-hitnormal.wav differ diff --git a/res/soft-hitwhistle.wav b/res/soft-hitwhistle.wav new file mode 100644 index 00000000..b67d5712 Binary files /dev/null and b/res/soft-hitwhistle.wav differ diff --git a/res/soft-sliderslide.wav b/res/soft-sliderslide.wav new file mode 100644 index 00000000..878bf23c Binary files /dev/null and b/res/soft-sliderslide.wav differ diff --git a/res/soft-slidertick.wav b/res/soft-slidertick.wav new file mode 100644 index 00000000..d01981b9 Binary files /dev/null and b/res/soft-slidertick.wav differ diff --git a/res/soft-sliderwhistle.wav b/res/soft-sliderwhistle.wav new file mode 100644 index 00000000..032431e2 Binary files /dev/null and b/res/soft-sliderwhistle.wav differ diff --git a/res/spinner-osu.wav b/res/spinner-osu.wav new file mode 100644 index 00000000..1e999541 Binary files /dev/null and b/res/spinner-osu.wav differ diff --git a/res/spinnerbonus.wav b/res/spinnerbonus.wav new file mode 100644 index 00000000..654f3802 Binary files /dev/null and b/res/spinnerbonus.wav differ diff --git a/res/spinnerspin.wav b/res/spinnerspin.wav new file mode 100644 index 00000000..878bf23c Binary files /dev/null and b/res/spinnerspin.wav differ diff --git a/src/itdelatrisu/opsu/GameScore.java b/src/itdelatrisu/opsu/GameScore.java index 1d9764a2..5b4e69fd 100644 --- a/src/itdelatrisu/opsu/GameScore.java +++ b/src/itdelatrisu/opsu/GameScore.java @@ -717,29 +717,41 @@ public class GameScore { } } + /** + * Resets the combo streak to zero. + */ + private void resetComboStreak() { + if (combo >= 20) + SoundController.playSound(SoundController.SOUND_COMBOBREAK); + combo = 0; + if (Options.isModActive(Options.MOD_SUDDEN_DEATH)) + health = 0f; + } + /** * Handles a slider tick result. * @param time the tick start time * @param result the hit result (HIT_* constants) * @param x the x coordinate * @param y the y coordinate + * @param hitSound the object's hit sound */ - public void sliderTickResult(int time, int result, float x, float y) { + public void sliderTickResult(int time, int result, float x, float y, byte hitSound) { int hitValue = 0; switch (result) { case HIT_SLIDER30: hitValue = 30; incrementComboStreak(); changeHealth(1f); + SoundController.playHitSound(hitSound); break; case HIT_SLIDER10: hitValue = 10; incrementComboStreak(); + SoundController.playHitSound(SoundController.HIT_SLIDERTICK); break; case HIT_MISS: - combo = 0; - if (Options.isModActive(Options.MOD_SUDDEN_DEATH)) - health = 0f; + resetComboStreak(); break; default: return; @@ -760,8 +772,10 @@ public class GameScore { * @param y the y coordinate * @param color the combo color * @param end true if this is the last hit object in the combo + * @param hitSound the object's hit sound */ - public void hitResult(int time, int result, float x, float y, Color color, boolean end) { + public void hitResult(int time, int result, float x, float y, Color color, + boolean end, byte hitSound) { int hitValue = 0; switch (result) { case HIT_300: @@ -784,15 +798,15 @@ public class GameScore { hitValue = 0; changeHealth(-10f); comboEnd |= 2; - combo = 0; - if (Options.isModActive(Options.MOD_SUDDEN_DEATH)) - health = 0f; + resetComboStreak(); objectCount++; break; default: return; } if (hitValue > 0) { + SoundController.playHitSound(hitSound); + // game mod score multipliers float modMultiplier = 1f; if (Options.isModActive(Options.MOD_NO_FAIL)) diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java index 4b657682..89c8f2cd 100644 --- a/src/itdelatrisu/opsu/Opsu.java +++ b/src/itdelatrisu/opsu/Opsu.java @@ -143,6 +143,9 @@ public class Opsu extends StateBasedGame { } Options.TMP_DIR.deleteOnExit(); + // load sounds + SoundController.init(); + app.start(); } catch (SlickException e) { Log.error("Error while creating game container.", e); diff --git a/src/itdelatrisu/opsu/OsuFile.java b/src/itdelatrisu/opsu/OsuFile.java index fbc31d78..dc5646cd 100644 --- a/src/itdelatrisu/opsu/OsuFile.java +++ b/src/itdelatrisu/opsu/OsuFile.java @@ -40,7 +40,7 @@ public class OsuFile implements Comparable { // public String audioHash = ""; // audio hash (deprecated) public int previewTime = -1; // start position of music preview (in ms) public byte countdown = 0; // countdown type (0:disabled, 1:normal, 2:half, 3:double) - public String sampleSet = ""; // ? ("Normal", "Soft") + public String sampleSet = ""; // sound samples ("None", "Normal", "Soft") public float stackLeniency = 0.7f; // how often closely placed hit objects will be stacked together public byte mode = 0; // game mode (0:osu!, 1:taiko, 2:catch the beat, 3:osu!mania) public boolean letterboxInBreaks = false; // whether the letterbox (top/bottom black bars) appears during breaks diff --git a/src/itdelatrisu/opsu/OsuHitObject.java b/src/itdelatrisu/opsu/OsuHitObject.java index 81da981d..9e159023 100644 --- a/src/itdelatrisu/opsu/OsuHitObject.java +++ b/src/itdelatrisu/opsu/OsuHitObject.java @@ -38,7 +38,6 @@ public class OsuHitObject { SOUND_NORMAL = 0, SOUND_WHISTLE = 2, SOUND_FINISH = 4, - SOUND_WHISTLEFINISH = 6, SOUND_CLAP = 8; /** diff --git a/src/itdelatrisu/opsu/OsuTimingPoint.java b/src/itdelatrisu/opsu/OsuTimingPoint.java index 65a8ef30..98f1d1ff 100644 --- a/src/itdelatrisu/opsu/OsuTimingPoint.java +++ b/src/itdelatrisu/opsu/OsuTimingPoint.java @@ -26,8 +26,8 @@ public class OsuTimingPoint { public float beatLength; // (non-inherited) ms per beat public int velocity = 0; // (inherited) slider multiplier = -100 / value public int meter; // beats per measure - public byte sampleType; // sound samples (0:none, 1:normal, 2:soft) - public byte sampleTypeCustom; // custom samples (0:default, 1:custom1, 2:custom2 + public byte sampleType; // sound samples (0:none, 1:normal, 2:soft, 3:drum) + public byte sampleTypeCustom; // custom samples (0:default, 1:custom1, 2:custom2) public int sampleVolume; // volume of samples (0~100) public boolean inherited; // is this timing point inherited? public boolean kiai; // is Kiai Mode active? diff --git a/src/itdelatrisu/opsu/SoundController.java b/src/itdelatrisu/opsu/SoundController.java new file mode 100644 index 00000000..4150cde5 --- /dev/null +++ b/src/itdelatrisu/opsu/SoundController.java @@ -0,0 +1,285 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014 Jeffrey Han + * + * opsu! is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * opsu! is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with opsu!. If not, see . + */ + +package itdelatrisu.opsu; + +import itdelatrisu.opsu.states.Options; + +import java.io.IOException; +import java.net.URL; + +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.Clip; +import javax.sound.sampled.FloatControl; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.UnsupportedAudioFileException; + +import org.newdawn.slick.util.Log; +import org.newdawn.slick.util.ResourceLoader; + +/** + * Controller for all sound effects. + * Note: Uses Java Sound because OpenAL lags too much for accurate hit sounds. + */ +public class SoundController { + /** + * Sound effect constants. + */ + public static final int + SOUND_APPLAUSE = 0, + SOUND_COMBOBREAK = 1, +// SOUND_COUNT = , // ? + SOUND_COUNT1 = 2, + SOUND_COUNT2 = 3, + SOUND_COUNT3 = 4, + SOUND_FAIL = 5, + SOUND_GO = 6, + SOUND_MENUBACK = 7, + SOUND_MENUCLICK = 8, + SOUND_MENUHIT = 9, + SOUND_READY = 10, + SOUND_SECTIONFAIL = 11, + SOUND_SECTIONPASS = 12, + SOUND_SHUTTER = 13, + SOUND_SPINNERBONUS = 14, + SOUND_SPINNEROSU = 15, + SOUND_SPINNERSPIN = 16, + SOUND_MAX = 17; // not a sound + + /** + * Sound effect names (indexed by SOUND_* constants). + */ + private static final String[] soundNames = { + "applause", + "combobreak", +// "count", // ? + "count1s", + "count2s", + "count3s", + "failsound", + "gos", + "menuback", + "menuclick", + "menuhit", + "readys", + "sectionfail", + "sectionpass", + "shutter", + "spinnerbonus", + "spinner-osu", + "spinnerspin", + }; + + /** + * Sound effects (indexed by SOUND_* constants). + */ + private static Clip[] sounds = new Clip[SOUND_MAX]; + + /** + * Sound sample sets. + */ + private static final String[] sampleSets = { + "normal", + "soft", + "drum", +// "taiko" + }; + + /** + * Current sample set (index in sampleSet[] array). + */ + private static int sampleSetIndex = -1; + + /** + * Hit sound effects. + */ + public static final int + HIT_CLAP = 0, + HIT_FINISH = 1, + HIT_NORMAL = 2, + HIT_WHISTLE = 3, + HIT_SLIDERSLIDE = 4, + HIT_SLIDERTICK = 5, + HIT_SLIDERWHISTLE = 6, + HIT_MAX = 7; // not a sound + + /** + * Hit sound effect names (indexed by HIT_* constants). + */ + private static final String[] hitSoundNames = { + "hitclap", + "hitfinish", + "hitnormal", + "hitwhistle", + "sliderslide", + "slidertick", + "sliderwhistle" + }; + + /** + * Hit sound effects (indexed by sampleSets[], HIT_* constants). + */ + private static Clip[][] hitSounds = new Clip[sampleSets.length][HIT_MAX]; + + /** + * Sample volume multiplier, from timing points [0, 1]. + */ + private static float sampleVolumeMultiplier = 1f; + + // This class should not be instantiated. + private SoundController() {} + + /** + * Loads and returns a Clip from a resource. + * @param ref the resource name + * @return the loaded and opened clip + */ + private static Clip loadClip(String ref) { + try { + URL url = ResourceLoader.getResource(ref); + AudioInputStream audioIn = AudioSystem.getAudioInputStream(url); + Clip clip = AudioSystem.getClip(); + clip.open(audioIn); + return clip; + } catch (UnsupportedAudioFileException | IOException | LineUnavailableException e) { + Log.error(String.format("Failed to load file '%s'.", ref), e); + } + return null; + } + + /** + * Loads all sound files. + */ + public static void init() { + // TODO: support MP3 sounds? + + // menu and game sounds + for (int i = 0; i < SOUND_MAX; i++) + sounds[i] = loadClip(String.format("%s.wav", soundNames[i])); + + // hit sounds + for (int i = 0; i < sampleSets.length; i++) { + for (int j = 0; j < HIT_MAX; j++) + hitSounds[i][j] = loadClip(String.format("%s-%s.wav", sampleSets[i], hitSoundNames[j])); + } + } + + /** + * Sets the sample set to use when playing hit sounds. + * @param sampleSet the sample set ("None", "Normal", "Soft", "Drum") + */ + public static void setSampleSet(String sampleSet) { + sampleSetIndex = -1; + for (int i = 0; i < sampleSets.length; i++) { + if (sampleSet.equalsIgnoreCase(sampleSets[i])) { + sampleSetIndex = i; + return; + } + } + } + + /** + * Sets the sample set to use when playing hit sounds. + * @param sampleType the sample set (0:none, 1:normal, 2:soft, 3:drum) + */ + public static void setSampleSet(byte sampleType) { + if (sampleType >= 0 && sampleType <= 3) + sampleSetIndex = sampleType - 1; + } + + /** + * Sets the sample volume (modifies the global sample volume). + * @param volume the sample volume [0, 100] + */ + public static void setSampleVolume(int volume) { + if (volume >= 0 && volume <= 100) + sampleVolumeMultiplier = volume / 100f; + } + + /** + * Plays a sound clip. + * @param clip the Clip to play + * @param volume the volume [0, 1] + */ + private static void playClip(Clip clip, float volume) { + if (volume > 0f) { + // stop clip if running + if (clip.isRunning()) { + clip.stop(); + clip.flush(); + } + + // set volume + FloatControl gainControl = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN); + float dB = (float) (Math.log(volume) / Math.log(10.0) * 20.0); + gainControl.setValue(dB * sampleVolumeMultiplier); + + // play clip + clip.setFramePosition(0); + clip.start(); + } + } + + /** + * Plays a sound. + * @param sound the sound (SOUND_* constant) + */ + public static void playSound(int sound) { + if (sound < 0 || sound >= SOUND_MAX) + return; + + playClip(sounds[sound], Options.getEffectVolume()); + } + + /** + * Plays hit sound(s) using an OsuHitObject bitmask. + * @param hitSound the sound (bitmask) + */ + public static void playHitSound(byte hitSound) { + if (sampleSetIndex < 0 || hitSound < 0) + return; + + float volume = Options.getEffectVolume(); + if (volume == 0f) + return; + + // play all sounds + if (hitSound == OsuHitObject.SOUND_NORMAL) + playClip(hitSounds[sampleSetIndex][HIT_NORMAL], volume); + else { + if ((hitSound & OsuHitObject.SOUND_WHISTLE) > 0) + playClip(hitSounds[sampleSetIndex][HIT_WHISTLE], volume); + if ((hitSound & OsuHitObject.SOUND_FINISH) > 0) + playClip(hitSounds[sampleSetIndex][HIT_FINISH], volume); + if ((hitSound & OsuHitObject.SOUND_CLAP) > 0) + playClip(hitSounds[sampleSetIndex][HIT_CLAP], volume); + } + } + + /** + * Plays a hit sound. + * @param sound (HIT_* constant) + */ + public static void playHitSound(int sound) { + if (sampleSetIndex < 0 || sound < 0 || sound > HIT_MAX) + return; + + playClip(hitSounds[sampleSetIndex][sound], Options.getEffectVolume()); + } +} diff --git a/src/itdelatrisu/opsu/objects/Circle.java b/src/itdelatrisu/opsu/objects/Circle.java index 252a99a6..a90616c8 100644 --- a/src/itdelatrisu/opsu/objects/Circle.java +++ b/src/itdelatrisu/opsu/objects/Circle.java @@ -172,7 +172,9 @@ public class Circle { if (distance < circleRadius) { int result = hitResult(hitObject.time); if (result > -1) { - score.hitResult(hitObject.time, result, hitObject.x, hitObject.y, color, comboEnd); + score.hitResult(hitObject.time, result, hitObject.x, hitObject.y, + color, comboEnd, hitObject.hitSound + ); return true; } } @@ -192,11 +194,11 @@ public class Circle { if (overlap || trackPosition > hitObject.time + hitResultOffset[GameScore.HIT_50]) { if (isAutoMod) // "auto" mod: catch any missed notes due to lag score.hitResult(hitObject.time, GameScore.HIT_300, - hitObject.x, hitObject.y, color, comboEnd); + hitObject.x, hitObject.y, color, comboEnd, hitObject.hitSound); else // no more points can be scored, so send a miss score.hitResult(hitObject.time, GameScore.HIT_MISS, - hitObject.x, hitObject.y, null, comboEnd); + hitObject.x, hitObject.y, null, comboEnd, hitObject.hitSound); return true; } @@ -204,7 +206,7 @@ public class Circle { else if (isAutoMod) { if (Math.abs(trackPosition - hitObject.time) < hitResultOffset[GameScore.HIT_300]) { score.hitResult(hitObject.time, GameScore.HIT_300, - hitObject.x, hitObject.y, color, comboEnd); + hitObject.x, hitObject.y, color, comboEnd, hitObject.hitSound); return true; } } diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index 93e56436..1503650f 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -384,10 +384,11 @@ public class Slider { if (currentRepeats % 2 == 0) // last circle score.hitResult(hitObject.time + (int) sliderTimeTotal, result, - hitObject.sliderX[lastIndex], hitObject.sliderY[lastIndex], color, comboEnd); + hitObject.sliderX[lastIndex], hitObject.sliderY[lastIndex], + color, comboEnd, hitObject.hitSound); else // first circle score.hitResult(hitObject.time + (int) sliderTimeTotal, result, - hitObject.x, hitObject.y, color, comboEnd); + hitObject.x, hitObject.y, color, comboEnd, hitObject.hitSound); return result; } @@ -420,7 +421,8 @@ public class Slider { if (result > -1) { sliderClicked = true; - score.sliderTickResult(hitObject.time, result, hitObject.x, hitObject.y); + score.sliderTickResult(hitObject.time, result, + hitObject.x, hitObject.y, hitObject.hitSound); return true; } } @@ -465,9 +467,11 @@ public class Slider { sliderClicked = true; if (isAutoMod) { // "auto" mod: catch any missed notes due to lag ticksHit++; - score.sliderTickResult(hitObject.time, GameScore.HIT_SLIDER30, hitObject.x, hitObject.y); + score.sliderTickResult(hitObject.time, GameScore.HIT_SLIDER30, + hitObject.x, hitObject.y, hitObject.hitSound); } else - score.sliderTickResult(hitObject.time, GameScore.HIT_MISS, hitObject.x, hitObject.y); + score.sliderTickResult(hitObject.time, GameScore.HIT_MISS, + hitObject.x, hitObject.y, hitObject.hitSound); } // "auto" mod: send a perfect hit result @@ -475,7 +479,8 @@ public class Slider { if (Math.abs(trackPosition - hitObject.time) < hitResultOffset[GameScore.HIT_300]) { ticksHit++; sliderClicked = true; - score.sliderTickResult(hitObject.time, GameScore.HIT_SLIDER30, hitObject.x, hitObject.y); + score.sliderTickResult(hitObject.time, GameScore.HIT_SLIDER30, + hitObject.x, hitObject.y, hitObject.hitSound); } } } @@ -540,24 +545,26 @@ public class Slider { ticksHit++; if (currentRepeats % 2 > 0) // last circle score.sliderTickResult(trackPosition, GameScore.HIT_SLIDER30, - hitObject.sliderX[lastIndex], hitObject.sliderY[lastIndex]); + hitObject.sliderX[lastIndex], hitObject.sliderY[lastIndex], + hitObject.hitSound); else // first circle score.sliderTickResult(trackPosition, GameScore.HIT_SLIDER30, - c[0], c[1]); + c[0], c[1], hitObject.hitSound); } // held during new tick if (isNewTick) { ticksHit++; - score.sliderTickResult(trackPosition, GameScore.HIT_SLIDER10, c[0], c[1]); + score.sliderTickResult(trackPosition, GameScore.HIT_SLIDER10, + c[0], c[1], (byte) -1); } } else { followCircleActive = false; if (isNewRepeat) - score.sliderTickResult(trackPosition, GameScore.HIT_MISS, 0, 0); + score.sliderTickResult(trackPosition, GameScore.HIT_MISS, 0, 0, hitObject.hitSound); if (isNewTick) - score.sliderTickResult(trackPosition, GameScore.HIT_MISS, 0, 0); + score.sliderTickResult(trackPosition, GameScore.HIT_MISS, 0, 0, (byte) -1); } return false; diff --git a/src/itdelatrisu/opsu/objects/Spinner.java b/src/itdelatrisu/opsu/objects/Spinner.java index 1369c74e..5086cff4 100644 --- a/src/itdelatrisu/opsu/objects/Spinner.java +++ b/src/itdelatrisu/opsu/objects/Spinner.java @@ -21,6 +21,7 @@ package itdelatrisu.opsu.objects; import itdelatrisu.opsu.GameScore; import itdelatrisu.opsu.MusicController; import itdelatrisu.opsu.OsuHitObject; +import itdelatrisu.opsu.SoundController; import itdelatrisu.opsu.states.Game; import itdelatrisu.opsu.states.Options; @@ -163,16 +164,19 @@ public class Spinner { int result; float ratio = rotations / rotationsNeeded; if (ratio >= 1.0f || - Options.isModActive(Options.MOD_AUTO) || Options.isModActive(Options.MOD_SPUN_OUT)) + Options.isModActive(Options.MOD_AUTO) || + Options.isModActive(Options.MOD_SPUN_OUT)) { result = GameScore.HIT_300; - else if (ratio >= 0.8f) + SoundController.playSound(SoundController.SOUND_SPINNEROSU); + } else if (ratio >= 0.8f) result = GameScore.HIT_100; else if (ratio >= 0.5f) result = GameScore.HIT_50; else result = GameScore.HIT_MISS; - score.hitResult(hitObject.endTime, result, width / 2, height / 2, Color.transparent, true); + score.hitResult(hitObject.endTime, result, width / 2, height / 2, + Color.transparent, true, (byte) -1); return result; } @@ -240,10 +244,13 @@ public class Spinner { // added one whole rotation... if (Math.floor(newRotations) > rotations) { - if (newRotations > rotationsNeeded) // extra rotations + if (newRotations > rotationsNeeded) { // extra rotations score.changeScore(1000); - else + SoundController.playSound(SoundController.SOUND_SPINNERBONUS); + } else { score.changeScore(100); + SoundController.playSound(SoundController.SOUND_SPINNERSPIN); + } } rotations = newRotations; diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index bded3dfe..f758aeaf 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -25,6 +25,7 @@ import itdelatrisu.opsu.Opsu; import itdelatrisu.opsu.OsuFile; import itdelatrisu.opsu.OsuHitObject; import itdelatrisu.opsu.OsuTimingPoint; +import itdelatrisu.opsu.SoundController; import itdelatrisu.opsu.objects.Circle; import itdelatrisu.opsu.objects.Slider; import itdelatrisu.opsu.objects.Spinner; @@ -134,6 +135,11 @@ public class Game extends BasicGameState { */ private int breakTime = 0; + /** + * Whether the break sound has been played. + */ + private boolean breakSound; + /** * Skip button (displayed at song start, when necessary). */ @@ -164,6 +170,13 @@ public class Game extends BasicGameState { countdown2, // "1" text countdownGo; // "GO!" text + /** + * Whether the countdown sound has been played. + */ + private boolean + countdownReadySound, countdown3Sound, countdown1Sound, + countdown2Sound, countdownGoSound; + /** * Glowing hit circle outline which must be clicked when returning from pause menu. */ @@ -276,10 +289,19 @@ public class Game extends BasicGameState { trackPosition - breakTime > 2000 && trackPosition - breakTime < 5000) { // show break start - if (score.getHealth() >= 50) + if (score.getHealth() >= 50) { breakStartPass.drawCentered(width / 2f, height / 2f); - else + if (!breakSound) { + SoundController.playSound(SoundController.SOUND_SECTIONPASS); + breakSound = true; + } + } else { breakStartFail.drawCentered(width / 2f, height / 2f); + if (!breakSound) { + SoundController.playSound(SoundController.SOUND_SECTIONFAIL); + breakSound = true; + } + } } else if (breakLength >= 4000) { // show break end (flash twice for 500ms) int endTimeDiff = endTime - trackPosition; @@ -326,18 +348,41 @@ public class Game extends BasicGameState { if (osu.countdown > 0) { // TODO: implement half/double rate settings int timeDiff = osu.objects[0].time - trackPosition; if (timeDiff >= 500 && timeDiff < 3000) { - if (timeDiff >= 1500) + if (timeDiff >= 1500) { countdownReady.drawCentered(width / 2, height / 2); - - if (timeDiff < 2000) + if (!countdownReadySound) { + SoundController.playSound(SoundController.SOUND_READY); + countdownReadySound = true; + } + } + if (timeDiff < 2000) { countdown3.draw(0, 0); - if (timeDiff < 1500) + if (!countdown3Sound) { + SoundController.playSound(SoundController.SOUND_COUNT3); + countdown3Sound = true; + } + } + if (timeDiff < 1500) { countdown2.draw(width - countdown2.getWidth(), 0); - if (timeDiff < 1000) + if (!countdown2Sound) { + SoundController.playSound(SoundController.SOUND_COUNT2); + countdown2Sound = true; + } + } + if (timeDiff < 1000) { countdown1.drawCentered(width / 2, height / 2); + if (!countdown1Sound) { + SoundController.playSound(SoundController.SOUND_COUNT1); + countdown1Sound = true; + } + } } else if (timeDiff >= -500 && timeDiff < 500) { countdownGo.setAlpha((timeDiff < 0) ? 1 - (timeDiff / -1000f) : 1); countdownGo.drawCentered(width / 2, height / 2); + if (!countdownGoSound) { + SoundController.playSound(SoundController.SOUND_GO); + countdownGoSound = true; + } } } @@ -432,6 +477,8 @@ public class Game extends BasicGameState { beatLengthBase = beatLength = timingPoint.beatLength; else beatLength = beatLengthBase * (timingPoint.velocity / -100f); + SoundController.setSampleSet(timingPoint.sampleType); + SoundController.setSampleVolume(timingPoint.sampleVolume); timingPointIndex++; } } @@ -456,6 +503,7 @@ public class Game extends BasicGameState { } else if (trackPosition >= breakValue) { // start a break breakTime = breakValue; + breakSound = false; breakIndex++; return; } @@ -638,22 +686,33 @@ public class Game extends BasicGameState { } } - // reset indexes + // reset data MusicController.setPosition(0); MusicController.pause(); score.clear(); objectIndex = 0; breakIndex = 0; breakTime = 0; + breakSound = false; timingPointIndex = 0; pauseTime = -1; pausedMouseX = -1; pausedMouseY = -1; + countdownReadySound = false; + countdown3Sound = false; + countdown1Sound = false; + countdown2Sound = false; + countdownGoSound = false; // load the first timingPoint - if (!osu.timingPoints.isEmpty() && osu.timingPoints.get(0).velocity >= 0) { - beatLengthBase = beatLength = osu.timingPoints.get(0).beatLength; - timingPointIndex++; + if (!osu.timingPoints.isEmpty()) { + OsuTimingPoint timingPoint = osu.timingPoints.get(0); + if (timingPoint.velocity >= 0) { + beatLengthBase = beatLength = timingPoint.beatLength; + SoundController.setSampleSet(timingPoint.sampleType); + SoundController.setSampleVolume(timingPoint.sampleVolume); + timingPointIndex++; + } } leadInTime = osu.audioLeadIn + approachTime; @@ -675,6 +734,7 @@ public class Game extends BasicGameState { MusicController.resume(); } MusicController.setPosition(osu.objects[0].time - skipOffsetTime); + SoundController.playSound(SoundController.SOUND_MENUHIT); return true; } return false; diff --git a/src/itdelatrisu/opsu/states/GamePauseMenu.java b/src/itdelatrisu/opsu/states/GamePauseMenu.java index a8e94f8d..e49463c9 100644 --- a/src/itdelatrisu/opsu/states/GamePauseMenu.java +++ b/src/itdelatrisu/opsu/states/GamePauseMenu.java @@ -21,6 +21,7 @@ package itdelatrisu.opsu.states; import itdelatrisu.opsu.GUIMenuButton; import itdelatrisu.opsu.MusicController; import itdelatrisu.opsu.Opsu; +import itdelatrisu.opsu.SoundController; import org.newdawn.slick.Color; import org.newdawn.slick.GameContainer; @@ -142,6 +143,7 @@ public class GamePauseMenu extends BasicGameState { if (Game.getRestart() == Game.RESTART_LOSE) { MusicController.stop(); MusicController.playAt(Game.getOsuFile().previewTime, true); + SoundController.playSound(SoundController.SOUND_MENUBACK); game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); } else unPause(Game.RESTART_FALSE); @@ -171,6 +173,7 @@ public class GamePauseMenu extends BasicGameState { } else if (backButton.contains(x, y)) { MusicController.pause(); // lose state MusicController.playAt(Game.getOsuFile().previewTime, true); + SoundController.playSound(SoundController.SOUND_MENUBACK); game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); } } @@ -179,9 +182,10 @@ public class GamePauseMenu extends BasicGameState { public void enter(GameContainer container, StateBasedGame game) throws SlickException { pauseStartTime = System.currentTimeMillis(); - if (Game.getRestart() == Game.RESTART_LOSE) + if (Game.getRestart() == Game.RESTART_LOSE) { MusicController.fadeOut(FADEOUT_TIME); - else + SoundController.playSound(SoundController.SOUND_FAIL); + } else MusicController.pause(); } @@ -189,6 +193,10 @@ public class GamePauseMenu extends BasicGameState { * Unpause and return to the Game state. */ private void unPause(byte restart) { + if (restart == Game.RESTART_MANUAL) + SoundController.playSound(SoundController.SOUND_MENUHIT); + else + SoundController.playSound(SoundController.SOUND_MENUBACK); Game.setRestart(restart); game.enterState(Opsu.STATE_GAME); } diff --git a/src/itdelatrisu/opsu/states/GameRanking.java b/src/itdelatrisu/opsu/states/GameRanking.java index 05fe85f1..cab08835 100644 --- a/src/itdelatrisu/opsu/states/GameRanking.java +++ b/src/itdelatrisu/opsu/states/GameRanking.java @@ -23,6 +23,7 @@ import itdelatrisu.opsu.GameScore; import itdelatrisu.opsu.MusicController; import itdelatrisu.opsu.Opsu; import itdelatrisu.opsu.OsuFile; +import itdelatrisu.opsu.SoundController; import org.lwjgl.opengl.Display; import org.newdawn.slick.Color; @@ -143,7 +144,9 @@ public class GameRanking extends BasicGameState { public void keyPressed(int key, char c) { switch (key) { case Input.KEY_ESCAPE: + MusicController.pause(); MusicController.playAt(Game.getOsuFile().previewTime, true); + SoundController.playSound(SoundController.SOUND_MENUBACK); game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); break; case Input.KEY_F12: @@ -162,12 +165,15 @@ public class GameRanking extends BasicGameState { OsuFile osu = Game.getOsuFile(); Display.setTitle(String.format("%s - %s", game.getTitle(), osu.toString())); Game.setRestart(Game.RESTART_MANUAL); + SoundController.playSound(SoundController.SOUND_MENUHIT); game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); - } else if (exitButton.contains(x, y)) + } else if (exitButton.contains(x, y)) { + SoundController.playSound(SoundController.SOUND_MENUBACK); game.enterState(Opsu.STATE_MAINMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); - else if (Options.getBackButton().contains(x, y)) { - MusicController.stop(); + } else if (Options.getBackButton().contains(x, y)) { + MusicController.pause(); MusicController.playAt(Game.getOsuFile().previewTime, true); + SoundController.playSound(SoundController.SOUND_MENUBACK); game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); } } @@ -176,5 +182,6 @@ public class GameRanking extends BasicGameState { public void enter(GameContainer container, StateBasedGame game) throws SlickException { Display.setTitle(game.getTitle()); + SoundController.playSound(SoundController.SOUND_APPLAUSE); } } diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index 1268950e..30bd7600 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -22,6 +22,7 @@ import itdelatrisu.opsu.GUIMenuButton; import itdelatrisu.opsu.MusicController; import itdelatrisu.opsu.Opsu; import itdelatrisu.opsu.OsuGroupNode; +import itdelatrisu.opsu.SoundController; import java.text.SimpleDateFormat; import java.util.Date; @@ -287,17 +288,22 @@ public class MainMenu extends BasicGameState { logoTimer = 0; playButton.getImage().setAlpha(0f); exitButton.getImage().setAlpha(0f); + SoundController.playSound(SoundController.SOUND_MENUHIT); } } // other button actions (if visible) else if (logoClicked) { - if (logo.contains(x, y)) + if (logo.contains(x, y)) { + SoundController.playSound(SoundController.SOUND_MENUHIT); logoTimer = MOVE_DELAY; - else if (playButton.contains(x, y)) + } else if (playButton.contains(x, y)) { + SoundController.playSound(SoundController.SOUND_MENUHIT); game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); - else if (exitButton.contains(x, y)) + } else if (exitButton.contains(x, y)) { + SoundController.playSound(SoundController.SOUND_MENUHIT); game.enterState(Opsu.STATE_MAINMENUEXIT); + } } } diff --git a/src/itdelatrisu/opsu/states/Options.java b/src/itdelatrisu/opsu/states/Options.java index 135bb7e2..37d67a9c 100644 --- a/src/itdelatrisu/opsu/states/Options.java +++ b/src/itdelatrisu/opsu/states/Options.java @@ -20,6 +20,7 @@ package itdelatrisu.opsu.states; import itdelatrisu.opsu.GUIMenuButton; import itdelatrisu.opsu.Opsu; +import itdelatrisu.opsu.SoundController; import java.awt.Font; import java.io.BufferedReader; @@ -148,12 +149,13 @@ public class Options extends BasicGameState { // OPTIONS_FULLSCREEN = , OPTIONS_TARGET_FPS = 1, OPTIONS_MUSIC_VOLUME = 2, - OPTIONS_MUSIC_OFFSET = 3, - OPTIONS_SCREENSHOT_FORMAT = 4, - OPTIONS_DISPLAY_FPS = 5, - OPTIONS_HIT_LIGHTING = 6, - OPTIONS_COMBO_BURSTS = 7, - OPTIONS_MAX = 8; // not an option + OPTIONS_EFFECT_VOLUME = 3, + OPTIONS_MUSIC_OFFSET = 4, + OPTIONS_SCREENSHOT_FORMAT = 5, + OPTIONS_DISPLAY_FPS = 6, + OPTIONS_HIT_LIGHTING = 7, + OPTIONS_COMBO_BURSTS = 8, + OPTIONS_MAX = 9; // not an option /** * Screen resolutions. @@ -208,7 +210,12 @@ public class Options extends BasicGameState { /** * Default music volume. */ - private static int musicVolume = 20; + private static int musicVolume = 30; + + /** + * Default sound effect volume. + */ + private static int effectVolume = 20; /** * Offset time, in milliseconds, for music position-related elements. @@ -258,8 +265,8 @@ public class Options extends BasicGameState { Options.game = game; this.input = container.getInput(); - // game settings; - container.setTargetFrameRate(60); + // game settings + container.setTargetFrameRate(targetFPS[targetFPSindex]); container.setMouseCursor("cursor.png", 16, 16); container.setMusicVolume(getMusicVolume()); container.setShowFPS(false); @@ -375,6 +382,10 @@ public class Options extends BasicGameState { String.format("%d%%", musicVolume), "Global music volume." ); + this.drawOption(g, OPTIONS_EFFECT_VOLUME, "Effect Volume", + String.format("%d%%", effectVolume), + "Sound effect volume." + ); this.drawOption(g, OPTIONS_MUSIC_OFFSET, "Music Offset", String.format("%dms", musicOffset), "Adjust this value if hit objects are out of sync." @@ -509,6 +520,14 @@ public class Options extends BasicGameState { container.setMusicVolume(getMusicVolume()); return; } + if (isOptionClicked(OPTIONS_EFFECT_VOLUME, oldy)) { + effectVolume += diff; + if (effectVolume < 0) + effectVolume = 0; + else if (effectVolume > 100) + effectVolume = 100; + return; + } if (isOptionClicked(OPTIONS_MUSIC_OFFSET, oldy)) { musicOffset += diff; if (musicOffset < -500) @@ -601,6 +620,12 @@ public class Options extends BasicGameState { */ public static float getMusicVolume() { return musicVolume / 100f; } + /** + * Returns the default sound effect volume. + * @return the sound volume [0, 1] + */ + public static float getEffectVolume() { return effectVolume / 100f; } + /** * Returns the music offset time. * @return the offset (in milliseconds) @@ -639,6 +664,8 @@ public class Options extends BasicGameState { SimpleDateFormat date = new SimpleDateFormat("yyyyMMdd_HHmmss"); String file = date.format(new Date()); + SoundController.playSound(SoundController.SOUND_SHUTTER); + // copy the screen Image screen = new Image(container.getWidth(), container.getHeight()); container.getGraphics().copyArea(screen, 0, 0); @@ -760,6 +787,11 @@ public class Options extends BasicGameState { if (i >= 0 && i <= 100) musicVolume = i; break; + case "VolumeEffect": + i = Integer.parseInt(value); + if (i >= 0 && i <= 100) + effectVolume = i; + break; case "Offset": i = Integer.parseInt(value); if (i >= -500 && i <= 500) @@ -827,6 +859,8 @@ public class Options extends BasicGameState { writer.newLine(); writer.write(String.format("VolumeMusic = %d", musicVolume)); writer.newLine(); + writer.write(String.format("VolumeEffect = %d", effectVolume)); + writer.newLine(); writer.write(String.format("Offset = %d", musicOffset)); writer.newLine(); writer.write(String.format("ScreenshotFormat = %d", screenshotFormatIndex)); diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index a8db88df..271473e8 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -25,6 +25,7 @@ import itdelatrisu.opsu.OsuFile; import itdelatrisu.opsu.OsuGroupList; import itdelatrisu.opsu.OsuGroupNode; import itdelatrisu.opsu.OsuParser; +import itdelatrisu.opsu.SoundController; import org.lwjgl.opengl.Display; import org.newdawn.slick.Color; @@ -343,6 +344,7 @@ public class SongMenu extends BasicGameState { // back if (Options.getBackButton().contains(x, y)) { + SoundController.playSound(SoundController.SOUND_MENUBACK); game.enterState(Opsu.STATE_MAINMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); return; } @@ -422,8 +424,10 @@ public class SongMenu extends BasicGameState { if (!search.getText().isEmpty()) { search.setText(""); searchTimer = SEARCH_DELAY; - } else + } else { + SoundController.playSound(SoundController.SOUND_MENUBACK); game.enterState(Opsu.STATE_MAINMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black)); + } break; case Input.KEY_F1: game.enterState(Opsu.STATE_OPTIONS, new EmptyTransition(), new FadeInTransition(Color.black)); @@ -592,8 +596,10 @@ public class SongMenu extends BasicGameState { if (MusicController.isConverting()) return; + SoundController.playSound(SoundController.SOUND_MENUHIT); Display.setTitle(String.format("%s - %s", game.getTitle(), osu.toString())); OsuParser.parseHitObjects(osu); + SoundController.setSampleSet(osu.sampleSet); Game.setOsuFile(osu); Game.setRestart(Game.RESTART_NEW); game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));