Implemented skinnable theme songs.

Plays a theme song when opsu! starts (can be disabled in options).  The default song is "welcome to osu!" by nekodex, uploaded by CyberKitsune.

To change the song, place a new "theme.osu" in the skins folder and edit the file name, metadata, and song length (at minimum).

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
Jeffrey Han 2014-12-20 18:17:04 -05:00
parent 9d3a12ad9c
commit ce0eccd32b
9 changed files with 101 additions and 7 deletions

View File

@ -26,4 +26,8 @@ The following projects were referenced in creating opsu!:
* "Wojtkosu" - Wojtek Kowaluk (https://osu.ppy.sh/forum/t/97260) * "Wojtkosu" - Wojtek Kowaluk (https://osu.ppy.sh/forum/t/97260)
* "osu-parser" - nojhamster (https://github.com/nojhamster/osu-parser) * "osu-parser" - nojhamster (https://github.com/nojhamster/osu-parser)
* "osu! web" - pictuga (https://github.com/pictuga/osu-web) * "osu! web" - pictuga (https://github.com/pictuga/osu-web)
Theme Song
----------
The theme song is "welcome to osu!" by nekodex (https://soundcloud.com/nekodex).

BIN
res/theme.ogg Normal file

Binary file not shown.

12
res/theme.osu Normal file
View File

@ -0,0 +1,12 @@
[General]
// theme song file name (mp3/ogg supported)
AudioFilename: theme.ogg
[Metadata]
// theme song title and artist
Title:welcome to osu!
Artist:nekodex
[HitObjects]
// length of theme song, in ms (third field)
1,1,48000,1,0,0:0:0:0:

View File

@ -22,6 +22,7 @@ import itdelatrisu.opsu.states.Options;
import java.io.File; import java.io.File;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.net.URL;
import java.nio.IntBuffer; import java.nio.IntBuffer;
import javazoom.jl.converter.Converter; import javazoom.jl.converter.Converter;
@ -34,6 +35,7 @@ import org.newdawn.slick.SlickException;
import org.newdawn.slick.openal.Audio; import org.newdawn.slick.openal.Audio;
import org.newdawn.slick.openal.SoundStore; import org.newdawn.slick.openal.SoundStore;
import org.newdawn.slick.util.Log; import org.newdawn.slick.util.Log;
import org.newdawn.slick.util.ResourceLoader;
/** /**
* Controller for all music. * Controller for all music.
@ -59,6 +61,11 @@ public class MusicController {
*/ */
private static Thread trackLoader; private static Thread trackLoader;
/**
* Whether the theme song is currently playing.
*/
private static boolean themePlaying = false;
// This class should not be instantiated. // This class should not be instantiated.
private MusicController() {} private MusicController() {}
@ -70,6 +77,8 @@ public class MusicController {
boolean play = (lastOsu == null || !osu.audioFilename.equals(lastOsu.audioFilename)); boolean play = (lastOsu == null || !osu.audioFilename.equals(lastOsu.audioFilename));
lastOsu = osu; lastOsu = osu;
if (play) { if (play) {
themePlaying = false;
// TODO: properly interrupt instead of using deprecated Thread.stop(); // TODO: properly interrupt instead of using deprecated Thread.stop();
// interrupt the conversion/track loading // interrupt the conversion/track loading
if (isTrackLoading()) if (isTrackLoading())
@ -248,6 +257,30 @@ public class MusicController {
return (trackExists() && player.setPosition(position / 1000f)); return (trackExists() && player.setPosition(position / 1000f));
} }
/**
* Plays the theme song.
*/
public static void playThemeSong() {
try {
URL themeURL = ResourceLoader.getResource(Options.OSU_THEME_NAME);
File themeFile = new File(themeURL.toURI());
OsuFile osu = OsuParser.parseFile(themeFile, null, false);
URL audioURL = ResourceLoader.getResource(osu.audioFilename.getName());
osu.audioFilename = new File(audioURL.toURI());
play(osu, true);
themePlaying = true;
} catch (Exception e) {
Log.error("Failed to load theme song.", e);
}
}
/**
* Returns whether or not the current track, if any, is the theme song.
*/
public static boolean isThemePlaying() {
return (themePlaying && trackExists());
}
/** /**
* Stops and releases all sources, clears each of the specified Audio * Stops and releases all sources, clears each of the specified Audio
* buffers, destroys the OpenAL context, and resets SoundStore for future use. * buffers, destroys the OpenAL context, and resets SoundStore for future use.

View File

@ -109,7 +109,7 @@ public class OsuParser {
* @param parseObjects if true, hit objects will be fully parsed now * @param parseObjects if true, hit objects will be fully parsed now
* @return the new OsuFile object * @return the new OsuFile object
*/ */
private static OsuFile parseFile(File file, ArrayList<OsuFile> osuFiles, boolean parseObjects) { public static OsuFile parseFile(File file, ArrayList<OsuFile> osuFiles, boolean parseObjects) {
OsuFile osu = new OsuFile(file); OsuFile osu = new OsuFile(file);
try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) { try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
@ -464,7 +464,9 @@ public class OsuParser {
parseHitObjects(osu); parseHitObjects(osu);
// add OsuFile to song group // add OsuFile to song group
osuFiles.add(osu); if (osuFiles != null)
osuFiles.add(osu);
return osu; return osu;
} }

View File

@ -293,11 +293,13 @@ public class MainMenu extends BasicGameState {
else if (!MusicController.isTrackLoading()) else if (!MusicController.isTrackLoading())
MusicController.resume(); MusicController.resume();
} else if (musicNext.contains(x, y)) { } else if (musicNext.contains(x, y)) {
boolean isTheme = MusicController.isThemePlaying();
SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU); SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
OsuGroupNode node = menu.setFocus(Opsu.groups.getRandomNode(), -1, true); OsuGroupNode node = menu.setFocus(Opsu.groups.getRandomNode(), -1, true);
if (node != null) if (node != null && !isTheme)
previous.add(node.index); previous.add(node.index);
if (Options.isDynamicBackgroundEnabled()) if (Options.isDynamicBackgroundEnabled() &&
MusicController.getOsuFile() != null && !MusicController.isThemePlaying())
bgAlpha = 0f; bgAlpha = 0f;
} else if (musicPrevious.contains(x, y)) { } else if (musicPrevious.contains(x, y)) {
if (!previous.isEmpty()) { if (!previous.isEmpty()) {

View File

@ -83,6 +83,11 @@ public class Options extends BasicGameState {
*/ */
public static final String FONT_NAME = "kochi-gothic.ttf"; public static final String FONT_NAME = "kochi-gothic.ttf";
/**
* The theme song OsuFile file name.
*/
public static final String OSU_THEME_NAME = "theme.osu";
/** /**
* The beatmap directory. * The beatmap directory.
*/ */
@ -134,7 +139,8 @@ public class Options extends BasicGameState {
DISABLE_SOUNDS, DISABLE_SOUNDS,
KEY_LEFT, KEY_LEFT,
KEY_RIGHT, KEY_RIGHT,
SHOW_UNICODE; SHOW_UNICODE,
ENABLE_THEME_SONG;
}; };
/** /**
@ -190,7 +196,8 @@ public class Options extends BasicGameState {
GameOption.EFFECT_VOLUME, GameOption.EFFECT_VOLUME,
GameOption.HITSOUND_VOLUME, GameOption.HITSOUND_VOLUME,
GameOption.MUSIC_OFFSET, GameOption.MUSIC_OFFSET,
GameOption.DISABLE_SOUNDS GameOption.DISABLE_SOUNDS,
GameOption.ENABLE_THEME_SONG
}; };
/** /**
@ -344,6 +351,11 @@ public class Options extends BasicGameState {
*/ */
private static boolean ignoreBeatmapSkins = false; private static boolean ignoreBeatmapSkins = false;
/**
* Whether or not to play the theme song.
*/
private static boolean themeSongEnabled = true;
/** /**
* Fixed difficulty overrides. * Fixed difficulty overrides.
*/ */
@ -631,6 +643,9 @@ public class Options extends BasicGameState {
} }
} }
break; break;
case ENABLE_THEME_SONG:
themeSongEnabled = !themeSongEnabled;
break;
default: default:
break; break;
} }
@ -938,6 +953,12 @@ public class Options extends BasicGameState {
"Press CTRL+L while playing to load a checkpoint, and CTRL+S to set one." "Press CTRL+L while playing to load a checkpoint, and CTRL+S to set one."
); );
break; break;
case ENABLE_THEME_SONG:
drawOption(pos, "Enable Theme Song",
themeSongEnabled ? "Yes" : "No",
"Whether to play the theme song upon starting opsu!"
);
break;
default: default:
break; break;
} }
@ -1168,6 +1189,12 @@ public class Options extends BasicGameState {
*/ */
public static boolean useUnicodeMetadata() { return showUnicode; } public static boolean useUnicodeMetadata() { return showUnicode; }
/**
* Returns whether or not to play the theme song.
* @return true if enabled
*/
public static boolean isThemSongEnabled() { return themeSongEnabled; }
/** /**
* Sets the track checkpoint time, if within bounds. * Sets the track checkpoint time, if within bounds.
* @param time the track position (in ms) * @param time the track position (in ms)
@ -1410,6 +1437,9 @@ public class Options extends BasicGameState {
case "Checkpoint": case "Checkpoint":
setCheckpoint(Integer.parseInt(value)); setCheckpoint(Integer.parseInt(value));
break; break;
case "MenuMusic":
themeSongEnabled = Boolean.parseBoolean(value);
break;
} }
} }
} catch (IOException e) { } catch (IOException e) {
@ -1501,6 +1531,8 @@ public class Options extends BasicGameState {
writer.newLine(); writer.newLine();
writer.write(String.format("Checkpoint = %d", checkpoint)); writer.write(String.format("Checkpoint = %d", checkpoint));
writer.newLine(); writer.newLine();
writer.write(String.format("MenuMusic = %b", themeSongEnabled));
writer.newLine();
writer.close(); writer.close();
} catch (IOException e) { } catch (IOException e) {
Log.error(String.format("Failed to write to file '%s'.", OPTIONS_FILE.getAbsolutePath()), e); Log.error(String.format("Failed to write to file '%s'.", OPTIONS_FILE.getAbsolutePath()), e);

View File

@ -501,6 +501,10 @@ public class SongMenu extends BasicGameState {
public void enter(GameContainer container, StateBasedGame game) public void enter(GameContainer container, StateBasedGame game)
throws SlickException { throws SlickException {
Display.setTitle(game.getTitle()); Display.setTitle(game.getTitle());
// stop playing the theme song
if (MusicController.isThemePlaying() && focusNode != null)
MusicController.play(focusNode.osuFiles.get(focusNode.osuFileIndex), true);
} }
@Override @Override

View File

@ -18,6 +18,7 @@
package itdelatrisu.opsu.states; package itdelatrisu.opsu.states;
import itdelatrisu.opsu.MusicController;
import itdelatrisu.opsu.Opsu; import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.OsuParser; import itdelatrisu.opsu.OsuParser;
import itdelatrisu.opsu.OszUnpacker; import itdelatrisu.opsu.OszUnpacker;
@ -140,6 +141,10 @@ public class Splash extends BasicGameState {
Opsu.groups.init(); Opsu.groups.init();
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).setFocus(Opsu.groups.getRandomNode(), -1, true); ((SongMenu) game.getState(Opsu.STATE_SONGMENU)).setFocus(Opsu.groups.getRandomNode(), -1, true);
// play the theme song
if (Options.isThemSongEnabled())
MusicController.playThemeSong();
game.enterState(Opsu.STATE_MAINMENU); game.enterState(Opsu.STATE_MAINMENU);
} }
} }