diff --git a/CMakeLists.txt b/CMakeLists.txt index b5f2bff..47bc589 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,7 +41,7 @@ target_sources(plugin PRIVATE ${PLUGIN_SOURCES} "${CMAKE_CURRENT_BINARY_DIR}/ver add_compile_options(plugin PRIVATE /W4 /WX) target_compile_definitions(plugin PRIVATE _UNION_API_DLL __G1 __G1A __G2 __G2A) target_include_directories(plugin PRIVATE "src/" "${CMAKE_BINARY_DIR}/src/") -target_link_libraries(plugin PRIVATE union-api gothic-api bass) +target_link_libraries(plugin PRIVATE union-api gothic-api bass_all) install(FILES $ "${CMAKE_BINARY_DIR}/zBassMusic.dll" TYPE BIN) install(FILES $ "${CMAKE_BINARY_DIR}/UnionAPI.dll" TYPE BIN) @@ -72,6 +72,9 @@ if (DEFINED ENV{COPY_DLL_TARGET}) COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_BINARY_DIR}/zBassMusic.dll" "$ENV{COPY_DLL_TARGET}/Autorun/zBassMusic.dll" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_BINARY_DIR}/UnionAPI.dll" "$ENV{COPY_DLL_TARGET}/Autorun/UnionAPI.dll" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dependencies/bass/lib/bass.dll" "$ENV{COPY_DLL_TARGET}/bass.dll" + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dependencies/bass/lib/bassmidi.dll" "$ENV{COPY_DLL_TARGET}/bassmidi.dll" + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dependencies/bass/lib/bassopus.dll" "$ENV{COPY_DLL_TARGET}/bassopus.dll" + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dependencies/bass/lib/bassflac.dll" "$ENV{COPY_DLL_TARGET}/bassflac.dll" DEPENDS plugin COMMENT "Copy plugin to target directory: $ENV{COPY_DLL_TARGET}") endif () diff --git a/cmake/bass.cmake b/cmake/bass.cmake index 9e56a80..11e96ea 100644 --- a/cmake/bass.cmake +++ b/cmake/bass.cmake @@ -2,3 +2,21 @@ add_library(bass SHARED IMPORTED) set_property(TARGET bass PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${BASS_DIR}/include") set_property(TARGET bass PROPERTY IMPORTED_LOCATION "${BASS_DIR}/lib/bass.dll") set_property(TARGET bass PROPERTY IMPORTED_IMPLIB "${BASS_DIR}/lib/bass.lib") + +add_library(bassmidi SHARED IMPORTED) +set_property(TARGET bassmidi PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${BASS_DIR}/include") +set_property(TARGET bassmidi PROPERTY IMPORTED_LOCATION "${BASS_DIR}/lib/bassmidi.dll") +set_property(TARGET bassmidi PROPERTY IMPORTED_IMPLIB "${BASS_DIR}/lib/bassmidi.lib") + +add_library(bassopus SHARED IMPORTED) +set_property(TARGET bassopus PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${BASS_DIR}/include") +set_property(TARGET bassopus PROPERTY IMPORTED_LOCATION "${BASS_DIR}/lib/bassopus.dll") +set_property(TARGET bassopus PROPERTY IMPORTED_IMPLIB "${BASS_DIR}/lib/bassopus.lib") + +add_library(bassflac SHARED IMPORTED) +set_property(TARGET bassflac PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${BASS_DIR}/include") +set_property(TARGET bassflac PROPERTY IMPORTED_LOCATION "${BASS_DIR}/lib/bassflac.dll") +set_property(TARGET bassflac PROPERTY IMPORTED_IMPLIB "${BASS_DIR}/lib/bassflac.lib") + +add_library(bass_all INTERFACE) +target_link_libraries(bass_all INTERFACE bass bassmidi bassopus bassflac) \ No newline at end of file diff --git a/dependencies/bass/include/bassflac.h b/dependencies/bass/include/bassflac.h new file mode 100644 index 0000000..28fd5a8 --- /dev/null +++ b/dependencies/bass/include/bassflac.h @@ -0,0 +1,98 @@ +/* + BASSFLAC 2.4 C/C++ header file + Copyright (c) 2004-2017 Un4seen Developments Ltd. + + See the BASSFLAC.CHM file for more detailed documentation +*/ + +#ifndef BASSFLAC_H +#define BASSFLAC_H + +#include "bass.h" + +#if BASSVERSION!=0x204 +#error conflicting BASS and BASSFLAC versions +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef BASSFLACDEF +#define BASSFLACDEF(f) WINAPI f +#endif + +// BASS_CHANNELINFO type +#define BASS_CTYPE_STREAM_FLAC 0x10900 +#define BASS_CTYPE_STREAM_FLAC_OGG 0x10901 + +// Additional tag types +#define BASS_TAG_FLAC_CUE 12 // cuesheet : TAG_FLAC_CUE structure +#define BASS_TAG_FLAC_PICTURE 0x12000 // + index #, picture : TAG_FLAC_PICTURE structure +#define BASS_TAG_FLAC_METADATA 0x12400 // + index #, application metadata : TAG_FLAC_METADATA structure + +typedef struct { + DWORD apic; // ID3v2 "APIC" picture type + const char *mime; // mime type + const char *desc; // description + DWORD width; + DWORD height; + DWORD depth; + DWORD colors; + DWORD length; // data length + const void *data; +} TAG_FLAC_PICTURE; + +typedef struct { + QWORD offset; // index offset relative to track offset (samples) + DWORD number; // index number +} TAG_FLAC_CUE_TRACK_INDEX; + +typedef struct { + QWORD offset; // track offset (samples) + DWORD number; // track number + const char *isrc; // ISRC + DWORD flags; + DWORD nindexes; // number of indexes + const TAG_FLAC_CUE_TRACK_INDEX *indexes; // the indexes +} TAG_FLAC_CUE_TRACK; + +typedef struct { + const char *catalog; // media catalog number + DWORD leadin; // lead-in (samples) + BOOL iscd; // a CD? + DWORD ntracks; // number of tracks + const TAG_FLAC_CUE_TRACK *tracks; // the tracks +} TAG_FLAC_CUE; + +// TAG_FLAC_CUE_TRACK flags +#define TAG_FLAC_CUE_TRACK_DATA 1 // data track +#define TAG_FLAC_CUE_TRACK_PRE 2 // pre-emphasis + +typedef struct { + char id[4]; + DWORD length; // data length + const void *data; +} TAG_FLAC_METADATA; + +HSTREAM BASSFLACDEF(BASS_FLAC_StreamCreateFile)(BOOL mem, const void *file, QWORD offset, QWORD length, DWORD flags); +HSTREAM BASSFLACDEF(BASS_FLAC_StreamCreateURL)(const char *url, DWORD offset, DWORD flags, DOWNLOADPROC *proc, void *user); +HSTREAM BASSFLACDEF(BASS_FLAC_StreamCreateFileUser)(DWORD system, DWORD flags, const BASS_FILEPROCS *procs, void *user); + +#ifdef __cplusplus +} + +#ifdef _WIN32 +static inline HSTREAM BASS_FLAC_StreamCreateFile(BOOL mem, const WCHAR *file, QWORD offset, QWORD length, DWORD flags) +{ + return BASS_FLAC_StreamCreateFile(mem, (const void*)file, offset, length, flags|BASS_UNICODE); +} + +static inline HSTREAM BASS_FLAC_StreamCreateURL(const WCHAR *url, DWORD offset, DWORD flags, DOWNLOADPROC *proc, void *user) +{ + return BASS_FLAC_StreamCreateURL((const char*)url, offset, flags|BASS_UNICODE, proc, user); +} +#endif +#endif + +#endif diff --git a/dependencies/bass/include/bassmidi.h b/dependencies/bass/include/bassmidi.h new file mode 100644 index 0000000..0c8b519 --- /dev/null +++ b/dependencies/bass/include/bassmidi.h @@ -0,0 +1,424 @@ +/* + BASSMIDI 2.4 C/C++ header file + Copyright (c) 2006-2022 Un4seen Developments Ltd. + + See the BASSMIDI.CHM file for more detailed documentation +*/ + +#ifndef BASSMIDI_H +#define BASSMIDI_H + +#include "bass.h" + +#if BASSVERSION!=0x204 +#error conflicting BASS and BASSMIDI versions +#endif + +#ifdef __OBJC__ +typedef int BOOL32; +#define BOOL BOOL32 // override objc's BOOL +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef BASSMIDIDEF +#define BASSMIDIDEF(f) WINAPI f +#endif + +typedef DWORD HSOUNDFONT; // soundfont handle + +// Additional error codes returned by BASS_ErrorGetCode +#define BASS_ERROR_MIDI_INCLUDE 7000 // SFZ include file could not be opened + +// Additional BASS_SetConfig options +#define BASS_CONFIG_MIDI_COMPACT 0x10400 +#define BASS_CONFIG_MIDI_VOICES 0x10401 +#define BASS_CONFIG_MIDI_AUTOFONT 0x10402 +#define BASS_CONFIG_MIDI_IN_PORTS 0x10404 +#define BASS_CONFIG_MIDI_SAMPLETHREADS 0x10406 +#define BASS_CONFIG_MIDI_SAMPLEMEM 0x10407 +#define BASS_CONFIG_MIDI_SAMPLEREAD 0x10408 +#define BASS_CONFIG_MIDI_SAMPLELOADING 0x1040a + +// Additional BASS_SetConfigPtr options +#define BASS_CONFIG_MIDI_DEFFONT 0x10403 +#define BASS_CONFIG_MIDI_SFZHEAD 0x10409 + +// Additional sync types +#define BASS_SYNC_MIDI_MARK 0x10000 +#define BASS_SYNC_MIDI_MARKER 0x10000 +#define BASS_SYNC_MIDI_CUE 0x10001 +#define BASS_SYNC_MIDI_LYRIC 0x10002 +#define BASS_SYNC_MIDI_TEXT 0x10003 +#define BASS_SYNC_MIDI_EVENT 0x10004 +#define BASS_SYNC_MIDI_TICK 0x10005 +#define BASS_SYNC_MIDI_TIMESIG 0x10006 +#define BASS_SYNC_MIDI_KEYSIG 0x10007 + +// Additional BASS_MIDI_StreamCreateFile/etc flags +#define BASS_MIDI_NODRUMPARAM 0x400 +#define BASS_MIDI_NOSYSRESET 0x800 +#define BASS_MIDI_DECAYEND 0x1000 +#define BASS_MIDI_NOFX 0x2000 +#define BASS_MIDI_DECAYSEEK 0x4000 +#define BASS_MIDI_NOCROP 0x8000 +#define BASS_MIDI_NOTEOFF1 0x10000 +#define BASS_MIDI_ASYNC 0x400000 +#define BASS_MIDI_SINCINTER 0x800000 + +// BASS_MIDI_FontInit flags +#define BASS_MIDI_FONT_MEM 0x10000 +#define BASS_MIDI_FONT_MMAP 0x20000 +#define BASS_MIDI_FONT_XGDRUMS 0x40000 +#define BASS_MIDI_FONT_NOFX 0x80000 +#define BASS_MIDI_FONT_LINATTMOD 0x100000 +#define BASS_MIDI_FONT_LINDECVOL 0x200000 +#define BASS_MIDI_FONT_NORAMPIN 0x400000 +#define BASS_MIDI_FONT_NOLIMITS 0x800000 +#define BASS_MIDI_FONT_MINFX 0x1000000 + +typedef struct { + HSOUNDFONT font; // soundfont + int preset; // preset number (-1=all) + int bank; +} BASS_MIDI_FONT; + +typedef struct { + HSOUNDFONT font; // soundfont + int spreset; // source preset number + int sbank; // source bank number + int dpreset; // destination preset/program number + int dbank; // destination bank number + int dbanklsb; // destination bank number LSB +} BASS_MIDI_FONTEX; + +typedef struct { + HSOUNDFONT font; // soundfont + int spreset; // source preset number + int sbank; // source bank number + int dpreset; // destination preset/program number + int dbank; // destination bank number + int dbanklsb; // destination bank number LSB + DWORD minchan; // minimum channel number + DWORD numchan; // number of channels from minchan +} BASS_MIDI_FONTEX2; + +// BASS_MIDI_StreamSet/GetFonts flag +#define BASS_MIDI_FONT_EX 0x1000000 // BASS_MIDI_FONTEX +#define BASS_MIDI_FONT_EX2 0x2000000 // BASS_MIDI_FONTEX2 + +typedef struct { + const char *name; + const char *copyright; + const char *comment; + DWORD presets; // number of presets/instruments + DWORD samsize; // total size (in bytes) of the sample data + DWORD samload; // amount of sample data currently loaded + DWORD samtype; // sample format (CTYPE) if packed +} BASS_MIDI_FONTINFO; + +typedef struct { + DWORD track; // track containing marker + DWORD pos; // marker position + const char *text; // marker text +} BASS_MIDI_MARK; + +// Marker types +#define BASS_MIDI_MARK_MARKER 0 // marker +#define BASS_MIDI_MARK_CUE 1 // cue point +#define BASS_MIDI_MARK_LYRIC 2 // lyric +#define BASS_MIDI_MARK_TEXT 3 // text +#define BASS_MIDI_MARK_TIMESIG 4 // time signature +#define BASS_MIDI_MARK_KEYSIG 5 // key signature +#define BASS_MIDI_MARK_COPY 6 // copyright notice +#define BASS_MIDI_MARK_TRACK 7 // track name +#define BASS_MIDI_MARK_INST 8 // instrument name +#define BASS_MIDI_MARK_TRACKSTART 9 // track start (SMF2) +#define BASS_MIDI_MARK_TICK 0x10000 // flag: get position in ticks (otherwise bytes) + +// MIDI events +#define MIDI_EVENT_NOTE 1 +#define MIDI_EVENT_PROGRAM 2 +#define MIDI_EVENT_CHANPRES 3 +#define MIDI_EVENT_PITCH 4 +#define MIDI_EVENT_PITCHRANGE 5 +#define MIDI_EVENT_DRUMS 6 +#define MIDI_EVENT_FINETUNE 7 +#define MIDI_EVENT_COARSETUNE 8 +#define MIDI_EVENT_MASTERVOL 9 +#define MIDI_EVENT_BANK 10 +#define MIDI_EVENT_MODULATION 11 +#define MIDI_EVENT_VOLUME 12 +#define MIDI_EVENT_PAN 13 +#define MIDI_EVENT_EXPRESSION 14 +#define MIDI_EVENT_SUSTAIN 15 +#define MIDI_EVENT_SOUNDOFF 16 +#define MIDI_EVENT_RESET 17 +#define MIDI_EVENT_NOTESOFF 18 +#define MIDI_EVENT_PORTAMENTO 19 +#define MIDI_EVENT_PORTATIME 20 +#define MIDI_EVENT_PORTANOTE 21 +#define MIDI_EVENT_MODE 22 +#define MIDI_EVENT_REVERB 23 +#define MIDI_EVENT_CHORUS 24 +#define MIDI_EVENT_CUTOFF 25 +#define MIDI_EVENT_RESONANCE 26 +#define MIDI_EVENT_RELEASE 27 +#define MIDI_EVENT_ATTACK 28 +#define MIDI_EVENT_DECAY 29 +#define MIDI_EVENT_REVERB_MACRO 30 +#define MIDI_EVENT_CHORUS_MACRO 31 +#define MIDI_EVENT_REVERB_TIME 32 +#define MIDI_EVENT_REVERB_DELAY 33 +#define MIDI_EVENT_REVERB_LOCUTOFF 34 +#define MIDI_EVENT_REVERB_HICUTOFF 35 +#define MIDI_EVENT_REVERB_LEVEL 36 +#define MIDI_EVENT_CHORUS_DELAY 37 +#define MIDI_EVENT_CHORUS_DEPTH 38 +#define MIDI_EVENT_CHORUS_RATE 39 +#define MIDI_EVENT_CHORUS_FEEDBACK 40 +#define MIDI_EVENT_CHORUS_LEVEL 41 +#define MIDI_EVENT_CHORUS_REVERB 42 +#define MIDI_EVENT_USERFX 43 +#define MIDI_EVENT_USERFX_LEVEL 44 +#define MIDI_EVENT_USERFX_REVERB 45 +#define MIDI_EVENT_USERFX_CHORUS 46 +#define MIDI_EVENT_DRUM_FINETUNE 50 +#define MIDI_EVENT_DRUM_COARSETUNE 51 +#define MIDI_EVENT_DRUM_PAN 52 +#define MIDI_EVENT_DRUM_REVERB 53 +#define MIDI_EVENT_DRUM_CHORUS 54 +#define MIDI_EVENT_DRUM_CUTOFF 55 +#define MIDI_EVENT_DRUM_RESONANCE 56 +#define MIDI_EVENT_DRUM_LEVEL 57 +#define MIDI_EVENT_DRUM_USERFX 58 +#define MIDI_EVENT_SOFT 60 +#define MIDI_EVENT_SYSTEM 61 +#define MIDI_EVENT_TEMPO 62 +#define MIDI_EVENT_SCALETUNING 63 +#define MIDI_EVENT_CONTROL 64 +#define MIDI_EVENT_CHANPRES_VIBRATO 65 +#define MIDI_EVENT_CHANPRES_PITCH 66 +#define MIDI_EVENT_CHANPRES_FILTER 67 +#define MIDI_EVENT_CHANPRES_VOLUME 68 +#define MIDI_EVENT_MOD_VIBRATO 69 +#define MIDI_EVENT_MODRANGE 69 +#define MIDI_EVENT_BANK_LSB 70 +#define MIDI_EVENT_KEYPRES 71 +#define MIDI_EVENT_KEYPRES_VIBRATO 72 +#define MIDI_EVENT_KEYPRES_PITCH 73 +#define MIDI_EVENT_KEYPRES_FILTER 74 +#define MIDI_EVENT_KEYPRES_VOLUME 75 +#define MIDI_EVENT_SOSTENUTO 76 +#define MIDI_EVENT_MOD_PITCH 77 +#define MIDI_EVENT_MOD_FILTER 78 +#define MIDI_EVENT_MOD_VOLUME 79 +#define MIDI_EVENT_VIBRATO_RATE 80 +#define MIDI_EVENT_VIBRATO_DEPTH 81 +#define MIDI_EVENT_VIBRATO_DELAY 82 +#define MIDI_EVENT_MASTER_FINETUNE 83 +#define MIDI_EVENT_MASTER_COARSETUNE 84 +#define MIDI_EVENT_MIXLEVEL 0x10000 +#define MIDI_EVENT_TRANSPOSE 0x10001 +#define MIDI_EVENT_SYSTEMEX 0x10002 +#define MIDI_EVENT_SPEED 0x10004 +#define MIDI_EVENT_DEFDRUMS 0x10006 + +#define MIDI_EVENT_END 0 +#define MIDI_EVENT_END_TRACK 0x10003 + +#define MIDI_EVENT_NOTES 0x20000 +#define MIDI_EVENT_VOICES 0x20001 + +#define MIDI_SYSTEM_DEFAULT 0 +#define MIDI_SYSTEM_GM1 1 +#define MIDI_SYSTEM_GM2 2 +#define MIDI_SYSTEM_XG 3 +#define MIDI_SYSTEM_GS 4 + +typedef struct { + DWORD event; // MIDI_EVENT_xxx + DWORD param; + DWORD chan; + DWORD tick; // event position (ticks) + DWORD pos; // event position (bytes) +} BASS_MIDI_EVENT; + +// BASS_MIDI_StreamEvents modes +#define BASS_MIDI_EVENTS_STRUCT 0 // BASS_MIDI_EVENT structures +#define BASS_MIDI_EVENTS_RAW 0x10000 // raw MIDI event data +#define BASS_MIDI_EVENTS_SYNC 0x1000000 // flag: trigger event syncs +#define BASS_MIDI_EVENTS_NORSTATUS 0x2000000 // flag: no running status +#define BASS_MIDI_EVENTS_CANCEL 0x4000000 // flag: cancel pending events +#define BASS_MIDI_EVENTS_TIME 0x8000000 // flag: delta-time info is present +#define BASS_MIDI_EVENTS_ABSTIME 0x10000000 // flag: absolute time info is present +#define BASS_MIDI_EVENTS_ASYNC 0x20000000 // flag: process asynchronously +#define BASS_MIDI_EVENTS_FILTER 0x40000000 // flag: apply filtering +#define BASS_MIDI_EVENTS_FLUSH 0x80000000 // flag: flush async events + +// BASS_MIDI_StreamGetChannel special channels +#define BASS_MIDI_CHAN_CHORUS (DWORD)-1 +#define BASS_MIDI_CHAN_REVERB (DWORD)-2 +#define BASS_MIDI_CHAN_USERFX (DWORD)-3 + +// BASS_CHANNELINFO type +#define BASS_CTYPE_STREAM_MIDI 0x10d00 + +// Additional attributes +#define BASS_ATTRIB_MIDI_PPQN 0x12000 +#define BASS_ATTRIB_MIDI_CPU 0x12001 +#define BASS_ATTRIB_MIDI_CHANS 0x12002 +#define BASS_ATTRIB_MIDI_VOICES 0x12003 +#define BASS_ATTRIB_MIDI_VOICES_ACTIVE 0x12004 +#define BASS_ATTRIB_MIDI_STATE 0x12005 +#define BASS_ATTRIB_MIDI_SRC 0x12006 +#define BASS_ATTRIB_MIDI_KILL 0x12007 +#define BASS_ATTRIB_MIDI_SPEED 0x12008 +#define BASS_ATTRIB_MIDI_REVERB 0x12009 +#define BASS_ATTRIB_MIDI_VOL 0x1200a +#define BASS_ATTRIB_MIDI_TRACK_VOL 0x12100 // + track # + +// Additional tag type +#define BASS_TAG_MIDI_TRACK 0x11000 // + track #, track text : array of null-terminated ANSI strings + +// BASS_ChannelGetLength/GetPosition/SetPosition mode +#define BASS_POS_MIDI_TICK 2 // tick position + +typedef BOOL (CALLBACK MIDIFILTERPROC)(HSTREAM handle, int track, BASS_MIDI_EVENT *event, BOOL seeking, void *user); +/* Event filtering callback function. +handle : MIDI stream handle +track : Track containing the event +event : The event +seeking: TRUE = the event is being processed while seeking, FALSE = it is being played +user : The 'user' parameter value given when calling BASS_MIDI_StreamSetFilter +RETURN : TRUE = process the event, FALSE = drop the event */ + +// BASS_MIDI_FontLoadEx flags +#define BASS_MIDI_FONTLOAD_NOWAIT 1 // don't want for the samples to load +#define BASS_MIDI_FONTLOAD_COMPACT 2 // compact samples +#define BASS_MIDI_FONTLOAD_NOLOAD 4 // don't load (only compact) +#define BASS_MIDI_FONTLOAD_TIME 8 // length is in milliseconds +#define BASS_MIDI_FONTLOAD_KEEPDEC 16 // keep decoders + +// BASS_MIDI_FontPack flags +#define BASS_MIDI_PACK_NOHEAD 1 // don't send a WAV header to the encoder +#define BASS_MIDI_PACK_16BIT 2 // discard low 8 bits of 24-bit sample data +#define BASS_MIDI_PACK_48KHZ 4 // set encoding rate to 48000 Hz (else 44100 Hz) + +typedef struct { + const char *name; // description + DWORD id; + DWORD flags; +} BASS_MIDI_DEVICEINFO; + +typedef void (CALLBACK MIDIINPROC)(DWORD device, double time, const BYTE *buffer, DWORD length, void *user); +/* MIDI input callback function. +device : MIDI input device +time : Timestamp +buffer : Buffer containing MIDI data +length : Number of bytes of data +user : The 'user' parameter value given when calling BASS_MIDI_InInit */ + +DWORD BASSMIDIDEF(BASS_MIDI_GetVersion)(void); + +HSTREAM BASSMIDIDEF(BASS_MIDI_StreamCreate)(DWORD channels, DWORD flags, DWORD freq); +HSTREAM BASSMIDIDEF(BASS_MIDI_StreamCreateFile)(BOOL mem, const void *file, QWORD offset, QWORD length, DWORD flags, DWORD freq); +HSTREAM BASSMIDIDEF(BASS_MIDI_StreamCreateURL)(const char *url, DWORD offset, DWORD flags, DOWNLOADPROC *proc, void *user, DWORD freq); +HSTREAM BASSMIDIDEF(BASS_MIDI_StreamCreateFileUser)(DWORD system, DWORD flags, const BASS_FILEPROCS *procs, void *user, DWORD freq); +HSTREAM BASSMIDIDEF(BASS_MIDI_StreamCreateEvents)(const BASS_MIDI_EVENT *events, DWORD ppqn, DWORD flags, DWORD freq); +BOOL BASSMIDIDEF(BASS_MIDI_StreamGetMark)(HSTREAM handle, DWORD type, DWORD index, BASS_MIDI_MARK *mark); +DWORD BASSMIDIDEF(BASS_MIDI_StreamGetMarks)(HSTREAM handle, int track, DWORD type, BASS_MIDI_MARK *marks); +BOOL BASSMIDIDEF(BASS_MIDI_StreamSetFonts)(HSTREAM handle, const void *fonts, DWORD count); +DWORD BASSMIDIDEF(BASS_MIDI_StreamGetFonts)(HSTREAM handle, void *fonts, DWORD count); +BOOL BASSMIDIDEF(BASS_MIDI_StreamLoadSamples)(HSTREAM handle); +BOOL BASSMIDIDEF(BASS_MIDI_StreamEvent)(HSTREAM handle, DWORD chan, DWORD event, DWORD param); +DWORD BASSMIDIDEF(BASS_MIDI_StreamEvents)(HSTREAM handle, DWORD mode, const void *events, DWORD length); +DWORD BASSMIDIDEF(BASS_MIDI_StreamGetEvent)(HSTREAM handle, DWORD chan, DWORD event); +DWORD BASSMIDIDEF(BASS_MIDI_StreamGetEvents)(HSTREAM handle, int track, DWORD filter, BASS_MIDI_EVENT *events); +DWORD BASSMIDIDEF(BASS_MIDI_StreamGetEventsEx)(HSTREAM handle, int track, DWORD filter, BASS_MIDI_EVENT *events, DWORD start, DWORD count); +BOOL BASSMIDIDEF(BASS_MIDI_StreamGetPreset)(HSTREAM handle, DWORD chan, BASS_MIDI_FONT *font); +HSTREAM BASSMIDIDEF(BASS_MIDI_StreamGetChannel)(HSTREAM handle, DWORD chan); +BOOL BASSMIDIDEF(BASS_MIDI_StreamSetFilter)(HSTREAM handle, BOOL seeking, MIDIFILTERPROC *proc, void *user); + +HSOUNDFONT BASSMIDIDEF(BASS_MIDI_FontInit)(const void *file, DWORD flags); +HSOUNDFONT BASSMIDIDEF(BASS_MIDI_FontInitUser)(const BASS_FILEPROCS *procs, void *user, DWORD flags); +BOOL BASSMIDIDEF(BASS_MIDI_FontFree)(HSOUNDFONT handle); +BOOL BASSMIDIDEF(BASS_MIDI_FontGetInfo)(HSOUNDFONT handle, BASS_MIDI_FONTINFO *info); +BOOL BASSMIDIDEF(BASS_MIDI_FontGetPresets)(HSOUNDFONT handle, DWORD *presets); +const char *BASSMIDIDEF(BASS_MIDI_FontGetPreset)(HSOUNDFONT handle, int preset, int bank); +BOOL BASSMIDIDEF(BASS_MIDI_FontLoad)(HSOUNDFONT handle, int preset, int bank); +BOOL BASSMIDIDEF(BASS_MIDI_FontLoadEx)(HSOUNDFONT handle, int preset, int bank, DWORD length, DWORD flags); +BOOL BASSMIDIDEF(BASS_MIDI_FontUnload)(HSOUNDFONT handle, int preset, int bank); +BOOL BASSMIDIDEF(BASS_MIDI_FontCompact)(HSOUNDFONT handle); +BOOL BASSMIDIDEF(BASS_MIDI_FontPack)(HSOUNDFONT handle, const void *outfile, const void *encoder, DWORD flags); +BOOL BASSMIDIDEF(BASS_MIDI_FontUnpack)(HSOUNDFONT handle, const void *outfile, DWORD flags); +DWORD BASSMIDIDEF(BASS_MIDI_FontFlags)(HSOUNDFONT handle, DWORD flags, DWORD mask); +BOOL BASSMIDIDEF(BASS_MIDI_FontSetVolume)(HSOUNDFONT handle, float volume); +float BASSMIDIDEF(BASS_MIDI_FontGetVolume)(HSOUNDFONT handle); + +DWORD BASSMIDIDEF(BASS_MIDI_ConvertEvents)(const BYTE *data, DWORD length, BASS_MIDI_EVENT *events, DWORD count, DWORD flags); + +BOOL BASSMIDIDEF(BASS_MIDI_InGetDeviceInfo)(DWORD device, BASS_MIDI_DEVICEINFO *info); +BOOL BASSMIDIDEF(BASS_MIDI_InInit)(DWORD device, MIDIINPROC *proc, void *user); +BOOL BASSMIDIDEF(BASS_MIDI_InFree)(DWORD device); +BOOL BASSMIDIDEF(BASS_MIDI_InStart)(DWORD device); +BOOL BASSMIDIDEF(BASS_MIDI_InStop)(DWORD device); + +#ifdef __cplusplus +} + +static inline BOOL BASS_MIDI_StreamSetFonts(HSTREAM handle, const BASS_MIDI_FONTEX *fonts, DWORD count) +{ + return BASS_MIDI_StreamSetFonts(handle, (const void*)fonts, count | BASS_MIDI_FONT_EX); +} + +static inline BOOL BASS_MIDI_StreamSetFonts(HSTREAM handle, const BASS_MIDI_FONTEX2 *fonts, DWORD count) +{ + return BASS_MIDI_StreamSetFonts(handle, (const void*)fonts, count | BASS_MIDI_FONT_EX2); +} + +static inline DWORD BASS_MIDI_StreamGetFonts(HSTREAM handle, BASS_MIDI_FONTEX *fonts, DWORD count) +{ + return BASS_MIDI_StreamGetFonts(handle, (void*)fonts, count | BASS_MIDI_FONT_EX); +} + +static inline DWORD BASS_MIDI_StreamGetFonts(HSTREAM handle, BASS_MIDI_FONTEX2 *fonts, DWORD count) +{ + return BASS_MIDI_StreamGetFonts(handle, (void*)fonts, count | BASS_MIDI_FONT_EX2); +} + +#ifdef _WIN32 +static inline HSTREAM BASS_MIDI_StreamCreateFile(BOOL mem, const WCHAR *file, QWORD offset, QWORD length, DWORD flags, DWORD freq) +{ + return BASS_MIDI_StreamCreateFile(mem, (const void*)file, offset, length, flags | BASS_UNICODE, freq); +} + +static inline HSTREAM BASS_MIDI_StreamCreateURL(const WCHAR *url, DWORD offset, DWORD flags, DOWNLOADPROC *proc, void *user, DWORD freq) +{ + return BASS_MIDI_StreamCreateURL((const char*)url, offset, flags | BASS_UNICODE, proc, user, freq); +} + +static inline HSOUNDFONT BASS_MIDI_FontInit(const WCHAR *file, DWORD flags) +{ + return BASS_MIDI_FontInit((const void*)file, flags|BASS_UNICODE); +} + +static inline BOOL BASS_MIDI_FontPack(HSOUNDFONT handle, const WCHAR *outfile, const WCHAR *encoder, DWORD flags) +{ + return BASS_MIDI_FontPack(handle, (const void*)outfile, (const void*)encoder, flags | BASS_UNICODE); +} + +static inline BOOL BASS_MIDI_FontUnpack(HSOUNDFONT handle, const WCHAR *outfile, DWORD flags) +{ + return BASS_MIDI_FontUnpack(handle, (const void*)outfile, flags | BASS_UNICODE); +} +#endif +#endif + +#ifdef __OBJC__ +#undef BOOL +#endif + +#endif diff --git a/dependencies/bass/include/bassopus.h b/dependencies/bass/include/bassopus.h new file mode 100644 index 0000000..7a535ae --- /dev/null +++ b/dependencies/bass/include/bassopus.h @@ -0,0 +1,61 @@ +/* + BASSOPUS 2.4 C/C++ header file + Copyright (c) 2012-2015 Un4seen Developments Ltd. + + See the BASSOPUS.CHM file for more detailed documentation +*/ + +#ifndef BASSOPUS_H +#define BASSOPUS_H + +#include "bass.h" + +#if BASSVERSION!=0x204 +#error conflicting BASS and BASSOPUS versions +#endif + +#ifdef __OBJC__ +typedef int BOOL32; +#define BOOL BOOL32 // override objc's BOOL +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef BASSOPUSDEF +#define BASSOPUSDEF(f) WINAPI f +#endif + +// BASS_CHANNELINFO type +#define BASS_CTYPE_STREAM_OPUS 0x11200 + +// Additional attributes +#define BASS_ATTRIB_OPUS_ORIGFREQ 0x13000 +#define BASS_ATTRIB_OPUS_GAIN 0x13001 + +HSTREAM BASSOPUSDEF(BASS_OPUS_StreamCreateFile)(BOOL mem, const void *file, QWORD offset, QWORD length, DWORD flags); +HSTREAM BASSOPUSDEF(BASS_OPUS_StreamCreateURL)(const char *url, DWORD offset, DWORD flags, DOWNLOADPROC *proc, void *user); +HSTREAM BASSOPUSDEF(BASS_OPUS_StreamCreateFileUser)(DWORD system, DWORD flags, const BASS_FILEPROCS *procs, void *user); + +#ifdef __cplusplus +} + +#if defined(_WIN32) && !defined(NOBASSOVERLOADS) +static inline HSTREAM BASS_OPUS_StreamCreateFile(BOOL mem, const WCHAR *file, QWORD offset, QWORD length, DWORD flags) +{ + return BASS_OPUS_StreamCreateFile(mem, (const void*)file, offset, length, flags|BASS_UNICODE); +} + +static inline HSTREAM BASS_OPUS_StreamCreateURL(const WCHAR *url, DWORD offset, DWORD flags, DOWNLOADPROC *proc, void *user) +{ + return BASS_OPUS_StreamCreateURL((const char*)url, offset, flags|BASS_UNICODE, proc, user); +} +#endif +#endif + +#ifdef __OBJC__ +#undef BOOL +#endif + +#endif diff --git a/dependencies/bass/lib/bassflac.dll b/dependencies/bass/lib/bassflac.dll new file mode 100644 index 0000000..d0684c0 Binary files /dev/null and b/dependencies/bass/lib/bassflac.dll differ diff --git a/dependencies/bass/lib/bassflac.lib b/dependencies/bass/lib/bassflac.lib new file mode 100644 index 0000000..e26020e Binary files /dev/null and b/dependencies/bass/lib/bassflac.lib differ diff --git a/dependencies/bass/lib/bassmidi.dll b/dependencies/bass/lib/bassmidi.dll new file mode 100644 index 0000000..2722419 Binary files /dev/null and b/dependencies/bass/lib/bassmidi.dll differ diff --git a/dependencies/bass/lib/bassmidi.lib b/dependencies/bass/lib/bassmidi.lib new file mode 100644 index 0000000..9e4947c Binary files /dev/null and b/dependencies/bass/lib/bassmidi.lib differ diff --git a/dependencies/bass/lib/bassopus.dll b/dependencies/bass/lib/bassopus.dll new file mode 100644 index 0000000..8059f03 Binary files /dev/null and b/dependencies/bass/lib/bassopus.dll differ diff --git a/dependencies/bass/lib/bassopus.lib b/dependencies/bass/lib/bassopus.lib new file mode 100644 index 0000000..5ba778d Binary files /dev/null and b/dependencies/bass/lib/bassopus.lib differ diff --git a/docs/docs/index.md b/docs/docs/index.md index 3ba8e20..3bd3f64 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -69,7 +69,7 @@ so you can use the plugin for free in any project[^1]. It's built using the new [union-api](https://gitlab.com/union-framework/union-api) and can be embedded either as a Union 1.0m plugin or as a completely standalone plugin for base Gothic with System Pack. -Check out [Getting Started](getting-started/index.md) for instructions how to start working with zBassMusic. +Check out [Getting Started](user-guide/getting-started/index.md) for instructions how to start working with zBassMusic. [^1]: zBassMusic depends on vendored libraries [union-api](https://gitlab.com/union-framework/union-api) and [gothic-api]() diff --git a/docs/docs/reference/classes/C_BassMusic_Theme.md b/docs/docs/reference/classes/C_BassMusic_Theme.md new file mode 100644 index 0000000..3d6a413 --- /dev/null +++ b/docs/docs/reference/classes/C_BassMusic_Theme.md @@ -0,0 +1,33 @@ +# C_BassMusic_Theme + +## Description + +The class represents a music theme with zBassMusic extensions that can be played by the plugin. + +## Definition + +```dae +const int BASSMUSIC_THEME_TYPE_NORMAL = 0; + +class C_BassMusic_Theme { + var string name; + var string zones; + var int type; +}; + +prototype BassMusic_Theme(C_BassMusic_Theme) { + type = BASSMUSIC_THEME_TYPE_NORMAL; +}; +``` + +## Fields + +| Field | Type | Description | +|-------|--------|-----------------------------------------------------------------------------------| +| name | string | The name (identifier) of a theme. | +| zones | string | Comma-separated list of zones which play this theme (e.g. "SYS_MENU,OC_DAY_STD"). | +| type | int | Reserved for future use. Set it to BASSMUSIC_THEME_TYPE_NORMAL for now. | + +| type | Value | Type | +|-----------------------------|-------|------------------| +| BASSMUSIC_THEME_TYPE_NORMAL | 0 | The normal type. | \ No newline at end of file diff --git a/docs/docs/reference/classes/C_BassMusic_ThemeAudio.md b/docs/docs/reference/classes/C_BassMusic_ThemeAudio.md new file mode 100644 index 0000000..16f6113 --- /dev/null +++ b/docs/docs/reference/classes/C_BassMusic_ThemeAudio.md @@ -0,0 +1,54 @@ +# C_BassMusic_ThemeAudio + +## Description + +The class represents an audio file assigned to a music theme. + +## Definition + +```dae +class C_BassMusic_ThemeAudio { + var string theme; + var string type; + var string filename; + var string midifile; + var float volume; + var int loop; + var int reverb; + var float reverbMix; + var float reverbTime; + var int fadeIn; + var int fadeInDuration; + var int fadeOut; + var int fadeOutDuration; +}; + +prototype BassMusic_ThemeAudio(C_BassMusic_ThemeAudio) { + type = "DEFAULT"; + volume = 1; + loop = 1; + reverb = 0; + fadeIn = 1; + fadeInDuration = 2000; + fadeOut = 1; + fadeOutDuration = 2000; +}; +``` + +## Fields + +| Field | Type | Description | +|-----------------|------------|-------------------------------------------------------------------------------------| +| theme | string | The name (identifier) of a theme. | +| type | string | Type of the audio. Use "DEFAULT" to play it as music. | +| filename | string | Filename of the audio file. | +| midifile | string | Filename of a MIDI file for transition control. | +| vol | float | Volume of the song in range [0, 1] as the % of master volume. | +| loop | int (bool) | If not zero, the theme will loop. Otherwise, it will play only once per zone enter. | +| reverb | int (bool) | If not zero, the Reverb DX8 effect is enabled. | +| reverbmix | float | Mix property for Reverb DX8 effect in range [-96, 0] dB. | +| reverbtime | float | Time property for Reverb DX8 effect in range [0.001, 3000] ms. | +| fadeIn | int (bool) | If not zero, the fade-in effect is enabled. | +| fadeInDuration | float | Fade-in effect duration in milliseconds. | +| fadeOut | int (bool) | If not zero, the fade-out effect is enabled. | +| fadeOutDuration | float | Fade-out effect duration in milliseconds. | \ No newline at end of file diff --git a/docs/docs/reference/classes/C_MUSICTHEME.md b/docs/docs/reference/classes/C_MUSICTHEME.md index fb6273b..d7606f9 100644 --- a/docs/docs/reference/classes/C_MUSICTHEME.md +++ b/docs/docs/reference/classes/C_MUSICTHEME.md @@ -17,16 +17,36 @@ class C_MUSICTHEME var int transtype; var int transsubtype; }; + +prototype C_MUSICTHEME_DEF(C_MUSICTHEME) +{ + vol = 1; + loop = 1; + transtype = TRANSITION_TYPE_NONE; + transsubtype = TRANSITION_SUB_TYPE_MEASURE; + reverbmix = -80; + reverbtime = 9000; +}; ``` ## Fields -| Field | Type | Description | -|----------------|--------|-------------------------------------| -| `file` | string | The filename of the music theme. | -| `vol` | float | The volume of the music theme. | -| `loop` | int | If true, music will loop after nd | -| `reverbmix` | float | The reverb mix of the music theme. | -| `reverbtime` | float | The reverb time of the music theme. | -| `transtype` | int | The type of the transition. | -| `transsubtype` | int | The subtype of the transition. | \ No newline at end of file +| Field | Type | Description | +|--------------|------------|-------------------------------------------------------------------------------------| +| file | string | The filename of an audio file. | +| vol | float | Volume of the song in range [0, 1] as the % of master volume. | +| loop | int (bool) | If not zero, the theme will loop. Otherwise, it will play only once per zone enter. | +| reverbmix | float | Mix property for Reverb DX8 effect in range [-96, 0] dB. | +| reverbtime | float | Time property for Reverb DX8 effect in range [0.001, 3000] ms. | +| transtype | int | Transition type. Check the table below. | +| transsubtype | int | Ignored. | + +| transtype | Value | Transition | +|-----------------------------|-------|------------------------------------------------------------------------------------------------------------| +| TRANSITION_TYPE_NONE | 1 | Ignored. | +| TRANSITION_TYPE_GROOVE | 2 | Ignored. | +| TRANSITION_TYPE_FILL | 3 | Ignored. | +| TRANSITION_TYPE_BREAK | 4 | Ignored. | +| TRANSITION_TYPE_INTRO | 5 | Enables fade-in transition with the duration form [BassMusic].TransitionTime ini option. | +| TRANSITION_TYPE_END | 6 | Enables fade-out transition with the duration form [BassMusic].TransitionTime ini option. | +| TRANSITION_TYPE_ENDANDINTRO | 7 | Enables both fade-in and fade-out transition with the duration form [BassMusic].TransitionTime ini option. | \ No newline at end of file diff --git a/docs/docs/reference/externals/index.md b/docs/docs/reference/externals/index.md index 2bcfee5..afff37a 100644 --- a/docs/docs/reference/externals/index.md +++ b/docs/docs/reference/externals/index.md @@ -109,22 +109,67 @@ func void BassMusic_Opt_ForceFadeTransition(var int enabled) {} * `#!dae var int enabled`
If true, the fade transition is globally enabled -## BassMusic_TransitionRule_OnBeat +## BassMusic_AddMidiFile -Add a new OnBeat rule to the Transition Scheduler. +Add a [MIDI File](../../user-guide/transitions/midi.md) to a theme. ```dae -func void BassMusic_TransitionRule_OnBeat(var string name, var string interval, var string timeCodes) {} +func void BassMusic_AddMidiFile(var string theme, var string filter, var string midiFilename) {} ``` **Parameters** -* `#!dae var string name`
- Input music theme name -* `#!dae var string interval`
- String formatted as double, the interval of a beat in seconds. Set "0" to disable. -* `#!dae var string timeCodes`
- String formatted as a list of semicolon-separated doubles, the time codes in seconds when the transition can happen, - eg. `"0.0;1.0;2.0;3.0"` +* `#!dae var string theme`
+ Music theme name +* `#!dae var string filter`
+ [Transition filter](../../user-guide/transitions/index.md#filter) +* `#!dae var string midiFilename`
+ Filename of a MIDI file +## BassMusic_AddTransitionTimePoint + +Add a time point for [Timing Transition](../../user-guide/transitions/timing.md). + +```dae +func void BassMusic_AddTransitionTimePoint(var string theme, var string filter, var float start, var float duration, var int effect, var float nextStart, var float nextDuration, var int nextEffect) {} +``` + +**Parameters** + +* `#!dae var string theme`
+ Music theme name +* `#!dae var string filter`
+ [Transition filter](../../user-guide/transitions/index.md#filter) +* `#!dae var float start`
+ Transition start in seconds +* `#!dae var float duration`
+ Transition duration in seconds +* `#!dae var int effect`
+ Effect ID (NONE = 0, CROSSFADE = 0) +* `#!dae var float nextStart`
+ Transition start in seconds (next song) +* `#!dae var float nextDuration`
+ Transition duration in seconds (next song) +* `#!dae var int nextEffect`
+ Effect ID (NONE = 0, CROSSFADE = 0) + +## BassMusic_AddJingle + +Add a [Jingle](../../user-guide/transitions/jingle.md) to a theme. + +```dae +func void BassMusic_AddJingle(var string theme, var string filter, var string jingle, var float delay) {} +``` + +**Parameters** + +* `#!dae var string theme`
+ Music theme name +* `#!dae var string filter`
+ [Transition filter](../../user-guide/transitions/index.md#filter) +* `#!dae var string jingle`
+ Filename of a jingle audio file +* `#!dae var float delay` + Delay in seconds before the jingle starts + \ No newline at end of file diff --git a/docs/docs/reference/globals/index.md b/docs/docs/reference/globals/index.md index 9c12578..368843e 100644 --- a/docs/docs/reference/globals/index.md +++ b/docs/docs/reference/globals/index.md @@ -12,10 +12,26 @@ var string BassMusic_ActiveThemeFilename; ## BassMusic_ActiveThemeID -Hods the ID (symbol name) of the currently active theme. +Holds the ID (symbol name) of the currently active theme. ```dae var string BassMusic_ActiveThemeID; ``` +## BassMusic_EventThemeFilename + +Holds the filename of the theme for current event. + +```dae +var string BassMusic_EventThemeFilename; +``` + +## BassMusic_EventThemeID + +Holds the ID (symbol name) of the theme for current event. + +```dae +var string BassMusic_EventThemeID; +``` + \ No newline at end of file diff --git a/docs/docs/reference/index.md b/docs/docs/reference/index.md index 2a75dc1..6293764 100644 --- a/docs/docs/reference/index.md +++ b/docs/docs/reference/index.md @@ -3,11 +3,15 @@ ## Classes * [C_MUSICTHEME](classes/C_MUSICTHEME.md) +* [C_BassMusic_Theme](classes/C_BassMusic_Theme.md) +* [C_BassMusic_ThemeAudio](classes/C_BassMusic_ThemeAudio.md) ## Globals * [BassMusic_ActiveThemeFilename](globals/index.md#bassmusic_activethemefilename) * [BassMusic_ActiveThemeID](globals/index.md#bassmusic_activethemeid) +* [BassMusic_EventThemeFilename](globals/index.md#bassmusic_eventthemefilename) +* [BassMusic_EventThemeID](globals/index.md#bassmusic_eventthemeid) ## Externals @@ -19,7 +23,9 @@ * [BassMusic_Opt_TransitionTime](externals/index.md#bassmusic_opt_transitiontime) * [BassMusic_Opt_ForceDisableReverb](externals/index.md#bassmusic_opt_forcedisablereverb) * [BassMusic_Opt_ForceFadeTransition](externals/index.md#bassmusic_opt_forcefadetransition) -* [BassMusic_TransitionRule_OnBeat](externals/index.md#bassmusic_transitionrule_onbeat) +* [BassMusic_AddMidiFile](externals/index.md#bassmusic_addmidifile) +* [BassMusic_AddTransitionTimePoint](externals/index.md#bassmusic_addtransitiontimepoint) +* [BassMusic_AddJingle](externals/index.md#bassmusic_addjingle) ## Options (.ini) diff --git a/docs/docs/roadmap/index.md b/docs/docs/roadmap/index.md index 3e721f2..b001580 100644 --- a/docs/docs/roadmap/index.md +++ b/docs/docs/roadmap/index.md @@ -14,7 +14,6 @@ hide: ## Done -### Core functions ??? success "Music Engine (v0.1.0)" Music engine capable of replacing the original DM system in the scope of playing audio and switching songs based on game events. @@ -25,13 +24,8 @@ hide: Every music theme can opt-in for simple fade-in and fade-out transitions to smoothly crossfade changing themes. The crossfade time is set globally and the transition starts instantly after receiving an event from the game. - -## In Progress - -### Transition Scheduler - -??? info "Transition Scheduler (v0.2.x)" - Scheduler for executing advanced transitions between songs based on defined rules. The system should be flexible and offer different +??? success "Transition Scheduler (v0.3.0)" + Scheduler for executing advanced transitions between songs based on defined rules. The system should be flexible and offer different transition effects behind an easy-to-use interface. The artist should be able to define rules with high precision (soft goal: <10ms latency). ??? success "Instant Transition (Done)" @@ -41,11 +35,11 @@ hide: Transition accepts a list of time points when the transition can happen and schedules it for the closest point. This way the song may switch exactly in a moment when the beat ends to match the rhythm. - ??? warning "Jingle Transition (To Do)" + ??? success "Jingle Transition (Done)" Transition plays an additional short audio during the transition as a one-time jingle. For example, battle music transitioning into normal can play some theme to emphasize the end of a fight. -??? info "MIDI bridge for Transition Scheduler (v0.2.x)" +??? success "MIDI bridge for Transition Scheduler (v0.3.0)" The MIDI format is the best option for defining rules for the Transition Scheduler. The composer can just put all the information about the transitions as MIDI events on some muted track in DAW and export it to the plugin. The composer uses a tool he knows, and we can extract precise information from the MIDI file perfectly synchronized @@ -54,11 +48,19 @@ hide: The bridge defines a spec how to interpret MIDI events and how they map to the internal structures of the Transition Scheduler. Then the bridge can load MIDI files and provide the rules to the scheduler. +## In Progress + +??? info "Additional Transitions (v0.4.0)" + Additional transition options based on ideas from the composer. + ## Future Plans ??? question "New features?" We still may plan to implement some new features if they can provide value. +??? info "Adaptive Audio" + Add additional options for setting up adaptive music. + ??? danger "Support late injection" Right now the plugin hooks early into Gothic initialization to set itself up and replace the `zmusic` pointer with a custom implementation. We should add support for late initialization and let the plugin be loaded by Daedalus/Ikaus scripts. diff --git a/docs/docs/user-guide/concepts/index.md b/docs/docs/user-guide/concepts/index.md new file mode 100644 index 0000000..adb09e4 --- /dev/null +++ b/docs/docs/user-guide/concepts/index.md @@ -0,0 +1,41 @@ +# Concepts + +## Music Theme + +Music Theme is a single song in the game soundtrack. It contains Audio Files, Effects, Transitions and Zones. + +### Audio File + +An audio file with the song or a jingle in a specific format (MP3, WAVE, OPUS, FLAC). +The audio files are placed in `_work/Data/Music/` directory and referenced by a Music Theme. + +### Effects + +Effects are the audio effects applied on a song during playback. Supported effects include: + +* Volume - volume level of the song as a percent of master volume. +* Reverb DX8 - reverb effects based on DirectX 8 implementation +* Fade In - fade-in transition when the song starts +* Fade OUt - fade-out transition when the song ends + +### Transitions + +Rules defining how the to schedule the change of a song. Transition may define the time span when change +occurs and the effects used to switch thet theme. + +### Zone + +The area where a song will play. Music zone are created using Spacer and a song may be attached to it. + +## VDFS + +Virtual file system used by Gothic to load assets. zBassMusic supports it fully, and we may reference some directory as: + +### Physical + +Located on your computer hard drive in the Gothic installation directory + +### Virtual + +A *.vdf or *.mod file loaded by Gothic, they contain the same directory structure as physical, +but all files are inside the file (like a ZIP archive). \ No newline at end of file diff --git a/docs/docs/user-guide/getting-started/plugin-loading.md b/docs/docs/user-guide/getting-started/plugin-loading.md index e6cccaf..b60f40f 100644 --- a/docs/docs/user-guide/getting-started/plugin-loading.md +++ b/docs/docs/user-guide/getting-started/plugin-loading.md @@ -24,7 +24,7 @@ runtime is there. Choose either ZIP or VDF. Both work the same. From ZIP release:
- Copy `zBassMusic.dll`, `UnionAPI.dll`, `bass.dll`
+ Copy `zBassMusic.dll`, `UnionAPI.dll`, `bass.dll`, `bassmidi.dll`, `bassopus.dll`, `bassflac.dll`
To `/System/Autorun/` From VDF release:
@@ -58,7 +58,7 @@ file but it's always global. ??? danger "Global Method" From ZIP release:
- Copy `zBassMusic.dll`, `UnionAPI.dll`, `bass.dll`
+ Copy `zBassMusic.dll`, `UnionAPI.dll`, `bass.dll`, `bassmidi.dll`, `bassopus.dll`, `bassflac.dll`
To `/System/` Create `pre.load` file in `/System/` with content: @@ -79,7 +79,7 @@ The function is conveniently provided by Ikarus and this method is always isolat ??? success "Isolated Method" From ZIP release:
- Copy `zBassMusic.dll`, `UnionAPI.dll`, `bass.dll`
+ Copy `zBassMusic.dll`, `UnionAPI.dll`, `bass.dll`, `bassmidi.dll`, `bassopus.dll`, `bassflac.dll`
To `/System/` In your `Startup.d` add to Init: diff --git a/docs/docs/user-guide/scripting/index.md b/docs/docs/user-guide/scripting/index.md index 5df70a1..2019e48 100644 --- a/docs/docs/user-guide/scripting/index.md +++ b/docs/docs/user-guide/scripting/index.md @@ -1,7 +1,7 @@ # Scripting (Daedalus) The plugin defines several external functions that let us interact with the Music Engine from Daedalus scripts. -The full list of available functions can be found in the [Reference](../reference/index.md) page, and here we +The full list of available functions can be found in the [Reference](../../reference/index.md) page, and here we will cover only the "Full Script Control" mode. ## Full Script Control diff --git a/docs/docs/user-guide/theme-definition/basstheme.md b/docs/docs/user-guide/theme-definition/basstheme.md new file mode 100644 index 0000000..0161edc --- /dev/null +++ b/docs/docs/user-guide/theme-definition/basstheme.md @@ -0,0 +1,77 @@ +--- +hide: + - toc +--- + +# zBassMusic Theme + +zBassMusic defines custom classes for music themes to support additional information. In order to use it, place +following code in your music scripts, for example in `System/Music/MusicInst.d`. + +```dae +const int BASSMUSIC_THEME_TYPE_NORMAL = 0; + +class C_BassMusic_Theme { + var string name; + var string zones; + var int type; +}; + +class C_BassMusic_ThemeAudio { + var string theme; + var string type; + var string filename; + var string midifile; + var float volume; + var int loop; + var int reverb; + var float reverbMix; + var float reverbTime; + var int fadeIn; + var int fadeInDuration; + var int fadeOut; + var int fadeOutDuration; +}; + +prototype BassMusic_Theme(C_BassMusic_Theme) { + type = BASSMUSIC_THEME_TYPE_NORMAL; +}; + +prototype BassMusic_ThemeAudio(C_BassMusic_ThemeAudio) { + type = "DEFAULT"; + volume = 1; + loop = 1; + reverb = 0; + fadeIn = 1; + fadeInDuration = 2000; + fadeOut = 1; + fadeOutDuration = 2000; +}; +``` + +To define a music theme you can create an instance of `BassMusic_Theme` prototype and also a default audio file +using `BassMusic_ThemeAudio` prototype. + +```dae +instance MyCustomTheme(BassMusic_Theme) +{ + name = "MyCustomTheme"; + zones = "SYS_MENU,OC_DAY_STD"; +}; + +instance MyCustomTheme_Audio(BassMusic_ThemeAudio) +{ + theme = "MyCustomTheme"; + filename = "SYS_MENU.mp3"; +}; +``` + +Theme defines zones to play in as a comma-separated list in `zones` field. +We treat both the zone (e.g. OC_DAY) and variant (e.g. STD) as a single entity. +Each theme variant would use a separate instance. + +The audio `theme` field must be the same as theme's `name` to assign it for this theme. + +The audio instance must use `DEFAULT` as a `type`, in order to play. +In the future, we may introduce features that use multiple files per theme, hence this separation. + diff --git a/docs/docs/user-guide/theme-definition/cmusictheme.md b/docs/docs/user-guide/theme-definition/cmusictheme.md new file mode 100644 index 0000000..87b59e0 --- /dev/null +++ b/docs/docs/user-guide/theme-definition/cmusictheme.md @@ -0,0 +1,53 @@ +# C_MUSICTHEME + +Original Gothic class for music is defined in `System/_Intern/Music.d` script. You can create its instances +to define music themes for zBassMusic. + +```dae +class C_MUSICTHEME +{ + var string file; + var float vol; + var int loop; + var float reverbmix; + var float reverbtime; + var int transtype; + var int transsubtype; +}; + +prototype C_MUSICTHEME_DEF(C_MUSICTHEME) +{ + vol = 1; + loop = 1; + transtype = TRANSITION_TYPE_NONE; + transsubtype = TRANSITION_SUB_TYPE_MEASURE; + reverbmix = -80; + reverbtime = 9000; +}; +``` + +To define a music theme using this class, you create an instance in `System/Music/MusicInst.d` script. +The name of an instance must be the same as the music zone where it plays. + +```dae +// OC_Day_Std = Old Camp, Day, Standard +instance OC_Day_Std(C_MUSICTHEME_DEF) +{ + file = "MyCustomMusic.mp3"; + transtype = TRANSITION_TYPE_ENDANDINTRO; + reverbmix = -12; + reverbtime = 3000; + vol = 1; + loop = 1; +}; +``` + +## Limitations + +The original class is not detailed enough to use all zBassMusic features. The limitations are: + +* The instance name must match music zone + variant. Only one song per music zone + variant is possible. +* `reverbmix` and `reverbtime` parameters have a strict restriction on range. zBassMusic will clamp the values to [-96, 0] and [0.001, 3000], respectively. +* `transtype` and `transsubtype` are DirectMusic-specific transitions that are impossible to 100% emulate in zBassMusic. + +If you would like to have more options, use [zBassMusic Theme](basstheme.md) classes instead. diff --git a/docs/docs/user-guide/theme-definition/index.md b/docs/docs/user-guide/theme-definition/index.md new file mode 100644 index 0000000..901553c --- /dev/null +++ b/docs/docs/user-guide/theme-definition/index.md @@ -0,0 +1,9 @@ +# Themes + +Music Theme is a single song in the soundtrack that needs to be defined in the music scripts for zBassMusic to know about. +Each theme has an audio file that needs to be placed inside `_work/Data/Music/` directory (physical or virtual). + +You can define a theme using either: + +* [C_MUSICTHEME](cmusictheme.md) - original class from Gothic, limited to basic settings +* [zBassMusic Theme](basstheme.md) - new class for defining advanced settings in zBassMusic \ No newline at end of file diff --git a/docs/docs/user-guide/transition-scheduler/index.md b/docs/docs/user-guide/transition-scheduler/index.md deleted file mode 100644 index 85e7323..0000000 --- a/docs/docs/user-guide/transition-scheduler/index.md +++ /dev/null @@ -1,15 +0,0 @@ -# Transitions Scheduler - -!!! note "Preview" -This feature is still under development and may change significantly in the future. - -Transition Scheduler is a module providing advanced transitions between music themes that need to be precisely -synchronized and accord to the defined rules. -For example, OnBeat rule limits the time points when the transition can occur and schedules the transition for -the closest point in the future, so the composer can design seamless transitions with a perfect match to the beat. - -Available transition rules: - -* [Instant](instant.md) - immediate transition [default] -* [OnBeat](on-beat.md) - transition only on specific time points -* [Jingle](jingle.md) - transition with another sound inbetween \ No newline at end of file diff --git a/docs/docs/user-guide/transition-scheduler/instant.md b/docs/docs/user-guide/transition-scheduler/instant.md deleted file mode 100644 index d5187e4..0000000 --- a/docs/docs/user-guide/transition-scheduler/instant.md +++ /dev/null @@ -1,3 +0,0 @@ -# Instant Transition - -Instant transition is the default transition rule. It immediately switches to the next theme without any delay. \ No newline at end of file diff --git a/docs/docs/user-guide/transition-scheduler/jingle.md b/docs/docs/user-guide/transition-scheduler/jingle.md deleted file mode 100644 index 8931387..0000000 --- a/docs/docs/user-guide/transition-scheduler/jingle.md +++ /dev/null @@ -1,5 +0,0 @@ -# Jingle Transition - -!!! danger "Not implemented" -This feature is not implemented yet. - diff --git a/docs/docs/user-guide/transition-scheduler/on-beat.md b/docs/docs/user-guide/transition-scheduler/on-beat.md deleted file mode 100644 index b8b99dc..0000000 --- a/docs/docs/user-guide/transition-scheduler/on-beat.md +++ /dev/null @@ -1,37 +0,0 @@ -# OnBeat Transition - -!!! note "Preview" -This feature is still under development and may change significantly in the future. - -OnBeat transition is a rule that schedules the transition for the closest point in the future from the list of -allowed points. -In this Release, the OnBeat rule can be activated only by using a Daedalus external and matches the input theme -to any output. -In the future, we plan to allow providing time points using a MIDI file synchronized with the music -and scope the rule to specific (input, output) pairs. - -## Daedalus API - -### Activate on an interval - -The second argument accepts a string formatted as a double that defines the interval of the beat in seconds. -Transition can happen at any multiplier of the interval. - -```dae -// Will activate on [2.66, 5.32, 7.98, ...] -BassMusic_TransitionRule_OnBeat("Input_Theme", "2.66", ""); -``` - -### Activate on a specific time points - -The third argument accepts a string formatted as a semicolon-separated list of doubles that defines the exact time -points in seconds. -Transition can happen only at the specified time points. - -```dae -// Will activate on [1.2, 2.4, 7.33, 12.97] -BassMusic_TransitionRule_OnBeat("Input_Theme", "0", "1.2;2.4;7.33;12.97"); -``` - -You can also mix both arguments to create a rule with constant interval + some additional points. -The scheduler will always choose the closest point in the future. \ No newline at end of file diff --git a/docs/docs/user-guide/transitions/a4.webp b/docs/docs/user-guide/transitions/a4.webp new file mode 100644 index 0000000..5f97b81 Binary files /dev/null and b/docs/docs/user-guide/transitions/a4.webp differ diff --git a/docs/docs/user-guide/transitions/asharp4.webp b/docs/docs/user-guide/transitions/asharp4.webp new file mode 100644 index 0000000..489a205 Binary files /dev/null and b/docs/docs/user-guide/transitions/asharp4.webp differ diff --git a/docs/docs/user-guide/transitions/index.md b/docs/docs/user-guide/transitions/index.md new file mode 100644 index 0000000..c09ef9e --- /dev/null +++ b/docs/docs/user-guide/transitions/index.md @@ -0,0 +1,46 @@ +# Transitions + +zBassMusic implements a scheduler to do advanced transition between music themes. +This allows a composer or developer to set up seamless transitions for a greater artistic value of a soundtrack. + +## Settings + +### Default + +If no transition was explicitly defined, the default strategy implements an immediate cross-fade, taking into +account the fade-in and fade-out effect settings of the themes. + +### Timing + +Timing settings let you limit on which moments a transition may happen and what time spans to use for effects. +Check [Timing](timing.md) for more information. + +### Jingle + +Jingle plays an additional audio file in-between transitioning songs. You can use it, for example to transition +from fight music to standard music with a nice finish sound. Check [Jingle](jingle.md) for more information. + +### MIDI Files + +Some time-dependent parameters of a transition can be set up in a special MIDI track to make it easier for an artist to design. +Check [MIDI Files](midi.md) for more information. + +## Filter + +A transition is assigned to Music Theme with a many-to-one relation. +Every function to add a transition has a `filter` parameter that defines for which target themes this transition applies. + +To set up default transition, use an empty string ("") for the filer. Default filter matches every transition which +wasn't explicitly matched by another. + +If `filter` is not an empty string, it contains a regular expression for the target theme to match. + +* `#!regex TargetThemeA` - matches every string that includes `TargetThemeA` +* `#!regex ^TargetThemeA$` - matches string that equals exactly `TargetThemeA` +* `#!regex TargetThemeA|TargetThemeB|TargetThemeC` - matches every string that includes one of `TargetThemeA`, `TargetThemeB`, `TargetThemeC` +* `#!regex ^(TargetThemeA|TargetThemeB|TargetThemeC)$` - matches every string that equals exactly one of `TargetThemeA`, `TargetThemeB`, `TargetThemeC` +* `#!regex ^Target` - matches every string that starts with `Target` +* `#!regex Fight$` - matches every string that ends with `Fight` + +Filters are tested in no guaranteed order and the first match wins. +If you are using fuzzy matchers, make sure to avoid overlaps or your transitions will be non-deterministic. diff --git a/docs/docs/user-guide/transitions/jingle-transition.webp b/docs/docs/user-guide/transitions/jingle-transition.webp new file mode 100644 index 0000000..942562c Binary files /dev/null and b/docs/docs/user-guide/transitions/jingle-transition.webp differ diff --git a/docs/docs/user-guide/transitions/jingle.md b/docs/docs/user-guide/transitions/jingle.md new file mode 100644 index 0000000..e355ab7 --- /dev/null +++ b/docs/docs/user-guide/transitions/jingle.md @@ -0,0 +1,26 @@ +# Jingle + +Jingle plays an additional audio file in-between transitioning songs. You can use it, for example to transition +from fight music to standard music with a nice finish sound. To define it you need: + +* Audio File - audio file located in `_work/Data/Music` +* Delay - delay in seconds from the start of ending transition before the jingle starts + +Jingle works alongside Default and Timing, scheduling itself on every transition. + +![Jingle Transition](jingle-transition.webp) + +## Daedalus + +TO set up jingle, you have to add it to a theme using Daedalus API. + +```dae +BassMusic_AddJingle( + "MyThemeToTransition", // theme + "OC_DAY_STD", // filter; this works only on MyThemeToTransition->OC_DAY_STD transition + "JingleFile.wav", // audio file + 1.0 // delay in seconds +); +``` + +Only one Jingle can be configured for (theme, filter) pair, but you can have multiple Jingles with different filters. \ No newline at end of file diff --git a/docs/docs/user-guide/transitions/midi-1.png b/docs/docs/user-guide/transitions/midi-1.png new file mode 100644 index 0000000..00551c3 Binary files /dev/null and b/docs/docs/user-guide/transitions/midi-1.png differ diff --git a/docs/docs/user-guide/transitions/midi.md b/docs/docs/user-guide/transitions/midi.md new file mode 100644 index 0000000..9e44199 --- /dev/null +++ b/docs/docs/user-guide/transitions/midi.md @@ -0,0 +1,61 @@ +# MIDI Files + +MIDI files can be used to set up time-depended properties for the transitions. +A MIDI file: + +* has the same duration and tempo as the song +* uses notes to mark time spans (press + release pair) +* if song has a variable tempo, MIDI file must emit TEMPO events (should be done by DAW) + +## Usage + +MIDI files are located in `_work/Data/Music`. + +### Default Transition (Instance) + +```dae +instance MyTheme_Audio(BassMusic_ThemeAudio) { + // skipped + midiFile = "mytheme.mid"; + // skipped +}; +``` + +### Default Transition (API) + +```dae +BassMusic_AddMidiFile("MyTheme", "", "mytheme.mid"); +``` + +### Filtered Transition (API) + +```dae +BassMusic_AddMidiFile("MyTheme", "TargetTheme", "mytheme.mid"); +``` + +## Notes + +### Timing + +#### A4 (Fade-out) + +A4 note marks a [Timing Transition](timing.md) with CROSSFADE effect. The note position is the start of transition +and the note duration is the fade-out duration. By default, the next song fade-in has the same timing unless you +set up A#4. + +![A4](a4.webp) + +#### A#4 (Fade-in) + +A#4 note is an optional addition to A4 (Fade-out) that sets up the start and duration of the fade-out effect. + +![A#4](asharp4.webp) + +## Working in DAW + +You can create the MIDI file as an additional track in your song project. This approach guarantees that the timing +is consistent. During export, please export the MIDI track as an additional file that contains only this track. + +For transitions, you can simulate them using the automation tools in your DAW and place the MIDI notes based on that. + +![MIDI in Ableton](midi-1.png) \ No newline at end of file diff --git a/docs/docs/user-guide/transitions/timing-transition.webp b/docs/docs/user-guide/transitions/timing-transition.webp new file mode 100644 index 0000000..5b2a792 Binary files /dev/null and b/docs/docs/user-guide/transitions/timing-transition.webp differ diff --git a/docs/docs/user-guide/transitions/timing.md b/docs/docs/user-guide/transitions/timing.md new file mode 100644 index 0000000..cfd21c9 --- /dev/null +++ b/docs/docs/user-guide/transitions/timing.md @@ -0,0 +1,52 @@ +# Timing + +Timing settings let you limit on which moments a transition may happen and what time spans to use for effects. +To define it, we are using a set of parameters: + +* Start - when to start the end effect of the ending theme +* Duration - how long the end effect of the ending theme takes +* Effect - which end effect to use for the ending theme +* NextStart - when to start the start effect of the starting theme +* NextDuration - how long the start effect of the starting theme takes +* NextEffect - which start effect to use for the starting theme + +![Timing Transition Timeline](timing-transition.webp) + +## Effects + +### NONE = 0 + +Disables the transition effect. + +### CROSSFADE = 1 + +Uses fade-in and fade-out effects. + +## Daedalus + +To set up a timing transition, you have to add a time point to a theme using the Deadalus API. + +```dae +BassMusic_AddTransitionTimePoint( + "MyThemeForTransition", // theme + "", // filter + 4.0, // start in seconds + 1.0, // duration in seconds + 1, // effect (CROSSFADE = 1) + 4.5, // start in seconds + 1.0, // duration in seconds + 1 // effect (CROSSFADE = 1) +); +``` + +This call will add a time point in the default transition of `MyThemeForTransition`. +Transition will start a 1s fade-out on the 4th second of the song and 0.5s later the next song will start its own 1s fade-in. + +You can add many time points to a single (theme, filter) pair. +It's recommended to set up at least 1 transition per each few seconds of the song, because they are the only moments when we can change themes. + +You should also create a time point `Duration` seconds before the end, because if no future time point exists, the scheduler fallbacks to the immediate default transition. + +## MIDI + +Timing points can be defined using a MIDI file. Check [MIDI Files](midi.md) for more information. \ No newline at end of file diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 34360ab..a23063c 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -11,11 +11,17 @@ nav: - user-guide/getting-started/plugin-loading.md - user-guide/getting-started/music-definition.md - user-guide/getting-started/cross-fading.md - - Transition Scheduler: - - user-guide/transition-scheduler/index.md - - user-guide/transition-scheduler/instant.md - - user-guide/transition-scheduler/on-beat.md - - user-guide/transition-scheduler/jingle.md + - Concepts: + - user-guide/concepts/index.md + - Themes: + - user-guide/theme-definition/index.md + - user-guide/theme-definition/cmusictheme.md + - user-guide/theme-definition/basstheme.md + - Transitions: + - user-guide/transitions/index.md + - user-guide/transitions/timing.md + - user-guide/transitions/jingle.md + - user-guide/transitions/midi.md - Scripting (Daedalus): - user-guide/scripting/index.md - user-guide/scripting/custom-scheduler.md @@ -25,6 +31,8 @@ nav: - reference/index.md - Classes: - reference/classes/C_MUSICTHEME.md + - reference/classes/C_BassMusic_Theme.md + - reference/classes/C_BassMusic_ThemeAudio.md - Globals: - reference/globals/index.md - Externals: diff --git a/src/Gothic/BassLoader.hpp b/src/Gothic/BassLoader.hpp index 125250d..78165f7 100644 --- a/src/Gothic/BassLoader.hpp +++ b/src/Gothic/BassLoader.hpp @@ -1,4 +1,5 @@ #include +#include namespace GOTHIC_NAMESPACE { @@ -13,16 +14,30 @@ namespace GOTHIC_NAMESPACE zTMus_TransSubType trSubType; }; + enum class BassMusicThemeType : size_t { NORMAL = 0 }; + struct BassMusicTheme { zSTRING Name; zSTRING Zones; + BassMusicThemeType Type = BassMusicThemeType::NORMAL; }; struct BassMusicThemeAudio { + zSTRING Theme; zSTRING Type; zSTRING Filename; + zSTRING MidiFile; + float Volume; + int Loop; + int Reverb; + float ReverbMix; + float ReverbTime; + int FadeIn; + int FadeInDuration; + int FadeOut; + int FadeOutDuration; }; class BassLoader @@ -46,6 +61,13 @@ namespace GOTHIC_NAMESPACE { LoadGothic(); LoadBass(); + + zSTRING initFuncName("BassMusic_Init"); + zCPar_Symbol* symbol = m_Parser->GetSymbol(initFuncName); + if (symbol && symbol->type == zPAR_TYPE_FUNC) + { + m_Parser->CallFunc(initFuncName); + } } private: @@ -54,12 +76,10 @@ namespace GOTHIC_NAMESPACE ForEachClass( "C_MUSICTHEME", [&]() { return m_GothicThemeInstances.emplace_back(new GothicMusicTheme{}); }, - [&](GothicMusicTheme* input, zCPar_Symbol* symbol) - { + [&](GothicMusicTheme* input, zCPar_Symbol* symbol) { std::shared_ptr theme = std::make_shared(symbol->name.ToChar()); theme->SetAudioFile(NH::Bass::AudioFile::DEFAULT, input->fileName.ToChar()); - theme->SetAudioEffects(NH::Bass::AudioFile::DEFAULT, [&](NH::Bass::AudioEffects& effects) - { + theme->SetAudioEffects(NH::Bass::AudioFile::DEFAULT, [&](NH::Bass::AudioEffects& effects) { effects.Loop.Active = input->loop; effects.Volume.Active = true; effects.Volume.Volume = input->vol; @@ -81,7 +101,9 @@ namespace GOTHIC_NAMESPACE effects.FadeOut.Duration = NH::Bass::Options->TransitionTime; } }); - theme->AddZone(symbol->name.ToChar()); + zSTRING zone = symbol->name; + zone.Upper(); + theme->AddZone(zone.ToChar()); NH::Bass::Engine::GetInstance()->GetMusicManager().AddTheme(symbol->name.ToChar(), theme); }); } @@ -89,35 +111,67 @@ namespace GOTHIC_NAMESPACE void LoadBass() { ForEachClass( - "C_BassMusic_Theme", + Globals->BassMusicThemeClassName, [&]() { return m_BassThemeInstances.emplace_back(new BassMusicTheme{}); }, - [&](BassMusicTheme* theme, zCPar_Symbol* symbol) - { - // @todo: + [&](BassMusicTheme* input, zCPar_Symbol* symbol) { + std::shared_ptr theme = std::make_shared(input->Name.ToChar()); + theme->SetAudioEffects(NH::Bass::AudioFile::DEFAULT, [](NH::Bass::AudioEffects& effects){}); + auto zones = NH::String(input->Zones.ToChar()).Split(","); + for (auto& zone: zones) { theme->AddZone(zone.MakeUpper()); } + // input->Type ignored for now + NH::Bass::Engine::GetInstance()->GetMusicManager().AddTheme(input->Name.ToChar(), theme); }); ForEachClass( - "C_BassMusic_ThemeAudio", + Globals->BassMusicThemeAudioClassName, [&]() { return m_BassThemeAudioInstances.emplace_back(new BassMusicThemeAudio{}); }, - [&](BassMusicThemeAudio* theme, zCPar_Symbol* symbol) - { - // @todo: + [&](BassMusicThemeAudio* input, zCPar_Symbol* symbol) { + std::shared_ptr theme = NH::Bass::Engine::GetInstance()->GetMusicManager().GetTheme(input->Theme.ToChar()); + NH::String type = NH::String(input->Type.ToChar()); + NH::HashString id = type == "DEFAULT" ? NH::Bass::AudioFile::DEFAULT : NH::HashString(type); + theme->SetAudioFile(id, input->Filename.ToChar()); + theme->SetAudioEffects(id, [&](NH::Bass::AudioEffects& effects) { + effects.Loop.Active = input->Loop; + effects.Volume.Active = true; + effects.Volume.Volume = input->Volume; + if (!NH::Bass::Options->ForceDisableReverb && input->Reverb) + { + effects.ReverbDX8.Active = true; + effects.ReverbDX8.Mix = input->ReverbMix; + effects.ReverbDX8.Time = input->ReverbTime; + } + bool forceFade = NH::Bass::Options->ForceFadeTransition; + if (forceFade || input->FadeIn) + { + effects.FadeIn.Active = true; + effects.FadeIn.Duration = input->FadeInDuration; + } + if (forceFade || input->FadeOut) + { + effects.FadeOut.Active = true; + effects.FadeOut.Duration = input->FadeOutDuration; + } + }); + if (!input->MidiFile.IsEmpty()) + { + auto midiFile = std::make_shared(theme->GetName(), NH::HashString(NH::String(input->MidiFile.ToChar()))); + theme->AddMidiFile(NH::HashString(""), midiFile); + } + NH::Bass::Engine::GetInstance()->GetMusicManager().RefreshTheme(theme->GetName()); }); } template void ForEachClass(const zSTRING& className, const std::function& classFactory, const std::function& instanceFunc) { - ForEachPrototype(className, [&](int index) - { + ForEachPrototype(className, [&](int index) { T* theme = classFactory(); if (theme) { m_Parser->CreatePrototype(index, theme); } }); - ForEachInstance(className, [&](int index, zCPar_Symbol* symbol) - { + ForEachInstance(className, [&](int index, zCPar_Symbol* symbol) { T* theme = classFactory(); if (theme) { diff --git a/src/Gothic/CMusicSys_Bass.hpp b/src/Gothic/CMusicSys_Bass.hpp index af837f7..08f3e81 100644 --- a/src/Gothic/CMusicSys_Bass.hpp +++ b/src/Gothic/CMusicSys_Bass.hpp @@ -149,8 +149,11 @@ namespace GOTHIC_NAMESPACE return; } + static zSTRING nowPlaying = ""; + zSTRING identifier = id; - if (m_ActiveTheme && identifier.Upper() == m_ActiveTheme->name) + identifier.Upper(); + if (m_ActiveTheme && identifier.Upper() == m_ActiveTheme->name || nowPlaying == identifier) { return; } @@ -158,13 +161,15 @@ namespace GOTHIC_NAMESPACE zCMusicTheme* theme = LoadThemeByScript(id); if (theme && IsDirectMusicFormat(theme->fileName)) { + nowPlaying = ""; m_ActiveTheme = theme; m_BassEngine->StopMusic(); return m_DirectMusic->PlayThemeByScript(id, manipulate, done); } - identifier.Upper(); - m_BassEngine->GetCommandQueue().AddCommand(std::make_shared(identifier.ToChar())); + nowPlaying = identifier; + m_ActiveTheme = nullptr; + m_BassEngine->GetCommandQueue().AddCommand(std::make_shared(NH::String(identifier.ToChar()))); if (done) { @@ -187,10 +192,12 @@ namespace GOTHIC_NAMESPACE return; } + // Called from an external m_DirectMusic->Stop(); m_ActiveTheme = theme; - log->Warning("This path in CMusicSys_Bass::PlayTheme() shouldn't be possible"); - PlayThemeByScript(theme->name, 0, nullptr); + zSTRING identifier = theme->name; + identifier.Upper(); + m_BassEngine->GetCommandQueue().AddCommand(std::make_shared(identifier.ToChar())); } zCMusicTheme* GetActiveTheme() override @@ -217,7 +224,7 @@ namespace GOTHIC_NAMESPACE m_DirectMusic->Stop(); } - float GetVolume() const override + [[nodiscard]] float GetVolume() const override { return m_BassEngine->GetVolume(); } diff --git a/src/Gothic/Externals.hpp b/src/Gothic/Externals.hpp index 84a1ed3..67dbd07 100644 --- a/src/Gothic/Externals.hpp +++ b/src/Gothic/Externals.hpp @@ -76,54 +76,83 @@ namespace GOTHIC_NAMESPACE return 0; } - // func void BassMusic_TransitionRule_OnBeat(var string name, var string interval, var string timePoints) - int BassMusic_TransitionRule_OnBeat() + // func void BassMusic_AddMidiFile(var string theme, var string filter, var midiFilename) + int BassMusic_AddMidiFile() { - NH::Logger* log = NH::CreateLogger("zBassMusic::BassMusic_TransitionRule_OnBeat"); + static NH::Logger* log = NH::CreateLogger("zBassMusic::AddMidiFile"); - zSTRING name; - zSTRING inInterval; - zSTRING inTimePoints; + zSTRING theme, filter, midiFilename; + parser->GetParameter(midiFilename); + parser->GetParameter(filter); + parser->GetParameter(theme); - parser->GetParameter(inTimePoints); - parser->GetParameter(inInterval); - parser->GetParameter(name); - - log->Trace("name = {0}", name.ToChar()); - log->Trace("interval = {0}", inInterval.ToChar()); - log->Trace("timePoints = {0}", inTimePoints.ToChar()); - - try + auto target = NH::Bass::Engine::GetInstance()->GetMusicManager().GetTheme(NH::String(theme)); + if (!target) { - // Parse interval - double interval = std::stod(inInterval.ToChar()); - log->Trace("parsed interval = {0}", interval); - - // Parse time points - std::vector timePoints; - std::stringstream timePointsStream(inTimePoints.ToChar()); - std::string timePoint; - while (std::getline(timePointsStream, timePoint, ';')) - { - double point = std::stod(timePoint); - log->Trace("parsed point = {0}", point); - timePoints.push_back(point); - } - - NH::Bass::Engine::GetInstance()->GetTransitionScheduler().AddRuleOnBeat(name.ToChar(), interval, timePoints); + log->Error("Theme {0} not found", theme.ToChar()); + return 0; } - catch (const std::invalid_argument& e) - { - log->Fatal("invalid_argument: {0}\n at {1}:{2}", e.what(), __FILE__, __LINE__); - } - catch (const std::out_of_range& e) + + target->AddMidiFile(NH::String(filter), std::make_shared(target->GetName(), NH::String(midiFilename.ToChar()))); + return 0; + } + + // func void BassMusic_AddTransitionTimePoint(var string theme, var string filter, var float start, var float duration, var int effect, var float nextStart, var float nextDuration, var int nextEffect) + int BassMusic_AddTransitionTimePoint() + { + static NH::Logger* log = NH::CreateLogger("zBassMusic::AddTransitionTimePoint"); + + zSTRING theme, filter; + float start, duration, nextStart, nextDuration; + int effect, nextEffect; + parser->GetParameter(nextEffect); + parser->GetParameter(nextDuration); + parser->GetParameter(nextStart); + parser->GetParameter(effect); + parser->GetParameter(duration); + parser->GetParameter(start); + parser->GetParameter(filter); + parser->GetParameter(theme); + + auto target = NH::Bass::Engine::GetInstance()->GetMusicManager().GetTheme(NH::String(theme.ToChar())); + if (!target) { - log->Fatal("out_of_range: {0}\n at {1}:{2}", e.what(), __FILE__, __LINE__); + log->Error("Theme {0} not found", theme.ToChar()); + return 0; } + NH::Bass::Transition::TimePoint tp { start, duration, (NH::Bass::TransitionEffect)effect, nextStart, nextDuration, (NH::Bass::TransitionEffect)nextEffect }; + target->GetTransitionInfo().AddTimePoint(tp, NH::String(filter.ToChar())); + return 0; } + // func void BassMusic_AddJingle(var string name, var string filter, var string jingle, var float delay) + int BassMusic_AddJingle() + { + static NH::Logger* log = NH::CreateLogger("zBassMusic::AddJingle"); + + zSTRING name, filter, jingle; + float delay; + parser->GetParameter(delay); + parser->GetParameter(jingle); + parser->GetParameter(filter); + parser->GetParameter(name); + + NH::Bass::Engine::GetInstance()->GetMusicManager().AddJingle(name.ToChar(), jingle.ToChar(), delay, filter.ToChar()); + + return 0; + } + + void DefineExternalsMusic() + { + parserMusic->AddClassOffset(Globals->BassMusicThemeClassName, sizeof(BassMusicTheme)); + parserMusic->AddClassOffset(Globals->BassMusicThemeAudioClassName, sizeof(BassMusicThemeAudio)); + parserMusic->DefineExternal("BassMusic_AddMidiFile", BassMusic_AddMidiFile, zPAR_TYPE_VOID, zPAR_TYPE_STRING, zPAR_TYPE_STRING, zPAR_TYPE_STRING, zPAR_TYPE_VOID); + parserMusic->DefineExternal("BassMusic_AddTransitionTimePoint", BassMusic_AddTransitionTimePoint, zPAR_TYPE_VOID, zPAR_TYPE_STRING, zPAR_TYPE_STRING, zPAR_TYPE_FLOAT, zPAR_TYPE_FLOAT, zPAR_TYPE_INT, zPAR_TYPE_FLOAT, zPAR_TYPE_FLOAT, zPAR_TYPE_INT, zPAR_TYPE_VOID); + parserMusic->DefineExternal("BassMusic_AddJingle", BassMusic_AddJingle, zPAR_TYPE_VOID, zPAR_TYPE_STRING, zPAR_TYPE_STRING, zPAR_TYPE_STRING, zPAR_TYPE_FLOAT, zPAR_TYPE_VOID); + } + void DefineExternals() { parser->DefineExternalVar("BassMusic_ActiveThemeFilename", &Globals->BassMusic_ActiveThemeFilename, zPAR_TYPE_STRING, 1); @@ -140,11 +169,15 @@ namespace GOTHIC_NAMESPACE parser->DefineExternal("BassMusic_Opt_ForceDisableReverb", BassMusic_Opt_ForceDisableReverb, zPAR_TYPE_VOID, zPAR_TYPE_INT, zPAR_TYPE_VOID); parser->DefineExternal("BassMusic_Opt_ForceFadeTransition", BassMusic_Opt_ForceFadeTransition, zPAR_TYPE_VOID, zPAR_TYPE_INT, zPAR_TYPE_VOID); - parser->DefineExternal("BassMusic_TransitionRule_OnBeat", BassMusic_TransitionRule_OnBeat, zPAR_TYPE_VOID, zPAR_TYPE_STRING, zPAR_TYPE_STRING, zPAR_TYPE_STRING, zPAR_TYPE_VOID); + parser->DefineExternal("BassMusic_AddMidiFile", BassMusic_AddMidiFile, zPAR_TYPE_VOID, zPAR_TYPE_STRING, zPAR_TYPE_STRING, zPAR_TYPE_STRING, zPAR_TYPE_VOID); + parser->DefineExternal("BassMusic_AddTransitionTimePoint", BassMusic_AddTransitionTimePoint, zPAR_TYPE_VOID, zPAR_TYPE_STRING, zPAR_TYPE_STRING, zPAR_TYPE_FLOAT, zPAR_TYPE_FLOAT, zPAR_TYPE_INT, zPAR_TYPE_FLOAT, zPAR_TYPE_FLOAT, zPAR_TYPE_INT, zPAR_TYPE_VOID); + parser->DefineExternal("BassMusic_AddJingle", BassMusic_AddJingle, zPAR_TYPE_VOID, zPAR_TYPE_STRING, zPAR_TYPE_STRING, zPAR_TYPE_STRING, zPAR_TYPE_FLOAT, zPAR_TYPE_VOID); if (NH::Bass::Options->CreateMainParserCMusicTheme) { zCMusicTheme theme; + parser->AddClassOffset(Globals->BassMusicThemeClassName, sizeof(BassMusicTheme)); + parser->AddClassOffset(Globals->BassMusicThemeAudioClassName, sizeof(BassMusicThemeAudio)); parser->AddClassOffset(Globals->CMusicThemeClass, reinterpret_cast(&theme.dScriptEnd) - reinterpret_cast(&theme.fileName)); } } diff --git a/src/Gothic/Globals.hpp b/src/Gothic/Globals.hpp index 74995c7..1d27877 100644 --- a/src/Gothic/Globals.hpp +++ b/src/Gothic/Globals.hpp @@ -5,6 +5,8 @@ namespace GOTHIC_NAMESPACE struct GlobalsDef { zSTRING CMusicThemeClass = "C_MUSICTHEME"; + zSTRING BassMusicThemeClassName = "C_BassMusic_Theme"; + zSTRING BassMusicThemeAudioClassName = "C_BassMusic_ThemeAudio"; zSTRING BassMusic_ActiveThemeFilename = ""; zSTRING BassMusic_ActiveThemeID = ""; zSTRING BassMusic_EventThemeFilename = ""; diff --git a/src/Gothic/Hooks.hpp b/src/Gothic/Hooks.hpp index 0aca9b2..88d615c 100644 --- a/src/Gothic/Hooks.hpp +++ b/src/Gothic/Hooks.hpp @@ -1,5 +1,11 @@ namespace GOTHIC_NAMESPACE { + // G1: 0x006E5920 public: int __thiscall zCParser::Parse(class zSTRING) + // G1A: 0x0071E400 public: int __thiscall zCParser::Parse(class zSTRING) + // G2: 0x0072F160 public: int __thiscall zCParser::Parse(class zSTRING) + // G2A: 0x0078EBA0 public: int __thiscall zCParser::Parse(class zSTRING) + void* zCParser_Parse = (void*)zSwitch(0x006E5920, 0x0071E400, 0x0072F160, 0x0078EBA0); + // G1: 0x00424AF0 public: void __thiscall CGameManager::Run(void) // G1A: 0x004275D0 public: void __thiscall CGameManager::Run(void) // G2: 0x004254F0 public: void __thiscall CGameManager::Run(void) @@ -24,10 +30,19 @@ namespace GOTHIC_NAMESPACE // G2A: 0x004DC1C0 public: virtual int __thiscall zCMenu::HandleFrame(void) void* zCMenu_HandleFrame = (void*)zSwitch(0x004CF470, 0x004DFD00, 0x004D9B20, 0x004DC1C0); - void __fastcall CGameManager_Run_PartialHook(); + void __fastcall zCParser_Parse_PartialHook(Union::Registers& reg); + auto Partial_zCParser_Parse = Union::CreatePartialHook(zCParser_Parse, zCParser_Parse_PartialHook); + void __fastcall zCParser_Parse_PartialHook(Union::Registers& reg) + { + auto* p = (zCParser*) reg.ecx; + if (p == parserMusic) + { + DefineExternalsMusic(); + } + } + void __fastcall CGameManager_Run_PartialHook(); auto Partial_CGameManager_Run = Union::CreatePartialHook(CGameManager_Run, CGameManager_Run_PartialHook); - void __fastcall CGameManager_Run_PartialHook() { static NH::Logger* log = NH::CreateLogger("zBassMusic::CGameManager::Run"); @@ -44,13 +59,14 @@ namespace GOTHIC_NAMESPACE zmusic->SetVolume(volume); log->Info("Set music system to CMusicSys_Bass"); + BassLoader bassLoaderMusic(parserMusic); + bassLoaderMusic.Load(); + if (NH::Bass::Options->CreateMainParserCMusicTheme) { - BassLoader bassLoader(parser); - bassLoader.Load(); + BassLoader bassLoaderMain(parser); + bassLoaderMain.Load(); } - BassLoader bassLoader(parserMusic); - bassLoader.Load(); } else { @@ -60,27 +76,21 @@ namespace GOTHIC_NAMESPACE } void __fastcall DefineExternals_Ulfi_PartialHook(Union::Registers& reg); - auto Partial_DefineExternals_Ulfi = Union::CreatePartialHook(oCGame_DefineExternals_Ulfi, &DefineExternals_Ulfi_PartialHook); - void __fastcall DefineExternals_Ulfi_PartialHook(Union::Registers& reg) { DefineExternals(); } void __fastcall oCGame_Render_PartialHook(Union::Registers& reg); - auto Partial_oCGame_Render = Union::CreatePartialHook(oCGame_Render, oCGame_Render_PartialHook); - void __fastcall oCGame_Render_PartialHook(Union::Registers& reg) { NH::Bass::Engine::GetInstance()->Update(); } void __fastcall oCMenu_Render_PartialHook(Union::Registers& reg); - auto Partial_oCMenu_HandleFrame = Union::CreatePartialHook(zCMenu_HandleFrame, oCMenu_Render_PartialHook); - void __fastcall oCMenu_Render_PartialHook(Union::Registers& reg) { NH::Bass::Engine::GetInstance()->Update(); diff --git a/src/NH/Bass/Channel.cpp b/src/NH/Bass/Channel.cpp index 1e7d7a9..bbac6ef 100644 --- a/src/NH/Bass/Channel.cpp +++ b/src/NH/Bass/Channel.cpp @@ -4,164 +4,154 @@ namespace NH::Bass { - void Channel::Play(const std::shared_ptr& theme, HashString id) + struct OnSyncPosition { HSTREAM Channel; const std::function& Function; }; + struct OnSyncWhenAudioEndsData { HSTREAM Channel; const std::function& Function; }; + struct OnSyncBeforeAudioEndsData { HSTREAM Channel; const std::function& Function; }; + + Channel::Result Channel::PlayInstant(const AudioFile& audioFile) { if (m_Stream > 0) { BASS_ChannelFree(m_Stream); } - m_Theme = theme; - m_AudioId = id; - const auto& file = theme->GetAudioFile(id); - const auto& effects = theme->GetAudioEffects(id); - - m_Stream = BASS_StreamCreateFile(true, file.Buffer.data(), 0, file.Buffer.size(), 0); + m_Stream = BASS_StreamCreateFile(true, audioFile.Buffer.data(), 0, audioFile.Buffer.size(), 0); if (!m_Stream) { - log->Error("Could not create stream: {0}\n error: {1}\n at {2}:{3}", - m_Theme->GetName(), Engine::ErrorCodeToString(BASS_ErrorGetCode()), - __FILE__, __LINE__); - return; + int code = BASS_ErrorGetCode(); + log->Error("Could not create stream: {0}\n error: {1}\n at {2}:{3}", audioFile.Filename, Engine::ErrorCodeToString(code), __FILE__, __LINE__); + return std::unexpected(Error{ IChannel::ErrorType::INVALID_BUFFER, code, Engine::ErrorCodeToString(code) }); } BASS_ChannelStart(m_Stream); - log->Info("Channel started: {0}", m_Theme->GetName()); + return {}; + } - float targetVolume = effects.Volume.Active ? effects.Volume.Volume : 1.0f; - BASS_ChannelSetAttribute(m_Stream, BASS_ATTRIB_VOL, targetVolume); + void Channel::StopInstant() + { + BASS_ChannelStop(m_Stream); + } - if (effects.FadeIn.Active) - { - BASS_ChannelSetAttribute(m_Stream, BASS_ATTRIB_VOL, 0.0f); - BASS_ChannelSlideAttribute(m_Stream, BASS_ATTRIB_VOL, targetVolume, effects.FadeIn.Duration); - } + void Channel::SetVolume(float volume) + { + if (volume < 0.0f) { log->Warning("Clamping invalid volume {0} to 0.0f", volume); volume = 0.0f; } + if (volume > 1.0f) { log->Warning("Clamping invalid volume {0} to 1.0f", volume); volume = 1.0f; } + BASS_ChannelSetAttribute(m_Stream, BASS_ATTRIB_VOL, volume); + } - if (effects.Loop.Active) - { - BASS_ChannelFlags(m_Stream, BASS_SAMPLE_LOOP, BASS_SAMPLE_LOOP); - log->Trace("Loop set {0}", m_Theme->GetName()); - } + void Channel::SlideVolume(float targetVolume, uint32_t time) + { + BASS_ChannelSlideAttribute(m_Stream, BASS_ATTRIB_VOL, targetVolume, time); + } - if (!Options->ForceDisableReverb && effects.ReverbDX8.Active) - { - HFX fx = BASS_ChannelSetFX(m_Stream, BASS_FX_DX8_REVERB, 1); - BASS_DX8_REVERB params{ 0, effects.ReverbDX8.Mix, effects.ReverbDX8.Time, 0.001f }; - if (!BASS_FXSetParameters(fx, (void*)¶ms)) - { - log->Error("Could not set reverb FX: {0}\n error: {1}\n at {2}:{3}", - m_Theme->GetName(), Engine::ErrorCodeToString(BASS_ErrorGetCode()), - __FILE__, __LINE__); - } - log->Trace("Reverb set: {0}", m_Theme->GetName()); - } + void Channel::SlideVolume(float targetVolume, uint32_t time, const std::function& onFinish) + { + SlideVolume(targetVolume, time); + BASS_ChannelSetSync(m_Stream, BASS_SYNC_SLIDE, 0, OnSlideVolumeSyncCallFunction, (void*)&onFinish); + } - if (effects.FadeOut.Active) + void Channel::SetDX8ReverbEffect(float reverbMix, float reverbTime, float inputGain, float highFreqRTRatio) + { + HFX effect = BASS_ChannelSetFX(m_Stream, BASS_FX_DX8_REVERB, 0); + if (reverbMix < -96) { log->Warning("Clamping DX8Reverb reverbMix({0}) to -96", reverbMix); reverbMix = -96; } + if (reverbMix > 0) { log->Warning("Clamping DX8Reverb reverbMix({0}) to 0", reverbMix); reverbMix = 0; } + if (reverbTime < 0.001f) { log->Warning("Clamping DX8Reverb reverbTime({0}) to 0.001f", reverbTime); reverbTime = 0.001f; } + if (reverbTime > 3000.0f) { log->Warning("Clamping DX8Reverb reverbTime({0}) to 3000.0f", reverbTime); reverbTime = 3000.0f; } + BASS_DX8_REVERB reverb{ + inputGain, reverbMix, reverbTime, highFreqRTRatio + }; + if (!BASS_FXSetParameters(effect, &reverb)) { - const QWORD length = BASS_ChannelGetLength(m_Stream, BASS_POS_BYTE); - const QWORD transitionBytes = BASS_ChannelSeconds2Bytes(m_Stream, effects.FadeOut.Duration / 1000.0f); - const QWORD offset = length - transitionBytes; - BASS_ChannelSetSync(m_Stream, BASS_SYNC_END, offset, OnVolumeSlideSync, this); - log->Trace("SyncEnd set: {0}", m_Theme->GetName()); + int code = BASS_ErrorGetCode(); + log->Error("Could not set DX8Reverb effect: {0}\n error: {1}\n at {2}:{3}", m_Stream, Engine::ErrorCodeToString(code), __FILE__, __LINE__); } + } - BASS_ChannelSetSync(m_Stream, BASS_SYNC_END, 0, OnEndSync, this); - log->Trace("SyncEnd set: {0}", m_Theme->GetName()); - - m_Status = ChannelStatus::PLAYING; - m_EventManager.DispatchEvent(MusicChangeEvent(m_Theme, m_AudioId)); + void Channel::OnPosition(double position, const std::function& callback) + { + size_t positionBytes = BASS_ChannelSeconds2Bytes(m_Stream, position); + BASS_ChannelSetSync(m_Stream, BASS_SYNC_POS, positionBytes, OnPositionSyncCallFunction, (void*)new OnSyncPosition{ m_Stream, callback }); } - double Channel::CurrentPosition() const + void Channel::OnAudioEnds(const std::function& onFinish) { - if (m_Stream > 0) - { - return BASS_ChannelBytes2Seconds(m_Stream, BASS_ChannelGetPosition(m_Stream, BASS_POS_BYTE)); - } - return -1; + BASS_ChannelSetSync(m_Stream, BASS_SYNC_END, 0, OnAudioEndSyncCallFunction, (void*)new OnSyncWhenAudioEndsData{ m_Stream, onFinish }); } - double Channel::CurrentLength() const + void Channel::BeforeAudioEnds(double aheadSeconds, const std::function& onFinish) { - if (m_Stream > 0) - { - return BASS_ChannelBytes2Seconds(m_Stream, BASS_ChannelGetLength(m_Stream, BASS_POS_BYTE)); - } - return -1; + double position = Length() - aheadSeconds; + size_t positionBytes = BASS_ChannelSeconds2Bytes(m_Stream, position); + BASS_ChannelSetSync(m_Stream, BASS_SYNC_POS, positionBytes, BeforeAudioEndsSyncCallFunction, (void*)new OnSyncBeforeAudioEndsData{ m_Stream, onFinish }); } - void Channel::Stop() + void Channel::Acquire() { - if (m_Stream > 0) + if (m_Status != ChannelStatus::AVAILABLE) { - const auto& effects = m_Theme->GetAudioEffects(m_AudioId); - if (effects.FadeOut.Active) - { - BASS_ChannelSlideAttribute(m_Stream, BASS_ATTRIB_VOL, 0.0f, effects.FadeOut.Duration); - BASS_ChannelSetSync(m_Stream, BASS_SYNC_SLIDE, 0, OnVolumeSlideSync, this); - m_Status = ChannelStatus::FADING_OUT; - } - else - { - BASS_ChannelFree(m_Stream); - m_Stream = 0; - m_Status = ChannelStatus::AVAILABLE; - } + log->Error("Trying to acquire a non-available channel. We will allow that to not crash the game but the music may be funky."); } + m_Status = ChannelStatus::ACQUIRED; } - std::shared_ptr Channel::CurrentTheme() const + void Channel::Release() { - return m_Theme; + m_Status = ChannelStatus::AVAILABLE; + BASS_ChannelStop(m_Stream); } - const AudioFile& Channel::CurrentAudioFile() const + bool Channel::IsPlaying() const { - return m_Theme->GetAudioFile(m_AudioId); + return BASS_ChannelIsActive(m_Stream); } - const AudioEffects& Channel::CurrentAudioEffects() const + double Channel::Position() const { - return m_Theme->GetAudioEffects(m_AudioId); + if (m_Stream > 0) + { + return BASS_ChannelBytes2Seconds(m_Stream, BASS_ChannelGetPosition(m_Stream, BASS_POS_BYTE)); + } + return -1; } - void Channel::OnTransitionSync(HSYNC, DWORD channel, DWORD data, void* userData) + double Channel::Length() const { - auto* _this = static_cast(userData); - if (_this->m_Stream == channel) + if (m_Stream > 0) { - _this->m_EventManager.DispatchEvent(MusicTransitionEvent(_this->m_Theme, _this->m_AudioId, _this->CurrentAudioEffects().FadeOut.Duration)); + return BASS_ChannelBytes2Seconds(m_Stream, BASS_ChannelGetLength(m_Stream, BASS_POS_BYTE)); } + return -1; } - void Channel::OnEndSync(HSYNC, DWORD channel, DWORD data, void* userData) + void Channel::OnPositionSyncCallFunction(HSYNC, DWORD channel, DWORD data, void* userData) { - auto* _this = static_cast(userData); - if (_this->m_Stream == channel) - { - const auto& effects = _this->m_Theme->GetAudioEffects(_this->m_AudioId); - if (!effects.Loop.Active) - { - _this->m_Status = ChannelStatus::AVAILABLE; - } + auto* payload = static_cast(userData); + if (channel != payload->Channel) return; + if (payload->Function) { payload->Function(); } + else { CreateLogger("HSYNC::OnPositionSyncCallFunction")->Error("onFinish is nullptr"); } + } - _this->m_EventManager.DispatchEvent(MusicEndEvent(_this->m_Theme, _this->m_AudioId)); - } + void Channel::OnSlideVolumeSyncCallFunction(HSYNC, DWORD channel, DWORD data, void* userData) + { + auto* onFinish = static_cast*>(userData); + if (onFinish) { (*onFinish)(); } + else { CreateLogger("HSYNC::OnSlideVolumeSyncCallFunction")->Error("onFinish is nullptr"); } } - void Channel::OnVolumeSlideSync(HSYNC, DWORD channel, DWORD data, void* userData) + void Channel::OnAudioEndSyncCallFunction(HSYNC, DWORD channel, DWORD data, void* userData) { - auto* _this = static_cast(userData); - if (_this->m_Stream == channel && _this->m_Status == ChannelStatus::FADING_OUT) - { - float volume; - BASS_ChannelGetAttribute(channel, BASS_ATTRIB_VOL, &volume); - if (volume < DBL_EPSILON) - { - BASS_ChannelFree(channel); - _this->m_Stream = 0; - _this->m_Status = ChannelStatus::AVAILABLE; - } - } + auto* payload = static_cast(userData); + if (channel != payload->Channel) return; + if (payload->Function) { payload->Function(); } + else { CreateLogger("HSYNC::OnAudioEndSyncCallFunction")->Error("onFinish is nullptr"); } + } + + void Channel::BeforeAudioEndsSyncCallFunction(HSYNC, DWORD channel, DWORD data, void* userData) + { + auto* payload = static_cast(userData); + if (channel != payload->Channel) return; + double aheadSeconds = BASS_ChannelBytes2Seconds(channel, BASS_ChannelGetLength(channel, BASS_POS_BYTE) - BASS_ChannelGetPosition(channel, BASS_POS_BYTE)); + if (payload->Function) { payload->Function(aheadSeconds); } + else { CreateLogger("HSYNC::BeforeAudioEndsSyncCallFunction")->Error("onFinish is nullptr"); } } } diff --git a/src/NH/Bass/Channel.h b/src/NH/Bass/Channel.h index dc559e9..92329c5 100644 --- a/src/NH/Bass/Channel.h +++ b/src/NH/Bass/Channel.h @@ -1,63 +1,51 @@ #pragma once -#include "NH/Bass/CommonTypes.h" #include "EventManager.h" #include "NH/Logger.h" #include +#include #include - namespace NH::Bass { - enum class ChannelStatus - { - AVAILABLE, - PLAYING, - FADING_OUT - }; + enum class ChannelStatus { AVAILABLE, ACQUIRED }; - class Channel + class Channel : public IChannel { NH::Logger* log; - size_t m_Index; - EventManager& m_EventManager; ChannelStatus m_Status = ChannelStatus::AVAILABLE; HSTREAM m_Stream = 0; - std::shared_ptr m_Theme = std::shared_ptr(); - HashString m_AudioId{""}; public: - explicit Channel(size_t index, EventManager& em) : m_Index(index), m_EventManager(em) + explicit Channel(size_t index) { log = NH::CreateLogger(Union::String::Format("zBassMusic::Channel({0})", index)); }; - void Play(const std::shared_ptr&, HashString id = AudioFile::DEFAULT); - - void Stop(); - - bool IsAvailable() - { - return m_Status == ChannelStatus::AVAILABLE; - }; - - [[nodiscard]] double CurrentPosition() const; - - [[nodiscard]] double CurrentLength() const; - - [[nodiscard]] std::shared_ptr CurrentTheme() const; - - [[nodiscard]] const AudioFile& CurrentAudioFile() const; - - [[nodiscard]] const AudioEffects& CurrentAudioEffects() const; + Result PlayInstant(const AudioFile& audioFile) override; + void StopInstant() override; + void SetVolume(float volume) override; + void SlideVolume(float targetVolume, uint32_t time) override; + void SlideVolume(float targetVolume, uint32_t time, const std::function& onFinish) override; + void SetDX8ReverbEffect(float reverbMix, float reverbTime, float inputGain, float highFreqRTRatio) override; - static void CALLBACK OnTransitionSync(HSYNC, DWORD channel, DWORD data, void* userData); + void OnPosition(double position, const std::function& callback) override; + void OnAudioEnds(const std::function& onFinish) override; + void BeforeAudioEnds(double aheadSeconds, const std::function& onFinish) override; - static void CALLBACK OnEndSync(HSYNC, DWORD channel, DWORD data, void* userData); + bool IsPlaying() const override; + [[nodiscard]] double Position() const override; + [[nodiscard]] double Length() const override; - static void CALLBACK OnVolumeSlideSync(HSYNC, DWORD channel, DWORD data, void* userData); + void Acquire() override; + void Release() override; + bool IsAvailable() override { return m_Status == ChannelStatus::AVAILABLE; }; private: + static void CALLBACK OnPositionSyncCallFunction(HSYNC, DWORD channel, DWORD data, void* userData); + static void CALLBACK OnSlideVolumeSyncCallFunction(HSYNC, DWORD channel, DWORD data, void* userData); + static void CALLBACK OnAudioEndSyncCallFunction(HSYNC, DWORD channel, DWORD data, void* userData); + static void CALLBACK BeforeAudioEndsSyncCallFunction(HSYNC, DWORD channel, DWORD data, void* userData); }; } diff --git a/src/NH/Bass/Command.cpp b/src/NH/Bass/Command.cpp index 0979526..6f5b0cf 100644 --- a/src/NH/Bass/Command.cpp +++ b/src/NH/Bass/Command.cpp @@ -13,26 +13,54 @@ namespace NH::Bass void CommandQueue::AddCommand(std::shared_ptr command) { - std::lock_guard lock(m_Mutex); - log->Info("Adding command to queue"); - m_Commands.push_back(std::move(command)); + std::lock_guard lock(m_DeferredMutex); + log->Trace("Adding command to queue"); + m_DeferredCommands.push_back(std::move(command)); } - void CommandQueue::AddCommandDeferred(std::shared_ptr command) + void CommandQueue::AddCommandOnFront(std::shared_ptr command) { std::lock_guard lock(m_DeferredMutex); - m_DeferredCommands.push_back(std::move(command)); + log->Trace("Adding command to queue"); + m_DeferredCommands.push_front(std::move(command)); } - void CommandQueue::AddCommandOnFront(std::shared_ptr command) + void CommandQueue::AddPerFrameCommand(std::shared_ptr command) { - std::lock_guard lock(m_Mutex); - log->Info("Adding command to queue"); - m_Commands.push_front(std::move(command)); + std::lock_guard lock(m_PerFrameDeferredMutex); + m_PerFrameCommandsDeferred.push_back(std::move(command)); } void CommandQueue::Update(Engine& engine) { + { + std::lock_guard lock(m_PerFrameDeferredMutex); + m_PerFrameCommands.insert(m_PerFrameCommands.end(), m_PerFrameCommandsDeferred.begin(), m_PerFrameCommandsDeferred.end()); + m_PerFrameCommandsDeferred.clear(); + } + + { + std::lock_guard lock(m_PerFrameMutex); + std::vector> finishedCommands{}; + for (auto& command: m_PerFrameCommands) + { + if (command->Execute(engine) == CommandResult::DONE) + { + finishedCommands.push_back(command); + } + } + std::erase_if(m_PerFrameCommands, [&finishedCommands](const std::shared_ptr& command) + { + return std::find(finishedCommands.begin(), finishedCommands.end(), command) != finishedCommands.end(); + }); + } + + { + std::lock_guard lock(m_DeferredMutex); + m_Commands.insert(m_Commands.end(), m_DeferredCommands.begin(), m_DeferredCommands.end()); + m_DeferredCommands.clear(); + } + { std::lock_guard lock(m_DeferredMutex); m_Commands.insert(m_Commands.end(), m_DeferredCommands.begin(), m_DeferredCommands.end()); diff --git a/src/NH/Bass/Command.h b/src/NH/Bass/Command.h index cac117e..ceeadd2 100644 --- a/src/NH/Bass/Command.h +++ b/src/NH/Bass/Command.h @@ -7,6 +7,7 @@ #include #include #include +#include namespace NH::Bass { @@ -22,43 +23,52 @@ namespace NH::Bass struct Command { virtual CommandResult Execute(Engine& engine) = 0; + virtual ~Command() = default; + }; + + class FunctionCommand : public Command + { + std::function m_Function; + public: + explicit FunctionCommand(std::function function) : m_Function(std::move(function)) {} + CommandResult Execute(Engine& engine) override { return m_Function(engine); } }; class OnTimeCommand : public Command { using TimePointType = std::chrono::high_resolution_clock::time_point; - TimePointType m_TimePoint; std::shared_ptr m_Command; bool m_ForceOrder = false; - public: OnTimeCommand(TimePointType timePoint, std::shared_ptr command, bool forceOrder = false) : m_TimePoint(timePoint), m_Command(std::move(command)), m_ForceOrder(forceOrder) {} - CommandResult Execute(Engine& engine) override; }; class CommandQueue { Logger* log = CreateLogger("zBassMusic::CommandQueue"); + // Commands for one-off actions may stay in queue while returning RETRY or DEFER std::deque> m_Commands; + // Commands added during execution of a command. They are added to the main queue after execution. std::deque> m_DeferredCommands; + // Commands which lifetime is expected to last several frames. + // They are executed from another queue to avoid locking with one-off commands that use RETRY. + std::vector> m_PerFrameCommands; + std::vector> m_PerFrameCommandsDeferred; std::mutex m_Mutex; std::mutex m_DeferredMutex; + std::mutex m_PerFrameMutex; + std::mutex m_PerFrameDeferredMutex; public: void AddCommand(std::shared_ptr command); - /** - * Method to add commands within an executing command. - * We can't use AddCommand() directly, because it would cause a deadlock. - * @param command - */ - void AddCommandDeferred(std::shared_ptr command); - void AddCommandOnFront(std::shared_ptr command); + void AddPerFrameCommand(std::shared_ptr command); + void Update(Engine& engine); }; } diff --git a/src/NH/Bass/CommonTypes.h b/src/NH/Bass/CommonTypes.h deleted file mode 100644 index 4c00cae..0000000 --- a/src/NH/Bass/CommonTypes.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include "bass.h" -#include "Union/String.h" - -namespace NH::Bass -{ - -} diff --git a/src/NH/Bass/Engine.cpp b/src/NH/Bass/Engine.cpp index a2749b5..c51641a 100644 --- a/src/NH/Bass/Engine.cpp +++ b/src/NH/Bass/Engine.cpp @@ -1,5 +1,10 @@ #include "Options.h" #include "Engine.h" + +#include +#include +#include + #include namespace NH::Bass @@ -29,13 +34,42 @@ namespace NH::Bass uint64_t delta = std::chrono::duration_cast(now - lastTimestamp).count(); lastTimestamp = now; - m_TransitionScheduler.Update(*this); m_CommandQueue.Update(*this); BASS_Update(delta); GetEM().Update(); } + std::shared_ptr Engine::AcquireFreeChannel() + { + for (auto channel: m_Channels) + { + if (channel->IsAvailable()) + { + channel->Acquire(); + return channel; + } + } + + log->Info("No channel available. Creating a new one."); + if (m_Channels.size() > 32) + { + log->Warning("There are more than 32 channels. Report this to the developers because some channels may not be released."); + } + if (m_Channels.size() > 128) + { + log->Error("There are more than 128 channels. We are stopping this now because it's too much and there is definitely some leak."); + throw std::runtime_error("Too many channels"); + } + + return m_Channels.emplace_back(std::make_shared(m_Channels.size())); + } + + void Engine::ReleaseChannel(const std::shared_ptr& channel) + { + channel->Release(); + } + void Engine::SetVolume(float volume) { if (!m_Initialized) @@ -68,11 +102,6 @@ namespace NH::Bass return m_EventManager; } - TransitionScheduler& Engine::GetTransitionScheduler() - { - return m_TransitionScheduler; - } - MusicManager& Engine::GetMusicManager() { return m_MusicManager; @@ -85,36 +114,49 @@ namespace NH::Bass void Engine::StopMusic() { - if (!m_Initialized) - { - return; - } - - for (const auto& channel: m_Channels) + if (!m_Initialized) { return; } + if (m_ActiveTheme) { - channel->Stop(); + m_ActiveTheme->Stop(*this); + m_ActiveTheme = nullptr; } - - m_ActiveChannel = nullptr; } Engine::Engine() { + HPLUGIN midiPlugin = BASS_PluginLoad("bassmidi.dll", 0); + if (midiPlugin) { BASS_PluginEnable(midiPlugin, true); } + else { + log->Warning("Could not load BASSMIDI plugin. Make sure that bassmidi.dll is in the working directory or Autorun."); + log->Warning("BASSMIDI plugin is required for MIDI metadata. Engine may crash without it."); + } + + HPLUGIN opusPlugin = BASS_PluginLoad("bassopus.dll", 0); + if (opusPlugin) { BASS_PluginEnable(opusPlugin, true); } + else { log->Warning("Could not load BASSOPUS plugin (bassopus.dll). Opus files won't play."); } + + HPLUGIN flacPlugin = BASS_PluginLoad("bassflac.dll", 0); + if (flacPlugin) { BASS_PluginEnable(flacPlugin, true); } + else { log->Warning("Could not load BASSFLAC plugin (bassflac.dll). FLAC files won't play."); } + size_t deviceIndex = 0; BASS_DEVICEINFO deviceInfo; for (size_t i = 1; BASS_GetDeviceInfo(i, &deviceInfo); i++) { bool enabled = deviceInfo.flags & BASS_DEVICE_ENABLED; bool isDefault = deviceInfo.flags & BASS_DEVICE_DEFAULT; - + log->Trace("Available device: {0} {1} {2}", deviceInfo.name, enabled ? "enabled" : "disabled", isDefault ? "default" : ""); if (enabled && isDefault) { deviceIndex = i; - break; } }; - m_Initialized = BASS_Init(deviceIndex, 44100, 0, nullptr, nullptr); + BASS_SetDevice(deviceIndex); + BASS_GetDeviceInfo(deviceIndex, &deviceInfo); + log->Info("Device Name: {0}", deviceInfo.name); + + m_Initialized = BASS_Init((int32_t) deviceIndex, 44100, 0, nullptr, nullptr); if (!m_Initialized) { log->Error("Could not initialize BASS using BASS_Init\n {0}\n at {1}:{2}", @@ -124,29 +166,14 @@ namespace NH::Bass BASS_INFO info; BASS_GetInfo(&info); - log->Trace("Sample Rate: {0} Hz", info.freq); + log->Info("Sample Rate: {0} Hz", info.freq); - static constexpr size_t Channels_Max = 8; + static constexpr size_t Channels_Max = 16; m_Channels.clear(); for (size_t i = 0; i < Channels_Max; i++) { - m_Channels.emplace_back(std::make_shared(i, m_EventManager)); + m_Channels.emplace_back(std::make_shared(i)); } - - log->Info("Initialized with device: {0}", deviceIndex); - } - - std::shared_ptr Engine::FindAvailableChannel() - { - for (auto channel: m_Channels) - { - if (channel->IsAvailable()) - { - return channel; - } - } - - return nullptr; } Union::StringUTF8 Engine::ErrorCodeToString(const int code) @@ -156,61 +183,4 @@ namespace NH::Bass // @formatter:on return map[code]; } - - Logger* ChangeZoneCommand::log = CreateLogger("zBassMusic::ChangeZoneCommand"); - Logger* PlayThemeCommand::log = CreateLogger("zBassMusic::PlayThemeCommand"); - Logger* ScheduleThemeChangeCommand::log = CreateLogger("zBassMusic::ScheduleThemeChangeCommand"); - - CommandResult ChangeZoneCommand::Execute(Engine& engine) - { - log->Trace("Executing ChangeZoneCommand for zone {0}", m_Zone); - - const auto themes = engine.GetMusicManager().GetThemesForZone(m_Zone); - if (themes.empty()) - { - log->Warning("No themes found for zone {0}", m_Zone); - return CommandResult::DONE; - } - engine.GetCommandQueue().AddCommandDeferred(std::make_shared(themes[0].first)); - return CommandResult::DONE; - } - - CommandResult PlayThemeCommand::Execute(Engine& engine) - { - log->Trace("Executing PlayThemeCommand for theme {0}", m_ThemeId); - - auto theme = engine.GetMusicManager().GetTheme(m_ThemeId); - if (!theme->IsAudioFileReady(AudioFile::DEFAULT)) - { - log->Trace("Theme {0} is not ready", m_ThemeId); - m_RetryCount++; - if (m_RetryCount > 10000) - { - log->Error("Theme {0} was not ready after 10000 retries (roughly 600ms @ 60 FPS), removing it from queue", m_ThemeId); - return CommandResult::DONE; - } - return CommandResult::RETRY; - } - - auto channel = engine.FindAvailableChannel(); - if (!channel) - { - log->Error("No available channels"); - return CommandResult::DEFER; - } - - if (engine.m_ActiveChannel) { engine.m_ActiveChannel->Stop(); } - channel->Play(theme, m_AudioId); - engine.m_ActiveChannel = channel; - - return CommandResult::DONE; - } - - CommandResult ScheduleThemeChangeCommand::Execute(Engine& engine) - { - log->Trace("Executing ScheduleThemeChangeCommand for theme {0}", m_ThemeId); - auto theme = engine.GetMusicManager().GetTheme(m_ThemeId); - engine.GetTransitionScheduler().Schedule(engine.m_ActiveChannel, theme); - return CommandResult::DONE; - } } \ No newline at end of file diff --git a/src/NH/Bass/Engine.h b/src/NH/Bass/Engine.h index fbfcec0..2393a4d 100644 --- a/src/NH/Bass/Engine.h +++ b/src/NH/Bass/Engine.h @@ -1,13 +1,13 @@ #pragma once -#include "CommonTypes.h" +#include #include "EventManager.h" #include "Channel.h" -#include "TransitionScheduler.h" #include "NH/Logger.h" #include "NH/HashString.h" -#include "NH/Bass/MusicManager.h" -#include "NH/Bass/Command.h" +#include +#include +#include #include #include #include @@ -16,13 +16,11 @@ namespace NH::Bass { class ChangeZoneCommand; - class PlayThemeCommand; class ScheduleThemeChangeCommand; - class Engine + class Engine : public IEngine { friend class ChangeZoneCommand; - friend class PlayThemeCommand; friend class ScheduleThemeChangeCommand; static NH::Logger* log; @@ -30,66 +28,30 @@ namespace NH::Bass bool m_Initialized = false; float m_MasterVolume = 1.0f; std::vector> m_Channels; - std::shared_ptr m_ActiveChannel = nullptr; + std::shared_ptr m_ActiveTheme = nullptr; CommandQueue m_CommandQueue{}; EventManager m_EventManager{}; MusicManager m_MusicManager{}; - TransitionScheduler m_TransitionScheduler{}; public: static Engine* GetInstance(); void Update(); - + void StopMusic(); void SetVolume(float volume); - [[nodiscard]] float GetVolume() const; - EventManager& GetEM(); - - TransitionScheduler& GetTransitionScheduler(); - - MusicManager& GetMusicManager(); + std::shared_ptr AcquireFreeChannel() override; + void ReleaseChannel(const std::shared_ptr& channel) override; - CommandQueue& GetCommandQueue(); - - void StopMusic(); + EventManager& GetEM() override; + MusicManager& GetMusicManager() override; + CommandQueue& GetCommandQueue() override; private: Engine(); - std::shared_ptr FindAvailableChannel(); - public: static Union::StringUTF8 ErrorCodeToString(int code); }; - - class ChangeZoneCommand : public Command - { - static Logger* log; - HashString m_Zone; - public: - explicit ChangeZoneCommand(HashString zone) : m_Zone(zone) {}; - CommandResult Execute(Engine& engine) override; - }; - - class PlayThemeCommand : public Command - { - static Logger* log; - HashString m_ThemeId; - HashString m_AudioId; - size_t m_RetryCount = 0; - public: - explicit PlayThemeCommand(HashString themeId, HashString audioId) : m_ThemeId(themeId), m_AudioId(audioId) {}; - CommandResult Execute(Engine& engine) override; - }; - - class ScheduleThemeChangeCommand : public Command - { - static Logger* log; - HashString m_ThemeId; - public: - explicit ScheduleThemeChangeCommand(HashString themeId) : m_ThemeId(themeId) {}; - CommandResult Execute(Engine& engine) override; - }; } diff --git a/src/NH/Bass/EngineCommands.cpp b/src/NH/Bass/EngineCommands.cpp new file mode 100644 index 0000000..1fcccec --- /dev/null +++ b/src/NH/Bass/EngineCommands.cpp @@ -0,0 +1,47 @@ +#include "EngineCommands.h" + +#include +#include + +namespace NH::Bass +{ + Logger* ChangeZoneCommand::log = CreateLogger("zBassMusic::ChangeZoneCommand"); + Logger* ScheduleThemeChangeCommand::log = CreateLogger("zBassMusic::ScheduleThemeChangeCommand"); + + CommandResult ChangeZoneCommand::Execute(Engine& engine) + { + log->Info("Music zone changed: {0}", m_Zone); + const auto themes = engine.GetMusicManager().GetThemesForZone(m_Zone); + if (themes.empty()) + { + log->Warning("No themes found for zone {0}", m_Zone); + return CommandResult::DONE; + } + engine.GetCommandQueue().AddCommand(std::make_shared(themes[0].first)); + return CommandResult::DONE; + } + + CommandResult ScheduleThemeChangeCommand::Execute(Engine& engine) + { + log->Info("Scheduling theme: {0}", m_ThemeId); + auto theme = engine.GetMusicManager().GetTheme(m_ThemeId); + if (!theme) + { + log->Error("Theme {0} doesn't exist", m_ThemeId); + return CommandResult::DONE; + } + + auto activeTheme = engine.m_ActiveTheme; + bool anyChannelPlaying = false; + for (auto& channel : engine.m_Channels) + { + if (channel->IsPlaying()) { anyChannelPlaying = true; break; } + } + + if (activeTheme && anyChannelPlaying) theme->Schedule(engine, activeTheme); + else theme->Play(engine); + engine.m_ActiveTheme = theme; + + return CommandResult::DONE; + } +} \ No newline at end of file diff --git a/src/NH/Bass/EngineCommands.h b/src/NH/Bass/EngineCommands.h new file mode 100644 index 0000000..8d3bb10 --- /dev/null +++ b/src/NH/Bass/EngineCommands.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include + +namespace NH::Bass +{ + class ChangeZoneCommand : public Command + { + static Logger* log; + String m_Zone; + public: + explicit ChangeZoneCommand(const String& zone) : m_Zone(zone) {}; + CommandResult Execute(Engine& engine) override; + }; + + class ScheduleThemeChangeCommand : public Command + { + static Logger* log; + HashString m_ThemeId; + public: + explicit ScheduleThemeChangeCommand(HashString themeId) : m_ThemeId(themeId) {}; + CommandResult Execute(Engine& engine) override; + }; +} \ No newline at end of file diff --git a/src/NH/Bass/EventManager.h b/src/NH/Bass/EventManager.h index 8579ed0..fbf4dff 100644 --- a/src/NH/Bass/EventManager.h +++ b/src/NH/Bass/EventManager.h @@ -1,9 +1,8 @@ #pragma once -#include "NH/Bass/CommonTypes.h" #include "NH/Logger.h" -#include "MusicTheme.h" #include + #include #include #include @@ -11,6 +10,7 @@ namespace NH::Bass { + class MusicTheme; enum class EventType { @@ -22,16 +22,17 @@ namespace NH::Bass struct Event { - struct MusicEnd { std::shared_ptr Theme; HashString AudioId; }; - struct MusicTransition { std::shared_ptr Theme; HashString AudioId; float TimeLeft; }; - struct MusicChange { std::shared_ptr Theme; HashString AudioId; }; + struct MusicEnd { MusicTheme const* Theme = nullptr; HashString AudioId; }; + struct MusicTransition { MusicTheme const* Theme = nullptr; HashString AudioId; float TimeLeft = 0; }; + struct MusicChange { MusicTheme const* Theme = nullptr; HashString AudioId; }; using DataType = std::variant; EventType Type = EventType::UNKNOWN; DataType Data; Event() = delete; - Event(EventType type, DataType data) : Type(type), Data(std::move(data)) {} + Event(EventType type, DataType data) : Type(type), Data(data) {} + virtual ~Event() = default; }; using EventSubscriberFn = void (*)(const Event&, void*); @@ -77,19 +78,19 @@ namespace NH::Bass struct MusicEndEvent : public Event { MusicEnd Data; - MusicEndEvent(std::shared_ptr theme, HashString audioId) : Event(EventType::MUSIC_END, MusicEnd{std::move(theme), audioId}) {} + MusicEndEvent(MusicTheme* theme, HashString audioId) : Event(EventType::MUSIC_END, MusicEnd{theme, audioId}) {} }; struct MusicTransitionEvent : public Event { MusicTransition Data; - MusicTransitionEvent(std::shared_ptr theme, HashString audioId, float timeLeft) - : Event(EventType::MUSIC_TRANSITION, MusicTransition{std::move(theme), audioId, timeLeft}) {} + MusicTransitionEvent(MusicTheme* theme, HashString audioId, float timeLeft) + : Event(EventType::MUSIC_TRANSITION, MusicTransition{theme, audioId, timeLeft}) {} }; struct MusicChangeEvent : public Event { MusicChange Data; - MusicChangeEvent(std::shared_ptr theme, HashString audioId) : Event(EventType::MUSIC_CHANGE, MusicChange{std::move(theme), audioId}) {} + MusicChangeEvent(MusicTheme* theme, HashString audioId) : Event(EventType::MUSIC_CHANGE, MusicChange{theme, audioId}) {} }; } \ No newline at end of file diff --git a/src/NH/Bass/IChannel.h b/src/NH/Bass/IChannel.h new file mode 100644 index 0000000..edc119d --- /dev/null +++ b/src/NH/Bass/IChannel.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +namespace NH::Bass +{ + class AudioFile; + + struct IChannel + { + enum class ErrorType { INVALID_BUFFER }; + constexpr static const char* ErrorTypeStrings[] = { "INVALID_BUFFER" }; + + class Error : public std::runtime_error + { + ErrorType Type; + int32_t Code; + String Details; + public: + Error(ErrorType type, int32_t code, const String& details) + : std::runtime_error(String::Format("{0} error: {1}, message: %s", ErrorTypeStrings[static_cast(type)], code, details)), + Type(type), Code(code), Details(details) {}; + Error(ErrorType type, int32_t code, const String&& details) + : std::runtime_error(String::Format("{0} error: {1}, message: %s", ErrorTypeStrings[static_cast(type)], code, details)), + Type(type), Code(code), Details(details) {}; + [[nodiscard]] ErrorType GetType() const { return Type; } + [[nodiscard]] int32_t GetCode() const { return Code; } + [[nodiscard]] const char* GetDetails() const { return Details; } + }; + + template + using Result = std::expected; + + virtual ~IChannel() = default; + + virtual Result PlayInstant(const AudioFile& audioFile) = 0; + virtual void StopInstant() = 0; + + virtual void SetVolume(float volume) = 0; + virtual void SlideVolume(float targetVolume, uint32_t time) = 0; + virtual void SlideVolume(float targetVolume, uint32_t time, const std::function& onFinish) = 0; + virtual void SetDX8ReverbEffect(float reverbMix, float reverbTime, float inputGain, float highFreqRTRatio) = 0; + + virtual void OnPosition(double position, const std::function& callback) = 0; + virtual void OnAudioEnds(const std::function& onFinish) = 0; + virtual void BeforeAudioEnds(double aheadSeconds, const std::function& onFinish) = 0; + + [[nodiscard]] virtual bool IsPlaying() const = 0; + [[nodiscard]] virtual double Position() const = 0; + [[nodiscard]] virtual double Length() const = 0; + + virtual void Acquire() = 0; + virtual void Release() = 0; + virtual bool IsAvailable() = 0; + }; +} diff --git a/src/NH/Bass/IEngine.h b/src/NH/Bass/IEngine.h new file mode 100644 index 0000000..e9d8f5a --- /dev/null +++ b/src/NH/Bass/IEngine.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +namespace NH::Bass +{ + class EventManager; + class MusicManager; + class CommandQueue; + + struct IEngine + { + virtual ~IEngine() = default; + + /** + * Get a free channel and mark it as busy + * @return shared_ptr to a free channel (or null if all channels are busy) + */ + virtual std::shared_ptr AcquireFreeChannel() = 0; + + /** + * Release a channel and mark it as free + * @param channel + */ + virtual void ReleaseChannel(const std::shared_ptr& channel) = 0; + + virtual EventManager& GetEM() = 0; + virtual MusicManager& GetMusicManager() = 0; + virtual CommandQueue& GetCommandQueue() = 0; + }; +} diff --git a/src/NH/Bass/MidiFile.cpp b/src/NH/Bass/MidiFile.cpp new file mode 100644 index 0000000..4dcbacc --- /dev/null +++ b/src/NH/Bass/MidiFile.cpp @@ -0,0 +1,106 @@ +#include "MidiFile.h" + +#include +#include +#include + +namespace NH::Bass +{ + Logger* MidiFile::log = CreateLogger("zBassMusic::MidiFile"); + + void MidiFile::LoadMidiFile(Executor& executor, const std::function& onReady) + { + if (m_LoadingStatus != LoadingStatusType::NOT_LOADED) + { + log->Warning("MidiFile::LoadMidiFile() called on a MidiFile that is not in NOT_LOADED state."); + return; + } + m_LoadingStatus = LoadingStatusType::LOADING; + executor.AddTask([this, onReady]() + { + int systems = VDF_VIRTUAL | VDF_PHYSICAL; + const Union::VDFS::File* file = Union::VDFS::GetDefaultInstance().GetFile(m_Filename, systems); + if (!file) + { + m_LoadingStatus = LoadingStatusType::FAILED; + m_Error = "File not found"; + return; + } + + Union::Stream* stream = file->Open(); + m_Buffer.resize(stream->GetSize()); + stream->Read(m_Buffer.data(), m_Buffer.size()); + stream->Close(); + m_LoadingStatus = LoadingStatusType::READY; + ParseMidiFile(); + + if (onReady) + { + onReady(*this); + } + }); + } + + void MidiFile::ParseMidiFile() + { + m_Status = StatusType::PARSING; + m_Tones.clear(); + m_MidiEvents.clear(); + HSTREAM stream = BASS_MIDI_StreamCreateFile(true, m_Buffer.data(), 0, m_Buffer.size(), BASS_SAMPLE_FLOAT, 0); + uint32_t count = BASS_MIDI_StreamGetEvents(stream, -1, 0, nullptr); + m_MidiEvents.resize(count); + BASS_MIDI_StreamGetEvents(stream, -1, 0, m_MidiEvents.data()); + + uint32_t tempo = 0; + float ppqn = 0.0f; // Ticks per quarter note + BASS_ChannelGetAttribute(stream, BASS_ATTRIB_MIDI_PPQN, &ppqn); + std::unordered_map tonesInProgress; + double ticksToSeconds = 0.0; + + for (const auto& event: m_MidiEvents) + { + switch (event.event) + { + case MIDI_EVENT_TEMPO: + tempo = event.param; + ticksToSeconds = (double) tempo / (double) ppqn / 1000000.0; + log->Trace("TempoEvent: tempo={0}, ticksToSeconds={1}", tempo, ticksToSeconds); + break; + case MIDI_EVENT_NOTE: + { + uint8_t key = event.param & 0xFF; + uint8_t velocity = (event.param >> 8) & 0xFF; + uint32_t tick = event.tick; + double time = tick * ticksToSeconds; + log->Trace("NoteEvent: key={0} action={1} time={2}", key, velocity == 0 ? "release" : (velocity == 255 ? "stop" : "press"), time); + if (velocity == 0 || velocity == 255) + { + // release || stop + if (!tonesInProgress.contains(key)) + { + log->Error("Error: key {0} released without being pressed at {1}", key, time); + continue; + } + Tone tone = tonesInProgress[key]; + tone.end = time; + m_Tones.emplace_back(tone); + tonesInProgress.erase(key); + } + else + { + // press + tonesInProgress[key] = { key, time, 0}; + } + break; + } + } + } + + for (const auto& tone : m_Tones) + { + log->Debug("Tone{ key={0}, start={1}, end={2} }", static_cast(tone.key), tone.start, tone.end); + } + + m_Status = StatusType::READY; + } +} \ No newline at end of file diff --git a/src/NH/Bass/MidiFile.h b/src/NH/Bass/MidiFile.h new file mode 100644 index 0000000..d795116 --- /dev/null +++ b/src/NH/Bass/MidiFile.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace NH::Bass +{ + /** + * @brief Represents a MIDI file with metadata for the music transitions. + * + * The MIDI file is parser and the notes are extracted and stored in the `m_Tones` vector. + * Specific note keys mean: + * - A4 (key=69): Fade out transition + * Fade-out starts at the START time and decreases the volume to 0 at the END time. + * By default, the next song is fading-in for the same period. + * - A#4 (key=70): Fade in transition for next song (optional) + * If present alongside A4, defines the fade-in period of the next song. + */ + class MidiFile + { + public: + struct Tone { + uint32_t key; + double start; + double end; + }; + + enum class StatusType : size_t { NOT_LOADED = 0, PARSING, READY, FAILED }; + enum class LoadingStatusType : size_t { NOT_LOADED = 0, LOADING, READY, FAILED }; + + private: + static Logger* log; + HashString m_ThemeId; + String m_Filename; + String m_TargetFilter; + std::vector m_Buffer{}; + StatusType m_Status = StatusType::NOT_LOADED; + LoadingStatusType m_LoadingStatus = LoadingStatusType::NOT_LOADED; + String m_Error{}; + std::vector m_MidiEvents{}; + std::vector m_Tones{}; + + public: + MidiFile(HashString themeId, const String& filename, const String& targetFilter = "") + : m_ThemeId(themeId), m_Filename(filename), m_TargetFilter(targetFilter) {} + + void LoadMidiFile(Executor& executor, const std::function& onReady = nullptr); + + [[nodiscard]] HashString GetThemeId() const { return m_ThemeId; } + + [[nodiscard]] const String& GetFilename() const { return m_Filename; } + + [[nodiscard]] const String& GetTargetFilter() const { return m_TargetFilter; } + + [[nodiscard]] StatusType GetStatus() const { return m_Status; } + + [[nodiscard]] LoadingStatusType GetLoadingStatus() const { return m_LoadingStatus; } + + [[nodiscard]] const String& GetError() const { return m_Error; } + + [[nodiscard]] const std::vector& GetTones() const { return m_Tones; } + + private: + void ParseMidiFile(); + }; +} diff --git a/src/NH/Bass/MusicManager.cpp b/src/NH/Bass/MusicManager.cpp index e03874d..39f3aae 100644 --- a/src/NH/Bass/MusicManager.cpp +++ b/src/NH/Bass/MusicManager.cpp @@ -2,12 +2,34 @@ namespace NH::Bass { - void MusicManager::AddTheme(HashString id, std::shared_ptr theme) + void MusicManager::AddTheme(HashString id, const std::shared_ptr& theme) { - m_Themes.emplace(id, theme); - m_Themes.at(id)->LoadAudioFiles(Executors.IO); + m_Themes[id] = theme; + m_Themes[id]->LoadAudioFiles(Executors.IO); log->Info("New theme {0}", id); - log->PrintRaw(LoggerLevel::Debug, m_Themes.at(id)->ToString()); + log->PrintRaw(LoggerLevel::Debug, m_Themes[id]->ToString()); + } + + void MusicManager::AddJingle(HashString id, const String& jingleFilename, double delay, HashString filter) + { + if (m_Themes.contains(id)) + { + m_Themes[id]->AddJingle(jingleFilename, delay, filter); + } + else + { + log->Error("Theme {0} does not exist", String(id)); + } + } + + void MusicManager::RefreshTheme(HashString id) + { + if (m_Themes.contains(id)) + { + m_Themes[id]->LoadAudioFiles(Executors.IO); + log->Info("Refresh theme {0}", id); + log->PrintRaw(LoggerLevel::Debug, m_Themes[id]->ToString()); + } } std::vector>> MusicManager::GetThemesForZone(HashString zone) @@ -29,6 +51,6 @@ namespace NH::Bass { return {}; } - return m_Themes.at(id); + return m_Themes[id]; } } \ No newline at end of file diff --git a/src/NH/Bass/MusicManager.h b/src/NH/Bass/MusicManager.h index eba0bb3..2f63835 100644 --- a/src/NH/Bass/MusicManager.h +++ b/src/NH/Bass/MusicManager.h @@ -12,10 +12,11 @@ namespace NH::Bass std::unordered_map> m_Themes; public: - void AddTheme(HashString id, std::shared_ptr theme); + void AddTheme(HashString id, const std::shared_ptr& theme); + void AddJingle(HashString id, const String& jingleFilename, double delay, HashString filter = ""_hs); + void RefreshTheme(HashString id); [[nodiscard]] std::shared_ptr GetTheme(HashString id); - std::vector>> GetThemesForZone(HashString zone); }; } diff --git a/src/NH/Bass/MusicTheme.cpp b/src/NH/Bass/MusicTheme.cpp index 48ae819..ad35ffc 100644 --- a/src/NH/Bass/MusicTheme.cpp +++ b/src/NH/Bass/MusicTheme.cpp @@ -1,6 +1,9 @@ #include "MusicTheme.h" +#include "EngineCommands.h" #include +#include +#include namespace NH::Bass { @@ -9,11 +12,7 @@ namespace NH::Bass MusicTheme MusicTheme::None = MusicTheme(""); Logger* MusicTheme::log = CreateLogger("zBassMusic::MusicTheme"); - MusicTheme::MusicTheme(const String& name) - : m_Name(name) - { - - } + MusicTheme::MusicTheme(const String& name) : m_Name(name), m_TransitionInfo{ m_Name } {} void MusicTheme::SetAudioFile(HashString type, const String& filename) { @@ -24,8 +23,15 @@ namespace NH::Bass void MusicTheme::SetAudioEffects(HashString type, const std::function& effectsSetter) { - m_AudioEffects.emplace(std::make_pair(type, AudioEffects{})); + if (!m_AudioFiles.contains(type)) + { + m_AudioEffects[type] = AudioEffects{}; + } effectsSetter(m_AudioEffects[type]); + if (m_AudioEffects[type].FadeOut.Active) + { + m_TransitionInfo.AddTransitionEffect(TransitionEffect::CROSSFADE, m_AudioEffects[type].FadeOut.Duration); + } } void MusicTheme::AddZone(HashString zone) @@ -33,6 +39,24 @@ namespace NH::Bass m_Zones.emplace_back(zone); } + void MusicTheme::AddMidiFile(HashString type, std::shared_ptr midiFile) + { + m_MidiFiles.emplace(std::make_pair(HashString(type), midiFile)); + m_MidiFiles[type]->LoadMidiFile(Executors.IO, [this, type](MidiFile& midi) { + m_TransitionInfo.AddMidiFile(midi, type); + }); + } + + void MusicTheme::AddJingle(const String& filename, double delay, HashString filter) + { + HashString key = String("Jingle_") + String(filter); + SetAudioFile(key, filename); + LoadAudioFiles(Executors.IO); + AudioFile& jingle = m_AudioFiles.at(key); + m_TransitionInfo.AddJingle(std::shared_ptr(&jingle), delay, filter); + log->Info("New jingle created {0}: {1}", String(filter), filename); + } + void MusicTheme::LoadAudioFiles(Executor& executor) { for (auto& [type, audioFile]: m_AudioFiles) @@ -40,36 +64,249 @@ namespace NH::Bass if (audioFile.Status == AudioFile::StatusType::NOT_LOADED) { audioFile.Status = AudioFile::StatusType::LOADING; - executor.AddTask([type, this]() - { - int systems = VDF_VIRTUAL | VDF_PHYSICAL; - const Union::VDFS::File* file = Union::VDFS::GetDefaultInstance().GetFile(m_AudioFiles[type].Filename, systems); - if (!file) - { - m_AudioFiles[type].Status = AudioFile::StatusType::FAILED; - m_AudioFiles[type].Error = "File not found"; - return; - } - - Union::Stream* stream = file->Open(); - m_AudioFiles[type].Buffer.resize(stream->GetSize()); - stream->Read(m_AudioFiles[type].Buffer.data(), m_AudioFiles[type].Buffer.size()); - stream->Close(); - - m_AudioFiles[type].Status = AudioFile::StatusType::READY; - }); + executor.AddTask([type, this]() { + int systems = VDF_VIRTUAL | VDF_PHYSICAL; + const Union::VDFS::File* file = Union::VDFS::GetDefaultInstance().GetFile(m_AudioFiles[type].Filename, systems); + if (!file) + { + m_AudioFiles[type].Status = AudioFile::StatusType::FAILED; + m_AudioFiles[type].Error = "File not found"; + return; + } + + Union::Stream* stream = file->Open(); + m_AudioFiles[type].Buffer.resize(stream->GetSize()); + stream->Read(m_AudioFiles[type].Buffer.data(), m_AudioFiles[type].Buffer.size()); + stream->Close(); + + m_AudioFiles[type].Status = AudioFile::StatusType::READY; + }); + } + } + } + + void MusicTheme::Schedule(IEngine& engine, const std::shared_ptr& currentTheme) + { + currentTheme->Transition(engine, *this); + } + + void MusicTheme::Transition(IEngine& engine, MusicTheme& nextTheme) + { + if (nextTheme.GetName() == GetName()) { return; } + auto channel = GetAcquiredChannel(); + if (!channel) channel = m_AcquiredChannels.emplace_back(engine.AcquireFreeChannel()); + const auto& transition = m_TransitionInfo.GetTransition(nextTheme.GetName()); + std::optional timePoint = transition.NextAvailableTimePoint(channel->Position()); + + log->Debug("Transition {0} to {1}", GetName(), nextTheme.GetName()); + log->PrintRaw(LoggerLevel::Trace, transition.ToString()); + + const std::function& playJingle = CreateSyncHandler([&engine, &transition, this]() { + if (transition.Jingle) + { + auto channel = engine.AcquireFreeChannel(); + auto result = channel->PlayInstant(*transition.Jingle); + if (result) { channel->OnAudioEnds(CreateSyncHandler([channel]() { channel->Release(); })); } + else + { + log->Error("Could not play jingle: {0}", result.error().what()); + channel->Release(); + } } + }); + + if (timePoint && channel->IsPlaying()) + { + log->Trace("OnPosition: {0}", String(timePoint->Start)); + channel->OnPosition(timePoint->Start, CreateSyncHandler([&engine, &transition, this]() { + Stop(engine, transition); + })); + engine.GetCommandQueue().AddCommand(std::make_shared( + std::chrono::high_resolution_clock::now() + std::chrono::milliseconds((long long)((timePoint->Start + transition.JingleDelay) * 1000)), + std::make_shared([&playJingle](Engine& engine) -> CommandResult { + playJingle(); + return CommandResult::DONE; + }))); + channel->OnPosition(timePoint->NextStart, CreateSyncHandler([&engine, &nextTheme, &transition, timePoint]() { + nextTheme.Play(engine, transition, timePoint); + })); + } + else + { + Stop(engine, transition); + engine.GetCommandQueue().AddCommand(std::make_shared( + std::chrono::high_resolution_clock::now() + std::chrono::milliseconds((long long)(transition.JingleDelay * 1000)), + std::make_shared([&playJingle](Engine& engine) -> CommandResult { + playJingle(); + return CommandResult::DONE; + }))); + nextTheme.Play(engine, transition); } } + void MusicTheme::Play(IEngine& engine) { Play(engine, Transition::EMPTY); } + void MusicTheme::Play(IEngine& engine, const struct Transition& transition, std::optional timePoint) + { + if (!ReadyToPlay(engine, AudioFile::DEFAULT)) { return; } + + auto channel = m_AcquiredChannels.emplace_back(engine.AcquireFreeChannel()); + auto& effects = GetAudioEffects(AudioFile::DEFAULT); + auto effect = timePoint.has_value() ? timePoint.value().NextEffect : transition.Effect; + auto result = channel->PlayInstant(GetAudioFile(AudioFile::DEFAULT)); + if (!result) + { + ReleaseChannels(); + return; + } + + if (effects.ReverbDX8.Active) + { + channel->SetDX8ReverbEffect(effects.ReverbDX8.Mix, effects.ReverbDX8.Time, 0, 0.001f); + } + + if (effect == TransitionEffect::CROSSFADE) + { + channel->SetVolume(0.0f); + channel->SlideVolume(1.0f, timePoint.has_value() ? (uint32_t)timePoint.value().Duration * 1000 : (uint32_t)effects.FadeIn.Duration); + } + else { channel->SetVolume(effects.Volume.Active ? effects.Volume.Volume : 1.0f); } + + if (effects.FadeOut.Active) + { + channel->BeforeAudioEnds(effects.FadeOut.Duration, CreateSyncHandler([this, &engine](double timeLeft) -> void { + engine.GetEM().DispatchEvent(MusicTransitionEvent{ this, AudioFile::DEFAULT, static_cast(timeLeft) }); + })); + } + + channel->OnAudioEnds(CreateSyncHandler([this, &engine]() -> void { engine.GetEM().DispatchEvent(MusicEndEvent{ this, AudioFile::DEFAULT }); })); + engine.GetEM().DispatchEvent(MusicChangeEvent{ this, AudioFile::DEFAULT }); + } + + void MusicTheme::Stop(IEngine& engine) { Stop(engine, m_TransitionInfo.GetDefaultTransition()); } + void MusicTheme::Stop(IEngine& engine, const struct Transition& transition) + { + auto channel = GetAcquiredChannel(); + if (!channel) { ReleaseChannels(); }; + std::optional timePoint = transition.NextAvailableTimePoint(channel->Position()); + auto effect = timePoint.has_value() ? timePoint.value().Effect : transition.Effect; + if (effect == TransitionEffect::CROSSFADE) + { + uint32_t time = timePoint.has_value() ? (uint32_t)timePoint.value().Duration * 1000 : (uint32_t)transition.EffectDuration; + log->Trace("Fadeout, time: {0}", time); + channel->SlideVolume(0.0f, time, CreateSyncHandler([channel, this]() -> void { + log->Trace("Fadeout done"); + channel->StopInstant(); + ReleaseChannels(); + })); + } + else + { + channel->StopInstant(); + ReleaseChannels(); + } + } + + bool MusicTheme::ReadyToPlay(IEngine& engine, HashString audio) + { + const auto& file = GetAudioFile(AudioFile::DEFAULT); + if (file.Status == AudioFile::StatusType::FAILED) + { + log->Error("Trying to play a music theme that has failed to load: {0}", file); + return false; + } + + if (file.Status != AudioFile::StatusType::READY) + { + engine.GetCommandQueue().AddCommand(std::make_shared( + [this, &engine](Engine& e) -> CommandResult { + const AudioFile& f = GetAudioFile(AudioFile::DEFAULT); + if (f.Status == AudioFile::StatusType::FAILED) + { + log->Error("Trying to play a music theme that has failed to load: {0}", f.Filename); + return CommandResult::DONE; + } + if (f.Status == AudioFile::StatusType::READY || (m_MidiFiles.contains(""_hs) && m_MidiFiles[""_hs]->GetStatus() == MidiFile::StatusType::PARSING)) + { + engine.GetCommandQueue().AddCommand(std::make_shared(m_Name)); + return CommandResult::DONE; + } + return CommandResult::DEFER; + })); + return false; + } + + return true; + } + const AudioEffects& MusicTheme::GetAudioEffects(HashString type) const { if (m_AudioFiles.contains(type)) { return m_AudioEffects.at(type); } return AudioEffects::None; } + std::shared_ptr MusicTheme::GetMidiFile(HashString type) const + { + if (m_MidiFiles.contains(type)) { return m_MidiFiles.at(type); } + return {}; + } + bool MusicTheme::HasZone(HashString zone) const { + zone = String(zone.GetValue()).MakeUpper(); return std::find(m_Zones.begin(), m_Zones.end(), zone) != m_Zones.end(); } + + std::shared_ptr MusicTheme::GetAcquiredChannel() + { + for (auto& channel: m_AcquiredChannels) + { + return channel; + } + return {}; + } + + void MusicTheme::ReleaseChannels() + { + for (auto& channel: m_AcquiredChannels) + { + channel->Release(); + } + m_AcquiredChannels.clear(); + } + + String MusicTheme::ToString() const + { + String result = String("MusicTheme{ \n\tName: ") + m_Name + ", \n\tAudioFiles: {\n"; + int i = 0; + for (auto& [type, audioFile]: m_AudioFiles) + { + result += String("\t\t") + String(type) + ": " + audioFile.ToString().Replace("\n", "\n\tt"); + if (++i < m_AudioFiles.size()) + { + result += ",\n"; + } + } + i = 0; + result += "\n\t},\n\tAudioEffects: { \n"; + for (auto& [type, audioEffects]: m_AudioEffects) + { + result += String("\t\t") + String(type) + ": " + audioEffects.ToString().Replace("\n", "\n\t\t"); + if (++i < m_AudioEffects.size()) + { + result += ",\n"; + } + } + i = 0; + result += "\n\t},\n\tZones: { "; + for (auto& zone: m_Zones) + { + result += String(zone); + if (++i < m_Zones.size()) + { + result += ", "; + } + } + result += " }\n }"; + return result; + } } \ No newline at end of file diff --git a/src/NH/Bass/MusicTheme.h b/src/NH/Bass/MusicTheme.h index b5dcaa0..b18e522 100644 --- a/src/NH/Bass/MusicTheme.h +++ b/src/NH/Bass/MusicTheme.h @@ -1,10 +1,13 @@ #pragma once -#include "CommonTypes.h" #include #include #include #include +#include +#include +#include +#include #include #include @@ -55,9 +58,15 @@ namespace NH::Bass static Logger* log; String m_Name; + size_t m_SyncHandlersId = 0; + TransitionInfo m_TransitionInfo; std::unordered_map m_AudioFiles; std::unordered_map m_AudioEffects; + std::unordered_map> m_MidiFiles; + std::unordered_map> m_SyncHandlers; + std::unordered_map> m_SyncHandlersWithDouble; std::vector m_Zones; + std::vector> m_AcquiredChannels; public: static MusicTheme None; @@ -65,61 +74,57 @@ namespace NH::Bass explicit MusicTheme(const String& name); void SetAudioFile(HashString type, const String& filename); - void SetAudioEffects(HashString type, const std::function& effectsSetter); - void AddZone(HashString zone); - + void AddMidiFile(HashString type, std::shared_ptr midiFile); + void AddJingle(const String& filename, double delay, HashString filter); void LoadAudioFiles(Executor& executor); - [[nodiscard]] const String& GetName() const { return m_Name; } + void Schedule(IEngine& engine, const std::shared_ptr& currentTheme); + void Transition(IEngine& engine, MusicTheme& nextTheme); + void Play(IEngine& engine); + void Play(IEngine& engine, const struct Transition& transition, std::optional timePoint = std::nullopt); + void Stop(IEngine& engine); + void Stop(IEngine& engine, const struct Transition& transition); + [[nodiscard]] const String& GetName() const { return m_Name; } + [[nodiscard]] TransitionInfo& GetTransitionInfo() { return m_TransitionInfo; }; [[nodiscard]] bool HasAudioFile(HashString type) const { return m_AudioFiles.find(type) != m_AudioFiles.end(); } - [[nodiscard]] bool IsAudioFileReady(HashString type) const { return HasAudioFile(type) && m_AudioFiles.at(type).Status == AudioFile::StatusType::READY; } - [[nodiscard]] const AudioFile& GetAudioFile(HashString type) const { return m_AudioFiles.at(type); } - [[nodiscard]] const AudioEffects& GetAudioEffects(HashString type) const; - + [[nodiscard]] std::shared_ptr GetMidiFile(HashString type) const; + [[nodiscard]] const std::vector& GetZones() const { return m_Zones; } [[nodiscard]] bool HasZone(HashString zone) const; + [[nodiscard]] String ToString() const override; - [[nodiscard]] String ToString() const override + private: + bool ReadyToPlay(IEngine& engine, HashString audio); + std::shared_ptr GetAcquiredChannel(); + void ReleaseChannels(); + + const std::function& CreateSyncHandler(std::function function) { - String result = String("MusicTheme{ \n\tName: ") + m_Name + ", \n\tAudioFiles: {\n"; - int i = 0; - for (auto& [type, audioFile]: m_AudioFiles) - { - result += String("\t\t") + String(type) + ": " + audioFile.ToString().Replace("\n", "\n\tt"); - if (++i < m_AudioFiles.size()) - { - result += ",\n"; - } - } - i = 0; - result += "\n\t},\n\tAudioEffects: { \n"; - for (auto& [type, audioEffects]: m_AudioEffects) - { - result += String("\t\t") + String(type) + ": " + audioEffects.ToString().Replace("\n", "\n\t\t"); - if (++i < m_AudioEffects.size()) - { - result += ",\n"; - } - } - i = 0; - result += "\n\t},\n\tZones: { "; - for (auto& zone: m_Zones) - { - result += String(zone); - if (++i < m_Zones.size()) - { - result += ", "; - } - } - result += " }\n }"; - return result; + size_t id = m_SyncHandlersId++; + log->Trace("SyncHandler id: {0}", id); + auto handler = [this, function = std::move(function), id](double value) { + function(value); + m_SyncHandlersWithDouble.erase(id); + }; + m_SyncHandlersWithDouble.emplace(id, std::move(handler)); + return m_SyncHandlersWithDouble.at(id); } - private: + const std::function& CreateSyncHandler(std::function function) + { + size_t id = m_SyncHandlersId++; + log->Trace("SyncHandler id: {0}", id); + auto handler = [this, function = std::move(function), id]() { + function(); + m_SyncHandlers.erase(id); + }; + m_SyncHandlers.emplace(id, std::move(handler)); + return m_SyncHandlers.at(id); + } }; } diff --git a/src/NH/Bass/Transition.cpp b/src/NH/Bass/Transition.cpp new file mode 100644 index 0000000..ab97d93 --- /dev/null +++ b/src/NH/Bass/Transition.cpp @@ -0,0 +1,32 @@ +#include "Transition.h" + +#include + +namespace NH::Bass +{ + Transition Transition::EMPTY = {}; + + std::optional Transition::NextAvailableTimePoint(double position) const + { + for (const auto& timePoint : TimePoints) + { + if (position >= timePoint.Start && position < timePoint.Start + timePoint.Duration) + { + return timePoint; + } + } + return std::nullopt; + } + + String Transition::ToString() const + { + static String effects[] = {"NONE", "CROSSFADE"}; + return String("Transition {\n") + + "\tEffect: " + effects[(size_t)Effect] + ",\n" + + "\tEffectDuration: " + String(EffectDuration) + ",\n" + + "\tTimePoints: " + "not_impl" + ",\n" + + "\tJingle: " + "not_impl" + ",\n" + + "\tJingleDelay: " + String(JingleDelay) + "\n" + + "}"; + } +} \ No newline at end of file diff --git a/src/NH/Bass/Transition.h b/src/NH/Bass/Transition.h new file mode 100644 index 0000000..4eaf1ff --- /dev/null +++ b/src/NH/Bass/Transition.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include + +namespace NH::Bass +{ + class MusicTheme; + + enum class TransitionEffect { NONE = 0, CROSSFADE = 1 }; + struct Transition : public HasToString + { + static Transition EMPTY; + + struct TimePoint + { + double Start{}; + double Duration{}; + TransitionEffect Effect = TransitionEffect::NONE; + double NextStart = Start; + double NextDuration = Duration; + TransitionEffect NextEffect = Effect; + }; + TransitionEffect Effect = TransitionEffect::NONE; + double EffectDuration = 0; + std::vector TimePoints; + std::shared_ptr Jingle; + double JingleDelay = 0; + + [[nodiscard]] std::optional NextAvailableTimePoint(double position) const; + + [[nodiscard]] String ToString() const override; + }; +} diff --git a/src/NH/Bass/TransitionInfo.cpp b/src/NH/Bass/TransitionInfo.cpp new file mode 100644 index 0000000..23faca5 --- /dev/null +++ b/src/NH/Bass/TransitionInfo.cpp @@ -0,0 +1,80 @@ +#include "TransitionInfo.h" +#include "Transition.h" + +#include +#include + +#include + +namespace NH::Bass +{ + const Transition& TransitionInfo::GetTransition(HashString targetTheme) const + { + for (const auto& [key, transition]: m_FilteredTransitions) + { + String regex = String(key.GetValue()).Replace(",", "|").Replace(" ", "\\s*").Replace("*", ".*");; + if (regex.IsMatchesMask(targetTheme.GetValue())) + { + return transition; + } + } + return m_DefaultTransition; + } + + const Transition& TransitionInfo::GetDefaultTransition() const + { + return m_DefaultTransition; + } + + void TransitionInfo::AddTransitionEffect(TransitionEffect effect, double duration, HashString filter) + { + Transition* transition = filter == ""_hs ? &m_DefaultTransition : GetFilteredTransition(filter); + transition->Effect = effect; + transition->EffectDuration = duration; + } + + void TransitionInfo::AddMidiFile(MidiFile& file, HashString filter) + { + Transition* transition = filter == ""_hs ? &m_DefaultTransition : GetFilteredTransition(filter); + Transition::TimePoint timePoint{-1}; + for (const auto& tone : file.GetTones()) + { + if (tone.key == 69) // A4 + { + if (timePoint.Start > -1) { transition->TimePoints.push_back(timePoint); } + timePoint = { tone.start, tone.end - tone.start, TransitionEffect::CROSSFADE }; + } + else if (tone.key == 70 && timePoint.Start > -1) // A4 + { + timePoint.NextStart = tone.start; + timePoint.NextDuration = tone.end - tone.start; + timePoint.NextEffect = TransitionEffect::CROSSFADE; + transition->TimePoints.push_back(timePoint); + timePoint = {-1}; + } + } + if (timePoint.Start > -1) { transition->TimePoints.push_back(timePoint); } + } + + void TransitionInfo::AddTimePoint(Transition::TimePoint timePoint, HashString filter) + { + Transition* transition = filter == ""_hs ? &m_DefaultTransition : GetFilteredTransition(filter); + transition->TimePoints.push_back(timePoint); + } + + void TransitionInfo::AddJingle(std::shared_ptr jingle, double delay, HashString filter) + { + Transition* transition = filter == ""_hs ? &m_DefaultTransition : GetFilteredTransition(filter); + transition->Jingle = std::move(jingle); + transition->JingleDelay = delay; + } + + Transition* TransitionInfo::GetFilteredTransition(HashString filter) + { + if (!m_FilteredTransitions.contains(filter)) + { + m_FilteredTransitions.emplace(filter, m_DefaultTransition); + } + return &m_FilteredTransitions[filter]; + } +} \ No newline at end of file diff --git a/src/NH/Bass/TransitionInfo.h b/src/NH/Bass/TransitionInfo.h new file mode 100644 index 0000000..894463c --- /dev/null +++ b/src/NH/Bass/TransitionInfo.h @@ -0,0 +1,37 @@ +#pragma once + +#include "Transition.h" +#include +#include + +#include + +namespace NH::Bass +{ + class MidiFile; + + class TransitionInfo + { + HashString m_SourceTheme; + Transition m_DefaultTransition; + std::unordered_map m_FilteredTransitions; + + public: + explicit TransitionInfo(HashString sourceTheme) + : m_SourceTheme(sourceTheme) + {} + + void AddTransitionEffect(TransitionEffect effect, double duration = 0, HashString filter = ""_hs); + void AddMidiFile(MidiFile& file, HashString filter = ""_hs); + void AddTimePoint(Transition::TimePoint timePoint, HashString filter = ""_hs); + void AddJingle(std::shared_ptr jingle, double delay, HashString filter = ""_hs); + + [[nodiscard]] const Transition& GetTransition(HashString targetTheme) const; + [[nodiscard]] const Transition& GetDefaultTransition() const; + [[nodiscard]] HashString GetSourceTheme() const { return m_SourceTheme; } + + private: + Transition* GetFilteredTransition(HashString filter); + }; + +} diff --git a/src/NH/Bass/TransitionScheduler.cpp b/src/NH/Bass/TransitionScheduler.cpp deleted file mode 100644 index 64dd44b..0000000 --- a/src/NH/Bass/TransitionScheduler.cpp +++ /dev/null @@ -1,154 +0,0 @@ -#include "TransitionScheduler.h" - -#include -#include -#include - -namespace NH::Bass -{ - void TransitionScheduler::Schedule(const std::shared_ptr& activeChannel, const std::shared_ptr& nextMusic) - { - if (!activeChannel) - { - m_ScheduledActions.emplace_back(ScheduledAction{ activeChannel, 0, [id = HashString(nextMusic->GetName())](Engine& engine) - { - engine.GetCommandQueue().AddCommand(std::make_shared(id, AudioFile::DEFAULT)); - }}); - return; - } - - const auto& currentTheme = activeChannel->CurrentTheme(); - const TransitionScheduleRule& rule = GetScheduleRule(currentTheme->GetName()); - - if (rule.Type == TransitionScheduleType::INSTANT) - { - ScheduleInstant(activeChannel, nextMusic); - return; - } - - if (rule.Type == TransitionScheduleType::ON_BEAT) - { - ScheduleOnBeat(activeChannel, nextMusic, rule); - return; - } - - log->Error("Unknown Schedule type = {0}", (size_t)rule.Type); - } - - void TransitionScheduler::ScheduleInstant(const std::shared_ptr& activeChannel, const std::shared_ptr& nextMusic) - { - m_ScheduledActions.emplace_back(ScheduledAction{ activeChannel, 0, [id = HashString(nextMusic->GetName())](Engine& engine) - { - engine.GetCommandQueue().AddCommand(std::make_shared(id, AudioFile::DEFAULT)); - }}); - } - - void TransitionScheduler::ScheduleOnBeat(const std::shared_ptr& activeChannel, const std::shared_ptr& nextMusic, const TransitionScheduleRule& rule) - { - TransitionScheduleRule::DataOnBeat data = std::get(rule.Data); - const auto& currentTheme = activeChannel->CurrentTheme(); - const auto& effects = currentTheme->GetAudioEffects(AudioFile::DEFAULT); - - double overhead = effects.FadeOut.Active ? effects.FadeOut.Duration : 0; - std::sort(data.TimePoints.begin(), data.TimePoints.end()); - - double length = activeChannel->CurrentLength(); - double currentPosition = activeChannel->CurrentPosition(); - log->Trace("length = {0}", length); - log->Trace("currentPosition = {0}", currentPosition); - double timePointCandidate = -1; - double intervalCandidate = -1; - - log->Trace("Rule checking TimePoints"); - for (const auto& item: data.TimePoints) - { - double minValue = currentPosition - overhead; - if (item > minValue) - { - log->Trace("found TimePoint = {0}", item); - timePointCandidate = item; - break; - } - } - - if (data.Interval > 0) - { - log->Trace("Rule checking Interval"); - - // Please don't abort me. Quick hack for preview. Fast enough. - for (double time = 0; time < length; time += data.Interval) - { - double minValue = currentPosition - overhead; - if (time > minValue) - { - log->Trace("found TimePoint = {0}", time); - intervalCandidate = time; - break; - } - } - } - - if (timePointCandidate <= 0 && intervalCandidate <= 0) - { - log->Trace("Not found any candidates, rollback to INSTANT"); - ScheduleInstant(activeChannel, nextMusic); - return; - } - - double target = timePointCandidate > intervalCandidate ? timePointCandidate : intervalCandidate; - log->Trace("target = {0}", target); - - log->Debug("Starting Monitor {0} -> {1}, position = {2}", currentTheme->GetName(), nextMusic->GetName(), target); - m_ScheduledActions.emplace_back(ScheduledAction{ activeChannel, target, [id = HashString(nextMusic->GetName())](Engine& engine) - { - engine.GetCommandQueue().AddCommand(std::make_shared(id, AudioFile::DEFAULT)); - }}); - } - - void TransitionScheduler::Update(Engine& engine) - { - for (auto& scheduledAction : m_ScheduledActions) - { - if (!scheduledAction.Channel) - { - scheduledAction.Done = true; - scheduledAction.Action(engine); - continue; - } - - if (scheduledAction.Position <= scheduledAction.Channel->CurrentPosition()) - { - if (scheduledAction.Position > 0) - { - double target = scheduledAction.Position; - double position = scheduledAction.Channel->CurrentPosition(); - double delay = position - target; - log->Debug( - "Monitor for {0} completed, calling onReady\n target = {1}\n current = {2}\n delay = {3}", - scheduledAction.Channel->CurrentTheme()->GetName(), target, position, delay); - } - scheduledAction.Done = true; - scheduledAction.Action(engine); - } - } - - std::erase_if(m_ScheduledActions, [](const ScheduledAction& monitor) { return monitor.Done; }); - } - - void TransitionScheduler::AddRuleOnBeat(const char* name, double interval, std::vector timePoints) - { - log->Debug("AddRule OnBeat for {0}, interval = {1}, timePoints.count = {2}", name, interval, timePoints.size()); - m_ScheduleRules.insert_or_assign(name, TransitionScheduleRule::OnBeat(interval, std::move(timePoints))); - } - - const TransitionScheduleRule& TransitionScheduler::GetScheduleRule(HashString id) - { - if (m_ScheduleRules.contains(id)) - { - return m_ScheduleRules.at(id); - } - - static TransitionScheduleRule defaultRule = TransitionScheduleRule::Instant(); - return defaultRule; - } -} \ No newline at end of file diff --git a/src/NH/Bass/TransitionScheduler.h b/src/NH/Bass/TransitionScheduler.h deleted file mode 100644 index 3dc5b16..0000000 --- a/src/NH/Bass/TransitionScheduler.h +++ /dev/null @@ -1,88 +0,0 @@ -#pragma once - -#include "NH/Bass/CommonTypes.h" -#include "Channel.h" -#include "NH/Logger.h" - -#include -#include -#include -#include -#include -#include - -namespace NH::Bass -{ - enum class TransitionScheduleType - { - // Performs the transition instantly when it's ready - INSTANT, - // Performs the transition only on defined timecodes (like beat grops) - ON_BEAT - }; - - struct TransitionScheduleRule - { - struct DataInstant - { - }; - - struct DataOnBeat - { - // Static interval when transition will be performed, in seconds. - // BeatInterval = 1.2; means that transition may happen only at {1.2, 2.4, 3.6, 4.8, ...} time points. - // Non-positive value disables static interval. - double Interval = 0; - - // Vector of time points in seconds when the transiton will be performed. - std::vector TimePoints; - }; - - struct DataJingle - { - - }; - - TransitionScheduleType Type; - std::variant Data; - - static TransitionScheduleRule Instant() - { - return { TransitionScheduleType::INSTANT }; - } - - static TransitionScheduleRule OnBeat(double interval = 0, std::vector timePoints = {}) - { - return { TransitionScheduleType::ON_BEAT, DataOnBeat{ interval, std::move(timePoints) }}; - } - }; - - struct ScheduledAction - { - std::shared_ptr Channel; - double Position; - std::function Action; - bool Done = false; - }; - - class TransitionScheduler - { - NH::Logger* log = NH::CreateLogger("zBassMusic::TransitionScheduler"); - std::unordered_map m_ScheduleRules; - std::vector m_ScheduledActions; - - public: - void Schedule(const std::shared_ptr& activeChannel, const std::shared_ptr& nextMusic); - - void Update(Engine& engine); - - void AddRuleOnBeat(const char* name, double interval = 0, std::vector timePoints = {}); - - private: - const TransitionScheduleRule& GetScheduleRule(HashString id); - - void ScheduleInstant(const std::shared_ptr& activeChannel, const std::shared_ptr& nextMusic); - - void ScheduleOnBeat(const std::shared_ptr& activeChannel, const std::shared_ptr& nextMusic, const TransitionScheduleRule& rule); - }; -} \ No newline at end of file diff --git a/src/NH/Commons.h b/src/NH/Commons.h new file mode 100644 index 0000000..4030c0e --- /dev/null +++ b/src/NH/Commons.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +namespace NH +{ + using String = Union::StringUTF8; +} diff --git a/src/NH/Executor.h b/src/NH/Executor.h index c66fa6a..7adb645 100644 --- a/src/NH/Executor.h +++ b/src/NH/Executor.h @@ -17,6 +17,7 @@ namespace NH { virtual void AddTask(TaskFN&& task) = 0; virtual void AddTask(const TaskFN& task) = 0; + virtual ~Executor() = default; }; class InstantExecutor : public Executor diff --git a/src/NH/HashString.h b/src/NH/HashString.h index 774a5c9..d9ec146 100644 --- a/src/NH/HashString.h +++ b/src/NH/HashString.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include diff --git a/src/NH/Logger.h b/src/NH/Logger.h index 1027a0e..3c4cb61 100644 --- a/src/NH/Logger.h +++ b/src/NH/Logger.h @@ -1,11 +1,11 @@ #pragma once +#include #include #include namespace NH { - using String = Union::StringUTF8; enum class LoggerLevel : size_t { @@ -42,6 +42,7 @@ namespace NH LoggerLevel m_Level; explicit ILoggerAdapter(LoggerLevel level = LoggerLevel::Debug) : m_Level(level) {} + virtual ~ILoggerAdapter() = default; bool CanLog(LoggerLevel level) const { return level <= m_Level; } diff --git a/src/Plugin.cpp b/src/Plugin.cpp index 3415c54..522813a 100644 --- a/src/Plugin.cpp +++ b/src/Plugin.cpp @@ -1,9 +1,12 @@ // Disable macro redefinition warning #pragma warning(disable: 4005) +// Headers required for GOTHIC_NAMESPACE files. Don't delete. #include -#include "NH/Bass/Options.h" -#include "NH/Bass/Engine.h" +#include +#include +#include +#include #include #include diff --git a/vdf/vdf.cmake b/vdf/vdf.cmake index ad401b1..c0059ff 100644 --- a/vdf/vdf.cmake +++ b/vdf/vdf.cmake @@ -2,3 +2,6 @@ file(MAKE_DIRECTORY "${CMAKE_INSTALL_PREFIX}/vdf/System/Autorun") file(COPY "${CMAKE_INSTALL_PREFIX}/bin/zBassMusic.dll" DESTINATION "${CMAKE_INSTALL_PREFIX}/vdf/System/Autorun") file(COPY "${CMAKE_INSTALL_PREFIX}/bin/UnionAPI.dll" DESTINATION "${CMAKE_INSTALL_PREFIX}/vdf/System/Autorun") file(COPY "${CMAKE_INSTALL_PREFIX}/bin/bass.dll" DESTINATION "${CMAKE_INSTALL_PREFIX}/vdf/System/Autorun") +file(COPY "${CMAKE_INSTALL_PREFIX}/bin/bassmidi.dll" DESTINATION "${CMAKE_INSTALL_PREFIX}/vdf/System/Autorun") +file(COPY "${CMAKE_INSTALL_PREFIX}/bin/bassflac.dll" DESTINATION "${CMAKE_INSTALL_PREFIX}/vdf/System/Autorun") +file(COPY "${CMAKE_INSTALL_PREFIX}/bin/bassopus.dll" DESTINATION "${CMAKE_INSTALL_PREFIX}/vdf/System/Autorun")