commit 84eee5f3c1fbb0bb440a547bcc3e87baf22a02fc Author: Vanessa Date: Mon Oct 13 15:12:35 2025 +0200 Initial commit diff --git a/.gitea/build-main.yaml b/.gitea/build-main.yaml new file mode 100644 index 0000000..f422e54 --- /dev/null +++ b/.gitea/build-main.yaml @@ -0,0 +1,102 @@ +name: findwindow build main + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build-debug-dos: + runs-on: ubuntu-mingw-cmake-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Build + run: | + make clean debug + - name: Save artifact + uses: actions/upload-artifact@v3 + with: + name: debug-dos + path: + debug + + build-release-dos: + runs-on: ubuntu-mingw-cmake-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: build + run: | + make clean release + - name: Save artifact + uses: actions/upload-artifact@v3 + with: + name: release-dos + path: + release + + build-debug-sdlwin64: + runs-on: ubuntu-mingw-cmake-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Build + run: | + make clean debug TARGET=SDLWIN64 + - name: Save artifact + uses: actions/upload-artifact@v3 + with: + name: debug-sdlwin64 + path: + debug + + build-release-sdlwin64: + runs-on: ubuntu-mingw-cmake-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: build + run: | + make clean release TARGET=SDLWIN64 + - name: Save artifact + uses: actions/upload-artifact@v3 + with: + name: release-sdlwin64 + path: + release + game.img + + build-debug-sdl: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Build + run: | + make clean debug TARGET=SDL + - name: Save artifact + uses: actions/upload-artifact@v3 + with: + name: debug-sdl + path: + debug + + build-release-sdl: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: build + run: | + make clean release TARGET=SDL + - name: Save artifact + uses: actions/upload-artifact@v3 + with: + name: release-sdl + path: + release + game.img diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5626be --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.idea +release +debug +*.o +*.gbm +gbmconv +game.exe +game.img diff --git a/CWSDPMI.EXE b/CWSDPMI.EXE new file mode 100644 index 0000000..6865892 Binary files /dev/null and b/CWSDPMI.EXE differ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..807581e --- /dev/null +++ b/Makefile @@ -0,0 +1,140 @@ +HOSTCXX:=$(CXX) + +ifeq ($(TARGET),SDL) +# SDL +CC=gcc +CXX=g++ +LD=g++ +STRIP=strip + +SDL_INCLUDE=$(shell pkg-config --cflags sdl2) +SDL_LIBS=$(shell pkg-config --libs sdl2) + +CFLAGS=-g -DBUILD_SDL $(SDL_INCLUDE) +CXXFLAGS=$(CFLAGS) +LDFLAGS= +LDLIBS=$(SDL_LIBS) -lstdc++ +EXE=game +EXTRA_FILES= +SYSTEM=SDL +PLATFORM_OBJ= +else ifeq ($(TARGET),SDLWIN) +# SDL on Windows +CC=i686-w64-mingw32-gcc +CXX=i686-w64-mingw32-g++ +LD=i686-w64-mingw32-g++ +STRIP=i686-w64-mingw32-strip +WINDRES=windres + +SDL_INCLUDE=-I../SDL2-2.32.10/i686-w64-mingw32/include/SDL2 +SDL_LDFLAGS=-L../SDL2-2.32.10/i686-w64-mingw32/lib +SDL_LIBS=-lmingw32 -lSDL2main -lSDL2.dll + +CFLAGS=-g -DBUILD_SDL $(SDL_INCLUDE) -m32 +CXXFLAGS=$(CFLAGS) +LDFLAGS=$(SDL_LDFLAGS) -mwindows +LDLIBS=$(SDL_LIBS) -lstdc++ +EXE=game.exe +EXTRA_FILES=../SDL2-2.32.10/i686-w64-mingw32/bin/SDL2.dll +EXTRA_FILES+=/usr/lib/gcc/i686-w64-mingw32/10-win32/libgcc_s_dw2-1.dll +EXTRA_FILES+=/usr/lib/gcc/i686-w64-mingw32/10-win32/libstdc++-6.dll +SYSTEM=SDL +PLATFORM_OBJ=app.res +else ifeq ($(TARGET),SDLWIN64) +# SDL on Windows +CC=x86_64-w64-mingw32-gcc +CXX=x86_64-w64-mingw32-g++ +LD=x86_64-w64-mingw32-g++ +STRIP=x86_64-w64-mingw32-strip +WINDRES=windres + +SDL_INCLUDE=-I../SDL2-2.32.10/x86_64-w64-mingw32/include/SDL2 +SDL_LDFLAGS=-L../SDL2-2.32.10/x86_64-w64-mingw32/lib +SDL_LIBS=-lmingw32 -lSDL2main -lSDL2.dll + +CFLAGS=-g -DBUILD_SDL $(SDL_INCLUDE) -m64 +CXXFLAGS=$(CFLAGS) +#LDFLAGS=$(SDL_LDFLAGS) -mwindows +LDFLAGS=$(SDL_LDFLAGS) +LDLIBS=$(SDL_LIBS) -lstdc++ +EXE=game.exe +EXTRA_FILES=../SDL2-2.32.10/x86_64-w64-mingw32/bin/SDL2.dll +EXTRA_FILES+=/mingw64/bin/libgcc_s_seh-1.dll +EXTRA_FILES+=/mingw64/bin/libstdc++-6.dll +SYSTEM=SDL +PLATFORM_OBJ=app.res +else +# DOS +CC=/usr/local/djgpp/bin/i586-pc-msdosdjgpp-gcc +CXX=/usr/local/djgpp/bin/i586-pc-msdosdjgpp-g++ +LD=/usr/local/djgpp/bin/i586-pc-msdosdjgpp-g++ +STRIP=i586-pc-msdosdjgpp-strip + +# FIXME: we override the host compiler to build tools here, maybe move tools to a separate Makefile +HOSTCXX=g++ + +CFLAGS=-O3 -march=i486 +CXXFLAGS=$(CFLAGS) +LDFLAGS= +LDLIBS= +EXE=game.exe +DPMI_HOST=CWSDPMI.EXE +EXTRA_FILES=install.bat $(DPMI_HOST) +SYSTEM=DOS +PLATFORM_OBJ= +endif + +OBJ=main.o +OBJ+=$(PLATFORM_OBJ) +OBJ+=$(patsubst %.cpp,%.o,$(wildcard graphics/*.cpp)) +OBJ+=$(patsubst %.cpp,%.o,$(wildcard audio/*.cpp)) +OBJ+=$(patsubst %.cpp,%.o,$(wildcard util/*.cpp)) +OBJ+=$(patsubst %.cpp,%.o,$(wildcard scenes/*.cpp)) + +ifeq ($(SYSTEM),SDL) +OBJ+=$(patsubst %.cpp,%.o,$(wildcard system/sdl/*.cpp)) +else ifeq ($(SYSTEM),DOS) +OBJ+=$(patsubst %.cpp,%.o,$(wildcard system/dos/*.cpp)) +else +$(error Unknown SYSTEM variable:$(SYSTEM)) +endif + +CONVERTER_CXX=$(HOSTCXX) +CONVERTER_EXE=gbmconv +CONVERTER_SRC=converter.cpp +CONVERTER_SRC+=graphics/Bitmap.cpp +CONVERTER_SRC+=util/Files.cpp util/Bmp.cpp util/Gbm.cpp + +COMPILED_GFX_ASSETS=assets/font1.gbm assets/cow.gbm assets/witch.gbm +GFX_ASSETS=assets/bg.bmp $(COMPILED_GFX_ASSETS) +MUSIC_ASSETS=assets/rain.rad assets/getup.rad assets/spiral.rad + +RELEASE_FILES=$(EXE) $(GFX_ASSETS) $(MUSIC_ASSETS) $(EXTRA_FILES) + +FLOPPY_IMG=game.img + +.PHONY: all clean release debug assets floppy + +all: $(CONVERTER_EXE) $(EXE) + +clean: ; rm -rf $(OBJ) $(EXE) $(CONVERTER_EXE) $(FLOPPY_IMG) $(COMPILED_GFX_ASSETS) release debug + +release: all assets; $(STRIP) $(EXE); upx $(EXE); mkdir -p release; cp -Rv $(RELEASE_FILES) release/ + +debug: all assets; mkdir -p debug; cp -Rv $(RELEASE_FILES) debug/ + +floppy: release; dd if=/dev/zero of=$(FLOPPY_IMG) bs=512 count=2880 && mkfs.fat -F12 $(FLOPPY_IMG) && mcopy -i $(FLOPPY_IMG) -s release/* :: + +$(CONVERTER_EXE): $(CONVERTER_SRC); $(CONVERTER_CXX) -o $@ $(CONVERTER_SRC) + +$(EXE): $(OBJ); $(LD) $(LDFLAGS) -o $@ $(OBJ) $(LDLIBS) + +%.o : %.c ; $(CC) $(CFLAGS) -c $< -o $@ + +%.o : %.cpp ; $(CXX) $(CXXFLAGS) -c $< -o $@ + +assets: $(CONVERTER_EXE) $(GFX_ASSETS) + +%.gbm : %.bmp %_m.bmp $(CONVERTER_EXE) ; ./$(CONVERTER_EXE) $@ $^ + +%.res : %.rc ; $(WINDRES) $< -O coff -o $@ diff --git a/app.rc b/app.rc new file mode 100644 index 0000000..ce2f053 --- /dev/null +++ b/app.rc @@ -0,0 +1,25 @@ +1 VERSIONINFO +FILEVERSION 0,1,0,0 +PRODUCTVERSION 0,1,0,0 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904E4" + BEGIN + VALUE "CompanyName", "neko-tools.de" + VALUE "FileDescription", "Just a game uwu" + VALUE "FileVersion", "0.1" + VALUE "InternalName", "game" + VALUE "LegalCopyright", "neko-tools.de" + VALUE "OriginalFilename", "game.exe" + VALUE "ProductName", "Game" + VALUE "ProductVersion", "0.1" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +1000 ICON "assets/appicon.ico" diff --git a/assets/appicon.ico b/assets/appicon.ico new file mode 100644 index 0000000..60ea1fd Binary files /dev/null and b/assets/appicon.ico differ diff --git a/assets/bg.bmp b/assets/bg.bmp new file mode 100644 index 0000000..62fa6b4 Binary files /dev/null and b/assets/bg.bmp differ diff --git a/assets/cow.bmp b/assets/cow.bmp new file mode 100644 index 0000000..65050a5 Binary files /dev/null and b/assets/cow.bmp differ diff --git a/assets/cow.xcf b/assets/cow.xcf new file mode 100644 index 0000000..e63a063 Binary files /dev/null and b/assets/cow.xcf differ diff --git a/assets/cow_m.bmp b/assets/cow_m.bmp new file mode 100644 index 0000000..9f52e24 Binary files /dev/null and b/assets/cow_m.bmp differ diff --git a/assets/flaunch.pcm b/assets/flaunch.pcm new file mode 100644 index 0000000..a6b15c6 Binary files /dev/null and b/assets/flaunch.pcm differ diff --git a/assets/font1.bmp b/assets/font1.bmp new file mode 100644 index 0000000..04af59e Binary files /dev/null and b/assets/font1.bmp differ diff --git a/assets/font1_m.bmp b/assets/font1_m.bmp new file mode 100644 index 0000000..04af59e Binary files /dev/null and b/assets/font1_m.bmp differ diff --git a/assets/getup.rad b/assets/getup.rad new file mode 100644 index 0000000..ac804e1 Binary files /dev/null and b/assets/getup.rad differ diff --git a/assets/rain.rad b/assets/rain.rad new file mode 100644 index 0000000..00e42c2 Binary files /dev/null and b/assets/rain.rad differ diff --git a/assets/spiral.rad b/assets/spiral.rad new file mode 100644 index 0000000..888bac6 Binary files /dev/null and b/assets/spiral.rad differ diff --git a/assets/witch.bmp b/assets/witch.bmp new file mode 100644 index 0000000..67b72ea Binary files /dev/null and b/assets/witch.bmp differ diff --git a/assets/witch.xcf b/assets/witch.xcf new file mode 100644 index 0000000..ae2243f Binary files /dev/null and b/assets/witch.xcf differ diff --git a/assets/witch_m.bmp b/assets/witch_m.bmp new file mode 100644 index 0000000..b770be9 Binary files /dev/null and b/assets/witch_m.bmp differ diff --git a/audio/Audio.cpp b/audio/Audio.cpp new file mode 100644 index 0000000..bc6787e --- /dev/null +++ b/audio/Audio.cpp @@ -0,0 +1,35 @@ +#include "Audio.h" +#include "../util/Files.h" + +#include "AudioPlayer.h" + +Audio::Audio() { +} + +Audio::Audio(const char *path) { + loadFromFile(path); +} + +Audio::~Audio() { + unload(); +} + +bool Audio::isValid() const { + return _data != nullptr; +} + +void Audio::loadFromFile(const char *path) { + unload(); + _length = Files::allocateBufferAndLoadFromFile(path, &_data); +} + +void Audio::unload() { + if (_data) { + if (audioPlayer.isPlaying(*this)) { + audioPlayer.stopAudio(*this); + } + Files::deleteBuffer(_data); + _length = 0; + } +} + diff --git a/audio/Audio.h b/audio/Audio.h new file mode 100644 index 0000000..e7fa7ac --- /dev/null +++ b/audio/Audio.h @@ -0,0 +1,22 @@ +#ifndef GAME_AUDIO_H +#define GAME_AUDIO_H + +#include + +class Audio { + Audio(); + explicit Audio(const char *path); + ~Audio(); + + [[nodiscard]] bool isValid() const; + void loadFromFile(const char* path); + void unload(); +private: + friend class AudioPlayer; + [[nodiscard]] void *getData() const { return _data; } + size_t _length{0}; + void *_data{nullptr}; +}; + + +#endif //GAME_AUDIO_H \ No newline at end of file diff --git a/audio/AudioPlayer.cpp b/audio/AudioPlayer.cpp new file mode 100644 index 0000000..4283214 --- /dev/null +++ b/audio/AudioPlayer.cpp @@ -0,0 +1,120 @@ + +#include "AudioPlayer.h" + +#include "../system/Opl.h" +#include "../system/Timer.h" + +#include "../util/Log.h" + +AudioPlayer audioPlayer; + +void radPlayerWriteReg(void *p, uint16_t reg, uint8_t data) { + auto player = static_cast(p); + player->writeOpl(reg, data); +} + +void audioPlayerOnTimer() { + audioPlayer.onTimer(); +} + +void audioPlayerOnTimerDelayed() { + DefaultLog.log("audioPlayerOnTimerDelayed()\n"); + static int idle = 10; + if (--idle) return; + Timer::instance().setCallback(audioPlayerOnTimer); +} + +AudioPlayer::AudioPlayer() { + Timer::instance().setCallback(audioPlayerOnTimerDelayed); +} + +AudioPlayer::~AudioPlayer() { + Timer::instance().setCallback(nullptr); +} + +void AudioPlayer::playMusic(Music &music) { + stopMusic(); + + _currentMusic = &music; + _musicPlayer.Init(_currentMusic->getData(), radPlayerWriteReg, this); + + auto hz = _musicPlayer.GetHertz(); + if (hz < 0) { + stopMusic(); + return; + } + + // Start music playback + Timer::instance().setFrequency(hz); + resumeMusic(); +} + +void AudioPlayer::stopMusic() { + if (!_currentMusic) return; + + _musicPlayer.Stop(); + _paused = true; + _currentMusic = nullptr; +} + +void AudioPlayer::pauseMusic() { + _paused = true; +} + +void AudioPlayer::resumeMusic() { + if (!_currentMusic) return; + + _paused = false; +} + +void AudioPlayer::playAudio(Audio &audio, int priority, int flags) { + if (priority > AUDIO_MAX_PRIORITY) priority = AUDIO_MAX_PRIORITY; + + +} + +bool AudioPlayer::isPlaying(const Audio &audio) { + for (auto &channel : _channels) { + if (channel.audio == &audio) return true; + } + return false; +} + +void AudioPlayer::stopAudio(Audio &audio) { + for (auto &channel : _channels) { + if (channel.audio == &audio) { + channel.audio = nullptr; + channel.loop = false; + } + } +} + +void AudioPlayer::stopAllAudio() { + for (auto &channel : _channels) { + channel.audio = nullptr; + channel.loop = false; + } +} + +void AudioPlayer::generateSamples(uint8_t *buffer, size_t size) { +} + +bool AudioPlayer::isPlaying(const Music &music) const { + return _currentMusic == &music; +} + +void AudioPlayer::writeOpl(uint16_t reg, uint8_t data) const { + Opl::write(reg, data); +} + +void AudioPlayer::onTimer() { + if (_paused) return; + if (!_currentMusic) return; + + _musicPlayer.Update(); +} + +void AudioPlayer::generateSamples() { + +} + diff --git a/audio/AudioPlayer.h b/audio/AudioPlayer.h new file mode 100644 index 0000000..1f53765 --- /dev/null +++ b/audio/AudioPlayer.h @@ -0,0 +1,64 @@ +#ifndef GAME_AUDIOPLAYER_H +#define GAME_AUDIOPLAYER_H + +#include + +#include "Audio.h" +#include "Music.h" +#include "rad20player.h" + +#define AUDIO_CHANNELS 8 +#define AUDIO_MAX_PRIORITY 7 + +#define AUDIO_PLAY_LOOP 0x01 +#define AUDIO_PLAY_FIXED_PRIORITY 0x02 + +class AudioPlayer { +public: + AudioPlayer(); + + ~AudioPlayer(); + + void playMusic(Music &music); + + void stopMusic(); + + void pauseMusic(); + + void resumeMusic(); + + void playAudio(Audio &audio, int priority, int flags = 0); + + bool isPlaying(const Audio & audio); + + void stopAudio(Audio &audio); + + void stopAllAudio(); + + void generateSamples(uint8_t *buffer, size_t size); +private: + friend class Music; + friend void radPlayerWriteReg(void *p, uint16_t reg, uint8_t data); + friend void audioPlayerOnTimer(); + + RADPlayer _musicPlayer; + Music *_currentMusic{nullptr}; + bool _paused{true}; + + struct { + Audio *audio{nullptr}; + bool loop{false}; + } _channels[AUDIO_CHANNELS]; + + [[nodiscard]] bool isPlaying(const Music &music) const; + void writeOpl(uint16_t reg, uint8_t data) const; + void onTimer(); + + void generateSamples(); + + void copySamples(); +}; + +extern AudioPlayer audioPlayer; + +#endif //GAME_AUDIOPLAYER_H diff --git a/audio/Music.cpp b/audio/Music.cpp new file mode 100644 index 0000000..0f077c9 --- /dev/null +++ b/audio/Music.cpp @@ -0,0 +1,34 @@ +#include "Music.h" + +#include "AudioPlayer.h" +#include "../util/Files.h" + +Music::Music() { +} + +Music::Music(const char *path) { + loadFromFile(path); +} + +Music::~Music() { + unload(); +} + +bool Music::isValid() const { + return _data != nullptr; +} + +void Music::loadFromFile(const char *path) { + unload(); + _length = Files::allocateBufferAndLoadFromFile(path, &_data); +} + +void Music::unload() { + if (_data) { + if (audioPlayer.isPlaying(*this)) { + audioPlayer.stopMusic(); + } + Files::deleteBuffer(_data); + _length = 0; + } +} diff --git a/audio/Music.h b/audio/Music.h new file mode 100644 index 0000000..9a98532 --- /dev/null +++ b/audio/Music.h @@ -0,0 +1,23 @@ +#ifndef GAME_MUSIC_H +#define GAME_MUSIC_H + +#include + +class Music { +public: + Music(); + explicit Music(const char *path); + ~Music(); + + [[nodiscard]] bool isValid() const; + void loadFromFile(const char* path); + void unload(); +private: + friend class AudioPlayer; + [[nodiscard]] void *getData() const { return _data; } + size_t _length{0}; + void *_data{nullptr}; +}; + + +#endif //GAME_MUSIC_H \ No newline at end of file diff --git a/audio/rad20player.cpp b/audio/rad20player.cpp new file mode 100644 index 0000000..71d33fe --- /dev/null +++ b/audio/rad20player.cpp @@ -0,0 +1,1097 @@ +/* + + C++ player code for Reality Adlib Tracker 2.0a (file version 2.1). + + Please note, this is just the player code. This does no checking of the tune data before + it tries to play it, as most use cases will be a known tune being used in a production. + So if you're writing an application that loads unknown tunes in at run time then you'll + want to do more validity checking. + + To use: + + - Instantiate the RADPlayer object + + - Initialise player for your tune by calling the Init() method. Supply a pointer to the + tune file and a function for writing to the OPL3 registers. + + - Call the Update() method a number of times per second as returned by GetHertz(). If + your tune is using the default BPM setting you can safely just call it 50 times a + second, unless it's a legacy "slow-timer" tune then it'll need to be 18.2 times a + second. + + - When you're done, stop calling Update() and call the Stop() method to turn off all + sound and reset the OPL3 hardware. + +*/ + +#include "rad20player.h" + +#ifndef RAD_DETECT_REPEATS +#define RAD_DETECT_REPEATS 0 +#endif + + +//-------------------------------------------------------------------------------------------------- +const int8_t RADPlayer::NoteSize[] = { 0, 2, 1, 3, 1, 3, 2, 4 }; +const uint16_t RADPlayer::ChanOffsets3[9] = { 0, 1, 2, 0x100, 0x101, 0x102, 6, 7, 8 }; // OPL3 first channel +const uint16_t RADPlayer::Chn2Offsets3[9] = { 3, 4, 5, 0x103, 0x104, 0x105, 0x106, 0x107, 0x108 }; // OPL3 second channel +const uint16_t RADPlayer::NoteFreq[] = { 0x16b,0x181,0x198,0x1b0,0x1ca,0x1e5,0x202,0x220,0x241,0x263,0x287,0x2ae }; +const uint16_t RADPlayer::OpOffsets3[9][4] = { + { 0x00B, 0x008, 0x003, 0x000 }, + { 0x00C, 0x009, 0x004, 0x001 }, + { 0x00D, 0x00A, 0x005, 0x002 }, + { 0x10B, 0x108, 0x103, 0x100 }, + { 0x10C, 0x109, 0x104, 0x101 }, + { 0x10D, 0x10A, 0x105, 0x102 }, + { 0x113, 0x110, 0x013, 0x010 }, + { 0x114, 0x111, 0x014, 0x011 }, + { 0x115, 0x112, 0x015, 0x012 } +}; +const bool RADPlayer::AlgCarriers[7][4] = { + { true, false, false, false }, // 0 - 2op - op < op + { true, true, false, false }, // 1 - 2op - op + op + { true, false, false, false }, // 2 - 4op - op < op < op < op + { true, false, false, true }, // 3 - 4op - op < op < op + op + { true, false, true, false }, // 4 - 4op - op < op + op < op + { true, false, true, true }, // 5 - 4op - op < op + op + op + { true, true, true, true }, // 6 - 4op - op + op + op + op +}; + + + +//================================================================================================== +// Initialise a RAD tune for playback. This assumes the tune data is valid and does minimal data +// checking. +//================================================================================================== +void RADPlayer::Init(const void *tune, void (*opl3)(void *, uint16_t, uint8_t), void *arg) { + + Initialised = false; + + // Version check; we only support version 2.1 tune files + if (*((uint8_t *)tune + 0x10) != 0x21) { + Hertz = -1; + return; + } + + // The OPL3 call-back + OPL3 = opl3; + OPL3Arg = arg; + + for (int i = 0; i < kTracks; i++) + Tracks[i] = 0; + + for (int i = 0; i < kRiffTracks; i++) + for (int j = 0; j < kChannels; j++) + Riffs[i][j] = 0; + + uint8_t *s = (uint8_t *)tune + 0x11; + + uint8_t flags = *s++; + Speed = flags & 0x1F; + + // Is BPM value present? + Hertz = 50; + if (flags & 0x20) { + Hertz = (s[0] | (int(s[1]) << 8)) * 2 / 5; + s += 2; + } + + // Slow timer tune? Return an approximate hz + if (flags & 0x40) + Hertz = 18; + + // Skip any description + while (*s) + s++; + s++; + + // Unpack the instruments + while (1) { + + // Instrument number, 0 indicates end of list + uint8_t inst_num = *s++; + if (inst_num == 0) + break; + + // Skip instrument name + s += *s++; + + CInstrument &inst = Instruments[inst_num - 1]; + + uint8_t alg = *s++; + inst.Algorithm = alg & 7; + inst.Panning[0] = (alg >> 3) & 3; + inst.Panning[1] = (alg >> 5) & 3; + + if (inst.Algorithm < 7) { + + uint8_t b = *s++; + inst.Feedback[0] = b & 15; + inst.Feedback[1] = b >> 4; + + b = *s++; + inst.Detune = b >> 4; + inst.RiffSpeed = b & 15; + + inst.Volume = *s++; + + for (int i = 0; i < 4; i++) { + uint8_t *op = inst.Operators[i]; + for (int j = 0; j < 5; j++) + op[j] = *s++; + } + + } else { + + // Ignore MIDI instrument data + s += 6; + } + + // Instrument riff? + if (alg & 0x80) { + int size = s[0] | (int(s[1]) << 8); + s += 2; + inst.Riff = s; + s += size; + } else + inst.Riff = 0; + } + + // Get order list + OrderListSize = *s++; + OrderList = s; + s += OrderListSize; + + // Locate the tracks + while (1) { + + // Track number + uint8_t track_num = *s++; + if (track_num >= kTracks) + break; + + // Track size in bytes + int size = s[0] | (int(s[1]) << 8); + s += 2; + + Tracks[track_num] = s; + s += size; + } + + // Locate the riffs + while (1) { + + // Riff id + uint8_t riffid = *s++; + uint8_t riffnum = riffid >> 4; + uint8_t channum = riffid & 15; + if (riffnum >= kRiffTracks || channum > kChannels) + break; + + // Track size in bytes + int size = s[0] | (int(s[1]) << 8); + s += 2; + + Riffs[riffnum][channum - 1] = s; + s += size; + } + + // Done parsing tune, now set up for play + for (int i = 0; i < 512; i++) + OPL3Regs[i] = 255; + Stop(); + + Initialised = true; +} + + + +//================================================================================================== +// Stop all sounds and reset the tune. Tune will play from the beginning again if you continue to +// Update(). +//================================================================================================== +void RADPlayer::Stop() { + + // Clear all registers + for (uint16_t reg = 0x20; reg < 0xF6; reg++) { + + // Ensure envelopes decay all the way + uint8_t val = (reg >= 0x60 && reg < 0xA0) ? 0xFF : 0; + + SetOPL3(reg, val); + SetOPL3(reg + 0x100, val); + } + + // Configure OPL3 + SetOPL3(1, 0x20); // Allow waveforms + SetOPL3(8, 0); // No split point + SetOPL3(0xbd, 0); // No drums, etc. + SetOPL3(0x104, 0); // Everything 2-op by default + SetOPL3(0x105, 1); // OPL3 mode on + +#if RAD_DETECT_REPEATS + // The order map keeps track of which patterns we've played so we can detect when the tune + // starts to repeat. Jump markers can't be reliably used for this + PlayTime = 0; + Repeating = false; + for (int i = 0; i < 4; i++) + OrderMap[i] = 0; +#endif + + // Initialise play values + SpeedCnt = 1; + Order = 0; + Track = GetTrack(); + Line = 0; + Entrances = 0; + MasterVol = 64; + + // Initialise channels + for (int i = 0; i < kChannels; i++) { + CChannel &chan = Channels[i]; + chan.LastInstrument = 0; + chan.Instrument = 0; + chan.Volume = 0; + chan.DetuneA = 0; + chan.DetuneB = 0; + chan.KeyFlags = 0; + chan.Riff.SpeedCnt = 0; + chan.IRiff.SpeedCnt = 0; + } +} + + + +//================================================================================================== +// Playback update. Call BPM * 2 / 5 times a second. Use GetHertz() for this number after the +// tune has been initialised. Returns true if tune is starting to repeat. +//================================================================================================== +bool RADPlayer::Update() { + + if (!Initialised) + return false; + + // Run riffs + for (int i = 0; i < kChannels; i++) { + CChannel &chan = Channels[i]; + TickRiff(i, chan.IRiff, false); + TickRiff(i, chan.Riff, true); + } + + // Run main track + PlayLine(); + + // Run effects + for (int i = 0; i < kChannels; i++) { + CChannel &chan = Channels[i]; + ContinueFX(i, &chan.IRiff.FX); + ContinueFX(i, &chan.Riff.FX); + ContinueFX(i, &chan.FX); + } + + // Update play time. We convert to seconds when queried + PlayTime++; + +#if RAD_DETECT_REPEATS + return Repeating; +#else + return false; +#endif +} + + + +//================================================================================================== +// Unpacks a single RAD note. +//================================================================================================== +bool RADPlayer::UnpackNote(uint8_t *&s, uint8_t &last_instrument) { + + uint8_t chanid = *s++; + + InstNum = 0; + EffectNum = 0; + Param = 0; + + // Unpack note data + uint8_t note = 0; + if (chanid & 0x40) { + uint8_t n = *s++; + note = n & 0x7F; + + // Retrigger last instrument? + if (n & 0x80) + InstNum = last_instrument; + } + + // Do we have an instrument? + if (chanid & 0x20) { + InstNum = *s++; + last_instrument = InstNum; + } + + // Do we have an effect? + if (chanid & 0x10) { + EffectNum = *s++; + Param = *s++; + } + + NoteNum = note & 15; + OctaveNum = note >> 4; + + return ((chanid & 0x80) != 0); +} + + + +//================================================================================================== +// Get current track as indicated by order list. +//================================================================================================== +uint8_t *RADPlayer::GetTrack() { + + // If at end of tune start again from beginning + if (Order >= OrderListSize) + Order = 0; + + uint8_t track_num = OrderList[Order]; + + // Jump marker? Note, we don't recognise multiple jump markers as that could put us into an + // infinite loop + if (track_num & 0x80) { + Order = track_num & 0x7F; + track_num = OrderList[Order] & 0x7F; + } + +#if RAD_DETECT_REPEATS + // Check for tune repeat, and mark order in order map + if (Order < 128) { + int byte = Order >> 5; + uint32_t bit = uint32_t(1) << (Order & 31); + if (OrderMap[byte] & bit) + Repeating = true; + else + OrderMap[byte] |= bit; + } +#endif + + return Tracks[track_num]; +} + + + +//================================================================================================== +// Skip through track till we reach the given line or the next higher one. Returns null if none. +//================================================================================================== +uint8_t *RADPlayer::SkipToLine(uint8_t *trk, uint8_t linenum, bool chan_riff) { + + while (1) { + + uint8_t lineid = *trk; + if ((lineid & 0x7F) >= linenum) + return trk; + if (lineid & 0x80) + break; + trk++; + + // Skip channel notes + uint8_t chanid; + do { + chanid = *trk++; + trk += NoteSize[(chanid >> 4) & 7]; + } while (!(chanid & 0x80) && !chan_riff); + } + + return 0; +} + + + +//================================================================================================== +// Plays one line of current track and advances pointers. +//================================================================================================== +void RADPlayer::PlayLine() { + + SpeedCnt--; + if (SpeedCnt > 0) + return; + SpeedCnt = Speed; + + // Reset channel effects + for (int i = 0; i < kChannels; i++) + ResetFX(&Channels[i].FX); + + LineJump = -1; + + // At the right line? + uint8_t *trk = Track; + if (trk && (*trk & 0x7F) <= Line) { + uint8_t lineid = *trk++; + + // Run through channels + bool last; + do { + int channum = *trk & 15; + CChannel &chan = Channels[channum]; + last = UnpackNote(trk, chan.LastInstrument); + PlayNote(channum, NoteNum, OctaveNum, InstNum, EffectNum, Param); + } while (!last); + + // Was this the last line? + if (lineid & 0x80) + trk = 0; + + Track = trk; + } + + // Move to next line + Line++; + if (Line >= kTrackLines || LineJump >= 0) { + + if (LineJump >= 0) + Line = LineJump; + else + Line = 0; + + // Move to next track in order list + Order++; + Track = GetTrack(); + } +} + + + +//================================================================================================== +// Play a single note. Returns the line number in the next pattern to jump to if a jump command was +// found, or -1 if none. +//================================================================================================== +void RADPlayer::PlayNote(int channum, int8_t notenum, int8_t octave, uint16_t instnum, uint8_t cmd, uint8_t param, e_Source src, int op) { + CChannel &chan = Channels[channum]; + + // Recursion detector. This is needed as riffs can trigger other riffs, and they could end up + // in a loop + if (Entrances >= 8) + return; + Entrances++; + + // Select which effects source we're using + CEffects *fx = &chan.FX; + if (src == SRiff) + fx = &chan.Riff.FX; + else if (src == SIRiff) + fx = &chan.IRiff.FX; + + bool transposing = false; + + // For tone-slides the note is the target + if (cmd == cmToneSlide) { + if (notenum > 0 && notenum <= 12) { + fx->ToneSlideOct = octave; + fx->ToneSlideFreq = NoteFreq[notenum - 1]; + } + goto toneslide; + } + + // Playing a new instrument? + if (instnum > 0) { + CInstrument *oldinst = chan.Instrument; + CInstrument *inst = &Instruments[instnum - 1]; + chan.Instrument = inst; + + // Ignore MIDI instruments + if (inst->Algorithm == 7) { + Entrances--; + return; + } + + LoadInstrumentOPL3(channum); + + // Bounce the channel + chan.KeyFlags |= fKeyOff | fKeyOn; + + ResetFX(&chan.IRiff.FX); + + if (src != SIRiff || inst != oldinst) { + + // Instrument riff? + if (inst->Riff && inst->RiffSpeed > 0) { + + chan.IRiff.Track = chan.IRiff.TrackStart = inst->Riff; + chan.IRiff.Line = 0; + chan.IRiff.Speed = inst->RiffSpeed; + chan.IRiff.LastInstrument = 0; + + // Note given with riff command is used to transpose the riff + if (notenum >= 1 && notenum <= 12) { + chan.IRiff.TransposeOctave = octave; + chan.IRiff.TransposeNote = notenum; + transposing = true; + } else { + chan.IRiff.TransposeOctave = 3; + chan.IRiff.TransposeNote = 12; + } + + // Do first tick of riff + chan.IRiff.SpeedCnt = 1; + TickRiff(channum, chan.IRiff, false); + + } else + chan.IRiff.SpeedCnt = 0; + } + } + + // Starting a channel riff? + if (cmd == cmRiff || cmd == cmTranspose) { + + ResetFX(&chan.Riff.FX); + + uint8_t p0 = param / 10; + uint8_t p1 = param % 10; + chan.Riff.Track = p1 > 0 ? Riffs[p0][p1 - 1] : 0; + if (chan.Riff.Track) { + + chan.Riff.TrackStart = chan.Riff.Track; + chan.Riff.Line = 0; + chan.Riff.Speed = Speed; + chan.Riff.LastInstrument = 0; + + // Note given with riff command is used to transpose the riff + if (cmd == cmTranspose && notenum >= 1 && notenum <= 12) { + chan.Riff.TransposeOctave = octave; + chan.Riff.TransposeNote = notenum; + transposing = true; + } else { + chan.Riff.TransposeOctave = 3; + chan.Riff.TransposeNote = 12; + } + + // Do first tick of riff + chan.Riff.SpeedCnt = 1; + TickRiff(channum, chan.Riff, true); + + } else + chan.Riff.SpeedCnt = 0; + } + + // Play the note + if (!transposing && notenum > 0) { + + // Key-off? + if (notenum == 15) + chan.KeyFlags |= fKeyOff; + + if (!chan.Instrument || chan.Instrument->Algorithm < 7) + PlayNoteOPL3(channum, octave, notenum); + } + + // Process effect + switch (cmd) { + + case cmSetVol: + SetVolume(channum, param); + break; + + case cmSetSpeed: + if (src == SNone) { + Speed = param; + SpeedCnt = param; + } else if (src == SRiff) { + chan.Riff.Speed = param; + chan.Riff.SpeedCnt = param; + } else if (src == SIRiff) { + chan.IRiff.Speed = param; + chan.IRiff.SpeedCnt = param; + } + break; + + case cmPortamentoUp: + fx->PortSlide = param; + break; + + case cmPortamentoDwn: + fx->PortSlide = -int8_t(param); + break; + + case cmToneVolSlide: + case cmVolSlide: { + int8_t val = param; + if (val >= 50) + val = -(val - 50); + fx->VolSlide = val; + if (cmd != cmToneVolSlide) + break; + } + // Fall through! + + case cmToneSlide: { +toneslide: + uint8_t speed = param; + if (speed) + fx->ToneSlideSpeed = speed; + GetSlideDir(channum, fx); + break; + } + + case cmJumpToLine: { + if (param >= kTrackLines) + break; + + // Note: jump commands in riffs are checked for within TickRiff() + if (src == SNone) + LineJump = param; + + break; + } + + case cmMultiplier: { + if (src == SIRiff) + LoadInstMultiplierOPL3(channum, op, param); + break; + } + + case cmVolume: { + if (src == SIRiff) + LoadInstVolumeOPL3(channum, op, param); + break; + } + + case cmFeedback: { + if (src == SIRiff) { + uint8_t which = param / 10; + uint8_t fb = param % 10; + LoadInstFeedbackOPL3(channum, which, fb); + } + break; + } + } + + Entrances--; +} + + + +//================================================================================================== +// Sets the OPL3 registers for a given instrument. +//================================================================================================== +void RADPlayer::LoadInstrumentOPL3(int channum) { + CChannel &chan = Channels[channum]; + + const CInstrument *inst = chan.Instrument; + if (!inst) + return; + + uint8_t alg = inst->Algorithm; + chan.Volume = inst->Volume; + chan.DetuneA = (inst->Detune + 1) >> 1; + chan.DetuneB = inst->Detune >> 1; + + // Turn on 4-op mode for algorithms 2 and 3 (algorithms 4 to 6 are simulated with 2-op mode) + if (channum < 6) { + uint8_t mask = 1 << channum; + SetOPL3(0x104, (GetOPL3(0x104) & ~mask) | (alg == 2 || alg == 3 ? mask : 0)); + } + + // Left/right/feedback/algorithm + SetOPL3(0xC0 + ChanOffsets3[channum], ((inst->Panning[1] ^ 3) << 4) | inst->Feedback[1] << 1 | (alg == 3 || alg == 5 || alg == 6 ? 1 : 0)); + SetOPL3(0xC0 + Chn2Offsets3[channum], ((inst->Panning[0] ^ 3) << 4) | inst->Feedback[0] << 1 | (alg == 1 || alg == 6 ? 1 : 0)); + + // Load the operators + for (int i = 0; i < 4; i++) { + + static const uint8_t blank[] = { 0, 0x3F, 0, 0xF0, 0 }; + const uint8_t *op = (alg < 2 && i >= 2) ? blank : inst->Operators[i]; + uint16_t reg = OpOffsets3[channum][i]; + + uint16_t vol = ~op[1] & 0x3F; + + // Do volume scaling for carriers + if (AlgCarriers[alg][i]) { + vol = vol * inst->Volume / 64; + vol = vol * MasterVol / 64; + } + + SetOPL3(reg + 0x20, op[0]); + SetOPL3(reg + 0x40, (op[1] & 0xC0) | ((vol ^ 0x3F) & 0x3F)); + SetOPL3(reg + 0x60, op[2]); + SetOPL3(reg + 0x80, op[3]); + SetOPL3(reg + 0xE0, op[4]); + } +} + + + +//================================================================================================== +// Play note on OPL3 hardware. +//================================================================================================== +void RADPlayer::PlayNoteOPL3(int channum, int8_t octave, int8_t note) { + CChannel &chan = Channels[channum]; + + uint16_t o1 = ChanOffsets3[channum]; + uint16_t o2 = Chn2Offsets3[channum]; + + // Key off the channel + if (chan.KeyFlags & fKeyOff) { + chan.KeyFlags &= ~(fKeyOff | fKeyedOn); + SetOPL3(0xB0 + o1, GetOPL3(0xB0 + o1) & ~0x20); + SetOPL3(0xB0 + o2, GetOPL3(0xB0 + o2) & ~0x20); + } + + if (note == 15) + return; + + bool op4 = (chan.Instrument && chan.Instrument->Algorithm >= 2); + + uint16_t freq = NoteFreq[note - 1]; + uint16_t frq2 = freq; + + chan.CurrFreq = freq; + chan.CurrOctave = octave; + + // Detune. We detune both channels in the opposite direction so the note retains its tuning + freq += chan.DetuneA; + frq2 -= chan.DetuneB; + + // Frequency low byte + if (op4) + SetOPL3(0xA0 + o1, frq2 & 0xFF); + SetOPL3(0xA0 + o2, freq & 0xFF); + + // Frequency high bits + octave + key on + if (chan.KeyFlags & fKeyOn) + chan.KeyFlags = (chan.KeyFlags & ~fKeyOn) | fKeyedOn; + if (op4) + SetOPL3(0xB0 + o1, (frq2 >> 8) | (octave << 2) | ((chan.KeyFlags & fKeyedOn) ? 0x20 : 0)); + else + SetOPL3(0xB0 + o1, 0); + SetOPL3(0xB0 + o2, (freq >> 8) | (octave << 2) | ((chan.KeyFlags & fKeyedOn) ? 0x20 : 0)); +} + + + +//================================================================================================== +// Prepare FX for new line. +//================================================================================================== +void RADPlayer::ResetFX(CEffects *fx) { + fx->PortSlide = 0; + fx->VolSlide = 0; + fx->ToneSlideDir = 0; +} + + + +//================================================================================================== +// Tick the channel riff. +//================================================================================================== +void RADPlayer::TickRiff(int channum, CChannel::CRiff &riff, bool chan_riff) { + uint8_t lineid; + + if (riff.SpeedCnt == 0) { + ResetFX(&riff.FX); + return; + } + + riff.SpeedCnt--; + if (riff.SpeedCnt > 0) + return; + riff.SpeedCnt = riff.Speed; + + uint8_t line = riff.Line++; + if (riff.Line >= kTrackLines) + riff.SpeedCnt = 0; + + ResetFX(&riff.FX); + + // Is this the current line in track? + uint8_t *trk = riff.Track; + if (trk && (*trk & 0x7F) == line) { + lineid = *trk++; + + if (chan_riff) { + + // Channel riff: play current note + UnpackNote(trk, riff.LastInstrument); + Transpose(riff.TransposeNote, riff.TransposeOctave); + PlayNote(channum, NoteNum, OctaveNum, InstNum, EffectNum, Param, SRiff); + + } else { + + // Instrument riff: here each track channel is an extra effect that can run, but is not + // actually a different physical channel + bool last; + do { + int col = *trk & 15; + last = UnpackNote(trk, riff.LastInstrument); + if (EffectNum != cmIgnore) + Transpose(riff.TransposeNote, riff.TransposeOctave); + PlayNote(channum, NoteNum, OctaveNum, InstNum, EffectNum, Param, SIRiff, col > 0 ? (col - 1) & 3 : 0); + } while (!last); + } + + // Last line? + if (lineid & 0x80) + trk = 0; + + riff.Track = trk; + } + + // Special case; if next line has a jump command, run it now + if (!trk || (*trk++ & 0x7F) != riff.Line) + return; + + UnpackNote(trk, lineid); // lineid is just a dummy here + if (EffectNum == cmJumpToLine && Param < kTrackLines) { + riff.Line = Param; + riff.Track = SkipToLine(riff.TrackStart, Param, chan_riff); + } +} + + + +//================================================================================================== +// This continues any effects that operate continuously (eg. slides). +//================================================================================================== +void RADPlayer::ContinueFX(int channum, CEffects *fx) { + CChannel &chan = Channels[channum]; + + if (fx->PortSlide) + Portamento(channum, fx, fx->PortSlide, false); + + if (fx->VolSlide) { + int8_t vol = chan.Volume; + vol -= fx->VolSlide; + if (vol < 0) + vol = 0; + SetVolume(channum, vol); + } + + if (fx->ToneSlideDir) + Portamento(channum, fx, fx->ToneSlideDir, true); +} + + + +//================================================================================================== +// Sets the volume of given channel. +//================================================================================================== +void RADPlayer::SetVolume(int channum, uint8_t vol) { + CChannel &chan = Channels[channum]; + + // Ensure volume is within range + if (vol > 64) + vol = 64; + + chan.Volume = vol; + + // Scale volume to master volume + vol = vol * MasterVol / 64; + + CInstrument *inst = chan.Instrument; + if (!inst) + return; + uint8_t alg = inst->Algorithm; + + // Set volume of all carriers + for (int i = 0; i < 4; i++) { + uint8_t *op = inst->Operators[i]; + + // Is this operator a carrier? + if (!AlgCarriers[alg][i]) + continue; + + uint8_t opvol = uint16_t((op[1] & 63) ^ 63) * vol / 64; + uint16_t reg = 0x40 + OpOffsets3[channum][i]; + SetOPL3(reg, (GetOPL3(reg) & 0xC0) | (opvol ^ 0x3F)); + } +} + + + +//================================================================================================== +// Starts a tone-slide. +//================================================================================================== +void RADPlayer::GetSlideDir(int channum, CEffects *fx) { + CChannel &chan = Channels[channum]; + + int8_t speed = fx->ToneSlideSpeed; + if (speed > 0) { + uint8_t oct = fx->ToneSlideOct; + uint16_t freq = fx->ToneSlideFreq; + + uint16_t oldfreq = chan.CurrFreq; + uint8_t oldoct = chan.CurrOctave; + + if (oldoct > oct) + speed = -speed; + else if (oldoct == oct) { + if (oldfreq > freq) + speed = -speed; + else if (oldfreq == freq) + speed = 0; + } + } + + fx->ToneSlideDir = speed; +} + + + +//================================================================================================== +// Load multiplier value into operator. +//================================================================================================== +void RADPlayer::LoadInstMultiplierOPL3(int channum, int op, uint8_t mult) { + uint16_t reg = 0x20 + OpOffsets3[channum][op]; + SetOPL3(reg, (GetOPL3(reg) & 0xF0) | (mult & 15)); +} + + + +//================================================================================================== +// Load volume value into operator. +//================================================================================================== +void RADPlayer::LoadInstVolumeOPL3(int channum, int op, uint8_t vol) { + uint16_t reg = 0x40 + OpOffsets3[channum][op]; + SetOPL3(reg, (GetOPL3(reg) & 0xC0) | ((vol & 0x3F) ^ 0x3F)); +} + + + +//================================================================================================== +// Load feedback value into instrument. +//================================================================================================== +void RADPlayer::LoadInstFeedbackOPL3(int channum, int which, uint8_t fb) { + + if (which == 0) { + + uint16_t reg = 0xC0 + Chn2Offsets3[channum]; + SetOPL3(reg, (GetOPL3(reg) & 0x31) | ((fb & 7) << 1)); + + } else if (which == 1) { + + uint16_t reg = 0xC0 + ChanOffsets3[channum]; + SetOPL3(reg, (GetOPL3(reg) & 0x31) | ((fb & 7) << 1)); + } +} + + + +//================================================================================================== +// This adjusts the pitch of the given channel's note. There may also be a limiting value on the +// portamento (for tone slides). +//================================================================================================== +void RADPlayer::Portamento(uint16_t channum, CEffects *fx, int8_t amount, bool toneslide) { + CChannel &chan = Channels[channum]; + + uint16_t freq = chan.CurrFreq; + uint8_t oct = chan.CurrOctave; + + freq += amount; + + if (freq < 0x156) { + + if (oct > 0) { + oct--; + freq += 0x2AE - 0x156; + } else + freq = 0x156; + + } else if (freq > 0x2AE) { + + if (oct < 7) { + oct++; + freq -= 0x2AE - 0x156; + } else + freq = 0x2AE; + } + + if (toneslide) { + + if (amount >= 0) { + + if (oct > fx->ToneSlideOct || (oct == fx->ToneSlideOct && freq >= fx->ToneSlideFreq)) { + freq = fx->ToneSlideFreq; + oct = fx->ToneSlideOct; + } + + } else { + + if (oct < fx->ToneSlideOct || (oct == fx->ToneSlideOct && freq <= fx->ToneSlideFreq)) { + freq = fx->ToneSlideFreq; + oct = fx->ToneSlideOct; + } + } + } + + chan.CurrFreq = freq; + chan.CurrOctave = oct; + + // Apply detunes + uint16_t frq2 = freq - chan.DetuneB; + freq += chan.DetuneA; + + // Write value back to OPL3 + uint16_t chan_offset = Chn2Offsets3[channum]; + SetOPL3(0xA0 + chan_offset, freq & 0xFF); + SetOPL3(0xB0 + chan_offset, (freq >> 8 & 3) | oct << 2 | (GetOPL3(0xB0 + chan_offset) & 0xE0)); + + chan_offset = ChanOffsets3[channum]; + SetOPL3(0xA0 + chan_offset, frq2 & 0xFF); + SetOPL3(0xB0 + chan_offset, (frq2 >> 8 & 3) | oct << 2 | (GetOPL3(0xB0 + chan_offset) & 0xE0)); +} + + + +//================================================================================================== +// Transpose the note returned by UnpackNote(). +// Note: due to RAD's wonky legacy middle C is octave 3 note number 12. +//================================================================================================== +void RADPlayer::Transpose(int8_t note, int8_t octave) { + + if (NoteNum >= 1 && NoteNum <= 12) { + + int8_t toct = octave - 3; + if (toct != 0) { + OctaveNum += toct; + if (OctaveNum < 0) + OctaveNum = 0; + else if (OctaveNum > 7) + OctaveNum = 7; + } + + int8_t tnot = note - 12; + if (tnot != 0) { + NoteNum += tnot; + if (NoteNum < 1) { + NoteNum += 12; + if (OctaveNum > 0) + OctaveNum--; + else + NoteNum = 1; + } + } + } +} + + + +//================================================================================================== +// Compute total time of tune if it didn't repeat. Note, this stops the tune so should only be done +// prior to initial playback. +//================================================================================================== +#if RAD_DETECT_REPEATS +static void RADPlayerDummyOPL3(void *arg, uint16_t reg, uint8_t data) {} +//-------------------------------------------------------------------------------------------------- +uint32_t RADPlayer::ComputeTotalTime() { + + Stop(); + void (*old_opl3)(void *, uint16_t, uint8_t) = OPL3; + OPL3 = RADPlayerDummyOPL3; + + while (!Update()) + ; + uint32_t total = PlayTime; + + Stop(); + OPL3 = old_opl3; + + return total / Hertz; +} +#endif + diff --git a/audio/rad20player.h b/audio/rad20player.h new file mode 100644 index 0000000..22c986e --- /dev/null +++ b/audio/rad20player.h @@ -0,0 +1,173 @@ +#ifndef RAD20PLAYER_H +#define RAD20PLAYER_H + +#include + +//================================================================================================== +// RAD player class. +//================================================================================================== +class RADPlayer { + + // Various constants + enum { + kTracks = 100, + kChannels = 9, + kTrackLines = 64, + kRiffTracks = 10, + kInstruments = 127, + + cmPortamentoUp = 0x1, + cmPortamentoDwn = 0x2, + cmToneSlide = 0x3, + cmToneVolSlide = 0x5, + cmVolSlide = 0xA, + cmSetVol = 0xC, + cmJumpToLine = 0xD, + cmSetSpeed = 0xF, + cmIgnore = ('I' - 55), + cmMultiplier = ('M' - 55), + cmRiff = ('R' - 55), + cmTranspose = ('T' - 55), + cmFeedback = ('U' - 55), + cmVolume = ('V' - 55), + }; + + enum e_Source { + SNone, SRiff, SIRiff, + }; + + enum { + fKeyOn = 1 << 0, + fKeyOff = 1 << 1, + fKeyedOn = 1 << 2, + }; + + struct CInstrument { + uint8_t Feedback[2]; + uint8_t Panning[2]; + uint8_t Algorithm; + uint8_t Detune; + uint8_t Volume; + uint8_t RiffSpeed; + uint8_t * Riff; + uint8_t Operators[4][5]; + }; + + struct CEffects { + int8_t PortSlide; + int8_t VolSlide; + uint16_t ToneSlideFreq; + uint8_t ToneSlideOct; + uint8_t ToneSlideSpeed; + int8_t ToneSlideDir; + }; + + struct CChannel { + uint8_t LastInstrument; + CInstrument * Instrument; + uint8_t Volume; + uint8_t DetuneA; + uint8_t DetuneB; + uint8_t KeyFlags; + uint16_t CurrFreq; + int8_t CurrOctave; + CEffects FX; + struct CRiff { + CEffects FX; + uint8_t * Track; + uint8_t * TrackStart; + uint8_t Line; + uint8_t Speed; + uint8_t SpeedCnt; + int8_t TransposeOctave; + int8_t TransposeNote; + uint8_t LastInstrument; + } Riff, IRiff; + }; + + public: + RADPlayer() : Initialised(false) {} + void Init(const void *tune, void (*opl3)(void *, uint16_t, uint8_t), void *arg); + void Stop(); + bool Update(); + int GetHertz() const { return Hertz; } + int GetPlayTimeInSeconds() const { return PlayTime / Hertz; } + int GetTunePos() const { return Order; } + int GetTuneLength() const { return OrderListSize; } + int GetTuneLine() const { return Line; } + void SetMasterVolume(int vol) { MasterVol = vol; } + int GetMasterVolume() const { return MasterVol; } + int GetSpeed() const { return Speed; } + +#if RAD_DETECT_REPEATS + uint32_t ComputeTotalTime(); +#endif + + private: + bool UnpackNote(uint8_t *&s, uint8_t &last_instrument); + uint8_t * GetTrack(); + uint8_t * SkipToLine(uint8_t *trk, uint8_t linenum, bool chan_riff = false); + void PlayLine(); + void PlayNote(int channum, int8_t notenum, int8_t octave, uint16_t instnum, uint8_t cmd = 0, uint8_t param = 0, e_Source src = SNone, int op = 0); + void LoadInstrumentOPL3(int channum); + void PlayNoteOPL3(int channum, int8_t octave, int8_t note); + void ResetFX(CEffects *fx); + void TickRiff(int channum, CChannel::CRiff &riff, bool chan_riff); + void ContinueFX(int channum, CEffects *fx); + void SetVolume(int channum, uint8_t vol); + void GetSlideDir(int channum, CEffects *fx); + void LoadInstMultiplierOPL3(int channum, int op, uint8_t mult); + void LoadInstVolumeOPL3(int channum, int op, uint8_t vol); + void LoadInstFeedbackOPL3(int channum, int which, uint8_t fb); + void Portamento(uint16_t channum, CEffects *fx, int8_t amount, bool toneslide); + void Transpose(int8_t note, int8_t octave); + void SetOPL3(uint16_t reg, uint8_t val) { + OPL3Regs[reg] = val; + OPL3(OPL3Arg, reg, val); + } + uint8_t GetOPL3(uint16_t reg) const { + return OPL3Regs[reg]; + } + + void (*OPL3)(void *, uint16_t, uint8_t); + void * OPL3Arg; + CInstrument Instruments[kInstruments]; + CChannel Channels[kChannels]; + uint32_t PlayTime; +#if RAD_DETECT_REPEATS + uint32_t OrderMap[4]; + bool Repeating; +#endif + int16_t Hertz; + uint8_t * OrderList; + uint8_t * Tracks[kTracks]; + uint8_t * Riffs[kRiffTracks][kChannels]; + uint8_t * Track; + bool Initialised; + uint8_t Speed; + uint8_t OrderListSize; + uint8_t SpeedCnt; + uint8_t Order; + uint8_t Line; + int8_t Entrances; + uint8_t MasterVol; + int8_t LineJump; + uint8_t OPL3Regs[512]; + + // Values exported by UnpackNote() + int8_t NoteNum; + int8_t OctaveNum; + uint8_t InstNum; + uint8_t EffectNum; + uint8_t Param; + bool LastNote; + + static const int8_t NoteSize[]; + static const uint16_t ChanOffsets3[9], Chn2Offsets3[9]; + static const uint16_t NoteFreq[]; + static const uint16_t OpOffsets3[9][4]; + static const bool AlgCarriers[7][4]; +}; + +#endif + diff --git a/converter.cpp b/converter.cpp new file mode 100644 index 0000000..19ed623 --- /dev/null +++ b/converter.cpp @@ -0,0 +1,53 @@ +#include + +#include "util/Bmp.h" +#include "util/Gbm.h" + +void usage() { + printf("Usage: gbmconv \n"); +} + +int main(int argc, char** argv) { + if (argc < 4) { + usage(); + return 1; + } + + char *gbmPath = argv[1]; + char *bmpPath = argv[2]; + char *maskBmpPath = argv[3]; + + auto bitmap = Bmp::loadFromFile(bmpPath); + if (bitmap.GetWidth() == 0 || bitmap.GetHeight() == 0) { + printf("Unable to load source bitmap\n"); + return 1; + } + + auto maskBitmap = Bmp::loadFromFile(maskBmpPath); + if (maskBitmap.GetWidth() == 0 || maskBitmap.GetHeight() == 0) { + printf("Unable to load mask bitmap\n"); + return 1; + } + + if (maskBitmap.GetWidth() != bitmap.GetWidth() || maskBitmap.GetHeight() != bitmap.GetHeight()) { + printf("Mask bitmap must be the same size as the source bitmap\n"); + return 1; + } + + // Create a combined bitmap where the most significant bit is a transparency bit + auto combined = Bitmap(bitmap.GetWidth(), bitmap.GetHeight()); + for (auto y = 0u; y < bitmap.GetHeight(); y++) { + for (auto x = 0u; x < bitmap.GetWidth(); x++) { + auto isTransparent = (maskBitmap.GetPixel(x, y) == 0); + auto pv = (isTransparent ? 0x80 : 0x00) | (bitmap.GetPixel(x, y) & 0x7f); + combined.SetPixel(x, y, pv); + } + } + for (auto i = 0u; i < 256; i++) { + combined.SetPaletteEntry(i, bitmap.GetPaletteEntry(i)); + } + + Gbm::writeToFile(gbmPath, combined); + printf("Wrote %s\n", gbmPath); + return 0; +} diff --git a/graphics/Bitmap.cpp b/graphics/Bitmap.cpp new file mode 100644 index 0000000..177d442 --- /dev/null +++ b/graphics/Bitmap.cpp @@ -0,0 +1,78 @@ +#include "Bitmap.h" + +#include +#include +#include + +Bitmap::~Bitmap() { + clear(); +} + +Bitmap::Bitmap(const Bitmap &other) { + copyFrom(other); +} + +Bitmap & Bitmap::operator=(const Bitmap &other) { + if (this != &other) { + clear(); + copyFrom(other); + } + + return *this; +} + +Bitmap::Bitmap(Bitmap &&other) noexcept { + moveFrom(std::move(other)); +} + +Bitmap & Bitmap::operator=(Bitmap &&other) noexcept { + moveFrom(std::move(other)); + + return *this; +} + +Bitmap::Bitmap(uint16_t width, uint16_t height) +: _width(width), _height(height), _data(new uint8_t[width * height]) { +} + +Bitmap::Bitmap(uint16_t width, uint16_t height, uint8_t *data) : Bitmap(width, height) { + memcpy(_data, data, width * height); +} + +void Bitmap::SetPaletteEntry(uint8_t index, Video::PaletteEntry entry) { + _pal[index] = entry; +} + +Video::PaletteEntry Bitmap::GetPaletteEntry(uint8_t index) const { + return _pal[index]; +} + +void Bitmap::clear() { + delete[] _data; + _data = nullptr; + _width = _height = 0; +} + +void Bitmap::copyFrom(const Bitmap &other) { + _width = other._width; + _height = other._height; + _data = new uint8_t[_width * _height]; + for (auto i = 0; i < 256; i++) _pal[i] = other._pal[i]; + + memcpy(_data, other._data, _width * _height); +} + +void Bitmap::moveFrom(Bitmap &&other) noexcept { + // Clear existing data + clear(); + + // Copy data from other + _data = other._data; + _width = other._width; + _height = other._height; + for (auto i = 0; i < 256; i++) _pal[i] = other._pal[i]; + + // Clear other + other._data = nullptr; + other._width = other._height = 0; +} diff --git a/graphics/Bitmap.h b/graphics/Bitmap.h new file mode 100644 index 0000000..3eacb8a --- /dev/null +++ b/graphics/Bitmap.h @@ -0,0 +1,66 @@ +#ifndef GAME_BITMAP_H +#define GAME_BITMAP_H + +#include + +#include "../system/Video.h" + +class Bitmap { +public: + Bitmap() = default; + + ~Bitmap(); + + Bitmap(const Bitmap& other); + Bitmap& operator=(const Bitmap& other); + + Bitmap(Bitmap &&other) noexcept ; + Bitmap& operator=(Bitmap &&other) noexcept; + + Bitmap(uint16_t width, uint16_t height); + + Bitmap(uint16_t width, uint16_t height, uint8_t* data); + + [[nodiscard]] uint8_t GetPixel(uint16_t x, uint16_t y) const { + return _data[y * _width + x]; + } + + void SetPixel(uint16_t x, uint16_t y, uint8_t value) { + _data[y * _width + x] = value; + } + + uint8_t &operator[](uint32_t offset) { + return _data[offset]; + } + + const uint8_t &operator[](uint32_t offset) const { + return _data[offset]; + } + + void SetPaletteEntry(uint8_t index, Video::PaletteEntry entry); + + [[nodiscard]] Video::PaletteEntry GetPaletteEntry(uint8_t index) const; + + [[nodiscard]] uint16_t GetWidth() const { + return _width; + } + + [[nodiscard]] uint16_t GetHeight() const { + return _height; + } + + void clear(); + +private: + uint16_t _width{0}; + uint16_t _height{0}; + uint8_t *_data{nullptr}; + + Video::PaletteEntry _pal[256]{}; + + void copyFrom(const Bitmap& other); + void moveFrom(Bitmap&& other) noexcept; +}; + + +#endif //GAME_BITMAP_H diff --git a/graphics/Font.h b/graphics/Font.h new file mode 100644 index 0000000..728c87d --- /dev/null +++ b/graphics/Font.h @@ -0,0 +1,67 @@ +#ifndef GAME_FONT_H +#define GAME_FONT_H + +#include "Bitmap.h" + +class Font { +public: + Font(Bitmap *bitmap, unsigned w, unsigned h, unsigned minCode = 32, unsigned maxCode = 127) + : _font(bitmap), _w(w), _h(h), _minCode(minCode), _maxCode(maxCode) { + _charsPerRow = _font->GetWidth() / _w; + } + + void renderCharAlpha(int c, int x, int y, uint8_t color, uint8_t *fb, unsigned fbWidth, unsigned fbHeight) { + if (c < _minCode || c > _maxCode) return; + c -= _minCode; + + auto bitmapX = (c % _charsPerRow) * _w; + auto bitmapY = (c / _charsPerRow) * _h; + + for (auto charY = 0; charY < _h; charY++) { + if (y + charY < 0) continue; + if (y + charY >= fbHeight) break; + auto yOffset = (y + charY) * fbWidth; + + for (auto charX = 0; charX < _w; charX++) { + if (x + charX < 0) continue; + if (x + charX >= fbWidth) break; + + auto pv = _font->GetPixel(bitmapX + charX, bitmapY + charY); + if (pv & 0x80) continue; + + fb[yOffset + x + charX] = color; + } + } + } + + void renderChar(int c, int x, int y, uint8_t fgColor, uint8_t bgColor, uint8_t *fb, unsigned fbWidth, unsigned fbHeight) { + if (c < _minCode || c > _maxCode) return; + c -= _minCode; + + auto bitmapX = (c % _charsPerRow) * _w; + auto bitmapY = (c / _charsPerRow) * _h; + + for (auto charY = 0; charY < _h; charY++) { + if (y + charY < 0) continue; + if (y + charY >= fbHeight) break; + auto yOffset = (y + charY) * fbWidth; + + for (auto charX = 0; charX < _w; charX++) { + if (x + charX < 0) continue; + if (x + charX >= fbWidth) break; + + auto pv = _font->GetPixel(bitmapX + charX, bitmapY + charY) & 0x80 ? bgColor : fgColor; + fb[yOffset + x + charX] = pv; + } + } + } +private: + Bitmap *_font; + unsigned _w, _h; + unsigned _minCode; + unsigned _maxCode; + unsigned _charsPerRow{0}; +}; + + +#endif //GAME_FONT_H \ No newline at end of file diff --git a/graphics/Rect.h b/graphics/Rect.h new file mode 100644 index 0000000..05e5a0a --- /dev/null +++ b/graphics/Rect.h @@ -0,0 +1,45 @@ +#ifndef GAME_RECT_H +#define GAME_RECT_H + +struct Rect { + int x1, y1, x2, y2; + + void clamp(int minX, int minY, int maxX, int maxY) { + if (x1 < minX) x1 = minX; + if (x2 > maxX) x2 = maxX; + if (y1 < minY) y1 = minY; + if (y2 > maxY) y2 = maxY; + } + + void sort() { + if (x1 > x2) { + auto x = x1; + x1 = x2; + x2 = x; + } + if (y1 > y2) { + auto y = y1; + y1 = y2; + y2 = y; + } + } + + Rect &operator+=(const Rect &other) { + if (other.x1 < x1) x1 = other.x1; + if (other.x2 > x2) x2 = other.x2; + if (other.y1 < y1) y1 = other.y1; + if (other.y2 > y2) y2 = other.y2; + + return *this; + } + + [[nodiscard]] bool contains(int x, int y) const { + return x >= x1 && y >= y1 && x < x2 && y < y2; + } + + [[nodiscard]] bool overlaps(const Rect &other) const { + return !(other.x1 > x2 || other.x2 < x1 || other.y1 > y2 || other.y2 < y1); + } +}; + +#endif //GAME_RECT_H \ No newline at end of file diff --git a/graphics/Sprite.h b/graphics/Sprite.h new file mode 100644 index 0000000..a98f0fe --- /dev/null +++ b/graphics/Sprite.h @@ -0,0 +1,68 @@ +#ifndef GAME_SPRITE_H +#define GAME_SPRITE_H +#include "Bitmap.h" + +#define SPR_SUBPX_TO_PX(v) ((v) >= 0 ? ((v) >> 8) : -((-(v)) >> 8)) +#define SPR_PX_TO_SUBPX(v) ((v) >= 0 ? ((v) << 8) : -((-(v)) << 8)) + +class Sprite { +public: + virtual ~Sprite() = default; + + virtual void Tick() = 0; + + [[nodiscard]] virtual int GetWidth() const { + return _w; + } + + [[nodiscard]] virtual int GetHeight() const { + return _h; + } + + [[nodiscard]] int GetX() const { + return SPR_SUBPX_TO_PX(_sx); + } + + [[nodiscard]] int GetY() const { + return SPR_SUBPX_TO_PX(_sy); + } + + void SetX(int x) { + _sx = x >= 0 ? (x << 8) : -((-x) << 8); + } + + void SetY(int y) { + _sy = y >= 0 ? (y << 8) : -((-y) << 8); + } + + [[nodiscard]] Rect GetRect() const { + auto x = SPR_SUBPX_TO_PX(_sx), y = SPR_SUBPX_TO_PX(_sy); + return { x, y, x + _w, y + _h }; + } + + void SetPalette(uint8_t palette) { _palette = palette << 4; } + + [[nodiscard]] uint8_t GetPalette() const { return _palette >> 4; } + + [[nodiscard]] Bitmap *GetBitmap() const { return _bitmap; } + + [[nodiscard]] unsigned GetYOffset() const { return _frame * _h; } + + [[nodiscard]] bool isMirroredX() const { return _mirrorX; } + + [[nodiscard]] bool isMirroredY() const { return _mirrorY; } + +protected: + Sprite() = default; + + Bitmap *_bitmap{nullptr}; + uint8_t _palette{0}; + int _sx{0}, _sy{0}; + int _w{0}, _h{0}; + unsigned _frame{0}; + bool _mirrorX{false}; + bool _mirrorY{false}; +}; + + +#endif //GAME_SPRITE_H \ No newline at end of file diff --git a/install.bat b/install.bat new file mode 100644 index 0000000..4579009 --- /dev/null +++ b/install.bat @@ -0,0 +1,2 @@ +mkdir C:\game\ +xcopy *.* C:\game\ diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..14aae1b --- /dev/null +++ b/main.cpp @@ -0,0 +1,111 @@ +#include +#include +#include + +#ifdef BUILD_SDL +#include +#endif + +#include "audio/AudioPlayer.h" +#include "audio/Music.h" +#include "system/Timer.h" +#include "system/Video.h" +#include "graphics/Bitmap.h" +#include "graphics/Font.h" + +#include "scenes/Scene.h" +#include "system/init.h" +#include "system/Keyboard.h" +#include "util/Gbm.h" +#include "util/Log.h" + +volatile bool showFps = true; +volatile bool running = true; + +int main(int argc, char *argv[]) { + System::init(); + + srand(time(nullptr)); + + DefaultLog.log("Loading music\n"); + Music music("spiral.rad"); + if (!music.isValid()) { + printf("Unable to load song\n"); + return 1; + } + + DefaultLog.log("Playing music\n"); + audioPlayer.playMusic(music); + + DefaultLog.log("Changing video mode\n"); + video.enter(); + + // Run scenes + auto fb = static_cast(video.GetFB()); + + unsigned fps{0}; + unsigned frameCount{0}; + uint32_t fpsTick{0}; + char fpsDigits[4]{'0', '0', '0', 0}; + auto fontBitmap = Gbm::loadFromFile("font1.gbm"); + Font font(&fontBitmap, 7, 9, 32, 127); + + keyboard.setKeyDownHandler([] (unsigned char key) { + DefaultLog.log("KeyDown %u (%s)\n", key, keyboard.getKeyName(key)); + }); + keyboard.setKeyUpHandler([] (unsigned char key) { + DefaultLog.log("KeyUp %u (%s)\n", key, keyboard.getKeyName(key)); + switch (key) { + case KeyEscape: + running = false; + break; + case KeyF: + showFps = !showFps; + video.UpdateRect({0, 0, 21, 9 }); + break; + default: + // Do nothing + break; + } + }); + keyboard.setKeyRepeatHandler([](unsigned char key) { + DefaultLog.log("KeyRepeat %u (%s)\n", key, keyboard.getKeyName(key)); + }); + + auto &timer = Timer::instance(); + while (running) { + const auto scene = Scenes::getCurrentScene(); + + scene->run(); + + // Render fps + ++frameCount; + auto ticks = timer.getTicks(); + if (ticks > fpsTick + 50) { + fpsTick = ticks; + fps = frameCount; + frameCount = 0; + fpsDigits[0] = '0' + (fps / 100) % 10; + fpsDigits[1] = '0' + (fps / 10) % 10; + fpsDigits[2] = '0' + fps % 10; + } + + if (showFps) { + font.renderChar(fpsDigits[0], 0, 0, 242, 0, fb, SCREEN_WIDTH, SCREEN_HEIGHT); + font.renderChar(fpsDigits[1], 7, 0, 242, 0, fb, SCREEN_WIDTH, SCREEN_HEIGHT); + font.renderChar(fpsDigits[2], 14, 0, 242, 0, fb, SCREEN_WIDTH, SCREEN_HEIGHT); + + video.UpdateRect({0, 0, 21, 9 }); + } + + // Wait for vertical retrace and then copy render buffer to screen + video.WaitForVerticalSync(); + video.Flip(); + } + + audioPlayer.stopMusic(); + + System::terminate(); + + return 0; +} diff --git a/scenes/GameScene.cpp b/scenes/GameScene.cpp new file mode 100644 index 0000000..052f75a --- /dev/null +++ b/scenes/GameScene.cpp @@ -0,0 +1,281 @@ +#include "GameScene.h" + +#include +#include + +#include "../system/Keyboard.h" +#include "../system/Timer.h" +#include "../system/Video.h" +#include "../util/Bmp.h" +#include "../util/Gbm.h" +#include "../util/Log.h" + +class TestSprite : public Sprite { +public: + enum { Width = 20, Height = 16, Frames = 12 }; + + explicit TestSprite(Bitmap *image) { + _bitmap = image; + _w = Width; + _h = Height; + SetX(10 + rand() % (SCREEN_WIDTH - 20 - Width)); + SetY(10 + rand() % (SCREEN_HEIGHT - 20 - Height)); + + int velXSign = ((rand() & 1) << 1) - 1; + int velYSign = ((rand() & 1) << 1) - 1; + _vx = (rand() % 128 + 64) * velXSign; + _vy = (rand() % 128 + 64) * velYSign; + } + + void Tick() override { + if (++_ticks >= 15) { + _ticks = 0; + if (++_frame >= Frames) { + _frame = 0; + } + } + + auto newX = _sx + _vx; + if (newX < 0) { + newX = -newX; + _vx = -_vx; + } else if (newX >= MaxX) { + auto d = newX - MaxX; + newX = MaxX - d - 1; + _vx = -_vx; + } + _mirrorX = (_vx < 0); + + auto newY = _sy + _vy; + if (newY < 0) { + newY = -newY; + _vy = -_vy; + } else if (newY >= MaxY) { + auto d = newY - MaxY; + newY = MaxY - d - 1; + _vy = -_vy; + } + + _sx = newX; + _sy = newY; + } + +protected: + int _vx{0}, _vy{0}; + unsigned _ticks{0}; + const int MaxX = (SCREEN_WIDTH - Width) << 8; + const int MaxY = (SCREEN_HEIGHT - Height) << 8; +}; + +class PlayerSprite : public Sprite { +public: + enum { Width = 24, Height = 32, Frames = 4, Speed = 480 }; + + explicit PlayerSprite(Bitmap *image) { + _bitmap = image; + _w = Width; + _h = Height; + SetX(20); + SetY((SCREEN_HEIGHT - Height) / 2); + } + + void Tick() override { + if (++_ticks >= 15) { + _ticks = 0; + if (++_animationStep >= Frames) { + _animationStep = 0; + } + + _frame = _animationMap[_animationStep]; + } + + if (keyboard.keyState[KeyLeft]) { + _sx -= Speed; + if (_sx < 0) _sx = 0; + } + if (keyboard.keyState[KeyRight]) { + _sx += Speed; + if (_sx > MaxX) _sx = MaxX; + } + if (keyboard.keyState[KeyUp]) { + _sy -= Speed; + if (_sy < 0) _sy = 0; + } + if (keyboard.keyState[KeyDown]) { + _sy += Speed; + if (_sy > MaxY) _sy = MaxY; + } + } + +protected: + unsigned _ticks{0}; + unsigned _animationStep = 0; + const int MaxX = (SCREEN_WIDTH - Width) << 8; + const int MaxY = (SCREEN_HEIGHT - Height) << 8; + const unsigned _animationMap[4] = { + 0, 1, 2, 1 + }; +}; + +GameScene::GameScene() { + +} + +GameScene::~GameScene() { +} + +void GameScene::run() { + if (_firstFrame) { + Rect bgRect{0, 0, SCREEN_WIDTH, SCREEN_HEIGHT}; + for (auto y = 0; y < SCREEN_HEIGHT; y++) { + auto yOffset = y * SCREEN_WIDTH; + for (auto x = 0; x < SCREEN_WIDTH; x++) { + _fb[yOffset + x] = _bg.GetPixel(x, y); + } + } + video.UpdateRect(bgRect); + _firstFrame = false; + } + + // Redraw background + for (auto &sprite : _sprites) { + clearSprite(sprite); + } + clearSprite(_playerSprite); + + // Update and paint all sprites after update + for (auto &sprite : _sprites) { + sprite->Tick(); + drawSprite(sprite); + } + + _playerSprite->Tick(); + drawSprite(_playerSprite); +} + +void GameScene::enter() { + DefaultLog.log("Entering GameScene\n"); + _fb = static_cast(video.GetFB()); + + _cow = Gbm::loadFromFile("cow.gbm"); + _witch = Gbm::loadFromFile("witch.gbm"); + _bg = Bmp::loadFromFile("bg.bmp"); + + _playerSprite = new PlayerSprite(&_witch); + _playerSprite->SetPalette(14); + + for (auto &sprite : _sprites) { + sprite = new TestSprite(&_cow); + sprite->SetPalette(15); + } + + // Load bg palette + for (auto i = 0u; i < 64; i++) { + video.SetPaletteEntry(i, _bg.GetPaletteEntry(i)); + } + + // Load sprite palette + for (auto i = 0u; i < 16; i++) { + video.SetPaletteEntry(i + 240, _cow.GetPaletteEntry(i)); + } + + // Load player sprite palette + for (auto i = 0u; i < 16; i++) { + video.SetPaletteEntry(i + 224, _witch.GetPaletteEntry(i)); + } + + _firstFrame = true; +} + +void GameScene::exit() { + DefaultLog.log("Exiting GameScene\n"); + + for (auto &sprite : _sprites) { + delete sprite; + } + + delete _playerSprite; + + _cow.clear(); + _bg.clear(); + _witch.clear(); +} + +void GameScene::clearSprite(Sprite *sprite) { + auto rect = sprite->GetRect(); + rect.clamp(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); + + for (auto y = rect.y1; y < rect.y2; y++) { + auto yOffset = y * SCREEN_WIDTH; + + for (auto x = rect.x1; x < rect.x2; x++) { + _fb[yOffset + x] = _bg.GetPixel(x, y); + } + } + + video.UpdateRect(rect); +} + +void GameScene::drawSprite(Sprite *sprite) { + auto rect = sprite->GetRect(); + rect.clamp(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); + + auto rx = rect.x1; + auto ry = rect.y1; + auto rx2 = rect.x2; + auto ry2 = rect.y2; + + auto paletteOffset = sprite->GetPalette() << 4; + auto bitmap = sprite->GetBitmap(); + auto syoff = sprite->GetYOffset(); + + if (sprite->isMirroredY()) { + for (auto y = ry; y < ry2; y++) { + auto yOffset = y * SCREEN_WIDTH; + auto offy = ry2 - y - 1; + + if (sprite->isMirroredX()) { + for (auto x = rx; x < rx2; x++) { + auto offx = rx2 - x - 1; + + auto pv = bitmap->GetPixel(offx, offy + syoff); + + if (!(pv & 0x80)) _fb[yOffset + x] = (pv & 0x7f) + paletteOffset; + } + } else { + for (auto x = rx; x < rx2; x++) { + auto offx = x - rx; + + auto pv = bitmap->GetPixel(offx, offy + syoff); + + if (!(pv & 0x80)) _fb[yOffset + x] = (pv & 0x7f) + paletteOffset; + } + } + } + } else { + for (auto y = ry; y < ry2; y++) { + auto yOffset = y * SCREEN_WIDTH; + auto offy = y -ry; + + if (sprite->isMirroredX()) { + for (auto x = rx; x < rx2; x++) { + auto offx = rx2 - x - 1; + + auto pv = bitmap->GetPixel(offx, offy + syoff); + + if (!(pv & 0x80)) _fb[yOffset + x] = (pv & 0x7f) + paletteOffset; + } + } else { + for (auto x = rx; x < rx2; x++) { + auto offx = x - rx; + + auto pv = bitmap->GetPixel(offx, offy + syoff); + + if (!(pv & 0x80)) _fb[yOffset + x] = (pv & 0x7f) + paletteOffset; + } + } + } + } + + video.UpdateRect(rect); +} diff --git a/scenes/GameScene.h b/scenes/GameScene.h new file mode 100644 index 0000000..1bfd07f --- /dev/null +++ b/scenes/GameScene.h @@ -0,0 +1,37 @@ +#ifndef GAME_GAMESCENE_H +#define GAME_GAMESCENE_H + +#include + +#include "Scene.h" +#include "../graphics/Bitmap.h" +#include "../graphics/Sprite.h" + +#define MAX_OBJECTS 16 + +class GameScene : public Scene { + public: + GameScene(); + ~GameScene() override; + + void run() override; + + void enter() override; + + void exit() override; + private: + uint8_t *_fb{nullptr}; + + Bitmap _bg; + Bitmap _cow; + Bitmap _witch; + Sprite *_playerSprite; + Sprite *_sprites[MAX_OBJECTS]{nullptr}; + bool _firstFrame = true; + + void clearSprite(Sprite *sprite); + void drawSprite(Sprite *sprite); +}; + + +#endif //GAME_GAMESCENE_H \ No newline at end of file diff --git a/scenes/IntroScene.cpp b/scenes/IntroScene.cpp new file mode 100644 index 0000000..505b377 --- /dev/null +++ b/scenes/IntroScene.cpp @@ -0,0 +1,11 @@ +#include "IntroScene.h" + +void IntroScene::run() { + Scenes::setScene(Scenes::MainMenuScene); +} + +void IntroScene::enter() { +} + +void IntroScene::exit() { +} diff --git a/scenes/IntroScene.h b/scenes/IntroScene.h new file mode 100644 index 0000000..b6cfb79 --- /dev/null +++ b/scenes/IntroScene.h @@ -0,0 +1,20 @@ +#ifndef GAME_INTROSCENE_H +#define GAME_INTROSCENE_H + +#include "Scene.h" + + +class IntroScene : public Scene { +public: + IntroScene() = default; + ~IntroScene() override = default; + + void run() override; + + void enter() override; + + void exit() override; +}; + + +#endif //GAME_INTROSCENE_H \ No newline at end of file diff --git a/scenes/MainMenuScene.cpp b/scenes/MainMenuScene.cpp new file mode 100644 index 0000000..232c7c5 --- /dev/null +++ b/scenes/MainMenuScene.cpp @@ -0,0 +1,11 @@ +#include "MainMenuScene.h" + +void MainMenuScene::run() { + Scenes::setScene(Scenes::GameScene); +} + +void MainMenuScene::enter() { +} + +void MainMenuScene::exit() { +} diff --git a/scenes/MainMenuScene.h b/scenes/MainMenuScene.h new file mode 100644 index 0000000..d2a7482 --- /dev/null +++ b/scenes/MainMenuScene.h @@ -0,0 +1,20 @@ +#ifndef GAME_MAINMENUSCENE_H +#define GAME_MAINMENUSCENE_H + +#include "Scene.h" + + +class MainMenuScene : public Scene { +public: + MainMenuScene() = default; + ~MainMenuScene() override = default; + + void run() override; + + void enter() override; + + void exit() override; +}; + + +#endif //GAME_MAINMENUSCENE_H \ No newline at end of file diff --git a/scenes/Scene.cpp b/scenes/Scene.cpp new file mode 100644 index 0000000..2c82212 --- /dev/null +++ b/scenes/Scene.cpp @@ -0,0 +1,19 @@ +#include "Scene.h" + +#include "GameScene.h" +#include "IntroScene.h" +#include "MainMenuScene.h" + +namespace Scenes { + static ::IntroScene introScene{}; + static ::MainMenuScene mainMenuScene{}; + static ::GameScene gameScene{}; + + SceneId currentSceneId = IntroScene; + Scene *sceneList[] = { + &introScene, + &mainMenuScene, + &gameScene + }; +} + diff --git a/scenes/Scene.h b/scenes/Scene.h new file mode 100644 index 0000000..9b48582 --- /dev/null +++ b/scenes/Scene.h @@ -0,0 +1,42 @@ +#ifndef GAME_SCENE_H +#define GAME_SCENE_H + +#include "../graphics/Rect.h" + +class Scene { +public: + virtual ~Scene() = default; + virtual void run() = 0; + virtual void enter() = 0; + virtual void exit() = 0; + +protected: + Scene() = default; +}; + + +namespace Scenes { + enum SceneId { + IntroScene, + MainMenuScene, + GameScene, + }; + + extern SceneId currentSceneId; + extern Scene *sceneList[]; + + inline SceneId setScene(SceneId scene) { + auto previousSceneId = currentSceneId; + sceneList[currentSceneId]->exit(); + currentSceneId = scene; + sceneList[currentSceneId]->enter(); + return previousSceneId; + } + + inline Scene *getCurrentScene() { return sceneList[currentSceneId]; } + inline SceneId getCurrentSceneId() { return currentSceneId; } + +} + + +#endif //GAME_SCENE_H diff --git a/system/Keyboard.h b/system/Keyboard.h new file mode 100644 index 0000000..243db4c --- /dev/null +++ b/system/Keyboard.h @@ -0,0 +1,5 @@ +#ifdef BUILD_SDL +#include "sdl/Keyboard.h" +#else +#include "dos/Keyboard.h" +#endif \ No newline at end of file diff --git a/system/Opl.h b/system/Opl.h new file mode 100644 index 0000000..638b42e --- /dev/null +++ b/system/Opl.h @@ -0,0 +1,10 @@ +#ifndef GAME_OPL_H +#define GAME_OPL_H + +#include + +namespace Opl { + void write(uint16_t reg, uint8_t data); +} + +#endif //GAME_OPL_H \ No newline at end of file diff --git a/system/Pic.h b/system/Pic.h new file mode 100644 index 0000000..89891fa --- /dev/null +++ b/system/Pic.h @@ -0,0 +1,12 @@ +#ifndef GAME_PIC_H +#define GAME_PIC_H + +#include "../util/Asm.h" + +namespace Pic { + static void clearInterrupt() { + outb(0x20, 0x20); + } +} + +#endif //GAME_PIC_H \ No newline at end of file diff --git a/system/SoundBlaster.h b/system/SoundBlaster.h new file mode 100644 index 0000000..55d533c --- /dev/null +++ b/system/SoundBlaster.h @@ -0,0 +1,37 @@ +#ifndef GAME_SOUNDBLASTER_H +#define GAME_SOUNDBLASTER_H + +#ifdef BUILD_SDL +#include +#else +#include +#endif +#include + +#define SB_BUFFER_SIZE 1024 +#define SB_BUFFERS 32 +#define SB_DMA_BUFFER_SIZE (SB_BUFFER_SIZE * 4) + +class SoundBlaster { +public: + SoundBlaster(); + ~SoundBlaster(); + +private: + friend void soundblasterIsr(); + void onInterrupt(); + +#ifdef BUILD_SDL +#else + _go32_dpmi_seginfo _dmaBuffer{}; + uint8_t (*_buffers)[SB_BUFFER_SIZE]{}; + uint8_t _dmaPage{0}; + uint16_t _dmaOffset{0}; + unsigned _nextBufferReadIndex{0}; + unsigned _nextBufferWriteIndex{0}; +#endif +}; + +extern SoundBlaster soundblaster; + +#endif //GAME_SOUNDBLASTER_H diff --git a/system/Timer.h b/system/Timer.h new file mode 100644 index 0000000..f75cf70 --- /dev/null +++ b/system/Timer.h @@ -0,0 +1,49 @@ +#ifndef TICKS_H +#define TICKS_H + +#include + +#ifdef BUILD_SDL +#else +#include +#endif + +class Timer { +public: + using Callback = void (*)(); + + Timer(); + + ~Timer(); + + void setFrequency(uint16_t freq); + + void setDivider(uint16_t div); + + Callback setCallback(Callback); + + [[nodiscard]] uint32_t getTicks() const; + + [[nodiscard]] uint16_t getFrequency() const; + + static Timer &instance(); + +private: + friend void timerISR(); + +#ifdef BUILD_SDL +#else + _go32_dpmi_seginfo _oldIsr{}, _newIsr{}; +#endif + + uint32_t _ticks{0}; + uint32_t _elapsed{0}; + uint16_t _div{0}; + uint16_t _freq{0}; + + void (*_callback)(){nullptr}; + + void update(); +}; + +#endif diff --git a/system/Video.h b/system/Video.h new file mode 100644 index 0000000..e7b621f --- /dev/null +++ b/system/Video.h @@ -0,0 +1,63 @@ +#ifndef VIDEO_H +#define VIDEO_H + +#ifdef BUILD_SDL +#include +#include +#else +#include +#endif + +#include "../graphics/Rect.h" + +#define SCREEN_WIDTH 320 +#define SCREEN_HEIGHT 200 +#define MAX_UPDATE_RECT_INDEX 255 + +class Video { + public: + struct PaletteEntry { + uint8_t r{0}, g{0}, b{0}; + }; + + Video(); + ~Video(); + + void enter(); + void exit(); + + void SetPaletteEntry(uint8_t index, PaletteEntry entry); + PaletteEntry GetPaletteEntry(uint8_t index); + + void SetPaletteEntry(uint8_t index, PaletteEntry *entry); + void GetPaletteEntry(uint8_t index, PaletteEntry *entry); + + void *GetFB(); + + void WaitForVerticalSync(); + + void UpdateRect(const Rect &rect); + void Flip(); + private: + uint8_t _oldMode{0}; + PaletteEntry _oldPalette[256]{}; +#ifdef BUILD_SDL + SDL_Window *_window; + SDL_Surface *_windowSurface; + SDL_Surface *_fb; + PaletteEntry _palette[256]{}; + std::chrono::steady_clock::time_point _lastUpdate{std::chrono::steady_clock::now()}; +#else + uint8_t *_fb{nullptr}; +#endif + uint8_t _renderBuffer[SCREEN_WIDTH*SCREEN_HEIGHT]{}; + Rect _updatedRects[MAX_UPDATE_RECT_INDEX+1]; + unsigned _updateRectIndex{0}; + + void SetMode(uint8_t mode); + uint8_t GetMode(); +}; + +extern Video video; + +#endif diff --git a/system/dos/Keyboard.cpp b/system/dos/Keyboard.cpp new file mode 100644 index 0000000..50f2972 --- /dev/null +++ b/system/dos/Keyboard.cpp @@ -0,0 +1,429 @@ +#include "../Keyboard.h" + +#include + +#include "../Pic.h" + +#define PIC +#define KEYB_DATA 0x60 + +Keyboard keyboard; + +static const char *keyNames[] = { + "", + "KeyEscape", // = 0x01, + "Key1", // = 0x02, + "Key2", // = 0x03, + "Key3", // = 0x04, + "Key4", // = 0x05, + "Key5", // = 0x06, + "Key6", // = 0x07, + "Key7", // = 0x08, + "Key8", // = 0x09, + "Key9", // = 0x0a, + "Key0", // = 0x0b, + "KeyMinus", // = 0x0c, + "KeyEqual", // = 0x0d, + "KeyBackspace", // = 0x0e, + "KeyTab", // = 0x0f, + + "KeyQ", // = 0x10, + "KeyW", // = 0x11, + "KeyE", // = 0x12, + "KeyR", // = 0x13, + "KeyT", // = 0x14, + "KeyY", // = 0x15, + "KeyU", // = 0x16, + "KeyI", // = 0x17, + "KeyO", // = 0x18, + "KeyP", // = 0x19, + "KeyBracketLeft", // = 0x1a, + "KeyBracketRight", // = 0x1b, + "KeyEnter", // = 0x1c, + "KeyLeftCtrl", // = 0x1d, + "KeyA", // = 0x1e, + "KeyS", // = 0x1f, + + "KeyD", // = 0x20, + "KeyF", // = 0x21, + "KeyG", // = 0x22, + "KeyH", // = 0x23, + "KeyJ", // = 0x24, + "KeyK", // = 0x25, + "KeyL", // = 0x26, + "KeySemicolon", // = 0x27, + "KeyApostrophe", // = 0x28, + "KeyBacktick", // = 0x29, + "KeyLeftShift", // = 0x2a, + "KeyBackslash", // = 0x2b, + "KeyZ", // = 0x2c, + "KeyX", // = 0x2d, + "KeyC", // = 0x2e, + "KeyV", // = 0x2f, + + "KeyB", // = 0x30, + "KeyN", // = 0x31, + "KeyM", // = 0x32, + "KeyComma", // = 0x33, + "KeyPeriod", // = 0x34, + "KeySlash", // = 0x35, + "KeyRightShift", // = 0x36, + "KeyKeypadMultiply", // = 0x37, + "KeyLeftAlt", // = 0x38, + "KeySpace", // = 0x39, + "KeyCapsLock", // = 0x3a, + "KeyF1", // = 0x3b, + "KeyF2", // = 0x3c, + "KeyF3", // = 0x3d, + "KeyF4", // = 0x3e, + "KeyF5", // = 0x3f, + + "KeyF6", // = 0x40, + "KeyF7", // = 0x41, + "KeyF8", // = 0x42, + "KeyF9", // = 0x43, + "KeyF10", // = 0x44, + "KeyNumLock", // = 0x45, + "KeyScrollLock", // = 0x46, + "KeyKeypad7", // = 0x47, + "KeyKeypad8", // = 0x48, + "KeyKeypad9", // = 0x49, + "KeyKeypadMinus", // = 0x4a, + "KeyKeypad4", // = 0x4b, + "KeyKeypad5", // = 0x4c, + "KeyKeypad6", // = 0x4d, + "KeyKeypadPlus", // = 0x4e, + "KeyKeypad1", // = 0x4f, + + "KeyKeypad2", // = 0x50, + "KeyKeypad3", // = 0x51, + "KeyKeypad0", // = 0x52, + "KeyKeypadPeriod", // = 0x53, + "", + "", + "", + "KeyF11", // = 0x57, + "KeyF12", // = 0x58, + "", + "", + "", + "", + "", + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + + "KeyMediaPrev", // = 0x90, + "", + "", + "", + "", + "", + "", + "", + "", + "KeyMediaNext", // = 0x99, + "", + "", + "KeyKeypadEnter", // = 0x9c, + "KeyRightControl", // = 0x9d, + "", + "", + + "KeyMediaMute", // = 0xa0, + "KeyMediaCalculator", // = 0xa1, + "KeyMediaPlay", // = 0xa2, + "", + "KeyMediaStop", // = 0xa4, + "", + "", + "", + "", + "", + "", + "", + "", + "", + "KeyMediaVolumeDown", // = 0xae, + "", + + "KeyMediaVolumeUp", // = 0xb0, + "", + "KeyMediaWww", // = 0xb2, + "", + "", + "KeyKeypadDivide", // = 0xb5, + "", + "", + "KeyRightAlt", // = 0xb8, + "", + "", + "", + "", + "", + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "KeyHome", // = 0xc7, + "KeyUp", // = 0xc8, + "KeyPageUp", // = 0xc9, + "", + "KeyLeft", // = 0xcb, + "", + "KeyRight", // = 0xcd, + "", + "KeyEnd", // = 0xcf, + + "KeyDown", // = 0xd0, + "KeyPageDown", // = 0xd1, + "KeyInsert", // = 0xd2, + "KeyDelete", // = 0xd3, + "", + "", + "", + "", + "", + "", + "", + "KeyLeftGui", // = 0xdb, + "KeyRightGui", // = 0xdc, + "KeyApps", // = 0xdd, + "KeyAcpiPower", // = 0xde, + "KeyAcpiSleep", // = 0xdf, + + "", + "", + "", + "KeyAcpiWake", // = 0xe3, + "", + "KeyMediaWwwSearch", // = 0xe5, + "KeyMediaWwwFavorites", // = 0xe6, + "KeyMediaWwwRefresh", // = 0xe7, + "KeyMediaWwwStop", // = 0xe8, + "KeyMediaWwwForward", // = 0xe9, + "KeyMediaWwwBack", // = 0xea, + "KeyMediaMyComputer", // = 0xeb, + "KeyMediaEmail", // = 0xec, + "KeyMediaSelect", // = 0xed, + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "KeyPrint", // = 0xfe, + "KeyPause", // = 0xff, +}; + +void keyboardIsr() { + keyboard.Isr(); +} + +Keyboard::Keyboard() { + _go32_dpmi_get_protected_mode_interrupt_vector(9, &_oldIsr); + + _newIsr.pm_offset = (int)keyboardIsr; + _newIsr.pm_selector = _go32_my_cs(); + _go32_dpmi_allocate_iret_wrapper(&_newIsr); + _go32_dpmi_set_protected_mode_interrupt_vector(9, &_newIsr); +} + +Keyboard::~Keyboard() { + _go32_dpmi_set_protected_mode_interrupt_vector(9, &_oldIsr); + _go32_dpmi_free_iret_wrapper(&_newIsr); +} + +Keyboard::KeyHandleFunction Keyboard::setKeyUpHandler(KeyHandleFunction handler) { + auto oldHandler = _keyUpHandler; + _keyUpHandler = handler; + return oldHandler; +} + +Keyboard::KeyHandleFunction Keyboard::setKeyDownHandler(KeyHandleFunction handler) { + auto oldHandler = _keyDownHandler; + _keyDownHandler = handler; + return oldHandler; +} + +Keyboard::KeyHandleFunction Keyboard::setKeyRepeatHandler(KeyHandleFunction handler) { + auto oldHandler = _keyRepeatHandler; + _keyRepeatHandler = handler; + return oldHandler; +} + +const char *Keyboard::getKeyName(unsigned char keyCode) { + return keyNames[keyCode]; +} + +void Keyboard::Isr() { + auto code = inb(KEYB_DATA); + switch (_state) { + case Start: + if (code == 0xe0) { + _state = Extended; + break; + } + if (code == 0xe1) { + _state = PauseBegin; + break; + } + if (code & 0x80) { + _state = Start; + keyUp(code & 0x7f); + } else { + _state = Start; + keyDown(code & 0x7f); + } + break; + case Extended: + if (code == 0x2a) { + _state = PrintPressed1; + break; + } + if (code == 0xb7) { + _state = PrintReleased1; + break; + } + if (code & 0x80) { + _state = Start; + keyUp(0x80 + (code & 0x7f)); + } else { + _state = Start; + keyDown(0x80 + code); + } + break; + case PauseBegin: + if (code == 0x1d) { + _state = PausePressed1; + break; + } + if (code == 0x9d) { + _state = PauseReleased1; + break; + } + _state = Start; + break; + case PausePressed1: + if (code == 45) { + keyDown(KeyPause); + } + _state = Start; + break; + case PauseReleased1: + if (code == 0xc5) { + keyUp(KeyPause); + } + _state = Start; + break; + case PrintPressed1: + if (code == 0xe0) { + _state = PrintPressed2; + break; + } + _state = Start; + break; + case PrintPressed2: + if (code == 0x37) { + keyDown(KeyPrint); + } + _state = Start; + break; + case PrintReleased1: + if (code == 0xe0) { + _state = PrintReleased2; + break; + } + _state = Start; + break; + case PrintReleased2: + if (code == 0xaa) { + keyUp(KeyPrint); + } + _state = Start; + break; + } + + Pic::clearInterrupt(); +} + +void Keyboard::keyDown(unsigned char c) { + if (keyState[c]) { + if (_keyRepeatHandler) _keyRepeatHandler(c); + } else { + keyState[c] = true; + if (_keyDownHandler) _keyDownHandler(c); + } +} + +void Keyboard::keyUp(unsigned char c) { + keyState[c] = false; + if (_keyUpHandler) _keyUpHandler(c); +} diff --git a/system/dos/Keyboard.h b/system/dos/Keyboard.h new file mode 100644 index 0000000..1e3b729 --- /dev/null +++ b/system/dos/Keyboard.h @@ -0,0 +1,182 @@ +#ifndef GAME_KEYBOARD_H +#define GAME_KEYBOARD_H + +#ifdef BUILD_SDL +#else +#include +#endif + +#define KEY_MAX 256 + +enum { + KeyEscape = 0x01, + Key1 = 0x02, + Key2 = 0x03, + Key3 = 0x04, + Key4 = 0x05, + Key5 = 0x06, + Key6 = 0x07, + Key7 = 0x08, + Key8 = 0x09, + Key9 = 0x0a, + Key0 = 0x0b, + KeyMinus = 0x0c, + KeyEqual = 0x0d, + KeyBackspace = 0x0e, + KeyTab = 0x0f, + KeyQ = 0x10, + KeyW = 0x11, + KeyE = 0x12, + KeyR = 0x13, + KeyT = 0x14, + KeyY = 0x15, + KeyU = 0x16, + KeyI = 0x17, + KeyO = 0x18, + KeyP = 0x19, + KeyBracketLeft = 0x1a, + KeyBracketRight = 0x1b, + KeyEnter = 0x1c, + KeyLeftCtrl = 0x1d, + KeyA = 0x1e, + KeyS = 0x1f, + KeyD = 0x20, + KeyF = 0x21, + KeyG = 0x22, + KeyH = 0x23, + KeyJ = 0x24, + KeyK = 0x25, + KeyL = 0x26, + KeySemicolon = 0x27, + KeyApostrophe = 0x28, + KeyBacktick = 0x29, + KeyLeftShift = 0x2a, + KeyBackslash = 0x2b, + KeyZ = 0x2c, + KeyX = 0x2d, + KeyC = 0x2e, + KeyV = 0x2f, + KeyB = 0x30, + KeyN = 0x31, + KeyM = 0x32, + KeyComma = 0x33, + KeyPeriod = 0x34, + KeySlash = 0x35, + KeyRightShift = 0x36, + KeyKeypadMultiply = 0x37, + KeyLeftAlt = 0x38, + KeySpace = 0x39, + KeyCapsLock = 0x3a, + KeyF1 = 0x3b, + KeyF2 = 0x3c, + KeyF3 = 0x3d, + KeyF4 = 0x3e, + KeyF5 = 0x3f, + KeyF6 = 0x40, + KeyF7 = 0x41, + KeyF8 = 0x42, + KeyF9 = 0x43, + KeyF10 = 0x44, + KeyNumLock = 0x45, + KeyScrollLock = 0x46, + KeyKeypad7 = 0x47, + KeyKeypad8 = 0x48, + KeyKeypad9 = 0x49, + KeyKeypadMinus = 0x4a, + KeyKeypad4 = 0x4b, + KeyKeypad5 = 0x4c, + KeyKeypad6 = 0x4d, + KeyKeypadPlus = 0x4e, + KeyKeypad1 = 0x4f, + KeyKeypad2 = 0x50, + KeyKeypad3 = 0x51, + KeyKeypad0 = 0x52, + KeyKeypadPeriod = 0x53, + KeyF11 = 0x57, + KeyF12 = 0x58, + KeyMediaPrev = 0x90, + KeyMediaNext = 0x99, + KeyKeypadEnter = 0x9c, + KeyRightControl = 0x9d, + KeyMediaMute = 0xa0, + KeyMediaCalculator = 0xa1, + KeyMediaPlay = 0xa2, + KeyMediaStop = 0xa4, + KeyMediaVolumeDown = 0xae, + KeyMediaVolumeUp = 0xb0, + KeyMediaWww = 0xb2, + KeyKeypadDivide = 0xb5, + KeyRightAlt = 0xb8, + KeyHome = 0xc7, + KeyUp = 0xc8, + KeyPageUp = 0xc9, + KeyLeft = 0xcb, + KeyRight = 0xcd, + KeyEnd = 0xcf, + KeyDown = 0xd0, + KeyPageDown = 0xd1, + KeyInsert = 0xd2, + KeyDelete = 0xd3, + KeyLeftGui = 0xdb, + KeyRightGui = 0xdc, + KeyApps = 0xdd, + KeyAcpiPower = 0xde, + KeyAcpiSleep = 0xdf, + KeyAcpiWake = 0xe3, + KeyMediaWwwSearch = 0xe5, + KeyMediaWwwFavorites = 0xe6, + KeyMediaWwwRefresh = 0xe7, + KeyMediaWwwStop = 0xe8, + KeyMediaWwwForward = 0xe9, + KeyMediaWwwBack = 0xea, + KeyMediaMyComputer = 0xeb, + KeyMediaEmail = 0xec, + KeyMediaSelect = 0xed, + KeyPrint = 0xfe, + KeyPause = 0xff, +}; + +class Keyboard { +public: + using KeyHandleFunction = void (*)(unsigned char); + + Keyboard(); + ~Keyboard(); + + KeyHandleFunction setKeyUpHandler(KeyHandleFunction handler); + KeyHandleFunction setKeyDownHandler(KeyHandleFunction handler); + KeyHandleFunction setKeyRepeatHandler(KeyHandleFunction handler); + + bool keyState[KEY_MAX]{}; + + const char *getKeyName(unsigned char keyCode); +private: + friend void keyboardIsr(); + void Isr(); + enum State { + Start, + Extended, + PauseBegin, + PausePressed1, + PauseReleased1, + PrintPressed1, + PrintPressed2, + PrintReleased1, + PrintReleased2, + }; + State _state; + KeyHandleFunction _keyDownHandler{nullptr}, _keyUpHandler{nullptr}, _keyRepeatHandler{nullptr}; + + void keyDown(unsigned char c); + + void keyUp(unsigned char c); + +#ifdef BUILD_SDL +#else + _go32_dpmi_seginfo _oldIsr, _newIsr; +#endif +}; + +extern Keyboard keyboard; + +#endif //GAME_KEYBOARD_H diff --git a/system/dos/Opl.cpp b/system/dos/Opl.cpp new file mode 100644 index 0000000..5d6794b --- /dev/null +++ b/system/dos/Opl.cpp @@ -0,0 +1,18 @@ +#include "../Opl.h" +#include "../../util/Asm.h" +#include "../../util/Log.h" + +#define OPL_REG 0x388 + +namespace Opl { + void write(uint16_t reg, uint8_t data) { + DefaultLog.log("Writing to OPL r=%u data=%u\n", reg, data); + if (reg >= 0x100) { + outb(OPL_REG + 2, reg & 0xff); + outb(OPL_REG + 3, data); + } else { + outb(OPL_REG, reg); + outb(OPL_REG + 1, data); + } + } +} diff --git a/system/dos/SoundBlaster.cpp b/system/dos/SoundBlaster.cpp new file mode 100644 index 0000000..fe2dc15 --- /dev/null +++ b/system/dos/SoundBlaster.cpp @@ -0,0 +1,47 @@ +#include "../SoundBlaster.h" + +#include + +SoundBlaster soundblaster; + +void soundblasterIsr() { + soundblaster.onInterrupt(); +} + +SoundBlaster::SoundBlaster() { + // Allocate twice the amount needed and select one half depending on whether it crosses a 64k boundary + _dmaBuffer.size = (SB_DMA_BUFFER_SIZE + 15) >> (4-1); + _go32_dpmi_allocate_dos_memory(&_dmaBuffer); + + auto physFirst = (_dmaBuffer.rm_segment << 4) + _dmaBuffer.rm_offset; + auto physSecond = physFirst + SB_DMA_BUFFER_SIZE; + auto pageFirst = physFirst >> 16; + auto pageSecond = physSecond >> 16; + + if (pageFirst == pageSecond) { + // Use first half + _buffers = reinterpret_cast(physFirst); + _dmaPage = pageFirst; + _dmaOffset = physFirst & 0xFFFF; + } else { + // Use second half + _buffers = reinterpret_cast(physSecond); + _dmaPage = pageSecond; + _dmaOffset = physSecond & 0xFFFF; + } +} + +SoundBlaster::~SoundBlaster() { + _go32_dpmi_free_dos_memory(&_dmaBuffer); +} + +void SoundBlaster::onInterrupt() { + // No data available - should not happen, but let's force generate a block + if (_nextBufferReadIndex == _nextBufferWriteIndex) { + // TODO generateSamples(); + } + + dosmemput(_buffers, SB_BUFFER_SIZE, (_dmaBuffer.rm_segment << 4) + _nextBufferReadIndex * SB_BUFFER_SIZE); + _nextBufferReadIndex = (_nextBufferReadIndex + 1) & 3; +} + diff --git a/system/dos/Timer.cpp b/system/dos/Timer.cpp new file mode 100644 index 0000000..7882022 --- /dev/null +++ b/system/dos/Timer.cpp @@ -0,0 +1,106 @@ +#include + +#include "../Timer.h" + +#include "../Pic.h" +#include "../../util/Asm.h" +#include "../../util/Log.h" + +#define PIT_FREQ 1193181 +#define PIT_MODE_CH0 0x00 +#define PIT_MODE_CH1 0x40 +#define PIT_MODE_CH2 0x80 +#define PIT_MODE_ACCESS_LO 0x10 +#define PIT_MODE_ACCESS_HI 0x20 +#define PIT_MODE_ACCESS_LOHI 0x30 +#define PIT_MODE_M0 0x00 +#define PIT_MODE_M1 0x02 +#define PIT_MODE_M2 0x04 +#define PIT_MODE_M3 0x06 +#define PIT_MODE_M4 0x08 +#define PIT_MODE_M5 0x0a +#define PIT_MODE_BIN 0x00 +#define PIT_MODE_BCD 0x01 + +#define PIT_REG_CH0 0x40 +#define PIT_REG_CH1 0x41 +#define PIT_REG_CH2 0x42 +#define PIT_REG_MODE 0x43 + +void timerISR() { + Timer::instance().update(); +} + +Timer::Timer() { + _go32_dpmi_get_protected_mode_interrupt_vector(8, &_oldIsr); + + _newIsr.pm_offset = reinterpret_cast(timerISR); + _newIsr.pm_selector = _go32_my_cs(); + _go32_dpmi_allocate_iret_wrapper(&_newIsr); + _go32_dpmi_set_protected_mode_interrupt_vector(8, &_newIsr); +} + +Timer::~Timer() { + setDivider(65535); + _go32_dpmi_set_protected_mode_interrupt_vector(8, &_oldIsr); + _go32_dpmi_free_iret_wrapper(&_newIsr); +} + +void Timer::update() { + _ticks++; + + if (_callback) { + DefaultLog.log("Timer tick with callback\n"); + _callback(); + } else { + DefaultLog.log("Timer tick without callback\n"); + } + + _elapsed += _div; + if (_elapsed >= 65535) { + _elapsed -= 65535; + // TODO call old isr + // Documentation is kind of nonexistant so we will not call the handler for now + Pic::clearInterrupt(); + } else { + Pic::clearInterrupt(); + } +} + +uint32_t Timer::getTicks() const { + return _ticks; +} + +uint16_t Timer::getFrequency() const { + return _freq; +} + +Timer &Timer::instance() { + static Timer inst; + return inst; +} + +void Timer::setFrequency(uint16_t freq) { + _freq = freq; + setDivider(PIT_FREQ / freq); +} + +void Timer::setDivider(uint16_t div) { + auto wasEnabled = interruptsEnabled(); + disableInterrupts(); + + _div = div; + + // Update PIT frequency + outb(PIT_REG_MODE, PIT_MODE_CH0|PIT_MODE_ACCESS_LOHI|PIT_MODE_M2|PIT_MODE_BIN); + outb(PIT_REG_CH0, _div & 0xff); + outb(PIT_REG_CH0, (_div >> 8) & 0xff); + + if (wasEnabled) enableInterrupts(); +} + +Timer::Callback Timer::setCallback(Timer::Callback callback) { + auto oldCallback = _callback; + _callback = callback; + return oldCallback; +} diff --git a/system/dos/Video.cpp b/system/dos/Video.cpp new file mode 100644 index 0000000..bf1dc57 --- /dev/null +++ b/system/dos/Video.cpp @@ -0,0 +1,135 @@ +#include +#include + +#include "../Video.h" +#include "../../util/Asm.h" +#include "../../util/Log.h" + +#define VGA_DAC_ADDR_RD 0x3c7 +#define VGA_DAC_ADDR_WR 0x3c8 +#define VGA_DAC_DATA 0x3c9 +#define VGA_EXT_INPUT_STATUS 0x3da +#define VGA_EXT_INPUT_STATUS_VRETRACE 8 + +Video video; + +Video::Video() { + __djgpp_nearptr_enable(); + + // Store current video mode + _oldMode = GetMode(); + + // Store current palette + for (auto i = 0; i < 256; i++) { + GetPaletteEntry(i, &_oldPalette[i]); + } +} + +Video::~Video() { + exit(); +} + +void Video::enter() { + SetMode(0x13); +} + +void Video::exit() { + // Restore old palette + for (auto i = 0; i < 256; i++) { + SetPaletteEntry(i, &_oldPalette[i]); + } + + // Restore old video mode + SetMode(_oldMode); +} + +void Video::SetMode(uint8_t mode) { + union REGS regs; + regs.h.ah = 0x00; + regs.h.al = mode; + int86(0x10, ®s, ®s); + + //__djgpp_nearptr_enable(); + switch (mode) { + default: + case 0x13: + _fb = (uint8_t *)(0xA0000 + __djgpp_conventional_base); + + } +} + +uint8_t Video::GetMode() { + union REGS regs; + regs.h.ah = 0x0f; + regs.h.al = 0; + int86(0x10, ®s, ®s); + + return regs.h.al; +} + +void Video::SetPaletteEntry(uint8_t index, PaletteEntry entry) { + SetPaletteEntry(index, &entry); +} + +Video::PaletteEntry Video::GetPaletteEntry(uint8_t index) { + PaletteEntry entry; + GetPaletteEntry(index, &entry); + return entry; +} + +void Video::SetPaletteEntry(uint8_t index, PaletteEntry *entry) { + outb(VGA_DAC_ADDR_WR, index); + outb(VGA_DAC_DATA, entry->r >> 2); + outb(VGA_DAC_DATA, entry->g >> 2); + outb(VGA_DAC_DATA, entry->b >> 2); +} + +void Video::GetPaletteEntry(uint8_t index, PaletteEntry *entry) { + outb(VGA_DAC_ADDR_RD, index); + entry->r = inb(VGA_DAC_DATA); + entry->g = inb(VGA_DAC_DATA); + entry->b = inb(VGA_DAC_DATA); +} + +void *Video::GetFB() { + return _renderBuffer; +} + +void Video::WaitForVerticalSync() { + auto state = inb(VGA_EXT_INPUT_STATUS) & VGA_EXT_INPUT_STATUS_VRETRACE; + if (!state) { + // We started before a retrace, so wait until it begins + while (!(inb(VGA_EXT_INPUT_STATUS) & VGA_EXT_INPUT_STATUS_VRETRACE)) {} + } + // Wait until it ends + while (inb(VGA_EXT_INPUT_STATUS) & VGA_EXT_INPUT_STATUS_VRETRACE); +} + +void Video::UpdateRect(const Rect &rect) { + // Merge rect if it overlaps with an existing rect + for (auto i = 0; i < _updateRectIndex; i++) { + auto &r = _updatedRects[i]; + if (r.overlaps(rect)) { + r += rect; + return; + } + } + + // Add a new rect to the list + _updatedRects[_updateRectIndex++] = rect; +} + +void Video::Flip() { + for (auto i = 0; i <= MAX_UPDATE_RECT_INDEX; i++) { + auto &rect = _updatedRects[i]; + for (auto y = rect.y1; y <= rect.y2; y++) { + auto yOffset = y * SCREEN_WIDTH; + + for (auto x = rect.x1; x <= rect.x2; x++) { + _fb[yOffset + x] = _renderBuffer[yOffset + x]; + } + } + } + + _updateRectIndex = 0; +} diff --git a/system/dos/init.cpp b/system/dos/init.cpp new file mode 100644 index 0000000..a4d86fc --- /dev/null +++ b/system/dos/init.cpp @@ -0,0 +1,12 @@ +#include "../init.h" + +namespace System { + void init() { + + } + + void terminate() { + + } +} + diff --git a/system/init.h b/system/init.h new file mode 100644 index 0000000..045f0b1 --- /dev/null +++ b/system/init.h @@ -0,0 +1,9 @@ +#ifndef GAME_INIT_H +#define GAME_INIT_H + +namespace System { + void init(); + void terminate(); +} + +#endif //GAME_INIT_H \ No newline at end of file diff --git a/system/sdl/AudioBackend.cpp b/system/sdl/AudioBackend.cpp new file mode 100644 index 0000000..067e627 --- /dev/null +++ b/system/sdl/AudioBackend.cpp @@ -0,0 +1,52 @@ +#include "AudioBackend.h" + +#include + +AudioBackend audioBackend; + +AudioBackend::AudioBackend() { +} + +AudioBackend::~AudioBackend() { + terminate(); +} + +void AudioBackend::init() { + SDL_AudioSpec desired; + desired.freq = 44100; + desired.format = AUDIO_S16LSB; + desired.channels = 2; + desired.samples = 1024; + desired.padding = 0; + desired.userdata = this; + desired.callback = [] (void *userdata, Uint8 *stream, int len) { + static_cast(userdata)->generate(stream, len); + }; + + _audioDevice = SDL_OpenAudioDevice(nullptr, 0, &desired, &_audioSpec, 0); + if (!_audioDevice) { + std::cerr << "Failed to open audio device: " << SDL_GetError() << std::endl; + abort(); + } + + SDL_PauseAudioDevice(_audioDevice, 0); +} + +void AudioBackend::terminate() { + if (_audioDevice) { + SDL_PauseAudioDevice(_audioDevice, 1); + SDL_CloseAudioDevice(_audioDevice); + } +} + +void AudioBackend::generate(Uint8 *stream, int len) { + len = len / 4; + auto s = reinterpret_cast(stream); + + for (int i = 0; i < len; i++) { + int16_t l, r; + Opl::generateSample(&l, &r); + *s++ = l; + *s++ = r; + } +} diff --git a/system/sdl/AudioBackend.h b/system/sdl/AudioBackend.h new file mode 100644 index 0000000..ca1431c --- /dev/null +++ b/system/sdl/AudioBackend.h @@ -0,0 +1,26 @@ +#ifndef GAME_AUDIOOUTPUT_H +#define GAME_AUDIOOUTPUT_H + +#include + +namespace Opl { + void generateSample(int16_t *left, int16_t *right); +} + +class AudioBackend { +public: + AudioBackend(); + ~AudioBackend(); + + void init(); + void terminate(); + + void generate(Uint8 *stream, int len); +private: + SDL_AudioDeviceID _audioDevice; + SDL_AudioSpec _audioSpec; +}; + +extern AudioBackend audioBackend; + +#endif //GAME_AUDIOOUTPUT_H \ No newline at end of file diff --git a/system/sdl/Events.cpp b/system/sdl/Events.cpp new file mode 100644 index 0000000..5311d56 --- /dev/null +++ b/system/sdl/Events.cpp @@ -0,0 +1,25 @@ +#include +#include "Events.h" + +#include "Keyboard.h" + +Events events; + +void Events::poll() { + SDL_Event event; + while (SDL_PollEvent(&event)) { + // TODO handle events + switch (event.type) { + case SDL_QUIT: + // TODO + exit(0); + break; + case SDL_KEYDOWN: + keyboard.keyDown(event.key.keysym.sym); + break; + case SDL_KEYUP: + keyboard.keyUp(event.key.keysym.sym); + break; + } + } +} diff --git a/system/sdl/Events.h b/system/sdl/Events.h new file mode 100644 index 0000000..b9ab17b --- /dev/null +++ b/system/sdl/Events.h @@ -0,0 +1,14 @@ +#ifndef GAME_EVENTS_H +#define GAME_EVENTS_H + +class Events { +public: + Events() = default; + ~Events() = default; + + void poll(); +}; + +extern Events events; + +#endif //GAME_EVENTS_H \ No newline at end of file diff --git a/system/sdl/Keyboard.cpp b/system/sdl/Keyboard.cpp new file mode 100644 index 0000000..f3fc090 --- /dev/null +++ b/system/sdl/Keyboard.cpp @@ -0,0 +1,821 @@ +#include +#include "../Keyboard.h" + +Keyboard keyboard; + +// TODO map unknown key codes +uint8_t mapSdlKeycode(SDL_Keycode keyCode) { + switch (keyCode) { + case SDLK_UNKNOWN: + return KeyUnknown; + case SDLK_RETURN: + return KeyUnknown; + case SDLK_ESCAPE: + return KeyEscape; + case SDLK_BACKSPACE: + return KeyBackspace; + case SDLK_TAB: + return KeyTab; + case SDLK_SPACE: + return KeySpace; + case SDLK_EXCLAIM: + return KeyUnknown; + case SDLK_QUOTEDBL: + return KeyUnknown; + case SDLK_HASH: + return KeyUnknown; + case SDLK_PERCENT: + return KeyUnknown; + case SDLK_DOLLAR: + return KeyUnknown; + case SDLK_AMPERSAND: + return KeyUnknown; + case SDLK_QUOTE: + return KeyUnknown; + case SDLK_LEFTPAREN: + return KeyUnknown; + case SDLK_RIGHTPAREN: + return KeyUnknown; + case SDLK_ASTERISK: + return KeyUnknown; + case SDLK_PLUS: + return KeyUnknown; + case SDLK_COMMA: + return KeyUnknown; + case SDLK_MINUS: + return KeyUnknown; + case SDLK_PERIOD: + return KeyUnknown; + case SDLK_SLASH: + return KeyUnknown; + case SDLK_0: + return Key0; + case SDLK_1: + return Key1; + case SDLK_2: + return Key2; + case SDLK_3: + return Key3; + case SDLK_4: + return Key4; + case SDLK_5: + return Key5; + case SDLK_6: + return Key6; + case SDLK_7: + return Key7; + case SDLK_8: + return Key8; + case SDLK_9: + return Key9; + case SDLK_COLON: + return KeyUnknown; + case SDLK_SEMICOLON: + return KeyUnknown; + case SDLK_LESS: + return KeyUnknown; + case SDLK_EQUALS: + return KeyUnknown; + case SDLK_GREATER: + return KeyUnknown; + case SDLK_QUESTION: + return KeyUnknown; + case SDLK_AT: + return KeyUnknown; + case SDLK_LEFTBRACKET: + return KeyUnknown; + case SDLK_BACKSLASH: + return KeyUnknown; + case SDLK_RIGHTBRACKET: + return KeyUnknown; + case SDLK_CARET: + return KeyUnknown; + case SDLK_UNDERSCORE: + return KeyUnknown; + case SDLK_BACKQUOTE: + return KeyUnknown; + case SDLK_a: + return KeyA; + case SDLK_b: + return KeyB; + case SDLK_c: + return KeyC; + case SDLK_d: + return KeyD; + case SDLK_e: + return KeyE; + case SDLK_f: + return KeyF; + case SDLK_g: + return KeyG; + case SDLK_h: + return KeyG; + case SDLK_i: + return KeyI; + case SDLK_j: + return KeyJ; + case SDLK_k: + return KeyK; + case SDLK_l: + return KeyL; + case SDLK_m: + return KeyM; + case SDLK_n: + return KeyN; + case SDLK_o: + return KeyO; + case SDLK_p: + return KeyP; + case SDLK_q: + return KeyQ; + case SDLK_r: + return KeyR; + case SDLK_s: + return KeyS; + case SDLK_t: + return KeyT; + case SDLK_u: + return KeyU; + case SDLK_v: + return KeyV; + case SDLK_w: + return KeyW; + case SDLK_x: + return KeyX; + case SDLK_y: + return KeyY; + case SDLK_z: + return KeyZ; + case SDLK_CAPSLOCK: + return KeyCapsLock; + case SDLK_F1: + return KeyF1; + case SDLK_F2: + return KeyF2; + case SDLK_F3: + return KeyF3; + case SDLK_F4: + return KeyF4; + case SDLK_F5: + return KeyF5; + case SDLK_F6: + return KeyF6; + case SDLK_F7: + return KeyF7; + case SDLK_F8: + return KeyF8; + case SDLK_F9: + return KeyF9; + case SDLK_F10: + return KeyF10; + case SDLK_F11: + return KeyF11; + case SDLK_F12: + return KeyF12; + case SDLK_PRINTSCREEN: + return KeyPrint; + case SDLK_SCROLLLOCK: + return KeyScrollLock; + case SDLK_PAUSE: + return KeyPause; + case SDLK_INSERT: + return KeyInsert; + case SDLK_HOME: + return KeyHome; + case SDLK_PAGEUP: + return KeyPageUp; + case SDLK_DELETE: + return KeyDelete; + case SDLK_END: + return KeyEnd; + case SDLK_PAGEDOWN: + return KeyPageDown; + case SDLK_RIGHT: + return KeyRight; + case SDLK_LEFT: + return KeyLeft; + case SDLK_DOWN: + return KeyDown; + case SDLK_UP: + return KeyUp; + case SDLK_NUMLOCKCLEAR: + return KeyUnknown; + case SDLK_KP_DIVIDE: + return KeyUnknown; + case SDLK_KP_MULTIPLY: + return KeyUnknown; + case SDLK_KP_MINUS: + return KeyUnknown; + case SDLK_KP_PLUS: + return KeyUnknown; + case SDLK_KP_ENTER: + return KeyUnknown; + case SDLK_KP_1: + return KeyUnknown; + case SDLK_KP_2: + return KeyUnknown; + case SDLK_KP_3: + return KeyUnknown; + case SDLK_KP_4: + return KeyUnknown; + case SDLK_KP_5: + return KeyUnknown; + case SDLK_KP_6: + return KeyUnknown; + case SDLK_KP_7: + return KeyUnknown; + case SDLK_KP_8: + return KeyUnknown; + case SDLK_KP_9: + return KeyUnknown; + case SDLK_KP_0: + return KeyUnknown; + case SDLK_KP_PERIOD: + return KeyUnknown; + case SDLK_APPLICATION: + return KeyUnknown; + case SDLK_POWER: + return KeyUnknown; + case SDLK_KP_EQUALS: + return KeyUnknown; + case SDLK_F13: + return KeyUnknown; + case SDLK_F14: + return KeyUnknown; + case SDLK_F15: + return KeyUnknown; + case SDLK_F16: + return KeyUnknown; + case SDLK_F17: + return KeyUnknown; + case SDLK_F18: + return KeyUnknown; + case SDLK_F19: + return KeyUnknown; + case SDLK_F20: + return KeyUnknown; + case SDLK_F21: + return KeyUnknown; + case SDLK_F22: + return KeyUnknown; + case SDLK_F23: + return KeyUnknown; + case SDLK_F24: + return KeyUnknown; + case SDLK_EXECUTE: + return KeyUnknown; + case SDLK_HELP: + return KeyUnknown; + case SDLK_MENU: + return KeyUnknown; + case SDLK_SELECT: + return KeyUnknown; + case SDLK_STOP: + return KeyUnknown; + case SDLK_AGAIN: + return KeyUnknown; + case SDLK_UNDO: + return KeyUnknown; + case SDLK_CUT: + return KeyUnknown; + case SDLK_COPY: + return KeyUnknown; + case SDLK_PASTE: + return KeyUnknown; + case SDLK_FIND: + return KeyUnknown; + case SDLK_MUTE: + return KeyUnknown; + case SDLK_VOLUMEUP: + return KeyUnknown; + case SDLK_VOLUMEDOWN: + return KeyUnknown; + case SDLK_KP_COMMA: + return KeyUnknown; + case SDLK_KP_EQUALSAS400: + return KeyUnknown; + case SDLK_ALTERASE: + return KeyUnknown; + case SDLK_SYSREQ: + return KeyUnknown; + case SDLK_CANCEL: + return KeyUnknown; + case SDLK_CLEAR: + return KeyUnknown; + case SDLK_PRIOR: + return KeyUnknown; + case SDLK_RETURN2: + return KeyUnknown; + case SDLK_SEPARATOR: + return KeyUnknown; + case SDLK_OUT: + return KeyUnknown; + case SDLK_OPER: + return KeyUnknown; + case SDLK_CLEARAGAIN: + return KeyUnknown; + case SDLK_CRSEL: + return KeyUnknown; + case SDLK_EXSEL: + return KeyUnknown; + case SDLK_KP_00: + return KeyUnknown; + case SDLK_KP_000: + return KeyUnknown; + case SDLK_THOUSANDSSEPARATOR: + return KeyUnknown; + case SDLK_DECIMALSEPARATOR: + return KeyUnknown; + case SDLK_CURRENCYUNIT: + return KeyUnknown; + case SDLK_CURRENCYSUBUNIT: + return KeyUnknown; + case SDLK_KP_LEFTPAREN: + return KeyUnknown; + case SDLK_KP_RIGHTPAREN: + return KeyUnknown; + case SDLK_KP_LEFTBRACE: + return KeyUnknown; + case SDLK_KP_RIGHTBRACE: + return KeyUnknown; + case SDLK_KP_TAB: + return KeyUnknown; + case SDLK_KP_BACKSPACE: + return KeyUnknown; + case SDLK_KP_A: + return KeyUnknown; + case SDLK_KP_B: + return KeyUnknown; + case SDLK_KP_C: + return KeyUnknown; + case SDLK_KP_D: + return KeyUnknown; + case SDLK_KP_E: + return KeyUnknown; + case SDLK_KP_F: + return KeyUnknown; + case SDLK_KP_XOR: + return KeyUnknown; + case SDLK_KP_POWER: + return KeyUnknown; + case SDLK_KP_PERCENT: + return KeyUnknown; + case SDLK_KP_LESS: + return KeyUnknown; + case SDLK_KP_GREATER: + return KeyUnknown; + case SDLK_KP_AMPERSAND: + return KeyUnknown; + case SDLK_KP_DBLAMPERSAND: + return KeyUnknown; + case SDLK_KP_VERTICALBAR: + return KeyUnknown; + case SDLK_KP_DBLVERTICALBAR: + return KeyUnknown; + case SDLK_KP_COLON: + return KeyUnknown; + case SDLK_KP_HASH: + return KeyUnknown; + case SDLK_KP_SPACE: + return KeyUnknown; + case SDLK_KP_AT: + return KeyUnknown; + case SDLK_KP_EXCLAM: + return KeyUnknown; + case SDLK_KP_MEMSTORE: + return KeyUnknown; + case SDLK_KP_MEMRECALL: + return KeyUnknown; + case SDLK_KP_MEMCLEAR: + return KeyUnknown; + case SDLK_KP_MEMADD: + return KeyUnknown; + case SDLK_KP_MEMSUBTRACT: + return KeyUnknown; + case SDLK_KP_MEMMULTIPLY: + return KeyUnknown; + case SDLK_KP_MEMDIVIDE: + return KeyUnknown; + case SDLK_KP_PLUSMINUS: + return KeyUnknown; + case SDLK_KP_CLEAR: + return KeyUnknown; + case SDLK_KP_CLEARENTRY: + return KeyUnknown; + case SDLK_KP_BINARY: + return KeyUnknown; + case SDLK_KP_OCTAL: + return KeyUnknown; + case SDLK_KP_DECIMAL: + return KeyUnknown; + case SDLK_KP_HEXADECIMAL: + return KeyUnknown; + case SDLK_LCTRL: + return KeyLeftCtrl; + case SDLK_LSHIFT: + return KeyLeftShift; + case SDLK_LALT: + return KeyLeftAlt; + case SDLK_LGUI: + return KeyUnknown; + case SDLK_RCTRL: + return KeyRightControl; + case SDLK_RSHIFT: + return KeyRightShift; + case SDLK_RALT: + return KeyRightAlt; + case SDLK_RGUI: + return KeyUnknown; + case SDLK_MODE: + return KeyUnknown; + case SDLK_AUDIONEXT: + return KeyUnknown; + case SDLK_AUDIOPREV: + return KeyUnknown; + case SDLK_AUDIOSTOP: + return KeyUnknown; + case SDLK_AUDIOPLAY: + return KeyUnknown; + case SDLK_AUDIOMUTE: + return KeyUnknown; + case SDLK_MEDIASELECT: + return KeyUnknown; + case SDLK_WWW: + return KeyUnknown; + case SDLK_MAIL: + return KeyUnknown; + case SDLK_CALCULATOR: + return KeyUnknown; + case SDLK_COMPUTER: + return KeyUnknown; + case SDLK_AC_SEARCH: + return KeyUnknown; + case SDLK_AC_HOME: + return KeyUnknown; + case SDLK_AC_BACK: + return KeyUnknown; + case SDLK_AC_FORWARD: + return KeyUnknown; + case SDLK_AC_STOP: + return KeyUnknown; + case SDLK_AC_REFRESH: + return KeyUnknown; + case SDLK_AC_BOOKMARKS: + return KeyUnknown; + case SDLK_BRIGHTNESSDOWN: + return KeyUnknown; + case SDLK_BRIGHTNESSUP: + return KeyUnknown; + case SDLK_DISPLAYSWITCH: + return KeyUnknown; + case SDLK_KBDILLUMTOGGLE: + return KeyUnknown; + case SDLK_KBDILLUMDOWN: + return KeyUnknown; + case SDLK_KBDILLUMUP: + return KeyUnknown; + case SDLK_EJECT: + return KeyUnknown; + case SDLK_SLEEP: + return KeyUnknown; + case SDLK_APP1: + return KeyUnknown; + case SDLK_APP2: + return KeyUnknown; + case SDLK_AUDIOREWIND: + return KeyUnknown; + case SDLK_AUDIOFASTFORWARD: + return KeyUnknown; + default: ; + } + + return KeyUnknown; +} + +static const char *keyNames[] = { + "", + "KeyEscape", // = 0x01, + "Key1", // = 0x02, + "Key2", // = 0x03, + "Key3", // = 0x04, + "Key4", // = 0x05, + "Key5", // = 0x06, + "Key6", // = 0x07, + "Key7", // = 0x08, + "Key8", // = 0x09, + "Key9", // = 0x0a, + "Key0", // = 0x0b, + "KeyMinus", // = 0x0c, + "KeyEqual", // = 0x0d, + "KeyBackspace", // = 0x0e, + "KeyTab", // = 0x0f, + + "KeyQ", // = 0x10, + "KeyW", // = 0x11, + "KeyE", // = 0x12, + "KeyR", // = 0x13, + "KeyT", // = 0x14, + "KeyY", // = 0x15, + "KeyU", // = 0x16, + "KeyI", // = 0x17, + "KeyO", // = 0x18, + "KeyP", // = 0x19, + "KeyBracketLeft", // = 0x1a, + "KeyBracketRight", // = 0x1b, + "KeyEnter", // = 0x1c, + "KeyLeftCtrl", // = 0x1d, + "KeyA", // = 0x1e, + "KeyS", // = 0x1f, + + "KeyD", // = 0x20, + "KeyF", // = 0x21, + "KeyG", // = 0x22, + "KeyH", // = 0x23, + "KeyJ", // = 0x24, + "KeyK", // = 0x25, + "KeyL", // = 0x26, + "KeySemicolon", // = 0x27, + "KeyApostrophe", // = 0x28, + "KeyBacktick", // = 0x29, + "KeyLeftShift", // = 0x2a, + "KeyBackslash", // = 0x2b, + "KeyZ", // = 0x2c, + "KeyX", // = 0x2d, + "KeyC", // = 0x2e, + "KeyV", // = 0x2f, + + "KeyB", // = 0x30, + "KeyN", // = 0x31, + "KeyM", // = 0x32, + "KeyComma", // = 0x33, + "KeyPeriod", // = 0x34, + "KeySlash", // = 0x35, + "KeyRightShift", // = 0x36, + "KeyKeypadMultiply", // = 0x37, + "KeyLeftAlt", // = 0x38, + "KeySpace", // = 0x39, + "KeyCapsLock", // = 0x3a, + "KeyF1", // = 0x3b, + "KeyF2", // = 0x3c, + "KeyF3", // = 0x3d, + "KeyF4", // = 0x3e, + "KeyF5", // = 0x3f, + + "KeyF6", // = 0x40, + "KeyF7", // = 0x41, + "KeyF8", // = 0x42, + "KeyF9", // = 0x43, + "KeyF10", // = 0x44, + "KeyNumLock", // = 0x45, + "KeyScrollLock", // = 0x46, + "KeyKeypad7", // = 0x47, + "KeyKeypad8", // = 0x48, + "KeyKeypad9", // = 0x49, + "KeyKeypadMinus", // = 0x4a, + "KeyKeypad4", // = 0x4b, + "KeyKeypad5", // = 0x4c, + "KeyKeypad6", // = 0x4d, + "KeyKeypadPlus", // = 0x4e, + "KeyKeypad1", // = 0x4f, + + "KeyKeypad2", // = 0x50, + "KeyKeypad3", // = 0x51, + "KeyKeypad0", // = 0x52, + "KeyKeypadPeriod", // = 0x53, + "", + "", + "", + "KeyF11", // = 0x57, + "KeyF12", // = 0x58, + "", + "", + "", + "", + "", + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + + "KeyMediaPrev", // = 0x90, + "", + "", + "", + "", + "", + "", + "", + "", + "KeyMediaNext", // = 0x99, + "", + "", + "KeyKeypadEnter", // = 0x9c, + "KeyRightControl", // = 0x9d, + "", + "", + + "KeyMediaMute", // = 0xa0, + "KeyMediaCalculator", // = 0xa1, + "KeyMediaPlay", // = 0xa2, + "", + "KeyMediaStop", // = 0xa4, + "", + "", + "", + "", + "", + "", + "", + "", + "", + "KeyMediaVolumeDown", // = 0xae, + "", + + "KeyMediaVolumeUp", // = 0xb0, + "", + "KeyMediaWww", // = 0xb2, + "", + "", + "KeyKeypadDivide", // = 0xb5, + "", + "", + "KeyRightAlt", // = 0xb8, + "", + "", + "", + "", + "", + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "KeyHome", // = 0xc7, + "KeyUp", // = 0xc8, + "KeyPageUp", // = 0xc9, + "", + "KeyLeft", // = 0xcb, + "", + "KeyRight", // = 0xcd, + "", + "KeyEnd", // = 0xcf, + + "KeyDown", // = 0xd0, + "KeyPageDown", // = 0xd1, + "KeyInsert", // = 0xd2, + "KeyDelete", // = 0xd3, + "", + "", + "", + "", + "", + "", + "", + "KeyLeftGui", // = 0xdb, + "KeyRightGui", // = 0xdc, + "KeyApps", // = 0xdd, + "KeyAcpiPower", // = 0xde, + "KeyAcpiSleep", // = 0xdf, + + "", + "", + "", + "KeyAcpiWake", // = 0xe3, + "", + "KeyMediaWwwSearch", // = 0xe5, + "KeyMediaWwwFavorites", // = 0xe6, + "KeyMediaWwwRefresh", // = 0xe7, + "KeyMediaWwwStop", // = 0xe8, + "KeyMediaWwwForward", // = 0xe9, + "KeyMediaWwwBack", // = 0xea, + "KeyMediaMyComputer", // = 0xeb, + "KeyMediaEmail", // = 0xec, + "KeyMediaSelect", // = 0xed, + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "KeyPrint", // = 0xfe, + "KeyPause", // = 0xff, +}; + +void keyboardIsr() { + keyboard.Isr(); +} + +Keyboard::Keyboard() { + // TODO +} + +Keyboard::~Keyboard() { + // TODO +} + +Keyboard::KeyHandleFunction Keyboard::setKeyUpHandler(KeyHandleFunction handler) { + auto oldHandler = _keyUpHandler; + _keyUpHandler = handler; + return oldHandler; +} + +Keyboard::KeyHandleFunction Keyboard::setKeyDownHandler(KeyHandleFunction handler) { + auto oldHandler = _keyDownHandler; + _keyDownHandler = handler; + return oldHandler; +} + +Keyboard::KeyHandleFunction Keyboard::setKeyRepeatHandler(KeyHandleFunction handler) { + auto oldHandler = _keyRepeatHandler; + _keyRepeatHandler = handler; + return oldHandler; +} + +const char *Keyboard::getKeyName(unsigned char keyCode) { + return keyNames[keyCode]; +} + +void Keyboard::Isr() { + // TODO +} + +void Keyboard::keyDown(SDL_Keycode keyCode) { + auto c = mapSdlKeycode(keyCode); + if (keyState[c]) { + if (_keyRepeatHandler) _keyRepeatHandler(c); + } else { + keyState[c] = true; + if (_keyDownHandler) _keyDownHandler(c); + } +} + +void Keyboard::keyUp(SDL_Keycode keyCode) { + auto c = mapSdlKeycode(keyCode); + keyState[c] = false; + if (_keyUpHandler) _keyUpHandler(c); +} diff --git a/system/sdl/Keyboard.h b/system/sdl/Keyboard.h new file mode 100644 index 0000000..3085602 --- /dev/null +++ b/system/sdl/Keyboard.h @@ -0,0 +1,178 @@ +#ifndef GAME_KEYBOARD_H +#define GAME_KEYBOARD_H + +#include + +#define KEY_MAX 256 + +enum { + KeyUnknown = 0x00, + KeyEscape = 0x01, + Key1 = 0x02, + Key2 = 0x03, + Key3 = 0x04, + Key4 = 0x05, + Key5 = 0x06, + Key6 = 0x07, + Key7 = 0x08, + Key8 = 0x09, + Key9 = 0x0a, + Key0 = 0x0b, + KeyMinus = 0x0c, + KeyEqual = 0x0d, + KeyBackspace = 0x0e, + KeyTab = 0x0f, + KeyQ = 0x10, + KeyW = 0x11, + KeyE = 0x12, + KeyR = 0x13, + KeyT = 0x14, + KeyY = 0x15, + KeyU = 0x16, + KeyI = 0x17, + KeyO = 0x18, + KeyP = 0x19, + KeyBracketLeft = 0x1a, + KeyBracketRight = 0x1b, + KeyEnter = 0x1c, + KeyLeftCtrl = 0x1d, + KeyA = 0x1e, + KeyS = 0x1f, + KeyD = 0x20, + KeyF = 0x21, + KeyG = 0x22, + KeyH = 0x23, + KeyJ = 0x24, + KeyK = 0x25, + KeyL = 0x26, + KeySemicolon = 0x27, + KeyApostrophe = 0x28, + KeyBacktick = 0x29, + KeyLeftShift = 0x2a, + KeyBackslash = 0x2b, + KeyZ = 0x2c, + KeyX = 0x2d, + KeyC = 0x2e, + KeyV = 0x2f, + KeyB = 0x30, + KeyN = 0x31, + KeyM = 0x32, + KeyComma = 0x33, + KeyPeriod = 0x34, + KeySlash = 0x35, + KeyRightShift = 0x36, + KeyKeypadMultiply = 0x37, + KeyLeftAlt = 0x38, + KeySpace = 0x39, + KeyCapsLock = 0x3a, + KeyF1 = 0x3b, + KeyF2 = 0x3c, + KeyF3 = 0x3d, + KeyF4 = 0x3e, + KeyF5 = 0x3f, + KeyF6 = 0x40, + KeyF7 = 0x41, + KeyF8 = 0x42, + KeyF9 = 0x43, + KeyF10 = 0x44, + KeyNumLock = 0x45, + KeyScrollLock = 0x46, + KeyKeypad7 = 0x47, + KeyKeypad8 = 0x48, + KeyKeypad9 = 0x49, + KeyKeypadMinus = 0x4a, + KeyKeypad4 = 0x4b, + KeyKeypad5 = 0x4c, + KeyKeypad6 = 0x4d, + KeyKeypadPlus = 0x4e, + KeyKeypad1 = 0x4f, + KeyKeypad2 = 0x50, + KeyKeypad3 = 0x51, + KeyKeypad0 = 0x52, + KeyKeypadPeriod = 0x53, + KeyF11 = 0x57, + KeyF12 = 0x58, + KeyMediaPrev = 0x90, + KeyMediaNext = 0x99, + KeyKeypadEnter = 0x9c, + KeyRightControl = 0x9d, + KeyMediaMute = 0xa0, + KeyMediaCalculator = 0xa1, + KeyMediaPlay = 0xa2, + KeyMediaStop = 0xa4, + KeyMediaVolumeDown = 0xae, + KeyMediaVolumeUp = 0xb0, + KeyMediaWww = 0xb2, + KeyKeypadDivide = 0xb5, + KeyRightAlt = 0xb8, + KeyHome = 0xc7, + KeyUp = 0xc8, + KeyPageUp = 0xc9, + KeyLeft = 0xcb, + KeyRight = 0xcd, + KeyEnd = 0xcf, + KeyDown = 0xd0, + KeyPageDown = 0xd1, + KeyInsert = 0xd2, + KeyDelete = 0xd3, + KeyLeftGui = 0xdb, + KeyRightGui = 0xdc, + KeyApps = 0xdd, + KeyAcpiPower = 0xde, + KeyAcpiSleep = 0xdf, + KeyAcpiWake = 0xe3, + KeyMediaWwwSearch = 0xe5, + KeyMediaWwwFavorites = 0xe6, + KeyMediaWwwRefresh = 0xe7, + KeyMediaWwwStop = 0xe8, + KeyMediaWwwForward = 0xe9, + KeyMediaWwwBack = 0xea, + KeyMediaMyComputer = 0xeb, + KeyMediaEmail = 0xec, + KeyMediaSelect = 0xed, + KeyPrint = 0xfe, + KeyPause = 0xff, +}; + +class Keyboard { +public: + using KeyHandleFunction = void (*)(unsigned char); + + Keyboard(); + ~Keyboard(); + + KeyHandleFunction setKeyUpHandler(KeyHandleFunction handler); + KeyHandleFunction setKeyDownHandler(KeyHandleFunction handler); + KeyHandleFunction setKeyRepeatHandler(KeyHandleFunction handler); + + bool keyState[KEY_MAX]{}; + + const char *getKeyName(unsigned char keyCode); +private: + friend class Events; + + friend void keyboardIsr(); + void Isr(); + enum State { + Start, + Extended, + PauseBegin, + PausePressed1, + PauseReleased1, + PrintPressed1, + PrintPressed2, + PrintReleased1, + PrintReleased2, + }; + State _state; + KeyHandleFunction _keyDownHandler{nullptr}, _keyUpHandler{nullptr}, _keyRepeatHandler{nullptr}; + + void keyDown(SDL_Keycode c); + + void keyUp(SDL_Keycode c); + +}; + +extern Keyboard keyboard; + +#endif //GAME_KEYBOARD_H diff --git a/system/sdl/Opl.cpp b/system/sdl/Opl.cpp new file mode 100644 index 0000000..c0b814e --- /dev/null +++ b/system/sdl/Opl.cpp @@ -0,0 +1,1348 @@ +#include + +#include "../Opl.h" + +/* + + The Opal OPL3 emulator. + + Note: this is not a complete emulator, just enough for Reality Adlib Tracker tunes. + + Missing features compared to a real OPL3: + + - Timers/interrupts + - OPL3 enable bit (it defaults to always on) + - CSW mode + - Test register + - Percussion mode + +*/ + +#include + +//================================================================================================== +// Opal class. +//================================================================================================== +class Opal { + + class Channel; + + // Various constants + enum { + OPL3SampleRate = 49716, + NumChannels = 18, + NumOperators = 36, + + EnvOff = -1, + EnvAtt, + EnvDec, + EnvSus, + EnvRel, + }; + + // A single FM operator + class Operator { + + public: + Operator(); + void SetMaster(Opal *opal) { Master = opal; } + void SetChannel(Channel *chan) { Chan = chan; } + + int16_t Output(uint16_t keyscalenum, uint32_t phase_step, int16_t vibrato, int16_t mod = 0, int16_t fbshift = 0); + + void SetKeyOn(bool on); + void SetTremoloEnable(bool on); + void SetVibratoEnable(bool on); + void SetSustainMode(bool on); + void SetEnvelopeScaling(bool on); + void SetFrequencyMultiplier(uint16_t scale); + void SetKeyScale(uint16_t scale); + void SetOutputLevel(uint16_t level); + void SetAttackRate(uint16_t rate); + void SetDecayRate(uint16_t rate); + void SetSustainLevel(uint16_t level); + void SetReleaseRate(uint16_t rate); + void SetWaveform(uint16_t wave); + + void ComputeRates(); + void ComputeKeyScaleLevel(); + + protected: + Opal * Master; // Master object + Channel * Chan; // Owning channel + uint32_t Phase; // The current offset in the selected waveform + uint16_t Waveform; // The waveform id this operator is using + uint16_t FreqMultTimes2; // Frequency multiplier * 2 + int EnvelopeStage; // Which stage the envelope is at (see Env* enums above) + int16_t EnvelopeLevel; // 0 - $1FF, 0 being the loudest + uint16_t OutputLevel; // 0 - $FF + uint16_t AttackRate; + uint16_t DecayRate; + uint16_t SustainLevel; + uint16_t ReleaseRate; + uint16_t AttackShift; + uint16_t AttackMask; + uint16_t AttackAdd; + const uint16_t *AttackTab; + uint16_t DecayShift; + uint16_t DecayMask; + uint16_t DecayAdd; + const uint16_t *DecayTab; + uint16_t ReleaseShift; + uint16_t ReleaseMask; + uint16_t ReleaseAdd; + const uint16_t *ReleaseTab; + uint16_t KeyScaleShift; + uint16_t KeyScaleLevel; + int16_t Out[2]; + bool KeyOn; + bool KeyScaleRate; // Affects envelope rate scaling + bool SustainMode; // Whether to sustain during the sustain phase, or release instead + bool TremoloEnable; + bool VibratoEnable; + }; + + // A single channel, which can contain two or more operators + class Channel { + + public: + Channel(); + void SetMaster(Opal *opal) { Master = opal; } + void SetOperators(Operator *a, Operator *b, Operator *c, Operator *d) { + Op[0] = a; + Op[1] = b; + Op[2] = c; + Op[3] = d; + if (a) + a->SetChannel(this); + if (b) + b->SetChannel(this); + if (c) + c->SetChannel(this); + if (d) + d->SetChannel(this); + } + + void Output(int16_t &left, int16_t &right); + void SetEnable(bool on) { Enable = on; } + void SetChannelPair(Channel *pair) { ChannelPair = pair; } + + void SetFrequencyLow(uint16_t freq); + void SetFrequencyHigh(uint16_t freq); + void SetKeyOn(bool on); + void SetOctave(uint16_t oct); + void SetLeftEnable(bool on); + void SetRightEnable(bool on); + void SetFeedback(uint16_t val); + void SetModulationType(uint16_t type); + + uint16_t GetFreq() const { return Freq; } + uint16_t GetOctave() const { return Octave; } + uint16_t GetKeyScaleNumber() const { return KeyScaleNumber; } + uint16_t GetModulationType() const { return ModulationType; } + + void ComputeKeyScaleNumber(); + + protected: + void ComputePhaseStep(); + + Operator * Op[4]; + + Opal * Master; // Master object + uint16_t Freq; // Frequency; actually it's a phase stepping value + uint16_t Octave; // Also known as "block" in Yamaha parlance + uint32_t PhaseStep; + uint16_t KeyScaleNumber; + uint16_t FeedbackShift; + uint16_t ModulationType; + Channel * ChannelPair; + bool Enable; + bool LeftEnable, RightEnable; + }; + + public: + Opal(int sample_rate); + ~Opal(); + + void SetSampleRate(int sample_rate); + void Port(uint16_t reg_num, uint8_t val); + void Sample(int16_t *left, int16_t *right); + + protected: + void Init(int sample_rate); + void Output(int16_t &left, int16_t &right); + + int32_t SampleRate; + int32_t SampleAccum; + int16_t LastOutput[2], CurrOutput[2]; + Channel Chan[NumChannels]; + Operator Op[NumOperators]; +// uint16_t ExpTable[256]; +// uint16_t LogSinTable[256]; + uint16_t Clock; + uint16_t TremoloClock; + uint16_t TremoloLevel; + uint16_t VibratoTick; + uint16_t VibratoClock; + bool NoteSel; + bool TremoloDepth; + bool VibratoDepth; + + static const uint16_t RateTables[4][8]; + static const uint16_t ExpTable[256]; + static const uint16_t LogSinTable[256]; +}; +//-------------------------------------------------------------------------------------------------- +const uint16_t Opal::RateTables[4][8] = { + { 1, 0, 1, 0, 1, 0, 1, 0 }, + { 1, 0, 1, 0, 0, 0, 1, 0 }, + { 1, 0, 0, 0, 1, 0, 0, 0 }, + { 1, 0, 0, 0, 0, 0, 0, 0 }, +}; +//-------------------------------------------------------------------------------------------------- +const uint16_t Opal::ExpTable[0x100] = { + 1018, 1013, 1007, 1002, 996, 991, 986, 980, 975, 969, 964, 959, 953, 948, 942, 937, + 932, 927, 921, 916, 911, 906, 900, 895, 890, 885, 880, 874, 869, 864, 859, 854, + 849, 844, 839, 834, 829, 824, 819, 814, 809, 804, 799, 794, 789, 784, 779, 774, + 770, 765, 760, 755, 750, 745, 741, 736, 731, 726, 722, 717, 712, 708, 703, 698, + 693, 689, 684, 680, 675, 670, 666, 661, 657, 652, 648, 643, 639, 634, 630, 625, + 621, 616, 612, 607, 603, 599, 594, 590, 585, 581, 577, 572, 568, 564, 560, 555, + 551, 547, 542, 538, 534, 530, 526, 521, 517, 513, 509, 505, 501, 496, 492, 488, + 484, 480, 476, 472, 468, 464, 460, 456, 452, 448, 444, 440, 436, 432, 428, 424, + 420, 416, 412, 409, 405, 401, 397, 393, 389, 385, 382, 378, 374, 370, 367, 363, + 359, 355, 352, 348, 344, 340, 337, 333, 329, 326, 322, 318, 315, 311, 308, 304, + 300, 297, 293, 290, 286, 283, 279, 276, 272, 268, 265, 262, 258, 255, 251, 248, + 244, 241, 237, 234, 231, 227, 224, 220, 217, 214, 210, 207, 204, 200, 197, 194, + 190, 187, 184, 181, 177, 174, 171, 168, 164, 161, 158, 155, 152, 148, 145, 142, + 139, 136, 133, 130, 126, 123, 120, 117, 114, 111, 108, 105, 102, 99, 96, 93, + 90, 87, 84, 81, 78, 75, 72, 69, 66, 63, 60, 57, 54, 51, 48, 45, + 42, 40, 37, 34, 31, 28, 25, 22, 20, 17, 14, 11, 8, 6, 3, 0, +}; +//-------------------------------------------------------------------------------------------------- +const uint16_t Opal::LogSinTable[0x100] = { + 2137, 1731, 1543, 1419, 1326, 1252, 1190, 1137, 1091, 1050, 1013, 979, 949, 920, 894, 869, + 846, 825, 804, 785, 767, 749, 732, 717, 701, 687, 672, 659, 646, 633, 621, 609, + 598, 587, 576, 566, 556, 546, 536, 527, 518, 509, 501, 492, 484, 476, 468, 461, + 453, 446, 439, 432, 425, 418, 411, 405, 399, 392, 386, 380, 375, 369, 363, 358, + 352, 347, 341, 336, 331, 326, 321, 316, 311, 307, 302, 297, 293, 289, 284, 280, + 276, 271, 267, 263, 259, 255, 251, 248, 244, 240, 236, 233, 229, 226, 222, 219, + 215, 212, 209, 205, 202, 199, 196, 193, 190, 187, 184, 181, 178, 175, 172, 169, + 167, 164, 161, 159, 156, 153, 151, 148, 146, 143, 141, 138, 136, 134, 131, 129, + 127, 125, 122, 120, 118, 116, 114, 112, 110, 108, 106, 104, 102, 100, 98, 96, + 94, 92, 91, 89, 87, 85, 83, 82, 80, 78, 77, 75, 74, 72, 70, 69, + 67, 66, 64, 63, 62, 60, 59, 57, 56, 55, 53, 52, 51, 49, 48, 47, + 46, 45, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, + 29, 28, 27, 26, 25, 24, 23, 23, 22, 21, 20, 20, 19, 18, 17, 17, + 16, 15, 15, 14, 13, 13, 12, 12, 11, 10, 10, 9, 9, 8, 8, 7, + 7, 7, 6, 6, 5, 5, 5, 4, 4, 4, 3, 3, 3, 2, 2, 2, + 2, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, +}; + + + +//================================================================================================== +// This is the temporary code for generating the above tables. Maths and data from this nice +// reverse-engineering effort: +// +// https://docs.google.com/document/d/18IGx18NQY_Q1PJVZ-bHywao9bhsDoAqoIn1rIm42nwo/edit +//================================================================================================== +#if 0 +#include + +void GenerateTables() { + + // Build the exponentiation table (reversed from the official OPL3 ROM) + FILE *fd = fopen("exptab.txt", "wb"); + if (fd) { + for (int i = 0; i < 0x100; i++) { + int v = (pow(2, (0xFF - i) / 256.0) - 1) * 1024 + 0.5; + if (i & 15) + fprintf(fd, " %4d,", v); + else + fprintf(fd, "\n\t%4d,", v); + } + fclose(fd); + } + + // Build the log-sin table + fd = fopen("sintab.txt", "wb"); + if (fd) { + for (int i = 0; i < 0x100; i++) { + int v = -log(sin((i + 0.5) * 3.1415926535897933 / 256 / 2)) / log(2) * 256 + 0.5; + if (i & 15) + fprintf(fd, " %4d,", v); + else + fprintf(fd, "\n\t%4d,", v); + } + fclose(fd); + } +} +#endif + + + +//================================================================================================== +// Constructor/destructor. +//================================================================================================== +Opal::Opal(int sample_rate) { + + Init(sample_rate); +} +//-------------------------------------------------------------------------------------------------- +Opal::~Opal() { +} + + + +//================================================================================================== +// Initialise the emulation. +//================================================================================================== +void Opal::Init(int sample_rate) { + + Clock = 0; + TremoloClock = 0; + VibratoTick = 0; + VibratoClock = 0; + NoteSel = false; + TremoloDepth = false; + VibratoDepth = false; + +// // Build the exponentiation table (reversed from the official OPL3 ROM) +// for (int i = 0; i < 0x100; i++) +// ExpTable[i] = (pow(2, (0xFF - i) / 256.0) - 1) * 1024 + 0.5; +// +// // Build the log-sin table +// for (int i = 0; i < 0x100; i++) +// LogSinTable[i] = -log(sin((i + 0.5) * 3.1415926535897933 / 256 / 2)) / log(2) * 256 + 0.5; + + // Let sub-objects know where to find us + for (int i = 0; i < NumOperators; i++) + Op[i].SetMaster(this); + + for (int i = 0; i < NumChannels; i++) + Chan[i].SetMaster(this); + + // Add the operators to the channels. Note, some channels can't use all the operators + // FIXME: put this into a separate routine + const int chan_ops[] = { + 0, 1, 2, 6, 7, 8, 12, 13, 14, 18, 19, 20, 24, 25, 26, 30, 31, 32, + }; + + for (int i = 0; i < NumChannels; i++) { + Channel *chan = &Chan[i]; + int op = chan_ops[i]; + if (i < 3 || (i >= 9 && i < 12)) + chan->SetOperators(&Op[op], &Op[op + 3], &Op[op + 6], &Op[op + 9]); + else + chan->SetOperators(&Op[op], &Op[op + 3], 0, 0); + } + + // Initialise the operator rate data. We can't do this in the Operator constructor as it + // relies on referencing the master and channel objects + for (int i = 0; i < NumOperators; i++) + Op[i].ComputeRates(); + + SetSampleRate(sample_rate); +} + + + +//================================================================================================== +// Change the sample rate. +//================================================================================================== +void Opal::SetSampleRate(int sample_rate) { + + // Sanity + if (sample_rate == 0) + sample_rate = OPL3SampleRate; + + SampleRate = sample_rate; + SampleAccum = 0; + LastOutput[0] = LastOutput[1] = 0; + CurrOutput[0] = CurrOutput[1] = 0; +} + + + +//================================================================================================== +// Write a value to an OPL3 register. +//================================================================================================== +void Opal::Port(uint16_t reg_num, uint8_t val) { + + const int op_lookup[] = { + // 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F + 0, 1, 2, 3, 4, 5, -1, -1, 6, 7, 8, 9, 10, 11, -1, -1, + // 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F + 12, 13, 14, 15, 16, 17, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + + uint16_t type = reg_num & 0xE0; + + // Is it BD, the one-off register stuck in the middle of the register array? + if (reg_num == 0xBD) { + TremoloDepth = (val & 0x80); + VibratoDepth = (val & 0x40); + return; + } + + // Global registers + if (type == 0x00) { + + // 4-OP enables + if (reg_num == 0x104) { + + // Enable/disable channels based on which 4-op enables + uint8_t mask = 1; + for (int i = 0; i < 6; i++, mask <<= 1) { + + // The 4-op channels are 0, 1, 2, 9, 10, 11 + uint16_t chan = i < 3 ? i : i + 6; + Channel *primary = &Chan[chan]; + Channel *secondary = &Chan[chan + 3]; + + if (val & mask) { + + // Let primary channel know it's controlling the secondary channel + primary->SetChannelPair(secondary); + + // Turn off the second channel in the pair + secondary->SetEnable(false); + + } else { + + // Let primary channel know it's no longer controlling the secondary channel + primary->SetChannelPair(0); + + // Turn on the second channel in the pair + secondary->SetEnable(true); + } + } + + // CSW / Note-sel + } else if (reg_num == 0x08) { + + NoteSel = (val & 0x40); + + // Get the channels to recompute the Key Scale No. as this varies based on NoteSel + for (int i = 0; i < NumChannels; i++) + Chan[i].ComputeKeyScaleNumber(); + } + + // Channel registers + } else if (type >= 0xA0 && type <= 0xC0) { + + // Convert to channel number + int chan_num = reg_num & 15; + + // Valid channel? + if (chan_num >= 9) + return; + + // Is it the other bank of channels? + if (reg_num & 0x100) + chan_num += 9; + + Channel &chan = Chan[chan_num]; + + // Do specific registers + switch (reg_num & 0xF0) { + + // Frequency low + case 0xA0: { + chan.SetFrequencyLow(val); + break; + } + + // Key-on / Octave / Frequency High + case 0xB0: { + chan.SetKeyOn(val & 0x20); + chan.SetOctave(val >> 2 & 7); + chan.SetFrequencyHigh(val & 3); + break; + } + + // Right Stereo Channel Enable / Left Stereo Channel Enable / Feedback Factor / Modulation Type + case 0xC0: { + chan.SetRightEnable(val & 0x20); + chan.SetLeftEnable(val & 0x10); + chan.SetFeedback(val >> 1 & 7); + chan.SetModulationType(val & 1); + break; + } + } + + // Operator registers + } else if ((type >= 0x20 && type <= 0x80) || type == 0xE0) { + + // Convert to operator number + int op_num = op_lookup[reg_num & 0x1F]; + + // Valid register? + if (op_num < 0) + return; + + // Is it the other bank of operators? + if (reg_num & 0x100) + op_num += 18; + + Operator &op = Op[op_num]; + + // Do specific registers + switch (type) { + + // Tremolo Enable / Vibrato Enable / Sustain Mode / Envelope Scaling / Frequency Multiplier + case 0x20: { + op.SetTremoloEnable(val & 0x80); + op.SetVibratoEnable(val & 0x40); + op.SetSustainMode(val & 0x20); + op.SetEnvelopeScaling(val & 0x10); + op.SetFrequencyMultiplier(val & 15); + break; + } + + // Key Scale / Output Level + case 0x40: { + op.SetKeyScale(val >> 6); + op.SetOutputLevel(val & 0x3F); + break; + } + + // Attack Rate / Decay Rate + case 0x60: { + op.SetAttackRate(val >> 4); + op.SetDecayRate(val & 15); + break; + } + + // Sustain Level / Release Rate + case 0x80: { + op.SetSustainLevel(val >> 4); + op.SetReleaseRate(val & 15); + break; + } + + // Waveform + case 0xE0: { + op.SetWaveform(val & 7); + break; + } + } + } +} + + + +//================================================================================================== +// Generate sample. Every time you call this you will get two signed 16-bit samples (one for each +// stereo channel) which will sound correct when played back at the sample rate given when the +// class was constructed. +//================================================================================================== +void Opal::Sample(int16_t *left, int16_t *right) { + + // If the destination sample rate is higher than the OPL3 sample rate, we need to skip ahead + while (SampleAccum >= SampleRate) { + + LastOutput[0] = CurrOutput[0]; + LastOutput[1] = CurrOutput[1]; + + Output(CurrOutput[0], CurrOutput[1]); + + SampleAccum -= SampleRate; + } + + // Mix with the partial accumulation + int32_t omblend = SampleRate - SampleAccum; + *left = (LastOutput[0] * omblend + CurrOutput[0] * SampleAccum) / SampleRate; + *right = (LastOutput[1] * omblend + CurrOutput[1] * SampleAccum) / SampleRate; + + SampleAccum += OPL3SampleRate; +} + + + +//================================================================================================== +// Produce final output from the chip. This is at the OPL3 sample-rate. +//================================================================================================== +void Opal::Output(int16_t &left, int16_t &right) { + + int32_t leftmix = 0, rightmix = 0; + + // Sum the output of each channel + for (int i = 0; i < NumChannels; i++) { + + int16_t chanleft, chanright; + Chan[i].Output(chanleft, chanright); + + leftmix += chanleft; + rightmix += chanright; + } + + // Clamp + if (leftmix < -0x8000) + left = -0x8000; + else if (leftmix > 0x7FFF) + left = 0x7FFF; + else + left = leftmix; + + if (rightmix < -0x8000) + right = -0x8000; + else if (rightmix > 0x7FFF) + right = 0x7FFF; + else + right = rightmix; + + Clock++; + + // Tremolo. According to this post, the OPL3 tremolo is a 13,440 sample length triangle wave + // with a peak at 26 and a trough at 0 and is simply added to the logarithmic level accumulator + // http://forums.submarine.org.uk/phpBB/viewtopic.php?f=9&t=1171 + TremoloClock = (TremoloClock + 1) % 13440; + TremoloLevel = ((TremoloClock < 13440 / 2) ? TremoloClock : 13440 - TremoloClock) / 256; + if (!TremoloDepth) + TremoloLevel >>= 2; + + // Vibrato. This appears to be a 8 sample long triangle wave with a magnitude of the three + // high bits of the channel frequency, positive and negative, divided by two if the vibrato + // depth is zero. It is only cycled every 1,024 samples. + VibratoTick++; + if (VibratoTick >= 1024) { + VibratoTick = 0; + VibratoClock = (VibratoClock + 1) & 7; + } +} + + + +//================================================================================================== +// Channel constructor. +//================================================================================================== +Opal::Channel::Channel() { + + Master = 0; + Freq = 0; + Octave = 0; + PhaseStep = 0; + KeyScaleNumber = 0; + FeedbackShift = 0; + ModulationType = 0; + ChannelPair = 0; + Enable = true; +} + + + +//================================================================================================== +// Produce output from channel. +//================================================================================================== +void Opal::Channel::Output(int16_t &left, int16_t &right) { + + // Has the channel been disabled? This is usually a result of the 4-op enables being used to + // disable the secondary channel in each 4-op pair + if (!Enable) { + left = right = 0; + return; + } + + int16_t vibrato = (Freq >> 7) & 7; + if (!Master->VibratoDepth) + vibrato >>= 1; + + // 0 3 7 3 0 -3 -7 -3 + uint16_t clk = Master->VibratoClock; + if (!(clk & 3)) + vibrato = 0; // Position 0 and 4 is zero + else { + if (clk & 1) + vibrato >>= 1; // Odd positions are half the magnitude + if (clk & 4) + vibrato = -vibrato; // The second half positions are negative + } + + vibrato <<= Octave; + + // Combine individual operator outputs + int16_t out, acc; + + // Running in 4-op mode? + if (ChannelPair) { + + // Get the secondary channel's modulation type. This is the only thing from the secondary + // channel that is used + if (ChannelPair->GetModulationType() == 0) { + + if (ModulationType == 0) { + + // feedback -> modulator -> modulator -> modulator -> carrier + out = Op[0]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, FeedbackShift); + out = Op[1]->Output(KeyScaleNumber, PhaseStep, vibrato, out, 0); + out = Op[2]->Output(KeyScaleNumber, PhaseStep, vibrato, out, 0); + out = Op[3]->Output(KeyScaleNumber, PhaseStep, vibrato, out, 0); + + } else { + + // (feedback -> carrier) + (modulator -> modulator -> carrier) + out = Op[0]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, FeedbackShift); + acc = Op[1]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, 0); + acc = Op[2]->Output(KeyScaleNumber, PhaseStep, vibrato, acc, 0); + out += Op[3]->Output(KeyScaleNumber, PhaseStep, vibrato, acc, 0); + } + + } else { + + if (ModulationType == 0) { + + // (feedback -> modulator -> carrier) + (modulator -> carrier) + out = Op[0]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, FeedbackShift); + out = Op[1]->Output(KeyScaleNumber, PhaseStep, vibrato, out, 0); + acc = Op[2]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, 0); + out += Op[3]->Output(KeyScaleNumber, PhaseStep, vibrato, acc, 0); + + } else { + + // (feedback -> carrier) + (modulator -> carrier) + carrier + out = Op[0]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, FeedbackShift); + acc = Op[1]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, 0); + out += Op[2]->Output(KeyScaleNumber, PhaseStep, vibrato, acc, 0); + out += Op[3]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, 0); + } + } + + } else { + + // Standard 2-op mode + if (ModulationType == 0) { + + // Frequency modulation (well, phase modulation technically) + out = Op[0]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, FeedbackShift); + out = Op[1]->Output(KeyScaleNumber, PhaseStep, vibrato, out, 0); + + } else { + + // Additive + out = Op[0]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, FeedbackShift); + out += Op[1]->Output(KeyScaleNumber, PhaseStep, vibrato); + } + } + + left = LeftEnable ? out : 0; + right = RightEnable ? out : 0; +} + + + +//================================================================================================== +// Set phase step for operators using this channel. +//================================================================================================== +void Opal::Channel::SetFrequencyLow(uint16_t freq) { + + Freq = (Freq & 0x300) | (freq & 0xFF); + ComputePhaseStep(); +} +//-------------------------------------------------------------------------------------------------- +void Opal::Channel::SetFrequencyHigh(uint16_t freq) { + + Freq = (Freq & 0xFF) | ((freq & 3) << 8); + ComputePhaseStep(); + + // Only the high bits of Freq affect the Key Scale No. + ComputeKeyScaleNumber(); +} + + + +//================================================================================================== +// Set the octave of the channel (0 to 7). +//================================================================================================== +void Opal::Channel::SetOctave(uint16_t oct) { + + Octave = oct & 7; + ComputePhaseStep(); + ComputeKeyScaleNumber(); +} + + + +//================================================================================================== +// Keys the channel on/off. +//================================================================================================== +void Opal::Channel::SetKeyOn(bool on) { + + Op[0]->SetKeyOn(on); + Op[1]->SetKeyOn(on); +} + + + +//================================================================================================== +// Enable left stereo channel. +//================================================================================================== +void Opal::Channel::SetLeftEnable(bool on) { + + LeftEnable = on; +} + + + +//================================================================================================== +// Enable right stereo channel. +//================================================================================================== +void Opal::Channel::SetRightEnable(bool on) { + + RightEnable = on; +} + + + +//================================================================================================== +// Set the channel feedback amount. +//================================================================================================== +void Opal::Channel::SetFeedback(uint16_t val) { + + FeedbackShift = val ? 9 - val : 0; +} + + + +//================================================================================================== +// Set frequency modulation/additive modulation +//================================================================================================== +void Opal::Channel::SetModulationType(uint16_t type) { + + ModulationType = type; +} + + + +//================================================================================================== +// Compute the stepping factor for the operator waveform phase based on the frequency and octave +// values of the channel. +//================================================================================================== +void Opal::Channel::ComputePhaseStep() { + + PhaseStep = uint32_t(Freq) << Octave; +} + + + +//================================================================================================== +// Compute the key scale number and key scale levels. +// +// From the Yamaha data sheet this is the block/octave number as bits 3-1, with bit 0 coming from +// the MSB of the frequency if NoteSel is 1, and the 2nd MSB if NoteSel is 0. +//================================================================================================== +void Opal::Channel::ComputeKeyScaleNumber() { + + uint16_t lsb = Master->NoteSel ? Freq >> 9 : (Freq >> 8) & 1; + KeyScaleNumber = Octave << 1 | lsb; + + // Get the channel operators to recompute their rates as they're dependent on this number. They + // also need to recompute their key scale level + for (int i = 0; i < 4; i++) { + + if (!Op[i]) + continue; + + Op[i]->ComputeRates(); + Op[i]->ComputeKeyScaleLevel(); + } +} + + + +//================================================================================================== +// Operator constructor. +//================================================================================================== +Opal::Operator::Operator() { + + Master = 0; + Chan = 0; + Phase = 0; + Waveform = 0; + FreqMultTimes2 = 1; + EnvelopeStage = EnvOff; + EnvelopeLevel = 0x1FF; + AttackRate = 0; + DecayRate = 0; + SustainLevel = 0; + ReleaseRate = 0; + KeyScaleShift = 0; + KeyScaleLevel = 0; + Out[0] = Out[1] = 0; + KeyOn = false; + KeyScaleRate = false; + SustainMode = false; + TremoloEnable = false; + VibratoEnable = false; +} + + + +//================================================================================================== +// Produce output from operator. +//================================================================================================== +int16_t Opal::Operator::Output(uint16_t keyscalenum, uint32_t phase_step, int16_t vibrato, int16_t mod, int16_t fbshift) { + + // Advance wave phase + if (VibratoEnable) + phase_step += vibrato; + Phase += (phase_step * FreqMultTimes2) / 2; + + uint16_t level = (EnvelopeLevel + OutputLevel + KeyScaleLevel + (TremoloEnable ? Master->TremoloLevel : 0)) << 3; + + switch (EnvelopeStage) { + + // Attack stage + case EnvAtt: { + if (AttackRate == 0) + break; + if (AttackMask && (Master->Clock & AttackMask)) + break; + uint16_t add = ((AttackAdd >> AttackTab[Master->Clock >> AttackShift & 7]) * ~EnvelopeLevel) >> 3; + EnvelopeLevel += add; + if (EnvelopeLevel <= 0) { + EnvelopeLevel = 0; + EnvelopeStage = EnvDec; + } + break; + } + + // Decay stage + case EnvDec: { + if (DecayRate == 0) + break; + if (DecayMask && (Master->Clock & DecayMask)) + break; + uint16_t add = DecayAdd >> DecayTab[Master->Clock >> DecayShift & 7]; + EnvelopeLevel += add; + if (EnvelopeLevel >= SustainLevel) { + EnvelopeLevel = SustainLevel; + EnvelopeStage = EnvSus; + } + break; + } + + // Sustain stage + case EnvSus: { + + if (SustainMode) + break; + + // Note: fall-through! + } + + // Release stage + case EnvRel: { + if (ReleaseRate == 0) + break; + if (ReleaseMask && (Master->Clock & ReleaseMask)) + break; + uint16_t add = ReleaseAdd >> ReleaseTab[Master->Clock >> ReleaseShift & 7]; + EnvelopeLevel += add; + if (EnvelopeLevel >= 0x1FF) { + EnvelopeLevel = 0x1FF; + EnvelopeStage = EnvOff; + Out[0] = Out[1] = 0; + return 0; + } + break; + } + + // Envelope, and therefore the operator, is not running + default: + Out[0] = Out[1] = 0; + return 0; + } + + // Feedback? In that case we modulate by a blend of the last two samples + if (fbshift) + mod += (Out[0] + Out[1]) >> fbshift; + + uint16_t phase = (Phase >> 10) + mod; + uint16_t offset = phase & 0xFF; + uint16_t logsin; + bool negate = false; + + switch (Waveform) { + + //------------------------------------ + // Standard sine wave + //------------------------------------ + case 0: + if (phase & 0x100) + offset ^= 0xFF; + logsin = Master->LogSinTable[offset]; + negate = (phase & 0x200); + break; + + //------------------------------------ + // Half sine wave + //------------------------------------ + case 1: + if (phase & 0x200) + offset = 0; + else if (phase & 0x100) + offset ^= 0xFF; + logsin = Master->LogSinTable[offset]; + break; + + //------------------------------------ + // Positive sine wave + //------------------------------------ + case 2: + if (phase & 0x100) + offset ^= 0xFF; + logsin = Master->LogSinTable[offset]; + break; + + //------------------------------------ + // Quarter positive sine wave + //------------------------------------ + case 3: + if (phase & 0x100) + offset = 0; + logsin = Master->LogSinTable[offset]; + break; + + //------------------------------------ + // Double-speed sine wave + //------------------------------------ + case 4: + if (phase & 0x200) + offset = 0; + + else { + + if (phase & 0x80) + offset ^= 0xFF; + + offset = (offset + offset) & 0xFF; + negate = (phase & 0x100); + } + + logsin = Master->LogSinTable[offset]; + break; + + //------------------------------------ + // Double-speed positive sine wave + //------------------------------------ + case 5: + if (phase & 0x200) + offset = 0; + + else { + + offset = (offset + offset) & 0xFF; + if (phase & 0x80) + offset ^= 0xFF; + } + + logsin = Master->LogSinTable[offset]; + break; + + //------------------------------------ + // Square wave + //------------------------------------ + case 6: + logsin = 0; + negate = (phase & 0x200); + break; + + //------------------------------------ + // Exponentiation wave + //------------------------------------ + default: + logsin = phase & 0x1FF; + if (phase & 0x200) { + logsin ^= 0x1FF; + negate = true; + } + logsin <<= 3; + break; + } + + uint16_t mix = logsin + level; + if (mix > 0x1FFF) + mix = 0x1FFF; + + // From the OPLx decapsulated docs: + // "When such a table is used for calculation of the exponential, the table is read at the + // position given by the 8 LSB's of the input. The value + 1024 (the hidden bit) is then the + // significand of the floating point output and the yet unused MSB's of the input are the + // exponent of the floating point output." + int16_t v = Master->ExpTable[mix & 0xFF] + 1024; + v >>= mix >> 8; + v += v; + if (negate) + v = ~v; + + // Keep last two results for feedback calculation + Out[1] = Out[0]; + Out[0] = v; + + return v; +} + + + +//================================================================================================== +// Trigger operator. +//================================================================================================== +void Opal::Operator::SetKeyOn(bool on) { + + // Already on/off? + if (KeyOn == on) + return; + KeyOn = on; + + if (on) { + + // The highest attack rate is instant; it bypasses the attack phase + if (AttackRate == 15) { + EnvelopeStage = EnvDec; + EnvelopeLevel = 0; + } else + EnvelopeStage = EnvAtt; + + Phase = 0; + + } else { + + // Stopping current sound? + if (EnvelopeStage != EnvOff && EnvelopeStage != EnvRel) + EnvelopeStage = EnvRel; + } +} + + + +//================================================================================================== +// Enable amplitude vibrato. +//================================================================================================== +void Opal::Operator::SetTremoloEnable(bool on) { + + TremoloEnable = on; +} + + + +//================================================================================================== +// Enable frequency vibrato. +//================================================================================================== +void Opal::Operator::SetVibratoEnable(bool on) { + + VibratoEnable = on; +} + + + +//================================================================================================== +// Sets whether we release or sustain during the sustain phase of the envelope. 'true' is to +// sustain, otherwise release. +//================================================================================================== +void Opal::Operator::SetSustainMode(bool on) { + + SustainMode = on; +} + + + +//================================================================================================== +// Key scale rate. Sets how much the Key Scaling Number affects the envelope rates. +//================================================================================================== +void Opal::Operator::SetEnvelopeScaling(bool on) { + + KeyScaleRate = on; + ComputeRates(); +} + + + +//================================================================================================== +// Multiplies the phase frequency. +//================================================================================================== +void Opal::Operator::SetFrequencyMultiplier(uint16_t scale) { + + // Needs to be multiplied by two (and divided by two later when we use it) because the first + // entry is actually .5 + const uint16_t mul_times_2[] = { + 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 20, 24, 24, 30, 30, + }; + + FreqMultTimes2 = mul_times_2[scale & 15]; +} + + + +//================================================================================================== +// Attenuates output level towards higher pitch. +//================================================================================================== +void Opal::Operator::SetKeyScale(uint16_t scale) { + + if (scale > 0) + KeyScaleShift = 3 - scale; + + // No scaling, ensure it has no effect + else + KeyScaleShift = 15; + + ComputeKeyScaleLevel(); +} + + + +//================================================================================================== +// Sets the output level (volume) of the operator. +//================================================================================================== +void Opal::Operator::SetOutputLevel(uint16_t level) { + + OutputLevel = level * 4; +} + + + +//================================================================================================== +// Operator attack rate. +//================================================================================================== +void Opal::Operator::SetAttackRate(uint16_t rate) { + + AttackRate = rate; + + ComputeRates(); +} + + + +//================================================================================================== +// Operator decay rate. +//================================================================================================== +void Opal::Operator::SetDecayRate(uint16_t rate) { + + DecayRate = rate; + + ComputeRates(); +} + + + +//================================================================================================== +// Operator sustain level. +//================================================================================================== +void Opal::Operator::SetSustainLevel(uint16_t level) { + + SustainLevel = level < 15 ? level : 31; + SustainLevel *= 16; +} + + + +//================================================================================================== +// Operator release rate. +//================================================================================================== +void Opal::Operator::SetReleaseRate(uint16_t rate) { + + ReleaseRate = rate; + + ComputeRates(); +} + + + +//================================================================================================== +// Assign the waveform this operator will use. +//================================================================================================== +void Opal::Operator::SetWaveform(uint16_t wave) { + + Waveform = wave & 7; +} + + + +//================================================================================================== +// Compute actual rate from register rate. From the Yamaha data sheet: +// +// Actual rate = Rate value * 4 + Rof, if Rate value = 0, actual rate = 0 +// +// Rof is set as follows depending on the KSR setting: +// +// Key scale 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +// KSR = 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 +// KSR = 1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +// +// Note: zero rates are infinite, and are treated separately elsewhere +//================================================================================================== +void Opal::Operator::ComputeRates() { + + int combined_rate = AttackRate * 4 + (Chan->GetKeyScaleNumber() >> (KeyScaleRate ? 0 : 2)); + int rate_high = combined_rate >> 2; + int rate_low = combined_rate & 3; + + AttackShift = rate_high < 12 ? 12 - rate_high : 0; + AttackMask = (1 << AttackShift) - 1; + AttackAdd = (rate_high < 12) ? 1 : 1 << (rate_high - 12); + AttackTab = Master->RateTables[rate_low]; + + // Attack rate of 15 is always instant + if (AttackRate == 15) + AttackAdd = 0xFFF; + + combined_rate = DecayRate * 4 + (Chan->GetKeyScaleNumber() >> (KeyScaleRate ? 0 : 2)); + rate_high = combined_rate >> 2; + rate_low = combined_rate & 3; + + DecayShift = rate_high < 12 ? 12 - rate_high : 0; + DecayMask = (1 << DecayShift) - 1; + DecayAdd = (rate_high < 12) ? 1 : 1 << (rate_high - 12); + DecayTab = Master->RateTables[rate_low]; + + combined_rate = ReleaseRate * 4 + (Chan->GetKeyScaleNumber() >> (KeyScaleRate ? 0 : 2)); + rate_high = combined_rate >> 2; + rate_low = combined_rate & 3; + + ReleaseShift = rate_high < 12 ? 12 - rate_high : 0; + ReleaseMask = (1 << ReleaseShift) - 1; + ReleaseAdd = (rate_high < 12) ? 1 : 1 << (rate_high - 12); + ReleaseTab = Master->RateTables[rate_low]; +} + + + +//================================================================================================== +// Compute the operator's key scale level. This changes based on the channel frequency/octave and +// operator key scale value. +//================================================================================================== +void Opal::Operator::ComputeKeyScaleLevel() { + + static const uint16_t levtab[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 12, 16, 20, 24, 28, 32, + 0, 0, 0, 0, 0, 12, 20, 28, 32, 40, 44, 48, 52, 56, 60, 64, + 0, 0, 0, 20, 32, 44, 52, 60, 64, 72, 76, 80, 84, 88, 92, 96, + 0, 0, 32, 52, 64, 76, 84, 92, 96, 104, 108, 112, 116, 120, 124, 128, + 0, 32, 64, 84, 96, 108, 116, 124, 128, 136, 140, 144, 148, 152, 156, 160, + 0, 64, 96, 116, 128, 140, 148, 156, 160, 168, 172, 176, 180, 184, 188, 192, + 0, 96, 128, 148, 160, 172, 180, 188, 192, 200, 204, 208, 212, 216, 220, 224, + }; + + // This uses a combined value of the top four bits of frequency with the octave/block + uint16_t i = (Chan->GetOctave() << 4) | (Chan->GetFreq() >> 6); + KeyScaleLevel = levtab[i] >> KeyScaleShift; +} + +Opal opal(44100); + +std::mutex mutex; + +namespace Opl { + void write(uint16_t reg, uint8_t data) { + mutex.lock(); + opal.Port(reg, data); + mutex.unlock(); + } + + void generateSample(int16_t *left, int16_t *right) { + mutex.lock(); + opal.Sample(left, right); + mutex.unlock(); + } +} diff --git a/system/sdl/SoundBlaster.cpp b/system/sdl/SoundBlaster.cpp new file mode 100644 index 0000000..36bcffa --- /dev/null +++ b/system/sdl/SoundBlaster.cpp @@ -0,0 +1,19 @@ +#include "../SoundBlaster.h" + +SoundBlaster soundblaster; + +void soundblasterIsr() { + soundblaster.onInterrupt(); +} + +SoundBlaster::SoundBlaster() { + +} + +SoundBlaster::~SoundBlaster() { +} + +void SoundBlaster::onInterrupt() { + +} + diff --git a/system/sdl/Timer.cpp b/system/sdl/Timer.cpp new file mode 100644 index 0000000..596aa6a --- /dev/null +++ b/system/sdl/Timer.cpp @@ -0,0 +1,55 @@ +#include + +#include "../Timer.h" + +SDL_TimerID timerId; + +void timerISR() { + Timer::instance().update(); +} + +Timer::Timer() { + _freq = 50; + timerId = SDL_AddTimer(1000/_freq, [] (Uint32, void*) -> Uint32 { + timerISR(); + return 1000 / instance().getFrequency(); + }, nullptr); +} + +Timer::~Timer() { + SDL_RemoveTimer(timerId); +} + +void Timer::update() { + // TODO + _ticks++; + if (_callback) _callback(); +} + +uint32_t Timer::getTicks() const { + return _ticks; +} + +void Timer::setFrequency(uint16_t freq) { + _freq = freq; + // TODO +} + +uint16_t Timer::getFrequency() const { + return _freq; +} + +void Timer::setDivider(uint16_t div) { + // TODO +} + +Timer::Callback Timer::setCallback(Timer::Callback callback) { + auto oldCallback = _callback; + _callback = callback; + return oldCallback; +} + +Timer &Timer::instance() { + static Timer inst; + return inst; +} diff --git a/system/sdl/Video.cpp b/system/sdl/Video.cpp new file mode 100644 index 0000000..3eceab3 --- /dev/null +++ b/system/sdl/Video.cpp @@ -0,0 +1,125 @@ +#include + +#include "../Video.h" + +#include "Events.h" + +#define WINDOW_WIDTH 1280 +#define WINDOW_HEIGHT 800 + +Video video; + +Video::Video() { + // TODO +} + +Video::~Video() { + exit(); +} + +inline void writePixel(SDL_Surface *surface, unsigned x, unsigned y, Uint8 r, Uint8 g, Uint8 b, Uint8 a) { + auto row = reinterpret_cast(static_cast(surface->pixels) + y * surface->pitch); + row[x] = SDL_MapRGBA(surface->format, r, g, b, a); +} + +void Video::enter() { + _window = SDL_CreateWindow("game", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 1280, 800, SDL_WINDOW_SHOWN); + SDL_SetWindowTitle(_window, "meow :3"); + if (!_window) { + std::cerr << "Error creating SDL window: " << SDL_GetError() << std::endl; + abort(); + } + + _windowSurface = SDL_GetWindowSurface(_window); + _fb = SDL_CreateRGBSurfaceWithFormat(0, SCREEN_WIDTH, SCREEN_HEIGHT, 32, SDL_PIXELFORMAT_ARGB8888); +} + +void Video::exit() { + if (_fb) SDL_FreeSurface(_fb); + _fb = nullptr; + + SDL_DestroyWindow(_window); +} + +void Video::SetMode(uint8_t mode) { + // Ignored +} + +uint8_t Video::GetMode() { + // Fixed + return 0x13; +} + +void Video::SetPaletteEntry(uint8_t index, PaletteEntry entry) { + SetPaletteEntry(index, &entry); +} + +Video::PaletteEntry Video::GetPaletteEntry(uint8_t index) { + PaletteEntry entry; + GetPaletteEntry(index, &entry); + return entry; +} + +void Video::SetPaletteEntry(uint8_t index, PaletteEntry *entry) { + _palette[index] = *entry; +} + +void Video::GetPaletteEntry(uint8_t index, PaletteEntry *entry) { + *entry = _palette[index]; +} + +void *Video::GetFB() { + return _renderBuffer; +} + +void Video::WaitForVerticalSync() { + auto targetTime = _lastUpdate + std::chrono::microseconds(14286); + auto currentTime = std::chrono::steady_clock::now(); + + while (currentTime < targetTime) { + currentTime = std::chrono::steady_clock::now(); + } + _lastUpdate = currentTime; +} + +void Video::UpdateRect(const Rect &rect) { + // Merge rect if it overlaps with an existing rect + for (auto i = 0; i < _updateRectIndex; i++) { + auto &r = _updatedRects[i]; + if (r.overlaps(rect)) { + r += rect; + return; + } + } + + // Add a new rect to the list + _updatedRects[_updateRectIndex++] = rect; +} + +void Video::Flip() { + SDL_LockSurface(_fb); + + for (auto i = 0; i <= MAX_UPDATE_RECT_INDEX; i++) { + auto &rect = _updatedRects[i]; + rect.clamp(0, 0, _fb->w, _fb->h); + + for (auto y = rect.y1; y < rect.y2; y++) { + for (auto x = rect.x1; x < rect.x2; x++) { + auto &paletteEntry = _palette[_renderBuffer[y * SCREEN_WIDTH + x]]; + + writePixel(_fb, x, y, paletteEntry.r, paletteEntry.g, paletteEntry.b, 255); + } + } + } + SDL_UnlockSurface(_fb); + + SDL_Rect srcRect{0, 0, SCREEN_WIDTH, SCREEN_HEIGHT}; + SDL_Rect dstRect{0, 0, WINDOW_WIDTH, WINDOW_HEIGHT}; + + SDL_BlitScaled(_fb, &srcRect, _windowSurface, &dstRect); + SDL_UpdateWindowSurface(_window); + + _updateRectIndex = 0; + + events.poll(); +} diff --git a/system/sdl/init.cpp b/system/sdl/init.cpp new file mode 100644 index 0000000..4aa0d0b --- /dev/null +++ b/system/sdl/init.cpp @@ -0,0 +1,25 @@ +#include "../init.h" + +#include +#include + +#include "AudioBackend.h" +#include "../Video.h" + +namespace System { + void init() { + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_AUDIO) < 0) { + std::cerr << "Error initializing SDL: " << SDL_GetError() << std::endl; + abort(); + } + + audioBackend.init(); + } + + void terminate() { + audioBackend.terminate(); + video.exit(); + SDL_Quit(); + } +} + diff --git a/util/Asm.h b/util/Asm.h new file mode 100644 index 0000000..cff9507 --- /dev/null +++ b/util/Asm.h @@ -0,0 +1,68 @@ +#ifndef ASM_H +#define ASM_H + +#include + +static inline uint8_t inb(uint16_t port) { + uint8_t value; + asm volatile ("inb %1, %0" : "=a"(value) : "Nd"(port)); + return value; +} + +static inline uint16_t inw(uint16_t port) { + uint16_t value; + asm volatile ("inw %1, %0" : "=a"(value) : "Nd"(port)); + return value; +} + +static inline uint32_t inl(uint16_t port) { + uint32_t value; + asm volatile ("inl %1, %0" : "=a"(value) : "Nd"(port)); + return value; +} + +static inline void outb(uint16_t port, uint8_t value) { + asm volatile ("outb %0, %1" : : "a"(value), "Nd"(port)); +} + +static inline void outw(uint16_t port, uint16_t value) { + asm volatile ("outw %0, %1" : : "a"(value), "Nd"(port)); +} + +static inline void outl(uint16_t port, uint32_t value) { + asm volatile ("outl %0, %1" : : "a"(value), "Nd"(port)); +} + +static inline void enableInterrupts() { + asm volatile ("sti": : :"memory"); +} + +static inline void disableInterrupts() { + asm volatile ("cli": : :"memory"); +} + +static inline uint32_t eflags() { + uint32_t value; + asm volatile ( "pushf\n\t" + "popl %0" + : "=g"(value)); + return value; +} + +static inline bool interruptsEnabled() { + return (eflags() & (1u << 9u)) != 0; +} + +static void pmInterruptCall(unsigned short selector, unsigned long offset) { + __asm__ __volatile__ ( + "pushf\n" // Push EFLAGS + "mov %0, %%ax\n" // Load the segment selector into AX + "mov %%ax, %%ds\n" // Move it to DS (or another segment register) + "call *%1\n" // Perform the indirect far call using the offset + : // No output operands + : "r" (selector), "r" (offset) // Input operands + : "%ax" // Clobbered register + ); +} + +#endif \ No newline at end of file diff --git a/util/Bmp.cpp b/util/Bmp.cpp new file mode 100644 index 0000000..8150b85 --- /dev/null +++ b/util/Bmp.cpp @@ -0,0 +1,97 @@ +#include "Bmp.h" + +#include + +#include "Files.h" + +namespace Bmp { + Bitmap loadFromMemory(void *buf, size_t len) { + if (len < sizeof (BmpFileHeader) + sizeof (BmpInfoHeader)) return {}; + + auto fileHeader = reinterpret_cast(buf); + auto infoHeader = reinterpret_cast(fileHeader + 1); + + if (fileHeader->signature != 0x4d42) { + printf("Invalid signature\n"); + return {}; + } + if (len < fileHeader->fileSize) { + printf("File too small\n"); + return {}; + } + if (infoHeader->headerSize != 40) { + printf("Invalid header size\n"); + return {}; + } + if (infoHeader->width < 0 || infoHeader->height < 0 || infoHeader->planes != 1) { + printf("Invalid width/height/bit planes\n"); + + return {}; + } + if (infoHeader->bitsPerPixel != 1 && infoHeader->bitsPerPixel != 4 && infoHeader->bitsPerPixel != 8) { + printf("Unsupported bpp %u\n", infoHeader->bitsPerPixel); + + return {}; + } + if (infoHeader->compression != BmpInfoHeader::CompressionType::Rgb) { + printf("Unsupported compression\n"); + + return {}; + } + + // TODO check imageSize + + Bitmap bitmap(infoHeader->width, infoHeader->height); + + // Read color table + auto colorTable = reinterpret_cast(infoHeader + 1); + for (auto i = 0; i < infoHeader->colorsUsed; i++) { + Video::PaletteEntry paletteEntry; + paletteEntry.b = colorTable[4 * i + 0]; + paletteEntry.g = colorTable[4 * i + 1]; + paletteEntry.r = colorTable[4 * i + 2]; + bitmap.SetPaletteEntry(i, paletteEntry); + } + + // Read pixel data + auto data = static_cast(buf) + fileHeader->offset; + auto rowLength = infoHeader->width; + if (infoHeader->bitsPerPixel == 1) rowLength = (rowLength + 7) >> 3; + else if (infoHeader->bitsPerPixel == 4) rowLength = (rowLength + 1) >> 1; + auto stride = (rowLength + 3) >> 2 << 2; + for (auto y = 0u; y < infoHeader->height; y++) { + for (auto x = 0u; x < infoHeader->width; x++) { + uint8_t pv = 0; + switch (infoHeader->bitsPerPixel) { + case 1: { + const auto byte = data[(infoHeader->height - y - 1) * stride + (x >> 3)]; + auto pixelOffset = x & 7; + pv = (byte >> (7 - pixelOffset)) & 1; + break; + } + case 4: { + const auto byte = data[(infoHeader->height - y - 1) * stride + (x >> 1)]; + pv = x & 1 ? byte & 0xf : byte >> 4; + break; + } + case 8: { + pv = data[(infoHeader->height - y - 1) * stride + x]; + break; + } + } + bitmap.SetPixel(x, y, pv); + } + } + + return bitmap; + } + + Bitmap loadFromFile(const char* path) { + void *data; + auto len = Files::allocateBufferAndLoadFromFile(path, &data); + auto bitmap = loadFromMemory(data, len); + Files::deleteBuffer(data); + + return bitmap; + } +} diff --git a/util/Bmp.h b/util/Bmp.h new file mode 100644 index 0000000..e0afd2a --- /dev/null +++ b/util/Bmp.h @@ -0,0 +1,48 @@ +#ifndef GAME_BMPLOADER_H +#define GAME_BMPLOADER_H + +#include +#include "../graphics/Bitmap.h" + +namespace Bmp { + struct BmpFileHeader { + uint16_t signature; + uint32_t fileSize; + uint16_t reserved1; + uint16_t reserved2; + uint32_t offset; + } __attribute__((__packed__)); + + struct BmpInfoHeader { + uint32_t headerSize; + uint32_t width; + uint32_t height; + uint16_t planes; + uint16_t bitsPerPixel; + uint32_t compression; + uint32_t imageSize; + uint32_t horizontalResolution; + uint32_t verticalResolution; + uint32_t colorsUsed; + uint32_t colorsImportant; + + enum CompressionType { + Rgb = 0, + Rle8, + Rle4, + Bitfields, + Jpeg, + Png, + Alphabitfields, + Cmyk, + CmykRle8, + CmykRle4, + }; + } __attribute__((__packed__)); + + Bitmap loadFromMemory(void *buf, size_t len); + Bitmap loadFromFile(const char* path); +}; + + +#endif //GAME_BMPLOADER_H \ No newline at end of file diff --git a/util/Files.cpp b/util/Files.cpp new file mode 100644 index 0000000..18d9a1f --- /dev/null +++ b/util/Files.cpp @@ -0,0 +1,27 @@ +#include "Files.h" + +#include + +size_t Files::allocateBufferAndLoadFromFile(const char *path, void **bufferPointer) { + auto fp = fopen(path, "rb"); + if (!fp) { + *bufferPointer = nullptr; + return 0; + } + + fseek(fp, 0, SEEK_END); + size_t len = ftell(fp); + fseek(fp, 0, SEEK_SET); + + auto data = new unsigned char[len]; + fread(data, 1, len, fp); + fclose(fp); + + *bufferPointer = data; + return len; +} + +void Files::deleteBuffer(void *buffer) { + delete[] static_cast(buffer); +} + diff --git a/util/Files.h b/util/Files.h new file mode 100644 index 0000000..e94f176 --- /dev/null +++ b/util/Files.h @@ -0,0 +1,10 @@ +#ifndef GAME_FILES_H +#define GAME_FILES_H +#include + +namespace Files { + size_t allocateBufferAndLoadFromFile(const char *path, void **bufferPointer); + void deleteBuffer(void *); +} + +#endif //GAME_FILES_H \ No newline at end of file diff --git a/util/Gbm.cpp b/util/Gbm.cpp new file mode 100644 index 0000000..cdf50b4 --- /dev/null +++ b/util/Gbm.cpp @@ -0,0 +1,68 @@ +#include "Gbm.h" + +#include + +#include "Files.h" + +#define GBM_SIGNATURE 0x204d4247 + +Bitmap Gbm::loadFromMemory(const void *data, size_t size) { + // Check for minimum + if (size < sizeof (uint32_t) + sizeof (uint16_t) + sizeof (uint16_t) + 16 * 3) return {}; + + auto gbm = static_cast(data); + + if (gbm->signature != GBM_SIGNATURE) return {}; + if (gbm->width == 0 || gbm->height == 0) return {}; + + Bitmap bitmap(gbm->width, gbm->height); + for (auto i = 0; i < 16; i++) { + bitmap.SetPaletteEntry(i, {gbm->palette[i].r, gbm->palette[i].g, gbm->palette[i].b}); + } + + for (auto y = 0; y < gbm->height; y++) { + for (auto x = 0; x < gbm->width; x++) { + auto pv = gbm->data[y * gbm->width + x]; + bitmap.SetPixel(x, y, pv); + } + } + + return bitmap; +} + +Bitmap Gbm::loadFromFile(const char *path) { + void *data; + auto len = Files::allocateBufferAndLoadFromFile(path, &data); + auto bitmap = loadFromMemory(data, len); + Files::deleteBuffer(data); + + return bitmap; +} + +void Gbm::writeToFile(const char *path, const Bitmap &bitmap) { + auto fp = fopen(path, "wb"); + if (!fp) return; + + // Write signature + uint32_t signature = GBM_SIGNATURE; + fwrite(&signature, 4, 1, fp); + uint16_t width = bitmap.GetWidth(), height = bitmap.GetHeight(); + fwrite(&width, 2, 1, fp); + fwrite(&height, 2, 1, fp); + for (auto i = 0; i < 16; i++) { + auto entry = bitmap.GetPaletteEntry(i); + auto r = entry.r, g = entry.g, b = entry.b; + fwrite(&r, 1, 1, fp); + fwrite(&g, 1, 1, fp); + fwrite(&b, 1, 1, fp); + } + + for (auto y = 0; y < height; y++) { + for (auto x = 0; x < width; x++) { + auto pv = bitmap.GetPixel(x, y); + fwrite(&pv, 1, 1, fp); + } + } + + fclose(fp); +} diff --git a/util/Gbm.h b/util/Gbm.h new file mode 100644 index 0000000..c49bac8 --- /dev/null +++ b/util/Gbm.h @@ -0,0 +1,28 @@ +#ifndef GAME_GBM_H +#define GAME_GBM_H + +#include + +#include "../graphics/Bitmap.h" + +namespace Gbm { + struct GbmHeader { + uint32_t signature; + uint16_t width; + uint16_t height; + struct { + uint8_t r; + uint8_t g; + uint8_t b; + } __attribute__((__packed__)) palette[16]; + uint8_t data[0]; + } __attribute__((__packed__)); + + Bitmap loadFromMemory(const void *data, size_t size); + Bitmap loadFromFile(const char *path); + + void writeToFile(const char *path, const Bitmap &bitmap); +}; + + +#endif //GAME_GBM_H \ No newline at end of file diff --git a/util/Log.cpp b/util/Log.cpp new file mode 100644 index 0000000..4694c99 --- /dev/null +++ b/util/Log.cpp @@ -0,0 +1,22 @@ +#include "Log.h" + +#include + +Log DefaultLog("log.txt"); + +Log::Log(const char *path) { + fp = fopen(path, "w"); +} + +Log::~Log() { + fclose(fp); +} + +void Log::log(const char *fmt, ...) { + va_list args; + va_start(args, fmt); + vfprintf(fp, fmt, args); + va_end(args); + + fflush(fp); +} diff --git a/util/Log.h b/util/Log.h new file mode 100644 index 0000000..09a58fc --- /dev/null +++ b/util/Log.h @@ -0,0 +1,19 @@ +#ifndef GAME_LOG_H +#define GAME_LOG_H +#include + +class Log { +public: + Log(const char *path); + ~Log(); + + void log(const char *fmt, ...); + +private: + FILE *fp; +}; + +extern Log DefaultLog; + + +#endif //GAME_LOG_H \ No newline at end of file