From e31c1c84d9cb4bf5ed00d0eac5cbd8b67e47878e Mon Sep 17 00:00:00 2001 From: ZyeByte <102230672+zyebytevt@users.noreply.github.com> Date: Tue, 24 Jan 2023 15:45:22 +0100 Subject: [PATCH] Add more audio features and versioning system (#8) * Improve audio system - Add loop points - Add volume and pitch control - Move `AudioDecoder` functionality into AudioSource - Make audio buffer size and count customizable * Fix documentation for `Application` class * Improve crash messages to incent bug reports * Add versioning system * Remove SemVer dependancy, fix .zwversion bug when out/ doesn't exist * Small changes - Null out deferred functions - Change Asset mangle to FQN - Make AudioThread synchronized * Fix versioning system Keeping it the way it was seemed to cause a recompilation of the engine every time. This is now fixed. * Fix LDC crashes, with caveats - AudioThread code is run on main thread currently - AudioStream is instantiated on heap for no apparent reason * Fix accidental commenting in dub.sdl * Change pointer to value-type again * Audio bug fix - Add state property to AudioSource - Made AudioThread an actual thread again - Disable automatic GC collection again - Change Timer deregistration to swap deletion --- .gitignore | 3 +- VERSIONING.md | 28 ++++ dub.sdl | 15 +- platform/openal/buffer.d | 108 ++++++------ platform/openal/source.d | 226 +++++++++++++------------- res/core-package/textures/zyebyte.png | Bin 25804 -> 0 bytes source/zyeware/audio/buffer.di | 41 ++--- source/zyeware/audio/bus.d | 4 +- source/zyeware/audio/decoder.d | 104 ------------ source/zyeware/audio/package.d | 1 - source/zyeware/audio/properties.d | 18 +- source/zyeware/audio/source.di | 58 ++----- source/zyeware/audio/thread.d | 58 +++++-- source/zyeware/core/application.d | 8 +- source/zyeware/core/asset.d | 45 +++-- source/zyeware/core/crash.d | 10 +- source/zyeware/core/engine.d | 26 ++- source/zyeware/core/startupapp.d | 12 +- source/zyeware/core/timer.d | 12 +- source/zyeware/utils/collection.d | 2 +- 20 files changed, 368 insertions(+), 411 deletions(-) create mode 100644 VERSIONING.md delete mode 100644 res/core-package/textures/zyebyte.png delete mode 100644 source/zyeware/audio/decoder.d diff --git a/.gitignore b/.gitignore index 76b5326..aaeaed5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ bin/ *.a .vscode dub.selections.json -core.zpk \ No newline at end of file +out/* +.zwversion \ No newline at end of file diff --git a/VERSIONING.md b/VERSIONING.md new file mode 100644 index 0000000..f510cd6 --- /dev/null +++ b/VERSIONING.md @@ -0,0 +1,28 @@ +# ZyeWare versioning system + +In general, we'll use a versioning system similar to [Semantic versioning](https://semver.org/): + +MAJOR.MINOR.PATCH, which increment: +1. MAJOR version when we make incompatible public API changes +2. MINOR version when we add functionality in a backwards compatible manner +3. PATCH version when we make backwards compatible bug fixes + +(Public API in this case is every piece of code that is reachable by the client application, in the case of D when the `public` modifier is applied) + +## Version string + +A small "v" should be prepended in front of the actual version number. + +If appropriate, a pre-release version name is appended with a hyphen (-), usually "alpha", "beta" and "rc". + +Which would result in this hypothetical version string: +`v0.1.2-alpha` + +Major version zero (0.y.z) is, as defined by SemVer, used for initial development and incompatible changes may happen anytime. + +With the introduction of this versioning schema, we'll arbitrarily start at 0.3.0, because 3 is +a nice number. + +## Querying the version number + +It should always be possible to get the version number from `ZyeWare.engineVersion` \ No newline at end of file diff --git a/dub.sdl b/dub.sdl index cee2fcb..c8322c8 100644 --- a/dub.sdl +++ b/dub.sdl @@ -3,26 +3,23 @@ description "Simple, general purpose 2D and 3D game engine." authors "ZyeByte" copyright "Copyright © 2022, ZyeByte" license "LGPL-3.0" -dependency "inmath" version="~>1.0.5" dependency "terminal" version="~>1.0.0" dependency "imagefmt" version="~>2.1.1" dependency "sdlang-d" version="~>0.10.6" -dependency "bmfont" version="~>0.2.0" dependency "audio-formats" version="~>2.0.2" +dependency "bmfont" version="~>0.2.0" +dependency "inmath" version="~>1.0.5" targetType "library" targetPath "out" -postBuildCommands "dub run zpklink -- -i res/core-package -o out/core.zpk" -copyFiles "out/core.zpk" - sourcePaths "source" - +copyFiles "out/core.zpk" +postBuildCommands "dub run zpklink -- -i res/core-package -o out/core.zpk" configuration "sdl-opengl" { platforms "posix" "windows" - dependency "bindbc-opengl" version="~>1.0.0" - dependency "bindbc-sdl" version="~>1.0.1" dependency "bindbc-openal" version="~>1.0.0" - + dependency "bindbc-sdl" version="~>1.0.1" + targetType "library" sourcePaths "platform/opengl" "platform/openal" versions "GL_41" "SDL_204" "GL_KHR_debug" } diff --git a/platform/openal/buffer.d b/platform/openal/buffer.d index 83e5785..8b9fb97 100644 --- a/platform/openal/buffer.d +++ b/platform/openal/buffer.d @@ -5,86 +5,82 @@ // Copyright 2021 ZyeByte module zyeware.audio.buffer; +import std.sumtype; + import bindbc.openal; import zyeware.common; import zyeware.audio; -// TODO: Check memory constness someday. @asset(Yes.cache) -class AudioStream +class Audio { protected: const(ubyte)[] mEncodedMemory; + LoopPoint mLoopPoint; public: - this(const(ubyte)[] encodedMemory) + this(const(ubyte)[] encodedMemory, AudioProperties properties = AudioProperties.init) { mEncodedMemory = encodedMemory; + mLoopPoint = properties.loopPoint; } - const(ubyte)[] encodedMemory() pure nothrow - { - return mEncodedMemory; - } - - static AudioStream load(string path) + LoopPoint loopPoint() pure const nothrow { - VFSFile source = VFS.getFile(path); - ubyte[] bestCommunityData = source.readAll!(ubyte[])(); - source.close(); - - Logger.core.log(LogLevel.debug_, "Loaded file '%s' as audio.", path); - - return new AudioStream(bestCommunityData); + return mLoopPoint; } -} - -/* -@asset(Yes.cache) -class Sound -{ -protected: - uint mId; -public: - this(size_t channels, size_t sampleRate, in float[] data) nothrow + void loopPoint(LoopPoint value) pure nothrow { - alGenBuffers(1, &mId); - alBufferData(mId, channels == 1 ? AL_FORMAT_MONO_FLOAT32 : AL_FORMAT_STEREO_FLOAT32, - data.ptr, cast(int) (data.length * float.sizeof), cast(int) sampleRate); + mLoopPoint = value; } - ~this() - { - alDeleteBuffers(1, &mId); - } - - uint id() const pure nothrow + const(ubyte)[] encodedMemory() pure nothrow { - return mId; + return mEncodedMemory; } - static Sound load(string path) + static Audio load(string path) { - auto decoder = AudioDecoder(VFS.getFile(path)); - - float[] data; - float[] readBuffer = new float[2048]; - size_t readCount; - - while ((readCount = decoder.read(readBuffer)) != 0) - data ~= readBuffer[0 .. readCount]; - - Logger.core.log(LogLevel.debug_, "data size: %d", data.length); + VFSFile source = VFS.getFile(path); + ubyte[] bestCommunityData = source.readAll!(ubyte[])(); + source.close(); - return new Sound(decoder.channels, decoder.sampleRate, data); + AudioProperties properties; + + if (VFS.hasFile(path ~ ".props")) // Properties file exists + { + import std.conv : to; + import sdlang; + + VFSFile propsFile = VFS.getFile(path ~ ".props"); + Tag root = parseSource(propsFile.readAll!string); + propsFile.close(); + + try + { + if (Tag loopTag = root.getTag("loop-point")) + { + int v1, v2; + + if ((v1 = loopTag.getAttribute!int("sample")) != int.init) + properties.loopPoint = LoopPoint(v1); + else if ((v1 = loopTag.getAttribute!int("pattern")) != int.init + && (v2 = loopTag.getAttribute!int("row")) != int.init) + properties.loopPoint = LoopPoint(ModuleLoopPoint(v1, v2)); + else + throw new Exception("Could not interpret loop point."); + } + } + catch (Exception ex) + { + Logger.core.log(LogLevel.warning, "Failed to parse properties file for '%s': %s", path, ex.msg); + } + } + + Logger.core.log(LogLevel.debug_, "Loaded file '%s' into memory for streaming.", path); + + return new Audio(bestCommunityData, properties); } -} - -@asset(Yes.cache) -class StreamingSound -{ -public: - static StreamingSound load(string path); -}*/ \ No newline at end of file +} \ No newline at end of file diff --git a/platform/openal/source.d b/platform/openal/source.d index f44d407..ae98809 100644 --- a/platform/openal/source.d +++ b/platform/openal/source.d @@ -5,9 +5,13 @@ // Copyright 2021 ZyeByte module zyeware.audio.source; +import std.exception : enforce; import std.algorithm : clamp; +import std.sumtype : match; +import std.math : isNaN; import bindbc.openal; +import audioformats; import zyeware.common; import zyeware.audio; @@ -16,39 +20,37 @@ import zyeware.audio.thread; class AudioSource { private: - static const(ubyte)[] sEmptyData = new ubyte[0]; - -protected: - enum State + /// Loads up `mProcBuffer` and returns the amount of samples read. + pragma(inline, true) + size_t readFromDecoder() + in (mDecoder.isOpenForReading(), "Tried to decode while decoder is not open for reading.") { - stopped, - paused, - playing + return mDecoder.readSamplesFloat(&mProcBuffer[0], cast(int)(mProcBuffer.length/mDecoder.getNumChannels())) + * mDecoder.getNumChannels(); } - // TODO: Add engine-wide buffer size settings - enum bufferSize = 4096 * 4; - enum bufferCount = 4; - +protected: float[] mProcBuffer; - AudioStream mAudioStream; - AudioDecoder mDecoder; + Audio mAudioStream; + AudioStream mDecoder; uint mSourceId; uint[] mBufferIDs; int mProcessed; State mState; - bool mLooping; // TODO: Maybe add loop point? + float mVolume; + float mPitch; + bool mLooping; AudioBus mBus; package(zyeware): - void updateBuffers() + final void updateBuffers() { if (mState == State.stopped) return; - long lastReadLength; + size_t lastReadLength; int processed; uint pBuf; alGetSourcei(mSourceId, AL_BUFFERS_PROCESSED, &processed); @@ -57,21 +59,38 @@ package(zyeware): { alSourceUnqueueBuffers(mSourceId, 1, &pBuf); - lastReadLength = mDecoder.read(mProcBuffer); + + lastReadLength = readFromDecoder(); if (lastReadLength <= 0) { if (mLooping) { - mDecoder.seekTo(0); // TODO: Replace with a loop point - lastReadLength = mDecoder.read(mProcBuffer); + mAudioStream.loopPoint.match!( + (int sample) + { + enforce!AudioException(!mDecoder.isModule, "Cannot seek by sample in tracker files."); + + if (!mDecoder.seekPosition(sample)) + Logger.core.log(LogLevel.warning, "Seeking to sample %d failed.", sample); + }, + (ModuleLoopPoint mod) + { + enforce!AudioException(mDecoder.isModule, "Cannot seek by pattern/row in non-tracker files."); + + if (!mDecoder.seekPosition(mod.pattern, mod.row)) + Logger.core.log(LogLevel.warning, "Seeking to pattern %d, row %d failed.", mod.pattern, mod.row); + } + ); + + lastReadLength = readFromDecoder(); } else break; } - alBufferData(pBuf, mDecoder.channels == 1 ? AL_FORMAT_MONO_FLOAT32 : AL_FORMAT_STEREO_FLOAT32, - &mProcBuffer[0], cast(int) (lastReadLength * float.sizeof), cast(int) mDecoder.sampleRate); + alBufferData(pBuf, mDecoder.getNumChannels() == 1 ? AL_FORMAT_MONO_FLOAT32 : AL_FORMAT_STEREO_FLOAT32, + &mProcBuffer[0], cast(int) (lastReadLength * float.sizeof), cast(int) mDecoder.getSamplerate()); alSourceQueueBuffers(mSourceId, 1, &pBuf); } @@ -82,28 +101,50 @@ package(zyeware): stop(); } + final void updateVolume() nothrow + { + alSourcef(mSourceId, AL_GAIN, mVolume * mBus.volume); + } + public: + enum State + { + stopped, + paused, + playing + } + this(AudioBus bus = null) { + mState = State.stopped; mBus = bus ? bus : AudioAPI.getBus("master"); - mProcBuffer = new float[bufferSize]; - mBufferIDs = new uint[bufferCount]; + mProcBuffer = new float[ZyeWare.projectProperties.audioBufferSize]; + mBufferIDs = new uint[ZyeWare.projectProperties.audioBufferCount]; + //mDecoder = new AudioStream(); alGenSources(1, &mSourceId); alGenBuffers(cast(int) mBufferIDs.length, &mBufferIDs[0]); AudioThread.register(this); + + mVolume = 1.0f; + mPitch = 1.0f; + mLooping = false; + + updateVolume(); } ~this() { - AudioThread.unregister(this); - - destroy!false(mDecoder); + if (mDecoder.isOpenForReading()) + destroy!false(mDecoder); alDeleteBuffers(cast(int) mBufferIDs.length, &mBufferIDs[0]); alDeleteSources(1, &mSourceId); + + dispose(mProcBuffer); + dispose(mBufferIDs); } void play() @@ -114,11 +155,13 @@ public: if (mState == State.stopped) { long lastReadLength; - for (size_t i; i < bufferCount; ++i) + for (size_t i; i < mBufferIDs.length; ++i) { - lastReadLength = mDecoder.read(mProcBuffer); - alBufferData(mBufferIDs[i], mDecoder.channels == 1 ? AL_FORMAT_MONO_FLOAT32 : AL_FORMAT_STEREO_FLOAT32, - &mProcBuffer[0], cast(int) (lastReadLength * float.sizeof), cast(int) mDecoder.sampleRate); + lastReadLength = readFromDecoder(); + + alBufferData(mBufferIDs[i], + mDecoder.getNumChannels() == 1 ? AL_FORMAT_MONO_FLOAT32 : AL_FORMAT_STEREO_FLOAT32, + &mProcBuffer[0], cast(int) (lastReadLength * float.sizeof), cast(int) mDecoder.getSamplerate()); alSourceQueueBuffers(mSourceId, 1, &mBufferIDs[i]); } } @@ -138,7 +181,8 @@ public: mState = State.stopped; alSourceStop(mSourceId); - mDecoder.seekTo(0); + if (mDecoder.isOpenForReading()) + mDecoder.seekPosition(0); int bufferCount; alGetSourcei(mSourceId, AL_BUFFERS_QUEUED, &bufferCount); @@ -150,121 +194,71 @@ public: } } - inout(AudioStream) stream() inout nothrow + inout(Audio) audio() inout nothrow { return mAudioStream; } - void stream(AudioStream value) + void audio(Audio value) + in (value, "Audio cannot be null.") { if (mState != State.stopped) stop(); mAudioStream = value; - mDecoder.setData(mAudioStream.encodedMemory); - } -} - -/* -class AudioSource -{ -protected: - uint mId; - float mSelfVolume = 1f; - AudioBus mBus; - - this(AudioBus bus) - { - mBus = bus ? bus : AudioAPI.getBus("master"); - - alGenSources(1, &mId); - } + + try + { + mDecoder.openFromMemory(mAudioStream.encodedMemory); + } + catch (AudioFormatsException ex) + { + // Copy manually managed memory to GC memory and rethrow exception. + string errMsg = ex.msg.dup; + string errFile = ex.file.dup; + size_t errLine = ex.line; + destroyAudioFormatException(ex); -public: - ~this() - { - alDeleteSources(1, &mId); + throw new AudioException(errMsg, errFile, errLine, null); + } } - Vector3f position() const nothrow + bool looping() pure const nothrow { - float x, y, z; - alGetSource3f(mId, AL_POSITION, &x, &y, &z); - return Vector3f(x, y, z); + return mLooping; } - void position(Vector3f value) nothrow + void looping(bool value) pure nothrow { - alSource3f(mId, AL_POSITION, value.x, value.y, value.z); + mLooping = value; } - float volume() const nothrow + float volume() pure const nothrow { - return mSelfVolume; + return mVolume; } void volume(float value) nothrow + in (!isNaN(value), "Cannot set NaN as a volume.") { - mSelfVolume = clamp(value, 0.0f, 1.0f); - //AudioServer._recalculateChannelGains(_audioBus); - } - - abstract void play() nothrow; - abstract void pause() nothrow; - abstract void stop() nothrow; - - abstract bool loop() nothrow; - abstract void loop(bool value) nothrow; -} - -class AudioSampleSource : AudioSource -{ -protected: - Sound mBuffer; - -public: - this(AudioBus bus) - { - super(bus); - } - - void buffer(Sound value) nothrow - { - stop(); - mBuffer = value; - - alSourcei(mId, AL_BUFFER, mBuffer ? mBuffer.id : 0); - } - - Sound buffer() nothrow - { - return mBuffer; - } - - override void play() nothrow - { - alSourcePlay(mId); - } - - override void pause() nothrow - { - alSourcePause(mId); + mVolume = clamp(value, 0f, 1f); + updateVolume(); } - override void stop() nothrow + float pitch() pure const nothrow { - alSourceStop(mId); + return mPitch; } - override bool loop() nothrow + void pitch(float value) nothrow + in (!isNaN(value), "Cannot set NaN as a pitch.") { - int loopValue; - alGetSourcei(mId, AL_LOOPING, &loopValue); - return loopValue == AL_TRUE; + mPitch = value; + alSourcef(mSourceId, AL_PITCH, mPitch); } - override void loop(bool value) nothrow + State state() pure const nothrow { - alSourcei(mId, AL_LOOPING, value); + return mState; } -}*/ \ No newline at end of file +} \ No newline at end of file diff --git a/res/core-package/textures/zyebyte.png b/res/core-package/textures/zyebyte.png deleted file mode 100644 index a85be567ba269146062dbe35f9bcb5e2879bd2c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25804 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

A=jhsZj>e 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 diff --git a/source/zyeware/audio/buffer.di b/source/zyeware/audio/buffer.di index 32b4433..a848722 100644 --- a/source/zyeware/audio/buffer.di +++ b/source/zyeware/audio/buffer.di @@ -5,39 +5,30 @@ // Copyright 2021 ZyeByte module zyeware.audio.buffer; +import std.sumtype; + import zyeware.common; import zyeware.audio; -alias NoiseBitties = AudioStream; -alias AirVibrationData = AudioStream; -alias EarMassager = AudioStream; -alias SonicStream = AudioStream; - -@asset(Yes.cache) -class AudioStream +deprecated("This was a joke.") { -public: - this(const(ubyte)[] encodedMemory); - - const(ubyte)[] encodedMemory() pure nothrow; - - static AudioStream load(string path); + alias NoiseBitties = Audio; + alias AirVibrationData = Audio; + alias EarMassager = Audio; + alias SonicStream = Audio; } -/* @asset(Yes.cache) -class Sound +class Audio { public: - static Sound load(string path); + this(const(ubyte)[] encodedMemory, AudioProperties properties = AudioProperties.init); - uint id() const pure nothrow; -} + LoopPoint loopPoint() pure const nothrow; -@asset(Yes.cache) -class StreamedSound -{ -public: - static StreamedSound load(string path); -} -*/ \ No newline at end of file + void loopPoint(LoopPoint value) pure nothrow; + + const(ubyte)[] encodedMemory() pure nothrow; + + static Audio load(string path); +} \ No newline at end of file diff --git a/source/zyeware/audio/bus.d b/source/zyeware/audio/bus.d index 263cd36..7452687 100644 --- a/source/zyeware/audio/bus.d +++ b/source/zyeware/audio/bus.d @@ -8,6 +8,7 @@ module zyeware.audio.bus; import std.algorithm : clamp; import zyeware.common; +import zyeware.audio.thread; class AudioBus { @@ -33,8 +34,9 @@ public: } /// ditto - void gain(float value) nothrow + void volume(float value) { mVolume = clamp(value, 0.0f, 1.0f); + AudioThread.updateVolumeForSources(); } } \ No newline at end of file diff --git a/source/zyeware/audio/decoder.d b/source/zyeware/audio/decoder.d deleted file mode 100644 index f961a47..0000000 --- a/source/zyeware/audio/decoder.d +++ /dev/null @@ -1,104 +0,0 @@ -module zyeware.audio.decoder; - -import std.exception : enforce; -import std.string : format; - -import audioformats; - -import zyeware.common; - -/// Takes a chunk of memory or a file (for streaming), and decodes -/// audio on-the-fly. -struct AudioDecoder -{ -protected: - const(ubyte)[] mEncodedMemory; - AudioStream mStream; - -public: - //@disable this(); - @disable this(this); - - ~this() - { - if (mStream.isOpenForReading()) - destroy!false(mStream); - } - - void setData(const(ubyte)[] encodedMemory) - { - mEncodedMemory = encodedMemory; - - try - { - mStream.openFromMemory(mEncodedMemory); - } - catch (AudioFormatsException ex) - { - // Copy manually managed memory to GC memory and rethrow exception. - string errMsg = ex.msg.dup; - string errFile = ex.file.dup; - size_t errLine = ex.line; - destroyAudioFormatException(ex); - - throw new AudioException(errMsg, errFile, errLine, null); - } - } - - /// Tries to read samples into the supplied buffer. - /// - /// Params: - /// buffer = The buffer to read into. It's length should be a multiple of the channel count. - /// Returns: The amount of samples actually read. - size_t read(T)(ref T[] buffer) - in (buffer.length % mStream.getNumChannels() == 0, "Buffer length is not a multiple of channel count.") - { - static if (is(T == float)) - return mStream.readSamplesFloat(buffer.ptr, cast(int)(buffer.length/mStream.getNumChannels())) - * mStream.getNumChannels(); - else static if (is(T == double)) - return mStream.readSamplesDouble(buffer.ptr, cast(int)(buffer.length/mStream.getNumChannels())) - * mStream.getNumChannels(); - else - static assert(false, "'read' cannot process type " ~ T.stringof); - } - - /// Tries to read the specified amount of samples. - /// - /// Params: - /// samples = The amount of samples to read. - /// Returns: A newly allocated buffer with the read samples. - /* - T[] read(T)(size_t samples) - { - auto buffer = new T[samples * mAudioInfo.channels]; - static if (is(T == float)) - return mStream.readSamplesFloat(buffer.ptr, buffer.length/mStream.getNumChannels()); - else static if (is(T == double)) - return mStream.readSamplesDouble(buffer.ptr, buffer.length/mStream.getNumChannels()); - else - static assert(false, "'read' cannot process type " ~ T.stringof); - - return buffer[0 .. sampleCount]; - }*/ - - void seekTo(size_t frame) - { - mStream.seekPosition(cast(int) frame); // ????? - } - - size_t sampleCount() - { - return mStream.getLengthInFrames(); - } - - size_t sampleRate() - { - return cast(size_t) mStream.getSamplerate(); - } - - size_t channels() - { - return mStream.getNumChannels(); - } -} \ No newline at end of file diff --git a/source/zyeware/audio/package.d b/source/zyeware/audio/package.d index 89e7cf2..4672484 100644 --- a/source/zyeware/audio/package.d +++ b/source/zyeware/audio/package.d @@ -10,7 +10,6 @@ public import zyeware.audio.api; import zyeware.audio.bus; import zyeware.audio.properties; - import zyeware.audio.decoder; import zyeware.audio.buffer; import zyeware.audio.source; } \ No newline at end of file diff --git a/source/zyeware/audio/properties.d b/source/zyeware/audio/properties.d index 8516867..6ea2b59 100644 --- a/source/zyeware/audio/properties.d +++ b/source/zyeware/audio/properties.d @@ -5,14 +5,20 @@ // Copyright 2021 ZyeByte module zyeware.audio.properties; +import std.sumtype : SumType; + import zyeware.common; import zyeware.audio; -struct PlayProperties +struct ModuleLoopPoint +{ + int pattern; + int row; +} + +alias LoopPoint = SumType!(int, ModuleLoopPoint); + +struct AudioProperties { - int channel = -1; /// Select on which channel to play. - AudioBus bus; /// The audio bus to use. - Vector3f position = Vector3f(0); /// Position to play the sound at. - float volume = 1f; /// The volume of the sound, relative to the used bus. - bool looping; /// If the sound should be looped. + LoopPoint loopPoint = LoopPoint(0); /// The point to loop at. It differentiates between a frame or pattern & row (for modules) } \ No newline at end of file diff --git a/source/zyeware/audio/source.di b/source/zyeware/audio/source.di index bf98e1c..d30efe6 100644 --- a/source/zyeware/audio/source.di +++ b/source/zyeware/audio/source.di @@ -12,8 +12,16 @@ class AudioSource { package(zyeware): void updateBuffers(); + void updateVolume(); public: + enum State + { + stopped, + paused, + playing + } + this(AudioBus bus = null); ~this(); @@ -22,49 +30,17 @@ public: void pause(); void stop(); - inout(AudioStream) stream() pure inout nothrow; - void stream(AudioStream value) pure nothrow; -} - -/* -class AudioSource -{ -protected: - uint mId; - float mSelfVolume = 1f; - AudioBus mBus; - - this(AudioBus bus); + inout(Audio) audio() pure inout nothrow; + void audio(Audio value) pure nothrow; -public: - Vector3f position() const nothrow; - void position(Vector3f value) nothrow; - float volume() const nothrow; + bool looping() pure const nothrow; + void looping(bool value) pure nothrow; + float volume() pure const nothrow; void volume(float value) nothrow; - abstract void play() nothrow; - abstract void pause() nothrow; - abstract void stop() nothrow; - - abstract bool loop() nothrow; - abstract void loop(bool value) nothrow; -} - -class AudioSampleSource : AudioSource -{ -protected: - Sound mBuffer; - -public: - this(AudioBus bus); - - void buffer(Sound value) nothrow; - Sound buffer() nothrow; + float pitch() pure const nothrow; + void pitch(float value) nothrow; - override void play() nothrow; - override void pause() nothrow; - override void stop() nothrow; - override bool loop() nothrow; - override void loop(bool value) nothrow; -}*/ \ No newline at end of file + State state() pure const nothrow; +} \ No newline at end of file diff --git a/source/zyeware/audio/thread.d b/source/zyeware/audio/thread.d index dabf6e6..101a3c2 100644 --- a/source/zyeware/audio/thread.d +++ b/source/zyeware/audio/thread.d @@ -1,43 +1,77 @@ module zyeware.audio.thread; -import core.thread : Thread, msecs; +import core.thread : Thread, Duration, msecs; import std.algorithm : countUntil, remove; import zyeware.common; import zyeware.audio; +import zyeware.core.weakref; + +debug import std.stdio; package(zyeware) struct AudioThread { private static: Thread sThread; - __gshared AudioSource[] sRegisteredSources; + __gshared WeakReference!AudioSource[] sRegisteredSources; __gshared bool sRunning; + __gshared Object sMutex = new Object(); void threadBody() { + // Determine the sleep time between updating the buffers. + // YukieVT supplied the following formula for this: + // (BuffTotalLen / BuffCount) / SampleRate / 2 * 1000 + // We assume a default sample rate of 44100 for audio. + + immutable Duration waitTime = msecs(ZyeWare.projectProperties.audioBufferSize + / ZyeWare.projectProperties.audioBufferCount / 44_100 / 2 * 1000); + while (sRunning) { - foreach (AudioSource source; sRegisteredSources) - source.updateBuffers(); + synchronized (sMutex) + { + for (size_t i; i < sRegisteredSources.length; ++i) + { + if (!sRegisteredSources[i].alive) + { + debug writefln("Removing source #%d...", i); + sRegisteredSources[i] = sRegisteredSources[$ - 1]; + --sRegisteredSources.length; + --i; + continue; + } - // TODO: Check if the buffer is still alive (or smth idk) - // TODO: Also change sleep time depending on buffer length - // (BuffTotalLen / BuffCount) / SampleRate / 2 * 1000 - - Thread.sleep(10.msecs); + sRegisteredSources[i].target.updateBuffers(); + } + } + + Thread.sleep(waitTime); } } package(zyeware): void register(AudioSource source) { - sRegisteredSources ~= source; + synchronized (sMutex) + { + sRegisteredSources ~= weakReference(source); + } } - void unregister(AudioSource source) + void updateVolumeForSources() { - sRegisteredSources.removeElement(source); + synchronized (sMutex) + { + for (size_t i; i < sRegisteredSources.length; ++i) + { + if (!sRegisteredSources[i].alive) + continue; + + sRegisteredSources[i].target.updateVolume(); + } + } } public static: diff --git a/source/zyeware/core/application.d b/source/zyeware/core/application.d index 0a2e7b0..5e3a240 100644 --- a/source/zyeware/core/application.d +++ b/source/zyeware/core/application.d @@ -17,7 +17,7 @@ import zyeware.rendering; /// Represents an application that can be run by ZyeWare. /// To write a ZyeWare app, you must inherit from this class and return an -/// instance of it with the `createZyeWareApplication` function. +/// instance of it with the `getProjectProperties` function. /// /// Examples: /// -------------------- @@ -26,9 +26,11 @@ import zyeware.rendering; /// ... /// } /// -/// extern(C) Application createZyeWareApplication(string[] args) +/// extern(C) ProjectProperties getProjectProperties(); /// { -/// return new MyApplication(args); +/// ProjectProperties props; +/// props.application = new MyApplication(); +/// return props; /// } /// -------------------- abstract class Application diff --git a/source/zyeware/core/asset.d b/source/zyeware/core/asset.d index f55b02d..081757a 100644 --- a/source/zyeware/core/asset.d +++ b/source/zyeware/core/asset.d @@ -8,6 +8,7 @@ module zyeware.core.asset; import std.string : format; import std.exception : collectException, assumeWontThrow, enforce; import std.typecons : Tuple; +import std.traits : fullyQualifiedName; import zyeware.common; import zyeware.core.weakref; @@ -40,7 +41,7 @@ struct AssetManager private static: struct AssetUID { - string typeMangle; + string typeFQN; string path; } @@ -49,22 +50,13 @@ private static: LoadFunction[string] sLoaders; WeakReference!Object[AssetUID] sCache; - Object getFromCache(AssetUID uid) nothrow - { - auto weakref = sCache.get(uid, null).assumeWontThrow; - if (weakref && weakref.alive) - return weakref.target; - - return null; - } - package(zyeware.core) static: void initialize() { //registerDefaultLoaders(); import zyeware.rendering : Shader, Image, Texture2D, TextureCubeMap, Mesh, Font, Material, SpriteFrames, Cursor; import zyeware.core.translation : Translation; - import zyeware.audio : AudioStream; + import zyeware.audio : Audio; register!Shader(); register!Image(); @@ -74,7 +66,7 @@ package(zyeware.core) static: register!Font(); register!Material(); register!Translation(); - register!AudioStream(); + register!Audio(); register!SpriteFrames(); register!Cursor(); } @@ -97,18 +89,25 @@ public static: if (isAsset!T) in (path, "Path cannot be null.") { - LoadFunction* loader = T.mangleof in sLoaders; - enforce!CoreException(loader, format!"'%s' was not registered as an asset."(T.stringof)); + enum fqn = fullyQualifiedName!T; - auto uid = AssetUID(T.mangleof, path); + LoadFunction* loader = fqn in sLoaders; + enforce!CoreException(loader, format!"'%s' was not registered as an asset."(fqn)); + + auto uid = AssetUID(fqn, path); // Check if we have it cached, and if so, if it's still alive - Object asset = getFromCache(uid); - if (asset) - return cast(T) asset; + auto weakref = sCache.get(uid, null).assumeWontThrow; + if (weakref && weakref.alive) + return cast(T) weakref.target; // Otherwise, load asset - asset = loader.callback(path); + if (weakref) + Logger.core.log(LogLevel.debug_, "Asset '%s' (%s) got collected, reloading...", uid.path, uid.typeFQN); + else + Logger.core.log(LogLevel.debug_, "Loading asset '%s' (%s)...", uid.path, uid.typeFQN); + + Object asset = loader.callback(path); assert(asset, format!"Loader for '%s' returned null!"(T.stringof)); if (loader.cache) @@ -127,7 +126,7 @@ public static: import std.traits : getUDAs; auto data = getUDAs!(T, asset)[0]; - sLoaders[T.mangleof] = LoadFunction(&T.load, data.cache); + sLoaders[fullyQualifiedName!T] = LoadFunction(&T.load, data.cache); } /// Unregisters an asset. @@ -139,7 +138,7 @@ public static: bool unregister(T)() if (isAsset!T) { - return sLoaders.remove(T.mangleof); + return sLoaders.remove(fullyQualifiedName!T); } /// Checks if the given file is already cached. @@ -151,7 +150,7 @@ public static: if (isAsset!T) in (path, "Path cannot be null.") { - auto weakref = sCache.get(AssetUID(T.mangleof, path), null).assumeWontThrow; + auto weakref = sCache.get(AssetUID(fullyQualifiedName!T, path), null).assumeWontThrow; return weakref && weakref.alive; } @@ -175,7 +174,7 @@ public static: if (!sCache[key].alive) { sCache.remove(key).assumeWontThrow; - Logger.core.log(LogLevel.trace, "Uncaching '%s'...", key.path); + Logger.core.log(LogLevel.trace, "Uncaching '%s' (%s)...", key.path, key.typeFQN); ++cleaned; } } diff --git a/source/zyeware/core/crash.d b/source/zyeware/core/crash.d index e6a3307..827dcf8 100644 --- a/source/zyeware/core/crash.d +++ b/source/zyeware/core/crash.d @@ -40,7 +40,8 @@ public: //if (!trace.startsWith("??:?")) Logger.core.log(LogLevel.info, trace); - Logger.core.log(LogLevel.fatal, "You are on your own now."); + Logger.core.log(LogLevel.fatal, "------------------------------"); + Logger.core.log(LogLevel.fatal, "If you suspect that this is a ZyeWare issue, please leave a bug report over at https://github.com/zyebytevt/zyeware!"); Logger.core.log(LogLevel.fatal, "================================================================="); Logger.core.flush(); @@ -91,8 +92,11 @@ public: { super.show(t); - enum title = "Sorry"; - enum message = "ZyeWare has crashed."; + enum title = "Can I go home yet?"; + enum message = "As it turns out, the application has crashed. ZyeByte is sorry for the inconvenience, be it as " + ~ "the game or engine developer alike.\nIf you do suspect it's an issue of the engine though, please leave " + ~ "a bug report over at https://github.com/zyebytevt/zyeware!\nWith this, I'm sure it can be fixed soon.\n\n" + ~ "(Restarting often fixes issues, I've been told!)"; if (executeShell("type kdialog").status == 0) showKDialog(message, t.toString(), title); diff --git a/source/zyeware/core/engine.d b/source/zyeware/core/engine.d index dbc26f7..6f9c0f0 100644 --- a/source/zyeware/core/engine.d +++ b/source/zyeware/core/engine.d @@ -39,6 +39,9 @@ struct ProjectProperties CrashHandler crashHandler; /// The crash handler to use. WindowProperties mainWindowProperties; /// The properties of the main window. + uint audioBufferSize = 4096 * 4; /// The size of an individual audio buffer in samples. + uint audioBufferCount = 4; /// The amount of audio buffers to cycle through for streaming. + uint targetFrameRate = 60; /// The frame rate the project should target to hold. This is not a guarantee. } @@ -49,6 +52,20 @@ struct FrameTime Duration unscaledDeltaTime; /// Time between this frame and the last, without being multiplied by `ZyeWare.timeScale`. } +/// Holds information about a SemVer version. +struct Version +{ + int major; + int minor; + int patch; + string prerelease; + + string toString() immutable pure + { + return format!"%d.%d.%d%s"(major, minor, patch, prerelease ? "-" ~ prerelease : ""); + } +} + /// Holds the core engine. Responsible for the main loop and generic engine settings. struct ZyeWare { @@ -144,7 +161,11 @@ private static: } for (size_t i; i < sDeferredFunctionsCount; ++i) + { + // After invoking set to null so that no references keep lingering. sDeferredFunctions[i](); + sDeferredFunctions[i] = null; + } sDeferredFunctionsCount = 0; } } @@ -268,7 +289,8 @@ package(zyeware.core) static: // In release mode, we want to display our fancy splash screen. debug sApplication = properties.mainApplication; - else sApplication = new StartupApplication(properties.mainApplication); + else + sApplication = new StartupApplication(properties.mainApplication); sApplication.initialize(); } @@ -299,6 +321,8 @@ package(zyeware.core) static: } public static: + immutable Version engineVersion = Version(0, 3, 0, "alpha"); + /// How the framebuffer should be scaled on resizing. enum ScaleMode { diff --git a/source/zyeware/core/startupapp.d b/source/zyeware/core/startupapp.d index 3c5d70f..e8d5e8f 100644 --- a/source/zyeware/core/startupapp.d +++ b/source/zyeware/core/startupapp.d @@ -8,9 +8,10 @@ final class StartupApplication : Application { protected: Texture2D mEngineLogo; - version(ZyeByteStartup) Texture2D mZyeByte; Application mMainApplication; OrthographicCamera mCamera; + Font mInternalFont; + string mVersionString; Gradient mBackgroundGradient; Interpolator!(float, lerp) mAlphaInterpolator; @@ -28,9 +29,8 @@ public: ZyeWare.scaleMode = ZyeWare.ScaleMode.keepAspect; mEngineLogo = AssetManager.load!Texture2D("core://textures/engine-logo.png"); - - version(ZyeByteStartup) - mZyeByte = AssetManager.load!Texture2D("core://textures/zyebyte.png"); + mInternalFont = AssetManager.load!Font("core://fonts/internal.fnt"); + mVersionString = "v" ~ ZyeWare.engineVersion.toString; mCamera = new OrthographicCamera(-1, 1, 1, -1); @@ -82,8 +82,8 @@ public: Renderer2D.drawRect(Rect2f(min, max), Matrix4f.identity, Color(1, 1, 1, alpha), mEngineLogo); - version(ZyeByteStartup) Renderer2D.drawRect(Rect2f(0, 0.82, 1, 1), Matrix4f.identity, - Color(1, 1, 1, alpha), mZyeByte); + Renderer2D.drawText(mVersionString, mInternalFont, Vector2f(-1, -1), Color(1, 1, 1, alpha), + Font.Alignment.left | Font.Alignment.bottom); Renderer2D.end(); } diff --git a/source/zyeware/core/timer.d b/source/zyeware/core/timer.d index 7646530..de61096 100644 --- a/source/zyeware/core/timer.d +++ b/source/zyeware/core/timer.d @@ -46,7 +46,13 @@ package(zyeware.core): timer.mCallback(timer); if (timer.mOneshot) - sTimerEntries = sTimerEntries.remove(i--); + { + sTimerEntries[i] = sTimerEntries[$ - 1]; + sTimerEntries[$ - 1].timer = null; + --sTimerEntries.length; + --i; + continue; + } else entry.timeLeft = timer.mInterval; } @@ -90,7 +96,9 @@ public: auto idx = countUntil!"a.timer is b"(sTimerEntries, this); if (idx >= 0) { - sTimerEntries = sTimerEntries.remove(idx); + sTimerEntries[idx] = sTimerEntries[$ - 1]; + sTimerEntries[$ - 1].timer = null; + --sTimerEntries.length; mIsRunning = false; } } diff --git a/source/zyeware/utils/collection.d b/source/zyeware/utils/collection.d index b818c2e..d01054e 100644 --- a/source/zyeware/utils/collection.d +++ b/source/zyeware/utils/collection.d @@ -119,7 +119,7 @@ public: { auto saved = mArray[mNextPointer - 1]; static if (hasIndirections!T) - mArray[mNextPointer] = T.init; + mArray[mNextPointer - 1] = T.init; --mNextPointer; return saved; }