diff --git a/input/events.d b/input/events.d index 8953f2735..60077de78 100644 --- a/input/events.d +++ b/input/events.d @@ -81,15 +81,15 @@ void toggleBattle() if(battle) { writefln("Changing to normal music"); - jukebox.resumeMusic(); - battleMusic.pauseMusic(); + jukebox.resume(); + battleMusic.pause(); battle=false; } else { writefln("Changing to battle music"); - jukebox.pauseMusic(); - battleMusic.resumeMusic(); + jukebox.pause(); + battleMusic.resume(); battle=true; } } @@ -298,8 +298,8 @@ extern(C) int d_frameStarted(float time) musCumTime += time; if(musCumTime > musRefresh) { - jukebox.addTime(musRefresh); - battleMusic.addTime(musRefresh); + jukebox.updateBuffers(); + battleMusic.updateBuffers(); musCumTime -= musRefresh; } diff --git a/monster/vm/arrays.d b/monster/vm/arrays.d index c5ea4da96..ff18656bd 100644 --- a/monster/vm/arrays.d +++ b/monster/vm/arrays.d @@ -30,6 +30,7 @@ import monster.vm.error; import std.string; import std.uni; import std.stdio; +import std.utf; // An index to an array. Array indices may be 0, unlike object indices // which span from 1 and upwards, and has 0 as the illegal 'null' @@ -152,6 +153,9 @@ struct Arrays alias createT!(dchar) create; alias createT!(AIndex) create; + ArrayRef *create(char[] arg) + { return create(toUTF32(arg)); } + // Generic element size ArrayRef *create(int[] data, int size) { diff --git a/monster/vm/stack.d b/monster/vm/stack.d index 3a83bdb87..61ea3ee98 100644 --- a/monster/vm/stack.d +++ b/monster/vm/stack.d @@ -99,7 +99,7 @@ struct CodeStack fleft = 0; return; } - fleft = left + (frm-pos); + fleft = left + (pos-frm); assert(fleft >= 0 && fleft <= total); } diff --git a/mscripts/jukebox.mn b/mscripts/jukebox.mn new file mode 100644 index 000000000..622cfc9d4 --- /dev/null +++ b/mscripts/jukebox.mn @@ -0,0 +1,212 @@ +/* + OpenMW - The completely unofficial reimplementation of Morrowind + Copyright (C) 2008 Nicolay Korslund + Email: < korslund@gmail.com > + WWW: http://openmw.snaptoad.com/ + + This file (jukebox.mn) is part of the OpenMW package. + + OpenMW is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program 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 + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +/* + A simple jukebox with a playlist. It can play, stop, pause with + fade in and fade out, and adjust volume. +*/ + +class Jukebox : Object; + +// Between 0 (off) and 1 (full volume) +float fadeLevel = 0.0; + +// How much to fade in and out each second +float fadeInRate = 0.10; +float fadeOutRate = 0.25; + +// Time between each fade step +float fadeInterval = 0.2; + +// List of sounds to play +char[][] playlist; + +// Index of current song +int index; + +// The music volume, set by the user. Does NOT change to adjust for +// fading, etc. TODO: This should be stored in a configuration class, +// not here. +float musVolume; + +bool isPlaying; // Is a song currently playing? + +// TODO: Make "isPaused" instead, makes more sense +bool hasSong; // Is a song currently selected (playing or paused) + +// TODO: Move to Object for now +native int randInt(int a, int b); + +// Native functions to control music +native setSound(char[] filename); +native setVolume(float f); +native playSound(); +native stopSound(); +idle waitUntilFinished(); + +// Fade out and then stop the music. TODO: Rename these to resume() +// etc and use super.resume, when this is possible. +pause() { state = fadeOut; } +resume() +{ + if(!hasSong) next(); + else playSound(); + + state = fadeIn; +} + +// Stop the current song. Calling resume again after this is called +// will start a new song. +stop() +{ + stopSound(); + + hasSong = false; + isPlaying = false; + fadeLevel = 0.0; + state = null; +} + +play() +{ + if(index >= playlist.length) + return; + + setSound(playlist[index]); + playSound(); + + isPlaying = true; + hasSong = true; +} + +// Play the next song in the playlist +next() +{ + if(isPlaying) + stop(); + + // Find the index of the next song, if any + if(playlist.length == 0) return; + + if(++index >= playlist.length) + { + index = 0; + randomize(); + } + + play(); +} + +// Set the new music volume setting. TODO: This should be read from a +// config object instead. +updateVolume(float vol) +{ + musVolume = vol; + if(isPlaying) + setVolume(musVolume*fadeLevel); +} + +setPlaylist(char[][] lst) +{ + playlist = lst; + randomize(); +} + +// Randomize playlist. +randomize() +{ + if(playlist.length < 2) return; + + foreach(int i, char[] s; playlist) + { + // Index to switch with + int idx = randInt(i,playlist.length-1); + + // To avoid playing the same song twice in a row, don't set the + // first song to the previous last. + if(i == 0 && idx == playlist.length-1) + idx--; + + if(idx == i) // Skip if swapping with self + continue; + + playlist[i] = playlist[idx]; + playlist[idx] = s; + } +} + +// Fade in +state fadeIn +{ + begin: + + setVolume(musVolume*fadeLevel); + + sleep(fadeInterval); + + fadeLevel += fadeInterval*fadeInRate; + + if(fadeLevel >= 1.0) + { + fadeLevel = 1.0; + setVolume(musVolume); + state = playing; + } + goto begin; +} + +// Fade out +state fadeOut +{ + begin: + + sleep(fadeInterval); + + fadeLevel -= fadeInterval*fadeOutRate; + + if(fadeLevel <= 0.0) + { + fadeLevel = 0.0; + + stopSound(); + isPlaying = false; + + state = null; + } + + setVolume(musVolume*fadeLevel); + goto begin; +} + +state playing +{ + begin: + // Wait for the song to play. Will return imediately if the song has + // already stopped or if no song is playing + waitUntilFinished(); + + // Start playing the next song + next(); + + goto begin; +} diff --git a/mscripts/object.d b/mscripts/object.d index 6a5e91ef1..a5defcb15 100644 --- a/mscripts/object.d +++ b/mscripts/object.d @@ -1,3 +1,26 @@ +/* + OpenMW - The completely unofficial reimplementation of Morrowind + Copyright (C) 2008 Nicolay Korslund + Email: < korslund@gmail.com > + WWW: http://openmw.snaptoad.com/ + + This file (object.d) is part of the OpenMW package. + + OpenMW is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program 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 + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + module mscripts.object; import monster.monster; diff --git a/mscripts/object.mn b/mscripts/object.mn index ed18a84bb..79aec0475 100644 --- a/mscripts/object.mn +++ b/mscripts/object.mn @@ -1,3 +1,26 @@ +/* + OpenMW - The completely unofficial reimplementation of Morrowind + Copyright (C) 2008 Nicolay Korslund + Email: < korslund@gmail.com > + WWW: http://openmw.snaptoad.com/ + + This file (object.mn) is part of the OpenMW package. + + OpenMW is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program 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 + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + // This is the base class of all OpenMW Monster classes. class Object; diff --git a/openmw.d b/openmw.d index 2973ca697..71137a497 100644 --- a/openmw.d +++ b/openmw.d @@ -347,7 +347,7 @@ void main(char[][] args) initializeInput(); // Start swangin' - if(!noSound) jukebox.enableMusic(); + if(!noSound) jukebox.play(); // Run it until the user tells us to quit startRendering(); diff --git a/sound/audio.d b/sound/audio.d index 7d34f0a5b..4b3544081 100644 --- a/sound/audio.d +++ b/sound/audio.d @@ -26,8 +26,6 @@ module sound.audio; public import sound.sfx; public import sound.music; -import monster.monster; - import sound.al; import sound.alc; @@ -56,14 +54,16 @@ void initializeSound() alcMakeContextCurrent(Context); + MusicManager.sinit(); + jukebox.initialize("Main"); battleMusic.initialize("Battle"); } void shutdownSound() { - jukebox.disableMusic(); - battleMusic.disableMusic(); + jukebox.shutdown(); + battleMusic.shutdown(); alcMakeContextCurrent(null); if(Context) alcDestroyContext(Context); @@ -72,14 +72,14 @@ void shutdownSound() Device = null; } -bool checkALError(char[] what = "") +void checkALError(char[] what = "") { - ALenum err = alGetError(); - if(what.length) what = " while " ~ what; - if(err != AL_NO_ERROR) - writefln("WARNING: OpenAL error%s: (%x) %s", what, err, - toString(alGetString(err))); - - - return err != AL_NO_ERROR; + ALenum err = alGetError(); + if(what.length) what = " while " ~ what; + if(err != AL_NO_ERROR) + throw new Exception(format("OpenAL error%s: (%x) %s", what, err, + toString(alGetString(err)))); } + +bool noALError() +{ return alGetError() == AL_NO_ERROR; } diff --git a/sound/music.d b/sound/music.d index 0975b4000..8d0a0ae60 100644 --- a/sound/music.d +++ b/sound/music.d @@ -27,27 +27,36 @@ import sound.avcodec; import sound.audio; import sound.al; +import monster.monster; + import std.stdio; import std.string; import core.config; import core.resource; +class Idle_waitUntilFinished : IdleFunction +{ + override: + bool initiate(MonsterObject *mo) { return true; } + + bool hasFinished(MonsterObject *mo) + { + MusicManager *mgr = cast(MusicManager*)mo.extra; + + // Return when the music is no longer playing + return !mgr.isPlaying(); + } +} + // Simple music player, has a playlist and can pause/resume music. struct MusicManager { private: - // How much to add to the volume each second when fading - const float fadeInRate = 0.10; - const float fadeOutRate = 0.20; - // Maximum buffer length, divided up among OpenAL buffers const uint bufLength = 128*1024; - // Volume - ALfloat volume, maxVolume; - char[] name; void fail(char[] msg) @@ -55,115 +64,122 @@ struct MusicManager throw new SoundException(name ~ " Jukebox", msg); } - // List of songs to play - char[][] playlist; - int index; // Index of next song to play - - bool musicOn; - ALuint sID; - ALuint bIDs[4]; + ALuint sID; // Sound id + ALuint bIDs[4]; // Buffers ALenum bufFormat; ALint bufRate; AVFile fileHandle; AVAudio audioHandle; - ubyte[] outData; - // Which direction are we currently fading, if any - enum Fade { None = 0, In, Out } - Fade fading; + static ubyte[] outData; + + // The Jukebox class + static MonsterClass mc; + + // The jukebox Monster object + MonsterObject *mo; public: + static MusicManager *get() + { return cast(MusicManager*)params.obj().extra; } + + static void sinit() + { + assert(mc is null); + mc = new MonsterClass("Jukebox", "jukebox.mn"); + mc.bind("randInt", + { stack.pushInt(rnd.randInt + (stack.popInt,stack.popInt));}); + mc.bind("waitUntilFinished", + new Idle_waitUntilFinished); + + mc.bind("setSound", { get().setSound(); }); + mc.bind("setVolume", { get().setVolume(); }); + mc.bind("playSound", { get().playSound(); }); + mc.bind("stopSound", { get().stopSound(); }); + + outData.length = bufLength / bIDs.length; + } + // Initialize the jukebox void initialize(char[] name) { this.name = name; sID = 0; - foreach(ref b; bIDs) b = 0; - outData.length = bufLength / bIDs.length; + bIDs[] = 0; fileHandle = null; - musicOn = false; + + mo = mc.createObject(); + mo.extra = this; } - // Get the new volume setting. + // Called whenever the volume configuration values are changed by + // the user. void updateVolume() { - maxVolume = config.calcMusicVolume(); - - if(!musicOn) return; - - // Adjust volume up to new setting, unless we are in the middle of - // a fade. Even if we are fading, though, the volume should never - // be over the max. - if(fading == Fade.None || volume > maxVolume) volume = maxVolume; - if(sID) - alSourcef(sID, AL_GAIN, volume); + stack.pushFloat(config.calcMusicVolume()); + mo.call("updateVolume"); } // Give a music play list void setPlaylist(char[][] pl) { - playlist = pl; - index = 0; + AIndex arr[]; + arr.length = pl.length; - randomize(); - } + // Create the array indices for each element string + foreach(i, ref elm; arr) + elm = arrays.create(pl[i]).getIndex(); - // Randomize playlist. If the argument is true, then we don't want - // the old last to be the new first. - private void randomize(bool checklast = false) - { - if(playlist.length < 2) return; + // Push the final array + stack.pushArray(arr); - // Get the index of the last song played - int lastidx = ((index==0) ? (playlist.length-1) : (index-1)); + mo.call("setPlaylist"); + } - foreach(int i, char[] s; playlist) - { - int idx = rnd.randInt(i,playlist.length-1); + // Pause current track + void pause() { mo.call("pause"); } - // Don't put the last idx as the first entry - if(i == 0 && checklast && lastidx == idx) - { - idx++; - if(idx == playlist.length) - idx = i; - } - if(idx == i) /* skip if swapping with self */ - continue; - playlist[i] = playlist[idx]; - playlist[idx] = s; - } + // Resume. Starts playing sound, with fade in + void resume() + { + if(!config.useMusic) return; + mo.call("resume"); } - // Skip to the next track - void playNext() + void play() { - // If music is disabled, do nothing - if(!musicOn) return; + if(!config.useMusic) return; + mo.call("play"); + } - // No tracks to play? - if(!playlist.length) return; + void setSound() + { + char[] fname = stack.popString8(); // Generate a source to play back with if needed if(!sID) { alGenSources(1, &sID); - if(checkALError()) - fail("Couldn't generate music sources"); + checkALError("generating buffers"); + + // Set listner relative coordinates (sound follows the player) alSourcei(sID, AL_SOURCE_RELATIVE, AL_TRUE); + alGenBuffers(bIDs.length, bIDs.ptr); + updateVolume(); } else { - // Kill current track + // Kill current track, but keep the sID source. alSourceStop(sID); alSourcei(sID, AL_BUFFER, 0); - alDeleteBuffers(bIDs.length, bIDs.ptr); - bIDs[] = 0; + //alDeleteBuffers(bIDs.length, bIDs.ptr); + //bIDs[] = 0; checkALError("killing current track"); } @@ -171,57 +187,30 @@ struct MusicManager fileHandle = null; audioHandle = null; - // End of list? Randomize and start over - if(index == playlist.length) - { - randomize(true); - index = 0; - } - - alGenBuffers(bIDs.length, bIDs.ptr); + //alGenBuffers(bIDs.length, bIDs.ptr); // If something fails, clean everything up. - scope(failure) - { - // This block is only executed if an exception is thrown. - - if(fileHandle) avc_closeAVFile(fileHandle); - - fileHandle = null; - audioHandle = null; - - alSourceStop(sID); - alDeleteSources(1, &sID); - alDeleteBuffers(bIDs.length, bIDs.ptr); - - checkALError("cleaning up after music failure"); + scope(failure) shutdown(); - sID = 0; - bIDs[] = 0; - - // Try the next track if playNext is called again - index++; - - // The function exits here. - } - - if(checkALError()) - fail("Couldn't generate buffers"); - - fileHandle = avc_openAVFile(toStringz(playlist[index])); + fileHandle = avc_openAVFile(toStringz(fname)); if(!fileHandle) - fail("Unable to open " ~ playlist[index]); + fail("Unable to open " ~ fname); audioHandle = avc_getAVAudioStream(fileHandle, 0); if(!audioHandle) - fail("Unable to load music track " ~ playlist[index]); + fail("Unable to load music track " ~ fname); - int ch, bits, rate; + int rate, ch, bits; if(avc_getAVAudioInfo(audioHandle, &rate, &ch, &bits) != 0) - fail("Unable to get info for music track " ~ playlist[index]); + fail("Unable to get info for music track " ~ fname); + + // Translate format from avformat to OpenAL bufRate = rate; bufFormat = 0; + + // TODO: These don't really fail gracefully for 4 and 6 channels + // if these aren't supported. if(bits == 8) { if(ch == 1) bufFormat = AL_FORMAT_MONO8; @@ -244,16 +233,17 @@ struct MusicManager } if(bufFormat == 0) - fail(format("Unhandled format (%d channels, %d bits) for music track %s", ch, bits, playlist[index])); + fail(format("Unhandled format (%d channels, %d bits) for music track %s", ch, bits, fname)); + // Fill the buffers foreach(int i, ref b; bIDs) { int length = avc_getAVAudioData(audioHandle, outData.ptr, outData.length); if(length) alBufferData(b, bufFormat, outData.ptr, length, bufRate); - if(length == 0 || checkALError()) + if(length == 0 || !noALError()) { if(i == 0) - fail("No audio data in music track " ~ playlist[index]); + fail("No audio data in music track " ~ fname); alDeleteBuffers(bIDs.length-i, bIDs.ptr+i); checkALError("running alDeleteBuffers"); @@ -262,144 +252,88 @@ struct MusicManager } } + // Associate the buffers with the sound id alSourceQueueBuffers(sID, bIDs.length, bIDs.ptr); - alSourcePlay(sID); - if(checkALError()) - fail("Unable to start music track " ~ playlist[index]); - - index++; - return; } - // Start playing the jukebox - void enableMusic() + void setVolume() { - if(!config.useMusic) return; + float volume = stack.popFloat(); - musicOn = true; - fading = Fade.None; - playNext(); + // Set the new volume + if(sID) alSourcef(sID, AL_GAIN, volume); } - // Disable music - void disableMusic() + void playSound() { - if(fileHandle) avc_closeAVFile(fileHandle); - fileHandle = null; - audioHandle = null; - - if(sID) - { - alSourceStop(sID); - alDeleteSources(1, &sID); - checkALError("disabling music"); - sID = 0; - } + if(!sID || !config.useMusic) + return; - alDeleteBuffers(bIDs.length, bIDs.ptr); - checkALError("deleting music buffers"); - bIDs[] = 0; - - musicOn = false; + alSourcePlay(sID); + checkALError("starting music"); } - // Pause current track - void pauseMusic() + void stopSound() { - fading = Fade.Out; + // How to stop / pause music + if(sID) alSourcePause(sID); } - // Resume. Can also be called in place of enableMusic for fading in. - void resumeMusic() + bool isPlaying() { - if(!config.useMusic) return; + ALint state; + alGetSourcei(sID, AL_SOURCE_STATE, &state); - volume = 0.0; - fading = Fade.In; - musicOn = true; - if(sID) addTime(0); - else playNext(); + return state == AL_PLAYING; } - // Checks if a stream is playing, filling more data as needed, and restarting - // if it stalled or was paused. - private bool isPlaying() + void updateBuffers() { - if(!sID) return false; + if(!sID || !isPlaying) + return; + // Get the number of processed buffers ALint count; alGetSourcei(sID, AL_BUFFERS_PROCESSED, &count); - if(checkALError("in isPlaying()")) return false; + + checkALError(); for(int i = 0;i < count;i++) { int length = avc_getAVAudioData(audioHandle, outData.ptr, outData.length); if(length <= 0) - { - if(i == 0) - { - ALint state; - alGetSourcei(sID, AL_SOURCE_STATE, &state); - if(checkALError() || state == AL_STOPPED) - return false; - } - break; - } + break; ALuint bid; alSourceUnqueueBuffers(sID, 1, &bid); - if(checkALError() == AL_NO_ERROR) + if(noALError()) { alBufferData(bid, bufFormat, outData.ptr, length, bufRate); alSourceQueueBuffers(sID, 1, &bid); checkALError(); } } - - ALint state = AL_PLAYING; - alGetSourcei(sID, AL_SOURCE_STATE, &state); - if(state != AL_PLAYING) alSourcePlay(sID); - return (checkALError() == AL_NO_ERROR); } - // Check if the music has died. This function is also used for fading. - void addTime(float time) + // Disable music + void shutdown() { - if(!musicOn) return; + mo.call("stop"); - if(!isPlaying()) playNext(); + if(fileHandle) avc_closeAVFile(fileHandle); + fileHandle = null; + audioHandle = null; - if(fading) + if(sID) { - // Fade the volume - if(fading == Fade.In) - { - volume += fadeInRate * time; - if(volume >= maxVolume) - { - fading = Fade.None; - volume = maxVolume; - } - } - else - { - assert(fading == Fade.Out); - volume -= fadeOutRate * time; - if(volume <= 0.0) - { - fading = Fade.None; - volume = 0.0; - - // We are done fading out, disable music. Don't call - // enableMusic (or isPlaying) unless you want it to start - // again. - if(sID) alSourcePause(sID); - musicOn = false; - } - } - - // Set the new volume - if(sID) alSourcef(sID, AL_GAIN, volume); + alSourceStop(sID); + alDeleteSources(1, &sID); + checkALError("disabling music"); + sID = 0; } + + alDeleteBuffers(bIDs.length, bIDs.ptr); + checkALError(); + bIDs[] = 0; } } diff --git a/sound/sfx.d b/sound/sfx.d index b1eb2785e..1bf8a1e19 100644 --- a/sound/sfx.d +++ b/sound/sfx.d @@ -119,7 +119,7 @@ struct SoundFile { alGenBuffers(1, &bID); alBufferData(bID, fmt, outData.ptr, total, rate); - if(checkALError()) + if(!noALError()) { writefln("Unable to load sound %s", file); alDeleteBuffers(1, &bID); @@ -140,11 +140,11 @@ struct SoundFile SoundInstance si; si.owner = this; alGenSources(1, &si.inst); - if(checkALError() || !si.inst) + if(!noALError() || !si.inst) fail("Failed to instantiate sound resource"); alSourcei(si.inst, AL_BUFFER, cast(ALint)bID); - if(checkALError()) + if(!noALError()) { alDeleteSources(1, &si.inst); fail("Failed to load sound resource"); @@ -237,7 +237,7 @@ struct SoundInstance alGetSourcef(inst, AL_MAX_DISTANCE, &dist); alGetSourcefv(inst, AL_POSITION, p.ptr); alGetListenerfv(AL_POSITION, lp.ptr); - if(!checkALError("updating sound position")) + if(noALError()) { p[0] -= lp[0]; p[1] -= lp[1];