├── src ├── Value.cpp ├── Debug.h ├── App.h ├── Player.h ├── Listener.h ├── Sample.h ├── WaveStore.h ├── SequenceRow.h ├── Synth.h ├── Lockable.h ├── OctaveInfo.h ├── SequenceInfo.h ├── MacroInfo.h ├── Debug.cpp ├── SectionListener.cpp ├── SongLengthInfo.h ├── PatternLengthInfo.h ├── TrackState.h ├── Macro.cpp ├── CopyBuffer.h ├── Macro.h ├── Color.h ├── MessageDisplayer.h ├── TooltipDisplayer.h ├── WaveView.h ├── Sequence.h ├── Pattern.h ├── TouchRegion.h ├── SequenceRow.cpp ├── IOscillator.cpp ├── Gamepad.h ├── Player.cpp ├── Listenable.h ├── WaveStore.cpp ├── MacroEditor.h ├── OctaveInfo.cpp ├── SectionListener.h ├── MacroInfo.cpp ├── Oscilloscope.h ├── Wave.h ├── SequenceInfo.cpp ├── SongLengthInfo.cpp ├── Listenable.cpp ├── Label.h ├── PatternLengthInfo.cpp ├── PlayerState.h ├── Random.h ├── MessageBox.h ├── EffectParam.h ├── Lockable.cpp ├── TextEditor.h ├── Prototracker.h ├── NoteState.h ├── PlayerState.cpp ├── SequenceRowEditor.h ├── Synth.cpp ├── ITrackState.cpp ├── TooltipManager.h ├── Color.cpp ├── CopyBuffer.cpp ├── TouchRegion.cpp ├── PatternEditor.h ├── AudioDeviceSelector.h ├── ColumnEditor.h ├── ITrackState.h ├── EditorState.h ├── Oscillator.h ├── TrackState.cpp ├── EffectParam.cpp ├── Emscripten.h ├── Main.cpp ├── ISynth.h ├── IOscillator.h ├── Label.cpp ├── MessageManager.h ├── CommandSelector.h ├── CommandOptionSelector.h ├── WaveGen.h ├── Sequence.cpp ├── IPlayer.h ├── TooltipDisplayer.cpp ├── MessageBox.cpp ├── FileSection.h ├── MessageDisplayer.cpp ├── Mixer.h ├── TooltipManager.cpp ├── GenericSelector.h ├── Song.h ├── Theme.h ├── TrackEditor.h ├── AudioDeviceSelector.cpp ├── Oscilloscope.cpp ├── PatternRow.h ├── WaveView.cpp ├── Renderer.h ├── FileSelector.h ├── MessageManager.cpp ├── PatternRow.cpp ├── ISynth.cpp ├── Gamepad.cpp ├── MacroEditor.cpp ├── Pattern.cpp ├── Wave.cpp ├── NoteState.cpp ├── Emscripten.cpp ├── MainEditor.h ├── TextEditor.cpp ├── WaveGen.cpp ├── Value.h ├── Oscillator.cpp ├── Random.cpp ├── CommandOptionSelector.cpp ├── ColumnEditor.cpp ├── Editor.h ├── EditorState.cpp ├── PatternEditor.cpp ├── CommandSelector.cpp ├── Prototracker.cpp ├── Theme.cpp └── FileSelector.cpp ├── assets ├── dub.song ├── font.png ├── gui.png └── elements ├── doc ├── README.TXT ├── EFFECTS.TXT ├── FORMAT.TXT ├── MERSENNE.TXT └── KEYS.TXT ├── Makefile ├── Makefile.macos ├── templates └── Editor │ ├── {{Name}}.h │ └── {{Name}}.cpp ├── Makefile.linux ├── Makefile.mingw ├── .gitignore ├── Makefile.chip8 ├── Makefile.emscripten ├── scripts └── buildComponent.js ├── LICENSE ├── README.md └── .circleci └── config.yml /src/Value.cpp: -------------------------------------------------------------------------------- 1 | #include "Value.h" 2 | #include "Editor.h" 3 | -------------------------------------------------------------------------------- /assets/dub.song: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kometbomb/prototracker/HEAD/assets/dub.song -------------------------------------------------------------------------------- /assets/font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kometbomb/prototracker/HEAD/assets/font.png -------------------------------------------------------------------------------- /assets/gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kometbomb/prototracker/HEAD/assets/gui.png -------------------------------------------------------------------------------- /doc/README.TXT: -------------------------------------------------------------------------------- 1 | README.TXT 2 | 3 | Prototracker is a minimal fakebit chiptune tracker. 4 | -------------------------------------------------------------------------------- /src/Debug.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | void debug(const char *message, ...) __attribute__((format(printf, 1, 2))); 6 | -------------------------------------------------------------------------------- /src/App.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define APP_NAME "Prototracker" 4 | #define SONG_EXT ".song" 5 | #define SONG_TRACKS 4 6 | #define SONG_EFFECT_COLUMNS 1 7 | -------------------------------------------------------------------------------- /src/Player.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "IPlayer.h" 4 | 5 | class Player: public IPlayer 6 | { 7 | public: 8 | Player(Song& song); 9 | virtual ~Player(); 10 | }; 11 | -------------------------------------------------------------------------------- /src/Listener.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct Listenable; 4 | 5 | class Listener 6 | { 7 | public: 8 | virtual void onListenableChange(Listenable *listenable) = 0; 9 | }; 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PLAT=none 2 | PLATS=mingw linux chip8 emscripten macos 3 | 4 | all: $(PLAT) 5 | 6 | $(PLATS) clean: 7 | $(MAKE) -f Makefile.$@ 8 | 9 | none: 10 | @echo Build with 'make PLATFORM'. The supported platforms are: 11 | @echo $(PLATS) 12 | -------------------------------------------------------------------------------- /Makefile.macos: -------------------------------------------------------------------------------- 1 | CC=g++ 2 | OUTPUT=prototracker 3 | SRC=src/*.cpp 4 | 5 | $(OUTPUT): $(SRC) src/*.h 6 | $(CC) -O3 -Wformat -std=c++11 -o $@ $(SRC) -lSDL2_image -lSDL2main `sdl2-config --cflags --libs` -s -DSCALE=2 7 | 8 | clean: 9 | rm $(OUTPUT) 10 | -------------------------------------------------------------------------------- /src/Sample.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | typedef int SampleType32; 4 | typedef short int SampleType16; 5 | 6 | struct Sample32 7 | { 8 | SampleType32 left, right; 9 | }; 10 | 11 | struct Sample16 12 | { 13 | SampleType16 left, right; 14 | }; 15 | 16 | -------------------------------------------------------------------------------- /src/WaveStore.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct Wave; 4 | 5 | class WaveStore 6 | { 7 | Wave *mWave; 8 | public: 9 | static const int maxWaves = 256; 10 | 11 | WaveStore(); 12 | ~WaveStore(); 13 | const Wave& getWave(int index) const; 14 | }; 15 | -------------------------------------------------------------------------------- /src/SequenceRow.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "App.h" 4 | 5 | struct SequenceRow 6 | { 7 | static const int maxTracks = SONG_TRACKS; 8 | 9 | int pattern[maxTracks]; 10 | 11 | void clear(); 12 | bool isEmpty() const; 13 | 14 | SequenceRow(); 15 | }; 16 | -------------------------------------------------------------------------------- /src/Synth.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ISynth.h" 4 | 5 | struct WaveStore; 6 | 7 | class Synth: public ISynth 8 | { 9 | WaveStore* mWaveStore; 10 | 11 | public: 12 | Synth(); 13 | virtual ~Synth(); 14 | 15 | const WaveStore& getWaveStore() const; 16 | }; 17 | -------------------------------------------------------------------------------- /templates/Editor/{{Name}}.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Editor.h" 4 | 5 | class {{Name}}: public Editor 6 | { 7 | public: 8 | {{Name}}(EditorState& editorState); 9 | virtual ~{{Name}}(); 10 | virtual void onDraw(Renderer& renderer, const SDL_Rect& area); 11 | }; 12 | -------------------------------------------------------------------------------- /Makefile.linux: -------------------------------------------------------------------------------- 1 | CC=g++ 2 | OUTPUT=prototracker 3 | SRC=src/*.cpp 4 | 5 | $(OUTPUT): $(SRC) src/*.h 6 | $(CC) -O3 -Wformat -std=c++11 -o $@ $(SRC) -lSDL2_image -lSDL2main `sdl2-config --cflags --libs` -s -DSCALE=2 -static-libstdc++ -static-libgcc 7 | 8 | clean: 9 | rm $(OUTPUT) 10 | -------------------------------------------------------------------------------- /src/Lockable.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "SDL.h" 4 | 5 | class Lockable 6 | { 7 | SDL_mutex *mMutex; 8 | SDL_SpinLock mSpinlock; 9 | int mLockCounter; 10 | 11 | public: 12 | Lockable(); 13 | ~Lockable(); 14 | 15 | void lock(); 16 | void unlock(); 17 | }; 18 | -------------------------------------------------------------------------------- /src/OctaveInfo.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Label.h" 4 | 5 | struct Song; 6 | 7 | class OctaveInfo: public Label 8 | { 9 | public: 10 | OctaveInfo(EditorState& editorState); 11 | virtual ~OctaveInfo(); 12 | virtual void onDraw(Renderer& renderer, const SDL_Rect& area); 13 | }; 14 | -------------------------------------------------------------------------------- /Makefile.mingw: -------------------------------------------------------------------------------- 1 | OUTPUT=prototracker.exe 2 | SRC=src/*.cpp 3 | 4 | $(OUTPUT): $(SRC) src/*.h 5 | g++ -DSCALE=2 -O3 -Wformat -std=c++11 -o $@ $(SRC) -lmingw32 -lSDL2_image -lSDL2main -lSDL2 -Ic:/mingw/include/SDL2 -Ic:/tdm-gcc-32/include/SDL2 -s -DENABLE_AUDIO_QUEUE=1 6 | 7 | clean: 8 | rm $(OUTPUT) 9 | -------------------------------------------------------------------------------- /src/SequenceInfo.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Label.h" 4 | 5 | struct Song; 6 | 7 | class SequenceInfo: public Label 8 | { 9 | public: 10 | SequenceInfo(EditorState& editorState); 11 | virtual ~SequenceInfo(); 12 | virtual void onDraw(Renderer& renderer, const SDL_Rect& area); 13 | }; 14 | -------------------------------------------------------------------------------- /src/MacroInfo.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Label.h" 4 | 5 | struct Song; 6 | 7 | class MacroInfo: public Label 8 | { 9 | Song& mSong; 10 | 11 | public: 12 | MacroInfo(EditorState& editorState, Song& song); 13 | virtual ~MacroInfo(); 14 | virtual void onDraw(Renderer& renderer, const SDL_Rect& area); 15 | }; 16 | -------------------------------------------------------------------------------- /src/Debug.cpp: -------------------------------------------------------------------------------- 1 | #include "Debug.h" 2 | #include 3 | 4 | 5 | void debug(const char * message, ...) 6 | { 7 | char dest[1024]; 8 | va_list argptr; 9 | va_start(argptr, message); 10 | vsnprintf(dest, sizeof(dest), message, argptr); 11 | va_end(argptr); 12 | printf("[DEBUG] %s\n", dest); 13 | } 14 | -------------------------------------------------------------------------------- /src/SectionListener.cpp: -------------------------------------------------------------------------------- 1 | #include "SectionListener.h" 2 | 3 | bool SectionListener::onFileSectionLoad(const FileSection& section, int& offset) 4 | { 5 | return true; 6 | } 7 | 8 | 9 | void SectionListener::onFileSectionSave(FileSection& section) 10 | { 11 | } 12 | 13 | 14 | void SectionListener::onSongClear() 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/SongLengthInfo.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Label.h" 4 | 5 | struct Song; 6 | 7 | class SongLengthInfo: public Label 8 | { 9 | Song& mSong; 10 | 11 | public: 12 | SongLengthInfo(EditorState& editorState, Song& song); 13 | virtual ~SongLengthInfo(); 14 | virtual void onDraw(Renderer& renderer, const SDL_Rect& area); 15 | }; 16 | -------------------------------------------------------------------------------- /src/PatternLengthInfo.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Label.h" 4 | 5 | struct Song; 6 | 7 | class PatternLengthInfo: public Label 8 | { 9 | Song& mSong; 10 | 11 | public: 12 | PatternLengthInfo(EditorState& editorState, Song& song); 13 | virtual ~PatternLengthInfo(); 14 | virtual void onDraw(Renderer& renderer, const SDL_Rect& area); 15 | }; 16 | -------------------------------------------------------------------------------- /src/TrackState.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ITrackState.h" 4 | 5 | struct TrackState: public ITrackState 6 | { 7 | int wave, queuedWave; 8 | 9 | TrackState(); 10 | 11 | virtual bool handleEffectZeroTick(const EffectParam& effect, PlayerState& playerState); 12 | virtual void handleEffectAnyTick(const EffectParam& effect, PlayerState& playerState); 13 | }; 14 | 15 | -------------------------------------------------------------------------------- /src/Macro.cpp: -------------------------------------------------------------------------------- 1 | #include "Macro.h" 2 | #include "PatternRow.h" 3 | #include 4 | 5 | Macro::Macro() 6 | : Pattern() 7 | { 8 | strcpy(mName, ""); 9 | //rows = new PatternRow[maxMacroLength]; 10 | } 11 | 12 | 13 | char *Macro::getName() 14 | { 15 | return mName; 16 | } 17 | 18 | 19 | void Macro::clear() 20 | { 21 | Pattern::clear(); 22 | 23 | strcpy(mName, ""); 24 | } 25 | -------------------------------------------------------------------------------- /src/CopyBuffer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Pattern.h" 4 | 5 | class CopyBuffer: public Pattern 6 | { 7 | protected: 8 | int mLength; 9 | public: 10 | CopyBuffer(); 11 | virtual ~CopyBuffer(); 12 | 13 | void copy(Pattern& source, int start, int end); 14 | void paste(Pattern& destination, int start); 15 | 16 | int getLength() const; 17 | void setLength(int length); 18 | }; 19 | -------------------------------------------------------------------------------- /src/Macro.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Pattern.h" 4 | 5 | struct PatternRow; 6 | 7 | class Macro: public Pattern 8 | { 9 | public: 10 | static const int macroNameLength = 8; 11 | 12 | char mName[macroNameLength + 1]; 13 | 14 | public: 15 | Macro(); 16 | 17 | char *getName(); 18 | 19 | void clear(); 20 | 21 | static const int maxMacroLength = Pattern::maxRows; 22 | }; 23 | -------------------------------------------------------------------------------- /src/Color.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct EffectParam; 4 | 5 | struct Color 6 | { 7 | int r, g, b, a; 8 | Color(int r = 255, int g = 255, int b = 255, int a = 255); 9 | 10 | static Color getEffectColor(const EffectParam& effect); 11 | }; 12 | 13 | inline Color operator*(const Color& lhs, const Color& rhs){ return Color(lhs.r * rhs.r / 255, lhs.g * rhs.g / 255, lhs.b * rhs.b / 255, lhs.a * rhs.a / 255); } 14 | -------------------------------------------------------------------------------- /src/MessageDisplayer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Editor.h" 4 | 5 | struct MessageManager; 6 | struct EditorState; 7 | 8 | class MessageDisplayer: public Editor 9 | { 10 | MessageManager& mMessageManager; 11 | public: 12 | MessageDisplayer(EditorState& editorState, MessageManager& manager); 13 | 14 | virtual void onDraw(Renderer& renderer, const SDL_Rect& area); 15 | virtual void onUpdate(int ms); 16 | }; 17 | -------------------------------------------------------------------------------- /src/TooltipDisplayer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Editor.h" 4 | 5 | struct TooltipManager; 6 | struct EditorState; 7 | 8 | class TooltipDisplayer: public Editor 9 | { 10 | TooltipManager& mTooltipManager; 11 | public: 12 | TooltipDisplayer(EditorState& editorState, TooltipManager& manager); 13 | 14 | virtual void onDraw(Renderer& renderer, const SDL_Rect& area); 15 | virtual void onUpdate(int ms); 16 | }; 17 | -------------------------------------------------------------------------------- /src/WaveView.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Editor.h" 4 | 5 | struct WaveStore; 6 | 7 | class WaveView: public Editor 8 | { 9 | const WaveStore& mWaveStore; 10 | int mX, mY; 11 | public: 12 | WaveView(EditorState& editorState, const WaveStore& waveStore); 13 | virtual ~WaveView(); 14 | virtual void onDraw(Renderer& renderer, const SDL_Rect& area); 15 | virtual bool onEvent(SDL_Event& event); 16 | }; 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Sequence.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct SequenceRow; 4 | 5 | class Sequence 6 | { 7 | SequenceRow *rows; 8 | 9 | public: 10 | Sequence(); 11 | ~Sequence(); 12 | 13 | void insertRow(int row, bool allTracks); 14 | void deleteRow(int row, bool allTracks); 15 | 16 | SequenceRow& getRow(int sequenceRow); 17 | int getLastSequenceRowUsed() const; 18 | void clear(); 19 | 20 | static const int maxRows = 256; 21 | }; 22 | -------------------------------------------------------------------------------- /src/Pattern.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct PatternRow; 4 | 5 | class Pattern 6 | { 7 | PatternRow *rows; 8 | 9 | public: 10 | static const int maxRows = 256; 11 | 12 | PatternRow& getRow(int row); 13 | int getLastUsedRow() const; 14 | void clear(); 15 | int getLastMacroUsed() const; 16 | bool isEmpty() const; 17 | 18 | void insertRow(int row, int flags); 19 | void deleteRow(int row, int flags); 20 | 21 | Pattern(); 22 | virtual ~Pattern(); 23 | }; 24 | -------------------------------------------------------------------------------- /src/TouchRegion.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Editor.h" 4 | 5 | struct EditorState; 6 | 7 | class TouchRegion: public Editor 8 | { 9 | SDL_Scancode mScancode; 10 | SDL_Keycode mKeycode; 11 | SDL_Keymod mKeymod; 12 | 13 | protected: 14 | virtual void onDraw(Renderer& renderer, const SDL_Rect& area); 15 | 16 | public: 17 | TouchRegion(EditorState& editorState, SDL_Scancode scancode, SDL_Keymod keymod); 18 | virtual bool onEvent(SDL_Event& event); 19 | }; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files 2 | *.slo 3 | *.lo 4 | *.o 5 | *.obj 6 | 7 | # Precompiled Headers 8 | *.gch 9 | *.pch 10 | 11 | # Compiled Dynamic libraries 12 | *.so 13 | *.dylib 14 | *.dll 15 | 16 | # Fortran module files 17 | *.mod 18 | *.smod 19 | 20 | # Compiled Static libraries 21 | *.lai 22 | *.la 23 | *.a 24 | *.lib 25 | 26 | # Executables 27 | *.exe 28 | *.out 29 | *.app 30 | assets/Thumbs.db 31 | 32 | # Project stuff 33 | .vscode/* 34 | node_modules/* 35 | -------------------------------------------------------------------------------- /src/SequenceRow.cpp: -------------------------------------------------------------------------------- 1 | #include "SequenceRow.h" 2 | 3 | const int SequenceRow::maxTracks; 4 | 5 | SequenceRow::SequenceRow() 6 | { 7 | clear(); 8 | } 9 | 10 | 11 | void SequenceRow::clear() 12 | { 13 | for (int i = 0 ; i < SequenceRow::maxTracks ; ++i) 14 | pattern[i] = 0; 15 | } 16 | 17 | 18 | bool SequenceRow::isEmpty() const 19 | { 20 | for (int i = 0 ; i < SequenceRow::maxTracks ; ++i) 21 | if (pattern[i] != 0) 22 | return false; 23 | 24 | return true; 25 | } 26 | -------------------------------------------------------------------------------- /src/IOscillator.cpp: -------------------------------------------------------------------------------- 1 | #include "IOscillator.h" 2 | #include "TrackState.h" 3 | 4 | IOscillator::IOscillator() 5 | : mSampleRate(0) 6 | { 7 | } 8 | 9 | 10 | IOscillator::~IOscillator() 11 | { 12 | } 13 | 14 | 15 | void IOscillator::handleTrackState(ITrackState& trackState) 16 | { 17 | } 18 | 19 | 20 | void IOscillator::setSampleRate(int rate) 21 | { 22 | mSampleRate = rate; 23 | } 24 | 25 | 26 | void IOscillator::setEnabled(bool state) 27 | { 28 | mIsEnabled = state; 29 | } 30 | -------------------------------------------------------------------------------- /Makefile.chip8: -------------------------------------------------------------------------------- 1 | # Makefile for the Chip8 architecture 2 | 3 | # The sample rate is 22 kHz and oversampling is disabled for performance, in addition 4 | # to other optimizations. Fullscreen mode is forced. 5 | 6 | OUTPUT=prototracker 7 | SRC=src/*.cpp 8 | 9 | $(OUTPUT): $(SRC) src/*.h 10 | g++ -Wformat -s -std=c++11 -o $@ $(SRC) -lSDL2_image -lSDL2main `sdl2-config --cflags --libs` -march=armv7-a -mtune=cortex-a7 -ffast-math -Ofast -DSCALE=1 -DOVERSAMPLE=0 -DFULLSCREEN=1 -DSAMPLERATE=22050 11 | 12 | clean: 13 | rm $(OUTPUT) 14 | -------------------------------------------------------------------------------- /src/Gamepad.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | Helper object to handle SDL_GameControllers and joystick axis to d-pad button translation 5 | */ 6 | 7 | #include "SDL.h" 8 | #include 9 | 10 | class Gamepad 11 | { 12 | bool controllerDefsLoaded; 13 | int controllerAxis[2]; 14 | 15 | std::vector controller; 16 | 17 | public: 18 | 19 | void translateAxisToDPad(const SDL_Event& event); 20 | bool loadDefinitions(const char *path); 21 | void initControllers(); 22 | void deinitControllers(); 23 | }; 24 | -------------------------------------------------------------------------------- /src/Player.cpp: -------------------------------------------------------------------------------- 1 | #include "Player.h" 2 | #include "TrackState.h" 3 | #include "SequenceRow.h" 4 | 5 | Player::Player(Song& song) 6 | : IPlayer(song) 7 | { 8 | /* 9 | 10 | Initialize the ITrackState derived TrackState for each track 11 | 12 | */ 13 | 14 | for (int i = 0 ; i < SequenceRow::maxTracks ; ++i) 15 | trackState[i] = new TrackState(); 16 | } 17 | 18 | Player::~Player() 19 | { 20 | /* 21 | 22 | NOTE: ~IPlayer() will handle cleanup, including deleting the 23 | TrackState objects initialized above! 24 | 25 | */ 26 | } 27 | -------------------------------------------------------------------------------- /templates/Editor/{{Name}}.cpp: -------------------------------------------------------------------------------- 1 | #include "{{Name}}.h" 2 | #include "Renderer.h" 3 | #include "Color.h" 4 | #include "EditorState.h" 5 | 6 | 7 | {{Name}}::{{Name}}(EditorState& editorState) 8 | : Editor(editorState) 9 | { 10 | mEditorState.octave.addListener(this); 11 | } 12 | 13 | 14 | {{Name}}::~{{Name}}() 15 | { 16 | } 17 | 18 | 19 | void {{Name}}::onDraw(Renderer& renderer, const SDL_Rect& area) 20 | { 21 | setDirty(false); 22 | 23 | renderer.renderBackground(area); 24 | renderer.renderText(area, Theme::ColorType::NormalText, "{{Name}}"); 25 | } 26 | -------------------------------------------------------------------------------- /src/Listenable.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct Listener; 4 | 5 | #include 6 | 7 | /* 8 | 9 | This interface defines the Listenable object that Listeners can subscribe to 10 | and are notified when the Listenable changes. User by the GUI elements to 11 | know when they need a redraw. 12 | 13 | */ 14 | 15 | class Listenable 16 | { 17 | protected: 18 | std::vector mListeners; 19 | 20 | public: 21 | Listenable(); 22 | 23 | bool addListener(Listener *listener); 24 | void removeListener(Listener *listener); 25 | void notify(); 26 | }; 27 | -------------------------------------------------------------------------------- /src/WaveStore.cpp: -------------------------------------------------------------------------------- 1 | #include "WaveStore.h" 2 | #include "Wave.h" 3 | #include "Oscillator.h" 4 | 5 | WaveStore::WaveStore() 6 | { 7 | mWave = new Wave[maxWaves]; 8 | for (int i = 0 ; i < maxWaves ; ++i) 9 | { 10 | int length = Oscillator::oscillatorLength; 11 | 12 | if (i == 255) 13 | length *= 32; 14 | 15 | mWave[i].init(length); 16 | mWave[i].generate(i); 17 | } 18 | } 19 | 20 | 21 | WaveStore::~WaveStore() 22 | { 23 | delete[] mWave; 24 | } 25 | 26 | 27 | const Wave& WaveStore::getWave(int index) const 28 | { 29 | return mWave[index]; 30 | } 31 | -------------------------------------------------------------------------------- /src/MacroEditor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "TrackEditor.h" 4 | 5 | struct Song; 6 | 7 | class MacroEditor: public TrackEditor 8 | { 9 | protected: 10 | virtual PatternRow& getPatternRow(int track, int row); 11 | virtual PatternRow& getCurrentPatternRow(); 12 | virtual Pattern& getCurrentPattern(int track); 13 | virtual void findUnusedTrack(int track); 14 | 15 | virtual void onRequestCommandRegistration(); 16 | 17 | public: 18 | MacroEditor(EditorState& editorState, IPlayer& player, Song& song); 19 | virtual ~MacroEditor(); 20 | 21 | virtual bool onEvent(SDL_Event& event); 22 | }; 23 | -------------------------------------------------------------------------------- /src/OctaveInfo.cpp: -------------------------------------------------------------------------------- 1 | #include "OctaveInfo.h" 2 | #include "Song.h" 3 | #include "Renderer.h" 4 | #include "Color.h" 5 | #include "EditorState.h" 6 | 7 | 8 | OctaveInfo::OctaveInfo(EditorState& editorState) 9 | : Label(editorState) 10 | { 11 | mEditorState.octave.addListener(this); 12 | } 13 | 14 | 15 | OctaveInfo::~OctaveInfo() 16 | { 17 | } 18 | 19 | 20 | void OctaveInfo::onDraw(Renderer& renderer, const SDL_Rect& area) 21 | { 22 | setDirty(false); 23 | 24 | renderer.renderBackground(area); 25 | renderer.renderTextV(area, Theme::ColorType::NormalText, "%02x", static_cast(mEditorState.octave)); 26 | } 27 | -------------------------------------------------------------------------------- /src/SectionListener.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct FileSection; 4 | 5 | /* SectionListener listens to unhandled Song FileSections 6 | * e.g. ISynth can then access the file to load/save its own settings 7 | */ 8 | 9 | class SectionListener 10 | { 11 | public: 12 | enum Flags { 13 | Load = 1, 14 | Save = 2 15 | }; 16 | 17 | /* Return false if failed 18 | */ 19 | virtual bool onFileSectionLoad(const FileSection& section, int& offset); 20 | virtual void onFileSectionSave(FileSection& section); 21 | 22 | // Triggered on Song::clear() if the Load event is listened to 23 | virtual void onSongClear(); 24 | }; 25 | -------------------------------------------------------------------------------- /src/MacroInfo.cpp: -------------------------------------------------------------------------------- 1 | #include "MacroInfo.h" 2 | #include "Song.h" 3 | #include "Renderer.h" 4 | #include "Color.h" 5 | #include "EditorState.h" 6 | 7 | 8 | MacroInfo::MacroInfo(EditorState& editorState, Song& song) 9 | : Label(editorState), mSong(song) 10 | { 11 | mEditorState.macro.addListener(this); 12 | } 13 | 14 | 15 | MacroInfo::~MacroInfo() 16 | { 17 | } 18 | 19 | 20 | void MacroInfo::onDraw(Renderer& renderer, const SDL_Rect& area) 21 | { 22 | setDirty(false); 23 | 24 | renderer.renderBackground(area); 25 | renderer.renderTextV(area, Theme::ColorType::NormalText, "%02x", static_cast(mEditorState.macro)); 26 | } 27 | -------------------------------------------------------------------------------- /src/Oscilloscope.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Editor.h" 4 | 5 | class IPlayer; 6 | struct Sample16; 7 | 8 | class Oscilloscope: public Editor 9 | { 10 | const Sample16 *mBuffer; 11 | int mBufferLength, mChannel, mOscilloscopePos; 12 | IPlayer& mPlayer; 13 | 14 | public: 15 | Oscilloscope(EditorState& editorState, IPlayer& player, int channel); 16 | 17 | void setBuffer(const Sample16 *buffer, int length); 18 | virtual void onListenableChange(Listenable *listenable); 19 | 20 | virtual ~Oscilloscope(); 21 | virtual void onDraw(Renderer& renderer, const SDL_Rect& area); 22 | virtual bool onEvent(SDL_Event& event); 23 | }; 24 | 25 | -------------------------------------------------------------------------------- /src/Wave.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | class Wave 4 | { 5 | int mLength; 6 | int mLevels; 7 | int *mData; 8 | int *mOffset; 9 | 10 | void generateSquare(float pw); 11 | void generateSine(); 12 | void generateNoise(); 13 | void generateSaw(); 14 | void generateTriangle(float step); 15 | 16 | void generateMipMap(); 17 | void generateMipMapLevel(int level); 18 | 19 | public: 20 | static const int waveAmplitude = 32768; 21 | 22 | Wave(); 23 | ~Wave(); 24 | 25 | void init(int length); 26 | 27 | int getLength(int level) const; 28 | int getLevels() const; 29 | const int* getData(int level) const; 30 | 31 | void generate(int index); 32 | }; 33 | -------------------------------------------------------------------------------- /src/SequenceInfo.cpp: -------------------------------------------------------------------------------- 1 | #include "SequenceInfo.h" 2 | #include "Song.h" 3 | #include "Renderer.h" 4 | #include "Color.h" 5 | #include "EditorState.h" 6 | 7 | 8 | SequenceInfo::SequenceInfo(EditorState& editorState) 9 | : Label(editorState) 10 | { 11 | mEditorState.sequenceEditor.currentRow.addListener(this); 12 | } 13 | 14 | 15 | SequenceInfo::~SequenceInfo() 16 | { 17 | } 18 | 19 | 20 | void SequenceInfo::onDraw(Renderer& renderer, const SDL_Rect& area) 21 | { 22 | setDirty(false); 23 | 24 | renderer.renderBackground(area); 25 | renderer.renderTextV(area, Theme::ColorType::NormalText, "%02x", static_cast(mEditorState.sequenceEditor.currentRow)); 26 | } 27 | -------------------------------------------------------------------------------- /src/SongLengthInfo.cpp: -------------------------------------------------------------------------------- 1 | #include "SongLengthInfo.h" 2 | #include "Song.h" 3 | #include "Renderer.h" 4 | #include "Color.h" 5 | #include "EditorState.h" 6 | 7 | 8 | SongLengthInfo::SongLengthInfo(EditorState& editorState, Song& song) 9 | : Label(editorState), mSong(song) 10 | { 11 | mEditorState.sequenceEditor.currentRow.addListener(this); 12 | } 13 | 14 | 15 | SongLengthInfo::~SongLengthInfo() 16 | { 17 | } 18 | 19 | 20 | void SongLengthInfo::onDraw(Renderer& renderer, const SDL_Rect& area) 21 | { 22 | setDirty(false); 23 | 24 | renderer.renderBackground(area); 25 | renderer.renderTextV(area, Theme::ColorType::NormalText, "%02x", mSong.getSequenceLength()); 26 | } 27 | -------------------------------------------------------------------------------- /src/Listenable.cpp: -------------------------------------------------------------------------------- 1 | #include "Listenable.h" 2 | #include "Listener.h" 3 | #include 4 | #include 5 | 6 | Listenable::Listenable() 7 | { 8 | } 9 | 10 | 11 | void Listenable::notify() 12 | { 13 | for (auto listener : mListeners) 14 | { 15 | listener->onListenableChange(this); 16 | } 17 | } 18 | 19 | 20 | bool Listenable::addListener(Listener *listener) 21 | { 22 | mListeners.push_back(listener); 23 | 24 | return true; 25 | } 26 | 27 | 28 | void Listenable::removeListener(Listener *listener) 29 | { 30 | auto iter = std::find(mListeners.begin(), mListeners.end(), listener); 31 | if (iter != mListeners.end()) 32 | mListeners.erase(iter); 33 | } 34 | -------------------------------------------------------------------------------- /src/Label.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Editor.h" 4 | #include "Color.h" 5 | 6 | struct Renderer; 7 | 8 | class Label: public Editor 9 | { 10 | static const int maxTextLength = 100; 11 | 12 | protected: 13 | char *mBuffer; 14 | Color mTextColor, mBackgroundColor; 15 | 16 | public: 17 | Label(EditorState& editorState); 18 | 19 | void setColor(const Color& color); 20 | void setBackground(const Color& color); 21 | void setText(const char *text); 22 | void setTextV(const char *text, ...) __attribute__((format(printf, 2, 3))); 23 | 24 | virtual ~Label(); 25 | virtual void onDraw(Renderer& renderer, const SDL_Rect& area); 26 | virtual bool onEvent(SDL_Event& event); 27 | }; 28 | -------------------------------------------------------------------------------- /src/PatternLengthInfo.cpp: -------------------------------------------------------------------------------- 1 | #include "PatternLengthInfo.h" 2 | #include "Song.h" 3 | #include "Renderer.h" 4 | #include "Color.h" 5 | #include "EditorState.h" 6 | 7 | 8 | PatternLengthInfo::PatternLengthInfo(EditorState& editorState, Song& song) 9 | : Label(editorState), mSong(song) 10 | { 11 | mEditorState.macroEditor.currentRow.addListener(this); 12 | } 13 | 14 | 15 | PatternLengthInfo::~PatternLengthInfo() 16 | { 17 | } 18 | 19 | 20 | void PatternLengthInfo::onDraw(Renderer& renderer, const SDL_Rect& area) 21 | { 22 | setDirty(false); 23 | 24 | renderer.renderBackground(area); 25 | renderer.renderTextV(area, Theme::ColorType::NormalText, "%02x", static_cast(mSong.getPatternLength())); 26 | } 27 | -------------------------------------------------------------------------------- /src/PlayerState.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct PlayerState 4 | { 5 | enum Mode 6 | { 7 | Stop, 8 | Play, 9 | PlaySequenceRow 10 | }; 11 | 12 | Mode mode; 13 | 14 | int tick; 15 | int songSpeed; 16 | int songRate; 17 | int sequenceRow; 18 | int patternRow; 19 | float cpuUse; 20 | 21 | int updateMask; 22 | 23 | PlayerState(); 24 | 25 | enum Updated { 26 | SequenceRow = 1, 27 | PatternRow = 2, 28 | Tick = 4, 29 | OscillatorProbe = 8, 30 | PatternRelevant = SequenceRow|PatternRow, 31 | Any = -1 32 | }; 33 | 34 | int getUpdated(); 35 | void ackUpdated(int mask); 36 | void setUpdated(int mask); 37 | 38 | bool isPlaying() const; 39 | bool shouldAdvanceSequenceRow() const; 40 | }; 41 | -------------------------------------------------------------------------------- /src/Random.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | class Random 4 | { 5 | static const unsigned long int N = 624; 6 | static const unsigned long int M = 397; 7 | static const unsigned long int MATRIX_A = 0x9908b0dfUL; /* constant vector a */ 8 | static const unsigned long int UPPER_MASK = 0x80000000UL; /* most significant w-r bits */ 9 | static const unsigned long int LOWER_MASK = 0x7fffffffUL; /* least significant r bits */ 10 | 11 | unsigned long mt[N]; /* the array for the state vector */ 12 | int mti; /* mti==N+1 means mt[N] is not initialized */ 13 | public: 14 | Random(); 15 | void seed(unsigned long s); 16 | unsigned int rndu(); 17 | int rnd(int min_val, int max_val); 18 | float rndf(); 19 | long double rndl(); 20 | }; 21 | -------------------------------------------------------------------------------- /src/MessageBox.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Editor.h" 4 | 5 | struct EditorState; 6 | struct Renderer; 7 | struct SDL_Rect; 8 | struct Label; 9 | 10 | #include 11 | #include 12 | 13 | class MessageBox: public Editor 14 | { 15 | static const int titleSize = 256; 16 | 17 | int mId; 18 | char mTitle[titleSize]; 19 | Label *mLabel; 20 | 21 | public: 22 | MessageBox(EditorState& editorState); 23 | virtual ~MessageBox(); 24 | 25 | void setId(int id); 26 | void setTitle(const char *title); 27 | int getId() const; 28 | 29 | virtual bool onEvent(SDL_Event& event); 30 | virtual void onDraw(Renderer& renderer, const SDL_Rect& area); 31 | virtual void onRendererMount(const Renderer& renderer); 32 | }; 33 | -------------------------------------------------------------------------------- /src/EffectParam.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct EffectParam 4 | { 5 | EffectParam(int effect, int param1, int param2); 6 | EffectParam(int effect = '0', int params = 0); 7 | 8 | int effect; 9 | int param1, param2; 10 | 11 | bool isEmpty() const; 12 | 13 | // Set param1 and param2 from a linear note value (and set effect to 'N') 14 | void setNoteAndOctave(int noteAndOctave); 15 | 16 | // Get a linear note value 0 = C-0, 12 = C-1 etc. if effect equals 'N' 17 | int getNoteWithOctave() const; 18 | 19 | // Unpack param1 and param2 (both 4-bit nibbles) from a single byte 20 | void setParamsFromByte(int byte); 21 | 22 | // Pack param1 and param2 (both 4-bit nibbles) in a single byte 23 | int getParamsAsByte() const; 24 | }; 25 | -------------------------------------------------------------------------------- /src/Lockable.cpp: -------------------------------------------------------------------------------- 1 | #include "Lockable.h" 2 | 3 | Lockable::Lockable() 4 | : mLockCounter(0), mSpinlock(0) 5 | { 6 | mMutex = SDL_CreateMutex(); 7 | } 8 | 9 | 10 | Lockable::~Lockable() 11 | { 12 | SDL_DestroyMutex(mMutex); 13 | } 14 | 15 | 16 | void Lockable::lock() 17 | { 18 | SDL_AtomicLock(&mSpinlock); 19 | if (++mLockCounter > 0) 20 | { 21 | SDL_AtomicUnlock(&mSpinlock); 22 | SDL_LockMutex(mMutex); 23 | } 24 | else 25 | { 26 | SDL_AtomicUnlock(&mSpinlock); 27 | } 28 | } 29 | 30 | 31 | void Lockable::unlock() 32 | { 33 | SDL_AtomicLock(&mSpinlock); 34 | if (mLockCounter-- > 0) 35 | { 36 | SDL_AtomicUnlock(&mSpinlock); 37 | SDL_UnlockMutex(mMutex); 38 | } 39 | else 40 | { 41 | SDL_AtomicUnlock(&mSpinlock); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/TextEditor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Editor.h" 4 | 5 | class TextEditor: public Editor 6 | { 7 | char *mBuffer; 8 | int mBufferSize; 9 | int mCursorPosition; 10 | bool mIsEditing; 11 | bool mAlwaysShowCursor; 12 | bool mSolidBackground; 13 | 14 | void typeText(const char *text); 15 | void handleBackspace(); 16 | 17 | public: 18 | TextEditor(EditorState& editorState); 19 | virtual ~TextEditor(); 20 | 21 | void setText(const char *text); 22 | void setBuffer(char *buffer, int bufferSize); 23 | void setIsEditing(bool state); 24 | void setSolidBackground(bool state); 25 | void setAlwaysShowCursor(bool state); 26 | 27 | virtual bool onEvent(SDL_Event& event); 28 | virtual void onDraw(Renderer& renderer, const SDL_Rect& area); 29 | }; 30 | -------------------------------------------------------------------------------- /src/Prototracker.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "SDL.h" 4 | #include 5 | 6 | struct EditorState; 7 | struct IPlayer; 8 | struct Song; 9 | struct ISynth; 10 | struct Mixer; 11 | struct MainEditor; 12 | struct Gamepad; 13 | struct Renderer; 14 | 15 | class Prototracker { 16 | Renderer *mRenderer; 17 | EditorState *mEditorState; 18 | IPlayer *mPlayer; 19 | Song *mSong; 20 | ISynth *mSynth; 21 | Mixer *mMixer; 22 | MainEditor *mMainEditor; 23 | Gamepad *mGamepad; 24 | 25 | Uint32 mPreviousTick; 26 | bool mReady; 27 | 28 | void initEditor(); 29 | bool initRenderer(); 30 | 31 | public: 32 | Prototracker(); 33 | ~Prototracker(); 34 | 35 | bool init(); 36 | void deinit(); 37 | bool handleEvents(); 38 | std::string getSongBase64() const; 39 | }; 40 | -------------------------------------------------------------------------------- /src/NoteState.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct EffectParam; 4 | struct ITrackState; 5 | struct PlayerState; 6 | 7 | struct NoteState 8 | { 9 | static const int slideDivider = 2048; 10 | 11 | float frequency, slideTarget; 12 | int volume; 13 | 14 | int effectMemory[256]; 15 | 16 | NoteState(); 17 | 18 | /* 19 | * Return true if the effect should restart pattern/macro effect processing 20 | * Used by Jxx to process the target row effects 21 | */ 22 | 23 | bool handleEffectZeroTick(const EffectParam& effect, ITrackState& trackState, PlayerState& playerState); 24 | void handleEffectAnyTick(const EffectParam& effect, ITrackState& trackState, PlayerState& playerState); 25 | 26 | void setFrequencyFromNote(int note); 27 | void setSlideTargetFromNote(int note); 28 | }; 29 | -------------------------------------------------------------------------------- /Makefile.emscripten: -------------------------------------------------------------------------------- 1 | # Makefile for Emscripten 2 | 3 | # Take note of the compilation options. Only the PNG image format is enabled 4 | # and various C-callbacks (available to the host webpage) are listed on the 5 | # command line. 6 | 7 | OUTPUT=prototracker 8 | SRC=src/*.cpp 9 | 10 | $(OUTPUT).html: $(SRC) src/*.h 11 | emcc -std=c++11 --preload-file assets -O3 -s NO_EXIT_RUNTIME=0 \ 12 | -s EXPORTED_FUNCTIONS='["_main"]' -s SDL2_IMAGE_FORMATS='["png"]' \ 13 | -s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall", "FS"]' -DSCALE=2 -DSAMPLERATE=22050 \ 14 | -s ALLOW_MEMORY_GROWTH=1 -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s BINARYEN_TRAP_MODE=clamp \ 15 | -s WASM=1 -s MODULARIZE=1 $(wildcard $(SRC)) -o $@ -s ASSERTIONS=1 16 | 17 | clean: 18 | rm $(OUTPUT).html $(OUTPUT).js $(OUTPUT).data $(OUTPUT).wasm 19 | -------------------------------------------------------------------------------- /src/PlayerState.cpp: -------------------------------------------------------------------------------- 1 | #include "PlayerState.h" 2 | 3 | PlayerState::PlayerState() 4 | : mode(Stop), songSpeed(6), songRate(50), sequenceRow(0), patternRow(0), cpuUse(0), tick(0), updateMask(-1) 5 | { 6 | 7 | } 8 | 9 | int PlayerState::getUpdated() 10 | { 11 | return updateMask; 12 | } 13 | 14 | 15 | void PlayerState::ackUpdated(int mask) 16 | { 17 | updateMask &= ~mask; 18 | } 19 | 20 | 21 | void PlayerState::setUpdated(int mask) 22 | { 23 | updateMask |= mask; 24 | } 25 | 26 | 27 | bool PlayerState::isPlaying() const 28 | { 29 | if (mode == Play || mode == PlaySequenceRow) 30 | return true; 31 | 32 | return false; 33 | } 34 | 35 | 36 | bool PlayerState::shouldAdvanceSequenceRow() const 37 | { 38 | if (mode == PlaySequenceRow) 39 | return false; 40 | 41 | return true; 42 | } 43 | -------------------------------------------------------------------------------- /src/SequenceRowEditor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ColumnEditor.h" 4 | 5 | struct Song; 6 | struct IPlayer; 7 | struct TrackEditorState; 8 | 9 | class SequenceRowEditor: public ColumnEditor 10 | { 11 | IPlayer& mPlayer; 12 | Song& mSong; 13 | 14 | void duplicateRow(); 15 | void insertRow(bool allTracks, int flags); 16 | void deleteRow(bool allTracks, int flags); 17 | void emptyRow(bool allTracks, int flags); 18 | virtual bool isRowActive(int track, int row) const; 19 | 20 | protected: 21 | virtual void onRequestCommandRegistration(); 22 | 23 | public: 24 | SequenceRowEditor(EditorState& editorState, IPlayer& player, Song& song); 25 | virtual ~SequenceRowEditor(); 26 | 27 | virtual bool onEvent(SDL_Event& event); 28 | virtual void onDraw(Renderer& renderer, const SDL_Rect& area); 29 | }; 30 | -------------------------------------------------------------------------------- /src/Synth.cpp: -------------------------------------------------------------------------------- 1 | #include "Synth.h" 2 | #include "Oscillator.h" 3 | #include "SequenceRow.h" 4 | #include "Sample.h" 5 | #include "WaveStore.h" 6 | #include "SDL.h" 7 | 8 | Synth::Synth() 9 | : ISynth() 10 | { 11 | mWaveStore = new WaveStore(); 12 | 13 | /* 14 | 15 | Initialize the audio tracks. 16 | 17 | */ 18 | 19 | for (int i = 0 ; i < SequenceRow::maxTracks ; ++i) 20 | { 21 | Oscillator *oscillator = new Oscillator(); 22 | oscillator->setWaveStore(*mWaveStore); 23 | mOscillator[i] = oscillator; 24 | } 25 | } 26 | 27 | 28 | Synth::~Synth() 29 | { 30 | delete mWaveStore; 31 | 32 | /* 33 | 34 | NOTE: ~ISynth() will delete the Oscillator objects we initialized 35 | above! No need to cleanup yourself. 36 | 37 | */ 38 | } 39 | 40 | 41 | const WaveStore& Synth::getWaveStore() const 42 | { 43 | return *mWaveStore; 44 | } 45 | -------------------------------------------------------------------------------- /src/ITrackState.cpp: -------------------------------------------------------------------------------- 1 | #include "ITrackState.h" 2 | #include "EffectParam.h" 3 | 4 | ITrackState::ITrackState() 5 | : triggered(false), enabled(true), tick(0), macro(0), macroSpeed(1), macroRow(0) 6 | { 7 | 8 | } 9 | 10 | 11 | ITrackState::~ITrackState() 12 | { 13 | } 14 | 15 | 16 | bool ITrackState::handleEffectZeroTick(const EffectParam& effect, PlayerState& playerState) 17 | { 18 | int asByte = effect.getParamsAsByte(); 19 | 20 | switch (effect.effect) 21 | { 22 | case 'j': 23 | macroRow = asByte; 24 | return true; 25 | 26 | case 'm': 27 | macro = asByte; 28 | macroRow = 0; 29 | break; 30 | 31 | case 's': 32 | macroSpeed = asByte; 33 | break; 34 | } 35 | 36 | return false; 37 | } 38 | 39 | 40 | void ITrackState::handleEffectAnyTick(const EffectParam& effect, PlayerState& playerState) 41 | { 42 | 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/TooltipManager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "Editor.h" 6 | #include "SDL.h" 7 | 8 | class TooltipManager 9 | { 10 | public: 11 | struct Tooltip { 12 | SDL_Rect area; 13 | std::string text; 14 | 15 | Tooltip(const SDL_Rect& area, const std::string& message); 16 | Tooltip(); 17 | }; 18 | 19 | private: 20 | static const int HoverDelayMs = 500; 21 | 22 | Tooltip mTooltip; 23 | bool mVisible, mWasVisible, mDidJustAppear; 24 | int mHoveredMs; 25 | 26 | public: 27 | TooltipManager(); 28 | void update(int ms); 29 | void setTooltip(const SDL_Rect& area, const std::string& message); 30 | void updateTooltipMotion(int x, int y); 31 | 32 | /* Get tooltip if visible 33 | */ 34 | const Tooltip* getVisibleTooltip() const; 35 | 36 | bool isDirty() const; 37 | void setDirty(bool state); 38 | }; 39 | -------------------------------------------------------------------------------- /src/Color.cpp: -------------------------------------------------------------------------------- 1 | #include "Color.h" 2 | #include "EffectParam.h" 3 | 4 | Color::Color(int _r, int _g, int _b, int _a) 5 | : r(_r), g(_g), b(_b), a(_a) 6 | { 7 | } 8 | 9 | 10 | Color Color::getEffectColor(const EffectParam& effect) 11 | { 12 | if (effect.isEmpty()) 13 | return Color(128, 128, 128); 14 | 15 | switch (effect.effect) 16 | { 17 | default: 18 | return Color(192, 192, 192); 19 | 20 | case 'n': 21 | return Color(128, 255, 128); 22 | 23 | case 'b': 24 | case 'd': 25 | case 'j': 26 | return Color(255, 128, 128); 27 | 28 | case 'a': 29 | case 'c': 30 | case 'k': 31 | return Color(192, 192, 255); 32 | 33 | case 'm': 34 | return Color(255, 255, 255); 35 | 36 | case 'w': 37 | case 'q': 38 | return Color(255, 192, 128); 39 | 40 | case 'f': 41 | case 's': 42 | return Color(192, 255, 128); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /doc/EFFECTS.TXT: -------------------------------------------------------------------------------- 1 | 1xx Portamento up by xx, track and macro pitches are separate 2 | 2xx Portamento down by xx 3 | 3xx Slide to note 4 | Axy Slide volume up by x, down by y 5 | Bxx Jump to sequence position xx (at the end of pattern row) 6 | Cxx Set volume to xx ($00..$40), track and macro volumes are separate 7 | Dxx Jump to next sequence position and start at pattern row xx (at the end of pattern row) 8 | Fxx Set speed to xx ticks per row (when xx < $20), set play rate to xx Hz (when xx >= $20) 9 | Jxx Jump to macro row xx (immediate) 10 | Kxx Set volume to zero at tick xx (K00 = immediately) 11 | Mxx Set macro to xx 12 | Sxx Set macro speed to xx ticks per row 13 | Wxx Set waveform to xx (including but not limited to: $00-$0f = square wave with varying PW, $7F = triangle, $FF = noise) 14 | Qxx Queue waveform xx (switch waveform after currently playing waveform loops) 15 | -------------------------------------------------------------------------------- /src/CopyBuffer.cpp: -------------------------------------------------------------------------------- 1 | #include "CopyBuffer.h" 2 | #include "PatternRow.h" 3 | #include 4 | 5 | CopyBuffer::CopyBuffer() 6 | : Pattern(), mLength(0) 7 | { 8 | 9 | } 10 | 11 | 12 | CopyBuffer::~CopyBuffer() 13 | { 14 | } 15 | 16 | 17 | int CopyBuffer::getLength() const 18 | { 19 | return mLength; 20 | } 21 | 22 | 23 | void CopyBuffer::setLength(int length) 24 | { 25 | mLength = length; 26 | } 27 | 28 | 29 | void CopyBuffer::copy(Pattern& source, int start, int end) 30 | { 31 | setLength(end - start + 1); 32 | 33 | for (int i = start, d = 0 ; i <= end ; ++i, ++d) 34 | { 35 | getRow(d) = source.getRow(i); 36 | } 37 | } 38 | 39 | 40 | void CopyBuffer::paste(Pattern& destination, int start) 41 | { 42 | for (int i = 0, d = start ; i < mLength ; ++i, ++d) 43 | { 44 | if (d >= Pattern::maxRows) 45 | break; 46 | destination.getRow(d) = getRow(i); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/TouchRegion.cpp: -------------------------------------------------------------------------------- 1 | #include "TouchRegion.h" 2 | #include "SDL.h" 3 | #include 4 | 5 | TouchRegion::TouchRegion(EditorState& editorState, SDL_Scancode scancode, SDL_Keymod keymod) 6 | : Editor(editorState, false), mScancode(scancode), mKeymod(keymod) 7 | { 8 | mKeycode = SDL_GetKeyFromScancode(scancode); 9 | } 10 | 11 | 12 | bool TouchRegion::onEvent(SDL_Event& event) 13 | { 14 | if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) 15 | { 16 | SDL_Event keyhit = {0}; 17 | keyhit.type = event.type == SDL_MOUSEBUTTONUP ? SDL_KEYUP : SDL_KEYDOWN; 18 | keyhit.key.keysym.mod = mKeymod; 19 | keyhit.key.keysym.scancode = mScancode; 20 | keyhit.key.keysym.sym = mKeycode; 21 | 22 | SDL_PushEvent(&keyhit); 23 | return true; 24 | } 25 | 26 | return false; 27 | } 28 | 29 | 30 | void TouchRegion::onDraw(Renderer& renderer, const SDL_Rect& area) 31 | { 32 | } 33 | -------------------------------------------------------------------------------- /src/PatternEditor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "TrackEditor.h" 4 | 5 | struct Song; 6 | struct SequenceRow; 7 | 8 | class PatternEditor: public TrackEditor 9 | { 10 | protected: 11 | PatternRow& getPatternRow(int track, int row); 12 | 13 | protected: 14 | virtual PatternRow& getCurrentPatternRow(); 15 | virtual Pattern& getCurrentPattern(int track); 16 | virtual SequenceRow& getCurrentSequenceRow(); 17 | virtual void findUnusedTrack(int track); 18 | virtual bool isRowActive(int track, int row) const; 19 | 20 | void setCurrentPattern(int pattern); 21 | 22 | virtual void onRequestCommandRegistration(); 23 | 24 | public: 25 | PatternEditor(EditorState& editorState, IPlayer& player, Song& song); 26 | virtual ~PatternEditor(); 27 | 28 | void setPattern(int track, int pattern); 29 | void setPatternRow(int row); 30 | void setSequenceRow(int row); 31 | 32 | virtual bool onEvent(SDL_Event& event); 33 | }; 34 | -------------------------------------------------------------------------------- /scripts/buildComponent.js: -------------------------------------------------------------------------------- 1 | const Scaffold = require('scaffold-generator') 2 | const mustache = require('mustache') 3 | const argv = require('yargs') 4 | .usage('Usage: $0 --name moduleName') 5 | .version(false) 6 | .option('name', { describe: 'Unique editor class name', type: 'string' }) 7 | .demandOption(['name'], 'Please specify a name') 8 | .argv; 9 | let editorName = ''; 10 | 11 | if (typeof argv.name === 'string') { 12 | editorName = argv.name; 13 | } 14 | 15 | if (!editorName || editorName === '') { 16 | console.error('Please set editor name with --name'); 17 | process.exit(1); 18 | } 19 | 20 | mustache.escape = v => v 21 | 22 | new Scaffold({ 23 | data: { 24 | Name: editorName, 25 | }, 26 | render: mustache.render 27 | }) 28 | .copy('templates/Editor', 'src') 29 | .then(() => { 30 | console.log(`${editorName} generated. Next steps: 31 | - Add enum to Theme::ElementType 32 | - Add handler to MainEditor::loadElements()`); 33 | }); 34 | -------------------------------------------------------------------------------- /src/AudioDeviceSelector.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Editor.h" 4 | 5 | struct EditorState; 6 | struct Renderer; 7 | struct SDL_Rect; 8 | struct Mixer; 9 | 10 | #include "GenericSelector.h" 11 | 12 | class AudioDeviceSelector: public GenericSelector 13 | { 14 | 15 | struct AudioDeviceItem: public Item { 16 | std::string name; 17 | AudioDeviceItem(const char *name); 18 | }; 19 | 20 | virtual void renderItem(Renderer& renderer, const SDL_Rect& area, const Item& item, bool isSelected); 21 | virtual void accept(bool isFinal = false); 22 | virtual void reject(bool isFinal = false); 23 | 24 | public: 25 | AudioDeviceSelector(EditorState& editorState); 26 | virtual ~AudioDeviceSelector(); 27 | 28 | /* Fill device list from Mixer 29 | */ 30 | void populate(const Mixer& mixer); 31 | 32 | 33 | /* After the dialog finishes this will return the path 34 | * and filename to selected (or new) file 35 | */ 36 | const char * getSelectedDevice() const; 37 | }; 38 | -------------------------------------------------------------------------------- /src/ColumnEditor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Editor.h" 4 | #include "SDL.h" 5 | 6 | struct TrackEditorState; 7 | 8 | class ColumnEditor: public Editor 9 | { 10 | protected: 11 | TrackEditorState& mTrackEditorState; 12 | int maxTracks, maxRows, mColumns; 13 | int mRowNumberMargin, mTrackMargin; 14 | 15 | int getNoteFromKey(const SDL_Keysym& sym) const; 16 | int getHexFromKey(const SDL_Keysym& sym) const; 17 | int getCharFromKey(const SDL_Keysym& sym) const; 18 | void changeTrack(int d); 19 | 20 | virtual void changeColumn(int d); 21 | 22 | void scrollView(int d, bool wrap = true); 23 | 24 | // Return true if play position is at this row 25 | virtual bool isRowActive(int track, int row) const; 26 | 27 | public: 28 | ColumnEditor(EditorState& editorState, TrackEditorState& trackEditorState, int tracks, int columns); 29 | virtual ~ColumnEditor(); 30 | 31 | void setRowNumberMargin(int margin); 32 | void setTrackMargin(int margin); 33 | 34 | void setMaxRows(int rows); 35 | }; 36 | -------------------------------------------------------------------------------- /src/ITrackState.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "NoteState.h" 4 | 5 | /* 6 | 7 | ITrackState handles the common track state. That is, things like macro state and channel mute 8 | are handled here so the class extending ITrackState doesn't have to. 9 | 10 | Override handleEffectAnyTick() and handleEffectZeroTick() to add your own behavior. 11 | 12 | */ 13 | 14 | struct ITrackState 15 | { 16 | NoteState trackState; 17 | NoteState macroState; 18 | 19 | // Note was triggered during current tick 20 | // Set to false after handled. 21 | bool triggered; 22 | 23 | // Track is enabled (not muted) 24 | bool enabled; 25 | 26 | int tick; 27 | int macro; 28 | int macroSpeed; 29 | int macroRow; 30 | 31 | ITrackState(); 32 | virtual ~ITrackState(); 33 | 34 | virtual bool handleEffectZeroTick(const EffectParam& effect, PlayerState& playerState); 35 | virtual void handleEffectAnyTick(const EffectParam& effect, PlayerState& playerState); 36 | 37 | static const int maxVolume = 64; 38 | }; 39 | -------------------------------------------------------------------------------- /src/EditorState.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Value.h" 4 | #include "CopyBuffer.h" 5 | #include 6 | 7 | struct FileSection; 8 | 9 | struct TrackEditorState 10 | { 11 | Value currentRow; 12 | Value currentTrack; 13 | Value currentColumn; 14 | Value editSkip; 15 | 16 | int blockStart, blockEnd; 17 | 18 | TrackEditorState(); 19 | 20 | FileSection * pack(); 21 | bool unpack(const FileSection& section); 22 | }; 23 | 24 | struct EditorState 25 | { 26 | Value macro; 27 | Value octave; 28 | Value editMode; 29 | 30 | TrackEditorState sequenceEditor; 31 | TrackEditorState patternEditor; 32 | TrackEditorState macroEditor; 33 | 34 | CopyBuffer copyBuffer; 35 | 36 | bool followPlayPosition; 37 | std::string audioDevice; 38 | 39 | EditorState(); 40 | 41 | FileSection * pack(); 42 | bool unpack(const FileSection& section); 43 | 44 | // Should use this to reset state instead of assigning 45 | // to an empty EditorState() to avoid double frees in CopyBuffer 46 | void reset(); 47 | }; 48 | -------------------------------------------------------------------------------- /src/Oscillator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "IOscillator.h" 4 | 5 | struct WaveStore; 6 | 7 | #ifndef OVERSAMPLE 8 | #define OVERSAMPLE 4 9 | #endif 10 | 11 | class Oscillator: public IOscillator 12 | { 13 | const WaveStore* mWaveStore; 14 | 15 | int mWave, mQueuedWave; 16 | int mSpeed; 17 | int mPosition; 18 | int mVolume; 19 | 20 | public: 21 | static const int volumeResolution = 8192; 22 | static const int oscillatorLength = 256; 23 | static const int oscillatorOversample = OVERSAMPLE; 24 | 25 | Oscillator(); 26 | virtual ~Oscillator(); 27 | 28 | virtual void triggerNote(); 29 | virtual void handleTrackState(ITrackState& trackState); 30 | void setWaveStore(const WaveStore& sampleStore); 31 | void setPosition(int newPosition); 32 | void setWave(int wave); 33 | void queueWave(int wave); 34 | 35 | virtual void setFrequency(float frequency); 36 | virtual void setVolume(int volume); 37 | virtual void update(int numSamples); 38 | virtual void render(Sample16 *buffer, int numSamples, int offset = 0); 39 | }; 40 | -------------------------------------------------------------------------------- /src/TrackState.cpp: -------------------------------------------------------------------------------- 1 | #include "TrackState.h" 2 | #include "EffectParam.h" 3 | 4 | TrackState::TrackState() 5 | : ITrackState(), wave(0), queuedWave(0) 6 | { 7 | 8 | } 9 | 10 | 11 | bool TrackState::handleEffectZeroTick(const EffectParam& effect, PlayerState& playerState) 12 | { 13 | int asByte = effect.getParamsAsByte(); 14 | 15 | switch (effect.effect) 16 | { 17 | /* 18 | 19 | Handle effects relevant to our ITrackState and IOscillator classes. 20 | 21 | */ 22 | 23 | case 'w': 24 | wave = asByte; 25 | queuedWave = asByte; 26 | break; 27 | 28 | case 'q': 29 | queuedWave = asByte; 30 | break; 31 | 32 | /* 33 | 34 | In case we didn't handle the effect, let the parent class handle it. 35 | 36 | */ 37 | 38 | default: 39 | return ITrackState::handleEffectZeroTick(effect, playerState); 40 | } 41 | 42 | return false; 43 | } 44 | 45 | 46 | void TrackState::handleEffectAnyTick(const EffectParam& effect, PlayerState& playerState) 47 | { 48 | ITrackState::handleEffectAnyTick(effect, playerState); 49 | } 50 | -------------------------------------------------------------------------------- /src/EffectParam.cpp: -------------------------------------------------------------------------------- 1 | #include "EffectParam.h" 2 | #include "PatternRow.h" 3 | #include 4 | 5 | EffectParam::EffectParam(int _effect, int _param1, int _param2) 6 | : effect(_effect), param1(_param1), param2(_param2) 7 | { 8 | } 9 | 10 | 11 | EffectParam::EffectParam(int _effect, int params) 12 | : effect(_effect) 13 | { 14 | setParamsFromByte(params); 15 | } 16 | 17 | 18 | int EffectParam::getParamsAsByte() const 19 | { 20 | return param2 + (param1 << 4); 21 | } 22 | 23 | 24 | void EffectParam::setParamsFromByte(int byte) 25 | { 26 | param1 = (byte >> 4) & 0xf; 27 | param2 = byte & 0xf; 28 | } 29 | 30 | 31 | void EffectParam::setNoteAndOctave(int noteAndOctave) 32 | { 33 | effect = 'n'; 34 | param1 = noteAndOctave % 12; 35 | param2 = std::min(15, noteAndOctave / 12); 36 | } 37 | 38 | int EffectParam::getNoteWithOctave() const 39 | { 40 | if (effect != 'n') 41 | return PatternRow::NoNote; 42 | 43 | return param1 + param2 * 12; 44 | } 45 | 46 | 47 | bool EffectParam::isEmpty() const 48 | { 49 | return effect == '0' && param1 == 0 && param2 == 0; 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tero Lindeman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Emscripten.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | 5 | C-callbacks for Emscripten 6 | 7 | */ 8 | 9 | #include "SDL.h" 10 | 11 | // We use SDL_Events to signal the app actions from the host webpage 12 | enum { 13 | EVERYTHING_DONE = SDL_USEREVENT, 14 | EVERYTHING_READY, 15 | SONG_IMPORTED, 16 | EXPORT_SONG, 17 | PLAY_SONG, 18 | NEW_SONG 19 | }; 20 | 21 | extern "C" 22 | { 23 | 24 | // Emscripten init routines 25 | void emSyncFs(); 26 | void emSyncFsAndShutdown(); 27 | void emSyncFsAndStartup(); 28 | 29 | // The webpage has imported a song in the Emscripten file system (imported-song.song), tells the app to load the song 30 | void emOnFileImported(); 31 | 32 | // Saves the song in the Emscripten file system and triggers the host webpage to open a file download 33 | void emExportFile(); 34 | 35 | // Used by the host webpage to trigger actions when webpage buttons are clicked 36 | void emNewSong(); 37 | void emPlaySong(); 38 | 39 | // Called by the app when ready and running (signals the host webpage we are ready to go) 40 | void emAppReady(); 41 | void emAppShutdown(); 42 | 43 | // Returns the current song as Base64 44 | const char * emRequestSong(); 45 | 46 | }; 47 | -------------------------------------------------------------------------------- /src/Main.cpp: -------------------------------------------------------------------------------- 1 | #include "SDL.h" 2 | #include "SDL_image.h" 3 | #include "Debug.h" 4 | #include "Prototracker.h" 5 | #include 6 | 7 | #ifdef __EMSCRIPTEN__ 8 | #include "Emscripten.h" 9 | #include 10 | 11 | Prototracker *g_prototracker = NULL; 12 | 13 | #endif 14 | 15 | void runLoop(void *prototracker) 16 | { 17 | static_cast(prototracker)->handleEvents(); 18 | } 19 | 20 | 21 | extern "C" int main(int argc, char **argv) 22 | { 23 | #ifdef __EMSCRIPTEN__ 24 | SDL_SetHint(SDL_HINT_GRAB_KEYBOARD, "1"); 25 | #endif 26 | SDL_Init(SDL_INIT_EVERYTHING|SDL_INIT_NOPARACHUTE); 27 | atexit(SDL_Quit); 28 | 29 | IMG_Init(IMG_INIT_PNG); 30 | atexit(IMG_Quit); 31 | 32 | Prototracker prototracker; 33 | 34 | if (!prototracker.init()) 35 | { 36 | return 1; 37 | } 38 | 39 | #ifdef __EMSCRIPTEN__ 40 | g_prototracker = &prototracker; 41 | 42 | emSyncFsAndStartup(); 43 | emscripten_set_main_loop_arg(runLoop, &prototracker, -1, 1); 44 | 45 | #else 46 | 47 | while (prototracker.handleEvents()) 48 | { 49 | // Spin around 50 | } 51 | #endif 52 | 53 | prototracker.deinit(); 54 | 55 | debug("Exiting..."); 56 | 57 | return 0; 58 | } 59 | -------------------------------------------------------------------------------- /src/ISynth.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct IOscillator; 4 | struct Sample16; 5 | 6 | /* 7 | 8 | ISynth handles the IOscillators and some things needed by the GUI 9 | (namely, data for visualization). 10 | 11 | Override ISynth() to initialize your own IOscillators. 12 | 13 | */ 14 | 15 | class ISynth 16 | { 17 | protected: 18 | IOscillator **mOscillator; 19 | Sample16 *mPreviousOscillatorOutput, *mTempBuffer; 20 | int mProbePosition; 21 | 22 | public: 23 | ISynth(); 24 | virtual ~ISynth(); 25 | 26 | virtual void reset(); 27 | 28 | static const int oscillatorProbeLength = 128; 29 | 30 | const Sample16* getOscillatorProbe(int oscillator) const; 31 | IOscillator& getOscillator(int i); 32 | int getProbePosition() const; 33 | void setSampleRate(int rate); 34 | 35 | // Update the synth state 36 | virtual void update(int numSamples); 37 | 38 | /* 39 | Get samples (does not change synth state) 40 | Note: ISynth should overwrite the values in the buffer; 41 | it might contain random values. Fill with zeroes if 42 | the output should be silent. 43 | 44 | This should also update mPreviousOscillatorOutput for 45 | the GUI. 46 | */ 47 | virtual void render(Sample16 *buffer, int numSamples); 48 | }; 49 | -------------------------------------------------------------------------------- /src/IOscillator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct Sample16; 4 | struct ITrackState; 5 | 6 | class IOscillator 7 | { 8 | protected: 9 | bool mIsEnabled; 10 | int mSampleRate; 11 | public: 12 | static const int oscillatorResolution = 4096; 13 | 14 | IOscillator(); 15 | virtual ~IOscillator(); 16 | virtual void setSampleRate(int rate); 17 | virtual void setEnabled(bool enabled); 18 | 19 | // The oscillator should update its own parameters from handleTrackState() 20 | virtual void handleTrackState(ITrackState& trackState); 21 | 22 | 23 | /* 24 | IOscillator needs to implement these. 25 | */ 26 | 27 | // Note has been triggered 28 | virtual void triggerNote() = 0; 29 | 30 | // Set oscillator frequency. 1.0 = 440 Hz. 31 | virtual void setFrequency(float frequency) = 0; 32 | 33 | // Needs to be implemented since the player uses this to mute/solo channels 34 | // Range is [0..TrackState::maxVolume] (i.e. [0..64]) 35 | virtual void setVolume(int volume) = 0; 36 | 37 | // Update internal oscillator state 38 | virtual void update(int numSamples) = 0; 39 | 40 | // Fill buffer with samples (do not change oscillator state) 41 | // NOTE: This method should add its output to the values already in the buffer 42 | virtual void render(Sample16 *buffer, int numSamples, int offset = 0) = 0; 43 | }; 44 | -------------------------------------------------------------------------------- /src/Label.cpp: -------------------------------------------------------------------------------- 1 | #include "Label.h" 2 | #include "Renderer.h" 3 | #include "Color.h" 4 | #include 5 | #include 6 | #include 7 | 8 | Label::Label(EditorState& editorState) 9 | : Editor(editorState, false), mTextColor(255, 255, 255), mBackgroundColor(0, 0, 0) 10 | { 11 | mBuffer = new char[maxTextLength + 1]; 12 | setText(""); 13 | } 14 | 15 | 16 | Label::~Label() 17 | { 18 | delete[] mBuffer; 19 | } 20 | 21 | 22 | void Label::setColor(const Color& color) 23 | { 24 | mTextColor = color; 25 | } 26 | 27 | 28 | void Label::setBackground(const Color& color) 29 | { 30 | mBackgroundColor = color; 31 | } 32 | 33 | 34 | void Label::setText(const char *text) 35 | { 36 | strncpy(mBuffer, text, maxTextLength); 37 | setDirty(true); 38 | } 39 | 40 | 41 | void Label::setTextV(const char *text, ...) 42 | { 43 | va_list argptr; 44 | va_start(argptr, text); 45 | vsnprintf(mBuffer, maxTextLength, text, argptr); 46 | va_end(argptr); 47 | 48 | setDirty(true); 49 | } 50 | 51 | 52 | bool Label::onEvent(SDL_Event& event) 53 | { 54 | return false; 55 | } 56 | 57 | 58 | void Label::onDraw(Renderer& renderer, const SDL_Rect& area) 59 | { 60 | setDirty(false); 61 | 62 | renderer.clearRect(area, mBackgroundColor); 63 | 64 | renderer.renderText(area, mTextColor, mBuffer); 65 | } 66 | -------------------------------------------------------------------------------- /src/MessageManager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "Editor.h" 6 | 7 | class MessageManager 8 | { 9 | public: 10 | static const int MessageVisibleMs = 2000; 11 | static const int MessageAnimationMs = 100; 12 | 13 | struct Message 14 | { 15 | int id; 16 | // Remaining milliseconds the message should still be displayed 17 | // value (will decrease) 18 | int delay; 19 | Editor::MessageClass messageClass; 20 | std::string text; 21 | 22 | Message(int id, Editor::MessageClass messageClass, const std::string& message); 23 | void resetDelay(); 24 | 25 | float getVisibility() const; 26 | }; 27 | 28 | private: 29 | bool mWasVisible; 30 | std::list mMessageQueue; 31 | int mMessageIDCounter; 32 | 33 | public: 34 | MessageManager(); 35 | void update(int ms); 36 | 37 | /* Push a new message in the queue or replace the message with the matching ID 38 | * Returns the message ID for the new message 39 | */ 40 | int pushMessage(Editor::MessageClass messageClass, const std::string& message, int messageId = 0); 41 | 42 | /* Get topmost queued message 43 | */ 44 | const Message* getVisibleMessage() const; 45 | 46 | /* Returns true if there was a message in the queue since last update() 47 | */ 48 | bool getWasMessageVisibleLastFrame() const; 49 | }; 50 | -------------------------------------------------------------------------------- /src/CommandSelector.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Editor.h" 4 | 5 | struct EditorState; 6 | struct Renderer; 7 | struct SDL_Rect; 8 | struct Mixer; 9 | struct MainEditor; 10 | struct Label; 11 | 12 | #include "GenericSelector.h" 13 | 14 | class CommandSelector: public GenericSelector 15 | { 16 | const MainEditor& mMainEditor; 17 | TextEditor *mFilterField; 18 | Label *mFilterLabel; 19 | char mFilter[200]; 20 | 21 | struct CommandItem: public Item { 22 | const Editor::CommandDescriptor& command; 23 | static bool sort(const Item* a, const Item* b); 24 | CommandItem(const Editor::CommandDescriptor& command); 25 | }; 26 | 27 | virtual void renderItem(Renderer& renderer, const SDL_Rect& area, const Item& item, bool isSelected); 28 | virtual void accept(bool isFinal = false); 29 | virtual void reject(bool isFinal = false); 30 | 31 | static bool caseInsensitiveFind(const char *haystack, const char *needle); 32 | 33 | public: 34 | CommandSelector(EditorState& editorState, const MainEditor& mainEditor); 35 | virtual ~CommandSelector(); 36 | 37 | /* Fill command list from MainEditor 38 | */ 39 | void populate(); 40 | 41 | 42 | /* After the dialog finishes this will return the command 43 | */ 44 | const CommandDescriptor& getSelectedCommand() const; 45 | 46 | virtual void onModalStatusChange(bool isNowModal); 47 | virtual bool onEvent(SDL_Event& event); 48 | virtual void onRendererMount(const Renderer& renderer); 49 | }; 50 | -------------------------------------------------------------------------------- /src/CommandOptionSelector.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Editor.h" 4 | 5 | struct EditorState; 6 | struct Renderer; 7 | struct SDL_Rect; 8 | struct Mixer; 9 | struct MainEditor; 10 | struct Label; 11 | 12 | #include "GenericSelector.h" 13 | 14 | class CommandOptionSelector: public GenericSelector 15 | { 16 | const CommandDescriptor& mCommandDescriptor; 17 | TextEditor *mFilterField; 18 | Label *mFilterLabel; 19 | char mFilter[200]; 20 | 21 | public: 22 | struct CommandOption: public Item { 23 | int value; 24 | CommandOption(int value); 25 | }; 26 | 27 | private: 28 | virtual void renderItem(Renderer& renderer, const SDL_Rect& area, const Item& item, bool isSelected); 29 | virtual void accept(bool isFinal = false); 30 | virtual void reject(bool isFinal = false); 31 | 32 | static bool caseInsensitiveFind(const char *haystack, const char *needle); 33 | 34 | public: 35 | CommandOptionSelector(EditorState& editorState, const CommandDescriptor& command); 36 | virtual ~CommandOptionSelector(); 37 | 38 | /* Fill command list from MainEditor 39 | */ 40 | void populate(); 41 | 42 | 43 | /* After the dialog finishes this will return the option 44 | */ 45 | const CommandOption& getSelectedOption() const; 46 | 47 | virtual void onModalStatusChange(bool isNowModal); 48 | virtual bool onEvent(SDL_Event& event); 49 | virtual void onRendererMount(const Renderer& renderer); 50 | 51 | /** 52 | * Use when registering commands 53 | */ 54 | 55 | void addIntItem(int value); 56 | }; 57 | -------------------------------------------------------------------------------- /assets/elements: -------------------------------------------------------------------------------- 1 | Font "font.png" 8 8 2 | GUI "gui.png" 320 240 3 | 4 | # this is a comment 5 | # element x y w h track 6 | Oscilloscope 2 2 55 61 0 7 | Oscilloscope 59 2 55 61 1 8 | Oscilloscope 116 2 55 61 2 9 | Oscilloscope 173 2 55 61 3 10 | 11 | # element x y w h m1 m2 12 | PatternEditor 3 78 232 160 4 4 13 | MacroEditor 241 78 76 160 4 4 14 | SequenceEditor 234 3 82 40 2 0 15 | 16 | # element x y w h 17 | SongName 4 67 128 8 18 | MacroName 258 67 56 8 19 | MacroNumber 241 67 16 8 20 | SequencePosition 235 54 16 8 21 | SequenceLength 256 54 16 8 22 | PatternLength 277 54 16 8 23 | OctaveNumber 298 54 16 8 24 | 25 | # Color name r g b 26 | Color "CurrentRow" 0 0 0 27 | Color "BlockMarker" 255 0 0 28 | Color "EditCursor" 255 0 0 29 | Color "NonEditCursor" 0 0 255 30 | Color "RowCounter" 128 128 255 31 | Color "SelectedRow" 255 0 0 32 | Color "ModalBackground" 0 0 0 33 | Color "ModalTitleBackground" 255 255 255 34 | Color "ModalTitleText" 0 0 0 35 | Color "ModalBorder" 255 255 255 36 | Color "NormalText" 255 255 255 37 | Color "CommandShortcut" 192 192 192 38 | Color "CommandShortcutBackground" 64 64 64 39 | Color "ScrollBar" 255 255 255 40 | Color "PlayHead" 0 64 0 41 | Color "TextCursor" 255 255 255 42 | Color "TextBackground" 0 0 0 43 | Color "TextFocus" 255 0 0 44 | Color "Oscilloscope" 255 255 255 45 | Color "MutedOscilloscope" 128 128 128 46 | -------------------------------------------------------------------------------- /src/WaveGen.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // This class is used to generate the various one-cycle waveforms. 4 | // Used to init the wavetable on app start, it's not used realtime! 5 | 6 | class WaveGen 7 | { 8 | int mCarrier, mModulator; 9 | int mModulatorRatio; 10 | int mPulseWidth, mBoost; 11 | void initSeed(int seed); 12 | 13 | public: 14 | WaveGen(); 15 | ~WaveGen(); 16 | 17 | void generate(int seed, int *data, int length, int amplitude); 18 | 19 | // Convert continuous input value into discrete steps 20 | // Basically the same as a "bit crush" effect 21 | static float step(float in, int steps); 22 | 23 | // Sine wave between -1..1 24 | static float sine(float phase); 25 | 26 | // Saw wave from -1 to 1 27 | static float saw(float phase); 28 | 29 | // Square wave alternating between -1 and 1 (variable pulse width) 30 | static float square(float phase, float pw = 0.5f); 31 | 32 | // Square wave alternating between 0 and 1 (variable pulse width) 33 | static float zSquare(float phase, float pw = 0.5f); 34 | 35 | // The seed number is divided into 1-2 bit integers that specify 36 | // various wave generator modes 37 | static const int pwBit = 0; 38 | static const int pwWidth = 2; 39 | static const int boostBit = 1; 40 | static const int boostWidth = 3; 41 | static const int carrierBit = 6; 42 | static const int carrierWidth = 2; 43 | static const int modulatorBit = 4; 44 | static const int modulatorWidth = 2; 45 | static const int modulatorRatioBit = 2; 46 | static const int modulatorRatioWidth = 2; 47 | }; 48 | -------------------------------------------------------------------------------- /src/Sequence.cpp: -------------------------------------------------------------------------------- 1 | #include "Sequence.h" 2 | #include "SequenceRow.h" 3 | #include "Song.h" 4 | #include 5 | 6 | Sequence::Sequence() 7 | { 8 | rows = new SequenceRow[Song::maxSequenceRows]; 9 | } 10 | 11 | 12 | Sequence::~Sequence() 13 | { 14 | delete[] rows; 15 | } 16 | 17 | 18 | int Sequence::getLastSequenceRowUsed() const 19 | { 20 | int last = -1; 21 | 22 | for (int i = 0 ; i < Song::maxSequenceRows ; ++i) 23 | { 24 | if (!rows[i].isEmpty()) 25 | last = std::max(last, i); 26 | } 27 | 28 | return last; 29 | } 30 | 31 | 32 | SequenceRow& Sequence::getRow(int sequenceRow) 33 | { 34 | return rows[sequenceRow]; 35 | } 36 | 37 | 38 | void Sequence::clear() 39 | { 40 | for (int i = 0 ; i < Song::maxSequenceRows ; ++i) 41 | { 42 | rows[i].clear(); 43 | } 44 | } 45 | 46 | 47 | void Sequence::insertRow(int row, bool allTracks) 48 | { 49 | for (int i = maxRows - 1 ; i > row ; --i) 50 | { 51 | SequenceRow& src = rows[i - 1]; 52 | SequenceRow& dest = rows[i]; 53 | 54 | for (int track = 0 ; track < SequenceRow::maxTracks ; ++track) 55 | dest.pattern[track] = src.pattern[track]; 56 | } 57 | 58 | rows[row].clear(); 59 | } 60 | 61 | 62 | void Sequence::deleteRow(int row, bool allTracks) 63 | { 64 | for (int i = row ; i < maxRows - 1 ; ++i) 65 | { 66 | SequenceRow& src = rows[i + 1]; 67 | SequenceRow& dest = rows[i]; 68 | 69 | for (int track = 0 ; track < SequenceRow::maxTracks ; ++track) 70 | dest.pattern[track] = src.pattern[track]; 71 | } 72 | 73 | rows[maxRows - 1].clear(); 74 | } 75 | -------------------------------------------------------------------------------- /doc/FORMAT.TXT: -------------------------------------------------------------------------------- 1 | SONG FILE FORMAT 2 | 3 | A song file consists of sections. A section is four-character identifier followed by 4 | the size of the section as one 32-bit integer (big-endian). A section can (and will) 5 | contain more sections. 6 | 7 | byte 8-bit integer 8 | interleaved interleaved byte data (see below) 9 | stringz zero-limited string 10 | 11 | Macros and patterns consist of rows which are just four bytes (effect/note, param/note 12 | number, effect, param). Patterns are interleaved, first come all the bytes in the first 13 | column, then second etc. Same applies to the sequence, first comes the whole first 14 | track sequence, then the second and so on. 15 | 16 | section SONG 17 | { 18 | byte version 19 | if version >= 17 20 | { 21 | byte trackCount 22 | byte effectColumnCount 23 | } 24 | stringz name 25 | byte numPatternRows 26 | byte numSequenceRows 27 | 28 | SEQU 29 | byte numRows 30 | { 31 | interleaved row 32 | } 33 | 34 | PATT 35 | if version >= 17 36 | { 37 | for all tracks 38 | { 39 | byte numPatterns 40 | { 41 | numPatternRows 42 | { 43 | interleaved row 44 | } 45 | } 46 | } 47 | } 48 | else 49 | { 50 | byte numPatterns 51 | { 52 | numPatternRows 53 | { 54 | interleaved row 55 | } 56 | } 57 | } 58 | 59 | MACR 60 | byte numMacros 61 | { 62 | stringz name 63 | byte numRows 64 | { 65 | interleaved row 66 | } 67 | } 68 | } 69 | 70 | 71 | 72 | MACRO FILE FORMAT 73 | 74 | MACR 75 | { 76 | stringz name 77 | numRows 78 | { 79 | row 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/IPlayer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PlayerState.h" 4 | #include "Lockable.h" 5 | 6 | struct Song; 7 | struct ITrackState; 8 | struct PatternRow; 9 | 10 | /* 11 | 12 | IPlayer handles most of the player logic. 13 | 14 | You need to override IPlayer() to setup your own ITrackStates for the tracks. 15 | 16 | */ 17 | 18 | class IPlayer: public Lockable 19 | { 20 | protected: 21 | Song& mSong; 22 | PlayerState state; 23 | ITrackState **trackState; 24 | 25 | void processZeroTick(); 26 | void processAnyTick(); 27 | 28 | /* 29 | Return false if processLastTick handled sequenceRow advance etc. 30 | */ 31 | bool processLastTick(); 32 | void advanceSequenceRow(); 33 | void advanceSequenceTick(); 34 | void advanceTrackTick(int track); 35 | void advancePatternRow(); 36 | void handleNoteOn(); 37 | void handleMacroNote(int track); 38 | void handleMacroTick(int track); 39 | void processZeroTick(int track, const PatternRow& row); 40 | 41 | public: 42 | IPlayer(Song& song); 43 | virtual ~IPlayer(); 44 | 45 | void reset(); 46 | 47 | int getTick() const; 48 | 49 | void play(int sequenceRow); 50 | void play(int sequenceRow, int mode); 51 | void stop(); 52 | void muteTracks(); 53 | void triggerNote(int track, int note); 54 | void triggerNote(int track, const PatternRow& row); 55 | void triggerNoteWithReset(int track, int note, int macro = -1); 56 | 57 | void setSequenceRow(int row); 58 | void setPatternRow(int row); 59 | PlayerState& getPlayerState(); 60 | ITrackState& getTrackState(int track); 61 | void runTick(); 62 | void advanceTick(); 63 | }; 64 | -------------------------------------------------------------------------------- /src/TooltipDisplayer.cpp: -------------------------------------------------------------------------------- 1 | #include "TooltipDisplayer.h" 2 | #include "TooltipManager.h" 3 | #include "Renderer.h" 4 | #include "Color.h" 5 | 6 | TooltipDisplayer::TooltipDisplayer(EditorState& editorState, TooltipManager& manager) 7 | : Editor(editorState, false), mTooltipManager(manager) 8 | { 9 | } 10 | 11 | 12 | void TooltipDisplayer::onDraw(Renderer& renderer, const SDL_Rect& area) 13 | { 14 | const TooltipManager::Tooltip *tooltip = mTooltipManager.getVisibleTooltip(); 15 | 16 | if (tooltip) 17 | { 18 | /* Center text inside the display area 19 | */ 20 | 21 | SDL_Rect textArea = renderer.getTextRect(tooltip->text.c_str()); 22 | SDL_Rect finalArea = area; 23 | finalArea.x = tooltip->area.x + tooltip->area.w / 2 - textArea.w / 2; 24 | finalArea.y = tooltip->area.y - textArea.h / 2; 25 | finalArea.w = textArea.w; 26 | finalArea.h = textArea.h; 27 | 28 | SDL_Rect frameArea; 29 | frameArea.x = finalArea.x - 2; 30 | frameArea.y = finalArea.y - 2; 31 | frameArea.w = finalArea.w + 4; 32 | frameArea.h = finalArea.h + 4; 33 | 34 | if (frameArea.w > 0 && frameArea.h > 0) 35 | { 36 | renderer.setClip(frameArea); 37 | renderer.clearRect(frameArea, Theme::ColorType::ModalBackground); 38 | renderer.drawRect(frameArea, Theme::ColorType::ModalBorder); 39 | 40 | renderer.renderText(finalArea, Theme::ColorType::NormalText, tooltip->text.c_str()); 41 | } 42 | } 43 | } 44 | 45 | 46 | void TooltipDisplayer::onUpdate(int ms) 47 | { 48 | if (mTooltipManager.isDirty()) 49 | { 50 | mTooltipManager.setDirty(false); 51 | this->setDirty(true); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/MessageBox.cpp: -------------------------------------------------------------------------------- 1 | #include "MessageBox.h" 2 | #include "Renderer.h" 3 | #include "Color.h" 4 | #include "SDL.h" 5 | #include "TextEditor.h" 6 | #include "Label.h" 7 | #include 8 | #include 9 | 10 | MessageBox::MessageBox(EditorState& editorState) 11 | : Editor(editorState) 12 | { 13 | mLabel = new Label(editorState); 14 | addChild(mLabel, 0, 0, 240, 8); 15 | strcpy(mTitle, ""); 16 | } 17 | 18 | 19 | MessageBox::~MessageBox() 20 | { 21 | delete mLabel; 22 | } 23 | 24 | 25 | void MessageBox::onRendererMount(const Renderer& renderer) 26 | { 27 | SDL_Rect area = mLabel->getArea(); 28 | area.h = renderer.getFontHeight(); 29 | mLabel->setArea(area); 30 | } 31 | 32 | 33 | void MessageBox::setTitle(const char *title) 34 | { 35 | strncpy(mTitle, title, titleSize); 36 | mLabel->setText(mTitle); 37 | } 38 | 39 | 40 | bool MessageBox::onEvent(SDL_Event& event) 41 | { 42 | switch (event.type) 43 | { 44 | case SDL_KEYDOWN: 45 | switch (event.key.keysym.sym) 46 | { 47 | case SDLK_RETURN: 48 | case SDLK_y: 49 | mParent->onMessageBoxEvent(*this, 1); 50 | return true; 51 | 52 | case SDLK_ESCAPE: 53 | case SDLK_n: 54 | mParent->onMessageBoxEvent(*this, 0); 55 | return true; 56 | } 57 | 58 | break; 59 | } 60 | 61 | return false; 62 | } 63 | 64 | 65 | void MessageBox::onDraw(Renderer& renderer, const SDL_Rect& area) 66 | { 67 | if (shouldRedrawBackground()) 68 | renderer.clearRect(area, Theme::ColorType::ModalBackground); 69 | } 70 | 71 | int MessageBox::getId() const 72 | { 73 | return mId; 74 | } 75 | 76 | 77 | void MessageBox::setId(int id) 78 | { 79 | mId = id; 80 | } 81 | -------------------------------------------------------------------------------- /src/FileSection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | struct Pattern; 5 | 6 | class FileSection 7 | { 8 | public: 9 | static const int nameSize = 4; 10 | 11 | /* 12 | * readByte() & readDword() return this if it failed 13 | * 14 | * NOTE: readFloat() returns NAN if it fails! 15 | */ 16 | static constexpr int invalidRead = 0xffffffff; 17 | 18 | private: 19 | char mName[nameSize + 1]; 20 | void* mData; 21 | void* mPackedData; 22 | char *mBase64; 23 | int mSize, mAllocated; 24 | 25 | static const int allocateChunkSize = 1024; 26 | 27 | /* 28 | * Construct for reading (size > 0)/writing (size == 0) 29 | */ 30 | FileSection(const char *name, void *bytes = NULL, int size = 0); 31 | 32 | void ensureAllocated(int newSize); 33 | 34 | public: 35 | 36 | static FileSection* openSection(void *bytes, int size); 37 | static FileSection* createSection(const char *name); 38 | 39 | ~FileSection(); 40 | 41 | const char *getName() const; 42 | void *getData(); 43 | int getSize() const; 44 | void *getPackedData(); 45 | int getPackedSize() const; 46 | 47 | const char * getBase64(); 48 | 49 | unsigned int readByte(int& offset) const; 50 | int readSignedByte(int& offset) const; 51 | unsigned int readDword(int& offset) const; 52 | float readFloat(int& offset) const; 53 | const char * readString(int& offset) const; 54 | FileSection * readSection(int& offset) const; 55 | bool readPattern(Pattern& pattern, int effectParamCount, int& offset) const; 56 | 57 | void writeByte(unsigned int byte); 58 | void writeDword(unsigned int dword); 59 | void writeFloat(float value); 60 | void writeString(const char *str); 61 | void writeSection(FileSection& section); 62 | void writePattern(Pattern& pattern); 63 | }; 64 | -------------------------------------------------------------------------------- /src/MessageDisplayer.cpp: -------------------------------------------------------------------------------- 1 | #include "MessageDisplayer.h" 2 | #include "MessageManager.h" 3 | #include "Renderer.h" 4 | #include "Color.h" 5 | 6 | MessageDisplayer::MessageDisplayer(EditorState& editorState, MessageManager& manager) 7 | : Editor(editorState, false), mMessageManager(manager) 8 | { 9 | } 10 | 11 | 12 | void MessageDisplayer::onDraw(Renderer& renderer, const SDL_Rect& area) 13 | { 14 | const MessageManager::Message *message = mMessageManager.getVisibleMessage(); 15 | 16 | if (message) 17 | { 18 | /* Center text inside the display area 19 | */ 20 | 21 | SDL_Rect textArea = renderer.getTextRect(message->text.c_str()); 22 | SDL_Rect finalArea = area; 23 | finalArea.x = area.x + area.w / 2 - textArea.w / 2; 24 | finalArea.y = area.y + area.h / 2 - textArea.h / 2; 25 | finalArea.w = textArea.w; 26 | finalArea.h = textArea.h; 27 | 28 | float scale = message->getVisibility(); 29 | SDL_Rect frameArea; 30 | frameArea.x = finalArea.x + finalArea.w / 2 - (finalArea.w / 2 + 2) * scale; 31 | frameArea.y = finalArea.y + finalArea.h / 2 - (finalArea.h / 2 + 2) * scale; 32 | frameArea.w = (finalArea.w + 4) * scale; 33 | frameArea.h = (finalArea.h + 4) * scale; 34 | 35 | if (frameArea.w > 0 && frameArea.h > 0) 36 | { 37 | renderer.setClip(frameArea); 38 | renderer.clearRect(frameArea, Theme::ColorType::ModalBackground); 39 | renderer.drawRect(frameArea, Theme::ColorType::ModalBorder); 40 | 41 | renderer.renderText(finalArea, Theme::ColorType::NormalText, message->text.c_str()); 42 | } 43 | } 44 | } 45 | 46 | 47 | void MessageDisplayer::onUpdate(int ms) 48 | { 49 | if (mMessageManager.getVisibleMessage() != NULL || mMessageManager.getWasMessageVisibleLastFrame()) 50 | setDirty(true); 51 | } 52 | -------------------------------------------------------------------------------- /src/Mixer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct IPlayer; 4 | struct ISynth; 5 | struct SDL_Thread; 6 | struct SDL_AudioCVT; 7 | struct Sample16; 8 | 9 | #include "SDL.h" 10 | 11 | class Mixer 12 | { 13 | static const int MAX_DEVICE_NAME_SIZE = 200; 14 | static const int MAX_DEVICES = 32; 15 | #ifdef ENABLE_AUDIO_QUEUE 16 | static const int queueLengthLowLimitMs = 40; 17 | static const int queueLengthTargetMs = 80; 18 | static const int queueGranularityMs = 5; 19 | #endif 20 | 21 | IPlayer& mPlayer; 22 | ISynth& mSynth; 23 | 24 | int mSampleRate; 25 | int mSamples; 26 | bool mAudioOpened; 27 | #ifdef ENABLE_AUDIO_QUEUE 28 | bool mThreadRunning; 29 | SDL_Thread *mThread; 30 | #endif 31 | SDL_AudioCVT *mConvert; 32 | Sample16 *mBuffer; 33 | int mBufferSize; 34 | char mCurrentDevice[MAX_DEVICE_NAME_SIZE]; 35 | char mDeviceList[MAX_DEVICES][MAX_DEVICE_NAME_SIZE]; 36 | int mNumDevices; 37 | SDL_AudioDeviceID mDeviceId; 38 | 39 | bool initAudio(const char *deviceName); 40 | void deinitAudio(); 41 | 42 | protected: 43 | static void audioCallback(void* userdata, unsigned char* stream, int len); 44 | #ifdef ENABLE_AUDIO_QUEUE 45 | static int queueThread(void *data); 46 | int queueThreadInner(); 47 | #endif 48 | IPlayer& getPlayer(); 49 | ISynth& getSynth(); 50 | 51 | public: 52 | Mixer(IPlayer& player, ISynth& synth); 53 | ~Mixer(); 54 | #ifdef ENABLE_AUDIO_QUEUE 55 | bool isThreadRunning() const; 56 | #endif 57 | bool runThread(const char *deviceName); 58 | void stopThread(); 59 | int& getSamples(); 60 | int getSampleRate() const; 61 | 62 | void buildDeviceList(); 63 | const int getNumDevices() const; 64 | const char* getDevice(int index) const; 65 | const char* getCurrentDeviceName() const; 66 | SDL_AudioDeviceID getCurrentDeviceID() const; 67 | }; 68 | -------------------------------------------------------------------------------- /src/TooltipManager.cpp: -------------------------------------------------------------------------------- 1 | #include "TooltipManager.h" 2 | 3 | TooltipManager::Tooltip::Tooltip(const SDL_Rect& _area, const std::string& _text) 4 | : area(_area), text(_text) 5 | { 6 | } 7 | 8 | 9 | TooltipManager::Tooltip::Tooltip() 10 | : area(), text() 11 | { 12 | 13 | } 14 | 15 | 16 | TooltipManager::TooltipManager() 17 | : mVisible(false), mDidJustAppear(false), mHoveredMs(0) 18 | { 19 | } 20 | 21 | 22 | void TooltipManager::update(int ms) 23 | { 24 | if (mTooltip.area.w == 0) 25 | { 26 | return; 27 | } 28 | 29 | mHoveredMs += ms; 30 | 31 | if (mHoveredMs >= HoverDelayMs) 32 | { 33 | if (!mVisible) 34 | { 35 | mDidJustAppear = true; 36 | } 37 | 38 | mVisible = true; 39 | } 40 | } 41 | 42 | 43 | void TooltipManager::setTooltip(const SDL_Rect& area, const std::string& message) 44 | { 45 | if (mTooltip.area.x == area.x && mTooltip.area.y == area.y 46 | && mTooltip.area.w == area.w && mTooltip.area.h == area.h 47 | && message == mTooltip.text) 48 | { 49 | return; 50 | } 51 | 52 | mTooltip = Tooltip(area, message); 53 | mHoveredMs = 0; 54 | mVisible = false; 55 | } 56 | 57 | 58 | void TooltipManager::updateTooltipMotion(int x, int y) 59 | { 60 | SDL_Point point = { x, y }; 61 | if (!Editor::pointInRect(point, mTooltip.area)) 62 | { 63 | mTooltip.area.w = 0; 64 | mTooltip.area.h = 0; 65 | mDidJustAppear = true; 66 | } 67 | } 68 | 69 | 70 | const TooltipManager::Tooltip* TooltipManager::getVisibleTooltip() const 71 | { 72 | if (mVisible && mTooltip.area.w > 0) 73 | { 74 | return &mTooltip; 75 | } 76 | 77 | return NULL; 78 | } 79 | 80 | 81 | bool TooltipManager::isDirty() const 82 | { 83 | return mDidJustAppear; 84 | } 85 | 86 | 87 | void TooltipManager::setDirty(bool state) 88 | { 89 | mDidJustAppear = state; 90 | } 91 | -------------------------------------------------------------------------------- /src/GenericSelector.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Editor.h" 4 | 5 | struct EditorState; 6 | struct Renderer; 7 | struct SDL_Rect; 8 | struct TextEditor; 9 | struct Label; 10 | struct MessageBox; 11 | 12 | #include 13 | #include 14 | 15 | class GenericSelector: public Editor 16 | { 17 | static const int titleSize = 256; 18 | int mId; 19 | char mTitle[titleSize]; 20 | int mSelectedItem, mScrollPosition, mRowHeight; 21 | 22 | protected: 23 | struct Item { 24 | }; 25 | 26 | Label *mLabel; 27 | 28 | void selectItem(int index); 29 | void setScrollPosition(int position); 30 | const Item& getSelectedItem() const; 31 | int getVisibleCount() const; 32 | void getVisibleItems(int& first, int& last) const; 33 | 34 | virtual void renderItem(Renderer& renderer, const SDL_Rect& area, const Item& item, bool isSelected) = 0; 35 | virtual void accept(bool isFinal = false) = 0; 36 | virtual void reject(bool isFinal = false) = 0; 37 | virtual void onSelectItem(const Item& item); 38 | virtual void onAreaChanged(const SDL_Rect& area); 39 | 40 | void addItem(Item* newItem); 41 | void clearItems(); 42 | void sortItems(bool (*comparator)(const Item* a, const Item* b)); 43 | int findClickedItem(int x, int y) const; 44 | 45 | private: 46 | std::vector mItems; 47 | 48 | public: 49 | GenericSelector(EditorState& editorState); 50 | virtual ~GenericSelector(); 51 | 52 | /* Set dialog id (freely set and used by the Editor that creates the dialog) 53 | */ 54 | void setId(int id); 55 | int getId() const; 56 | 57 | /* Set dialog title 58 | */ 59 | void setTitle(const char *title); 60 | 61 | virtual bool onEvent(SDL_Event& event); 62 | virtual void onDraw(Renderer& renderer, const SDL_Rect& area); 63 | virtual void onRendererMount(const Renderer& renderer); 64 | }; 65 | -------------------------------------------------------------------------------- /src/Song.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct Pattern; 4 | struct Sequence; 5 | struct Macro; 6 | struct FileSection; 7 | struct SectionListener; 8 | 9 | class Song 10 | { 11 | public: 12 | static const int songNameLength = 16; 13 | static const int songVersion = 17; 14 | 15 | // Error codes returned by unpack() 16 | 17 | enum UnpackError { 18 | Success, // Song was unpacked successfully 19 | NotASong, // Data format unknown 20 | ErrorVersion, // Version number higher than our version 21 | ErrorRead, // Something went wrong while reading data 22 | SectionUnhandled, // Nobody handled a file section 23 | }; 24 | 25 | private: 26 | Sequence *sequence; 27 | Pattern **patterns; 28 | Macro *macros; 29 | 30 | int patternLength; 31 | int sequenceLength; 32 | 33 | char name[songNameLength + 1]; 34 | 35 | struct SectionListenerInfo { 36 | int flags; 37 | const char *sectionId; 38 | SectionListener *listener; 39 | }; 40 | 41 | static const int maxListeners = 32; 42 | SectionListenerInfo mListeners[maxListeners]; 43 | int mNumListeners; 44 | 45 | public: 46 | Song(); 47 | ~Song(); 48 | 49 | bool addSectionListener(const char *sectionId, SectionListener *sectionListener, int flags); 50 | 51 | int getPatternLength() const; 52 | int getSequenceLength() const; 53 | void setPatternLength(int length); 54 | void setSequenceLength(int length); 55 | 56 | Sequence& getSequence(); 57 | Pattern& getPattern(int track, int pattern); 58 | Macro& getMacro(int macro); 59 | 60 | int getLastPatternUsed(int track) const; 61 | int getLastMacroUsed() const; 62 | char *getSongName(); 63 | 64 | void clear(); 65 | FileSection* pack(); 66 | UnpackError unpack(const FileSection& section); 67 | 68 | int getSize() const; 69 | 70 | static const int maxPatterns = 256; 71 | static const int maxSequenceRows = 256; 72 | static const int maxMacros = 256; 73 | }; 74 | -------------------------------------------------------------------------------- /doc/MERSENNE.TXT: -------------------------------------------------------------------------------- 1 | Mersenne Twister License 2 | 3 | Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura, 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 1. Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 3. The names of its contributors may not be used to endorse or promote 15 | products derived from this software without specific prior written 16 | permission. 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 21 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 22 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 23 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 24 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | Any feedback is very welcome. 29 | http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html 30 | email: m-mat @ math.sci.hiroshima-u.ac.jp (remove space) 31 | 32 | -------------------------------------------------------------------------------- /src/Theme.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "Color.h" 6 | 7 | class Theme 8 | { 9 | public: 10 | enum ElementType 11 | { 12 | PatternEditor, 13 | SequenceEditor, 14 | MacroEditor, 15 | Oscilloscope, 16 | SongName, 17 | MacroName, 18 | MacroNumber, 19 | SequencePosition, 20 | SequenceLength, 21 | PatternLength, 22 | OctaveNumber, 23 | TouchRegion, 24 | Unknown 25 | }; 26 | 27 | struct Element 28 | { 29 | ElementType type; 30 | int parameters[10]; 31 | char strParameters[10][50]; 32 | }; 33 | 34 | enum ColorType 35 | { 36 | CurrentRow, 37 | BlockMarker, 38 | EditCursor, 39 | NonEditCursor, 40 | RowCounter, 41 | SelectedRow, 42 | ModalBackground, 43 | ModalBorder, 44 | ModalTitleBackground, 45 | ModalTitleText, 46 | NormalText, 47 | CommandShortcut, 48 | CommandShortcutBackground, 49 | ScrollBar, 50 | PlayHead, 51 | TextCursor, 52 | TextBackground, 53 | TextFocus, 54 | OscilloscopeColor, 55 | MutedOscilloscopeColor, 56 | NumColors 57 | }; 58 | 59 | static const int numColors = ColorType::NumColors; 60 | 61 | private: 62 | std::string mName; 63 | std::string mPath, mBackgroundPath, mFontPath, mBasePath; 64 | int mWidth, mHeight, mFontWidth, mFontHeight; 65 | std::vector mElement; 66 | Color mColors[numColors]; 67 | 68 | bool loadDefinition(const std::string& path); 69 | 70 | public: 71 | 72 | Theme(); 73 | 74 | bool load(const std::string& path); 75 | 76 | const std::string& getName() const; 77 | const std::string& getPath() const; 78 | const std::string& getFontPath() const; 79 | const std::string& getBackgroundPath() const; 80 | int getFontWidth() const; 81 | int getFontHeight() const; 82 | int getWidth() const; 83 | int getHeight() const; 84 | const Element& getElement(int index) const; 85 | int getElementCount() const; 86 | const Color& getColor(ColorType type) const; 87 | }; 88 | -------------------------------------------------------------------------------- /doc/KEYS.TXT: -------------------------------------------------------------------------------- 1 | CTRL/ALT + SHIFT + P Display a list of misc GUI actions 2 | CTRL + O Load song 3 | CTRL + S Save song 4 | CTRL + P Export song (copies song on clipboard / opens a binary url on web builds) 5 | CTRL + N Erase song (new song) 6 | F7 Save editor state (song and pattern positions etc.) 7 | ESCAPE Cycle focus 8 | Q2W3ER5T6Y7U Upper octave 9 | ZSXDCVGBHNM Lower octave 10 | SHIFT + KEY Enter effect in note column (instead of note) 11 | CURSORS/PGUP/PGDOWN/HOME/END Navigate in editor 12 | NUMPAD + / - Change selected macro 13 | TAB Jump to next channel 14 | SHIFT + TAB Jump to prev channel 15 | SHIFT + LEFT/RIGHT Change song position 16 | SHIFT + UP/DOWN Change current track pattern at song position 17 | F1/F2 Select octaves 18 | F3/F4 Copy/paste whole track 19 | F9/F10 Change pattern length 20 | CTRL+B/E Mark block start/end 21 | CTRL+C/V Copy/paste block 22 | CTRL + K Kill track (macro/pattern) 23 | CTRL + U Get unused track (macro/pattern) 24 | RIGHT CTRL / F5 Play song 25 | RIGHT SHIFT / F6 Play and loop sequence position 26 | SPACE Stop/switch to edit mode/disable edit mode 27 | ALT + 1 2 3 4 Mute/unmute channel 28 | CTRL + 0 1 2 3 4 5 6 7 8 9 Set edit skip (pattern editor and macro editor have their own skip) 29 | RETURN Play current row and go forward 30 | BACKSPACE Go backwards and play row 31 | SHIFT + RETURN Insert track row and move the rest forward 32 | CTRL + RETURN Insert track row and move the rest forward (only notes) 33 | ALT + RETURN Insert track row and move the rest forward (only effects) 34 | SHIFT + BACKSPACE Remove previous track row 35 | CTRL + BACKSPACE Remove previous track row (only notes) 36 | SHIFT + BACKSPACE Remove previous track row (only effects) 37 | DELETE Empty track row 38 | CTRL + DELETE Empty track note 39 | ALT + DELETE Empty track effect 40 | CTRL + D Duplicate row 41 | CAPS LOCK/SCROLL LOCK Toggle follow song position 42 | CTRL + A Select output device 43 | -------------------------------------------------------------------------------- /src/TrackEditor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ColumnEditor.h" 4 | 5 | struct PatternRow; 6 | struct Song; 7 | struct IPlayer; 8 | struct Color; 9 | struct Pattern; 10 | struct TrackEditorState; 11 | 12 | class TrackEditor: public ColumnEditor 13 | { 14 | protected: 15 | IPlayer& mPlayer; 16 | Song& mSong; 17 | bool mTriggerNotes; 18 | bool mAddMacroEffect; 19 | 20 | int getColumnFlagsFromModifier(int mod) const; 21 | 22 | void killCurrentTrack(); 23 | void findCurrentUnusedTrack(); 24 | void copyCurrentTrack(); 25 | void pasteCurrentTrack(); 26 | void copyCurrentBlock(); 27 | void pasteCurrentBlock(); 28 | void setEditSkip(int skip); 29 | void setBlockStartToCurrentRow(); 30 | void setBlockEndToCurrentRow(); 31 | 32 | protected: 33 | virtual Pattern& getCurrentPattern(int track) = 0; 34 | virtual PatternRow& getCurrentPatternRow() = 0; 35 | virtual PatternRow& getPatternRow(int track, int row) = 0; 36 | 37 | virtual void changeColumn(int d); 38 | 39 | void playRow(); 40 | void insertRow(bool allTracks, int flags); 41 | void deleteRow(bool allTracks, int flags); 42 | void emptyRow(bool allTracks, int flags); 43 | 44 | void setBlockStart(int row); 45 | void setBlockEnd(int row); 46 | void copyBlock(int track); 47 | void pasteBlock(int track); 48 | void copyTrack(int track); 49 | void pasteTrack(int track); 50 | void killTrack(int track); 51 | virtual void findUnusedTrack(int track) = 0; 52 | 53 | void renderPatternRow(Renderer& renderer, const SDL_Rect& area, const PatternRow& row, const Color& color, int columnFlags = -1); 54 | 55 | public: 56 | TrackEditor(EditorState& editorState, TrackEditorState& trackEditorState, IPlayer& player, Song& song, int tracks); 57 | virtual ~TrackEditor(); 58 | 59 | void setTriggerNotes(bool state); 60 | void setAddMacroEffect(bool state); 61 | 62 | virtual bool onEvent(SDL_Event& event); 63 | virtual void onDraw(Renderer& renderer, const SDL_Rect& area); 64 | }; 65 | -------------------------------------------------------------------------------- /src/AudioDeviceSelector.cpp: -------------------------------------------------------------------------------- 1 | #include "AudioDeviceSelector.h" 2 | #include "Renderer.h" 3 | #include "Color.h" 4 | #include "SDL.h" 5 | #include "TextEditor.h" 6 | #include "Label.h" 7 | #include "MessageBox.h" 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include "Mixer.h" 14 | 15 | AudioDeviceSelector::AudioDeviceSelector(EditorState& editorState) 16 | : GenericSelector(editorState) 17 | { 18 | 19 | } 20 | 21 | 22 | AudioDeviceSelector::~AudioDeviceSelector() 23 | { 24 | } 25 | 26 | 27 | void AudioDeviceSelector::accept(bool isFinal) 28 | { 29 | mParent->onFileSelectorEvent(*this, true); 30 | } 31 | 32 | 33 | void AudioDeviceSelector::reject(bool isFinal) 34 | { 35 | mParent->onFileSelectorEvent(*this, false); 36 | } 37 | 38 | 39 | void AudioDeviceSelector::renderItem(Renderer& renderer, const SDL_Rect& area, const Item& item, bool isSelected) 40 | { 41 | const AudioDeviceItem& deviceItem = static_cast(item); 42 | Theme::ColorType color = Theme::ColorType::NormalText; 43 | 44 | if (isSelected) 45 | color = Theme::ColorType::SelectedRow; 46 | 47 | renderer.clearRect(area, Theme::ColorType::ModalBackground); 48 | 49 | int width = area.w / renderer.getFontWidth() - 10; 50 | 51 | renderer.renderTextV(area, color, "%s", deviceItem.name.c_str()); 52 | } 53 | 54 | 55 | void AudioDeviceSelector::populate(const Mixer& mixer) 56 | { 57 | invalidateParent(); 58 | 59 | clearItems(); 60 | 61 | int numDevices = mixer.getNumDevices(); 62 | for (int index = 0 ; index < numDevices ; ++index) 63 | addItem(new AudioDeviceItem(mixer.getDevice(index))); 64 | 65 | selectItem(0); 66 | } 67 | 68 | 69 | const char * AudioDeviceSelector::getSelectedDevice() const 70 | { 71 | return static_cast(getSelectedItem()).name.c_str(); 72 | } 73 | 74 | 75 | AudioDeviceSelector::AudioDeviceItem::AudioDeviceItem(const char *_name) 76 | : name(_name) 77 | { 78 | } 79 | -------------------------------------------------------------------------------- /src/Oscilloscope.cpp: -------------------------------------------------------------------------------- 1 | #include "Oscilloscope.h" 2 | #include "Renderer.h" 3 | #include "Color.h" 4 | #include "Sample.h" 5 | #include "Value.h" 6 | #include "IPlayer.h" 7 | #include "ITrackState.h" 8 | #include "IOscillator.h" 9 | 10 | Oscilloscope::Oscilloscope(EditorState& editorState, IPlayer& player, int channel) 11 | : Editor(editorState, false), mPlayer(player), mChannel(channel), mOscilloscopePos(0) 12 | { 13 | } 14 | 15 | 16 | Oscilloscope::~Oscilloscope() 17 | { 18 | } 19 | 20 | 21 | void Oscilloscope::setBuffer(const Sample16 *buffer, int length) 22 | { 23 | mBuffer = buffer; 24 | mBufferLength = length; 25 | setDirty(true); 26 | } 27 | 28 | 29 | void Oscilloscope::onDraw(Renderer& renderer, const SDL_Rect& area) 30 | { 31 | setDirty(false); 32 | renderer.renderBackground(area); 33 | 34 | SDL_Point points[area.w]; 35 | 36 | for (int x = 0 ; x < area.w ; ++x) 37 | { 38 | int bufferPos = mBufferLength * x / area.w; 39 | const Sample16& sample = mBuffer[(bufferPos + mOscilloscopePos) % mBufferLength]; 40 | int y = (sample.left + sample.right) * area.h / IOscillator::oscillatorResolution / 2 + area.h / 2; 41 | 42 | if (y >= area.h) 43 | y = area.h - 1; 44 | else if (y < 0) 45 | y = 0; 46 | 47 | points[x].x = area.x + x; 48 | points[x].y = area.y + y; 49 | } 50 | 51 | Theme::ColorType color; 52 | 53 | if (mPlayer.getTrackState(mChannel).enabled) 54 | color = Theme::ColorType::OscilloscopeColor; 55 | else 56 | color = Theme::ColorType::MutedOscilloscopeColor; 57 | 58 | renderer.renderPoints(points, area.w, color); 59 | } 60 | 61 | 62 | bool Oscilloscope::onEvent(SDL_Event& event) 63 | { 64 | if (event.type == SDL_MOUSEBUTTONDOWN) 65 | { 66 | mPlayer.getTrackState(mChannel).enabled ^= true; 67 | return true; 68 | } 69 | 70 | return false; 71 | } 72 | 73 | 74 | void Oscilloscope::onListenableChange(Listenable *listenable) 75 | { 76 | setDirty(true); 77 | 78 | // Assumes this will always be called by mOscillatorsProbePos->notify() 79 | mOscilloscopePos = *static_cast(listenable); 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prototracker 2 | 3 | Note: Prototracker is not the same thing as Prototracker-modular or Prototracker-OPLL. They have their own forks. 4 | 5 | Prototracker is a multiplatform fakebit chiptune tracker. [Try the online version.](http://kometbomb.net/prototracker/) 6 | 7 | The editor is a fairly normal tracker. The synth is an absolutely minimal single-oscillator synth (with 256 preset waveforms). Macros are used to create "instruments" and also some normal channel effects. Most keyboard shortcuts are the same as in Protracker. See the docs/ directory for help. 8 | 9 | Supported platforms: 10 | * HTML5 (see the link above) 11 | * Windows (MinGW) 12 | * Linux 13 | * Mac (just use the Linux makefile) 14 | * Chip8 (runs great on a PocketCHIP) 15 | * Probably anything SDL2 supports 16 | 17 | ## Building 18 | 19 | Simply do ```make mingw``` where "mingw" is your platform of choice. Do ```make``` to see the supported platforms. 20 | 21 | ### Building for OSX 22 | 23 | To install SDL/SDL2 on OSX(using brew), type ```brew install sdl2 sdl sdl_image sdl_mixer sdl_ttf portmidi```. If you are getting the ```fatal error: 'SDL_image.h' file not found``` error, type ```apt-get install libsdl2-image-dev```. You should then be able to run ```make linux```. 24 | 25 | The binary needs the files in the assets/ directory to work properly. 26 | 27 | ## Prototracker as a synth testbed 28 | 29 | Prototracker was also created for testing audio/synth code (while having some minimal setup for a song structure), so it is quite easily extendable. Each of the channels runs an instance of ITrackState and IOscillator, and will simply extend IPlayer and ISynth to setup your own ITrackState (from IPlayer) and/or IOscillator (from ISynth). 30 | 31 | The TrackState, Oscillator and Synth classes included (see src/) implement a minimal wavetable synth that has two extra track effects (W and Q). Since it's all per track, you can have each channel use a different set of effects and oscillators (think something like the audio capabilities of the NES). Just init the tracks in IPlayer::IPlayer() and ISynth::ISynth(). 32 | -------------------------------------------------------------------------------- /src/PatternRow.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "App.h" 4 | #include "EffectParam.h" 5 | 6 | /* 7 | 8 | PatternRow is the most basic component of a song. Both Patterns and Macros 9 | are just a list of these. 10 | 11 | A PatternRow consists of two EffectParams. The first one has a special meaning 12 | in the editor, that is the note column. It still works internally exactly like 13 | the "normal" effect column, except for the first column being able to trigger 14 | new notes. 15 | 16 | */ 17 | 18 | struct PatternRow 19 | { 20 | static const int effectParams = SONG_EFFECT_COLUMNS; 21 | static const int numColumns = effectParams * 3 + 3; 22 | 23 | /* 24 | * Column order when a row has a note and an effect 25 | */ 26 | enum Column 27 | { 28 | NoteType, 29 | NoteParam1, 30 | NoteParam2, 31 | EffectType, 32 | EffectParam1, 33 | EffectParam2, 34 | 35 | Note = NoteType, 36 | Octave = NoteParam2, 37 | NumColumns = numColumns, 38 | }; 39 | 40 | enum ColumnFlag 41 | { 42 | FlagNote = 1 << Note, 43 | FlagOctave = 1 << Octave, 44 | FlagEffectType = 1 << EffectType, 45 | FlagEffectParam1 = 1 << EffectParam1, 46 | FlagEffectParam2 = 1 << EffectParam2, 47 | FlagEffect = FlagEffectType, 48 | FlagAllColumns = -1 49 | }; 50 | 51 | PatternRow(); 52 | 53 | static const char *getNoteName(int note); 54 | 55 | // Translate column index into 56 | static void translateColumnEnum(int columnIndex, int& effectParam, Column& column); 57 | 58 | bool shouldSkipParam1() const; 59 | int getNoteNr() const; 60 | int getOctave() const; 61 | void clear(int flags = FlagAllColumns); 62 | int getNoteWithOctave() const; 63 | void setOctave(int octave); 64 | void setNoteAndOctave(int noteAndOctave); 65 | 66 | static const int NoNote = 0xff; 67 | 68 | EffectParam& getNote(); 69 | EffectParam& getEffect(int index); 70 | const EffectParam& getNote() const; 71 | const EffectParam& getEffect(int index) const; 72 | 73 | // Get note and effects (note index is 0) 74 | const EffectParam& getAnyParam(int index) const; 75 | 76 | private: 77 | // Includes the note column 78 | EffectParam effect[effectParams + 1]; 79 | }; 80 | -------------------------------------------------------------------------------- /src/WaveView.cpp: -------------------------------------------------------------------------------- 1 | #include "WaveView.h" 2 | #include "Renderer.h" 3 | #include "Color.h" 4 | #include "SDL.h" 5 | #include "Wave.h" 6 | #include "WaveStore.h" 7 | 8 | 9 | WaveView::WaveView(EditorState& editorState, const WaveStore& waveStore) 10 | : Editor(editorState), mWaveStore(waveStore), mX(0), mY(0) 11 | { 12 | } 13 | 14 | 15 | WaveView::~WaveView() 16 | { 17 | } 18 | 19 | 20 | void WaveView::onDraw(Renderer& renderer, const SDL_Rect& area) 21 | { 22 | setDirty(false); 23 | 24 | const int thumbW = 32; 25 | const int thumbH = 24; 26 | 27 | for (int y = mY ; y < mY + 8 ; y++) 28 | { 29 | for (int x = mX ; x < mX + 8 ; x++) 30 | { 31 | int w = x + y * 16; 32 | 33 | SDL_Rect rect = { area.x + (x - mX) * thumbW, area.y + (y - mY) * thumbH, thumbW, thumbH }; 34 | renderer.clearRect(rect, Color(0,0,0)); 35 | renderer.drawRect(rect, Color(255,255,255)); 36 | 37 | const Wave& wave = mWaveStore.getWave(w); 38 | const int * data = wave.getData(0); 39 | 40 | int py = data[0] * (rect.h - 1) / Wave::waveAmplitude / 2; 41 | for (int x = 1 ; x < rect.w ; ++x) 42 | { 43 | int bufferPos = wave.getLength(0) * x / rect.w; 44 | int y = data[bufferPos] * (rect.h - 1) / Wave::waveAmplitude; 45 | renderer.renderLine(rect.x + x - 1, rect.y + rect.h / 2 + py, rect.x + x, rect.y + rect.h / 2 + y, Color()); 46 | py = y; 47 | } 48 | 49 | renderer.renderTextV(rect, Color(), "%d", w); 50 | } 51 | } 52 | } 53 | 54 | 55 | bool WaveView::onEvent(SDL_Event& event) 56 | { 57 | if (event.type == SDL_KEYDOWN) 58 | { 59 | switch (event.key.keysym.sym) 60 | { 61 | case SDLK_UP: 62 | mY--; 63 | if (mY < 0) 64 | mY = 0; 65 | setDirty(true); 66 | return true; 67 | 68 | case SDLK_DOWN: 69 | mY++; 70 | if (mY > 8) 71 | mY = 8; 72 | setDirty(true); 73 | return true; 74 | 75 | case SDLK_LEFT: 76 | mX--; 77 | if (mX < 0) 78 | mX = 0; 79 | setDirty(true); 80 | return true; 81 | 82 | case SDLK_RIGHT: 83 | mX++; 84 | if (mX > 8) 85 | mX = 8; 86 | setDirty(true); 87 | return true; 88 | } 89 | } 90 | 91 | 92 | return false; 93 | } 94 | -------------------------------------------------------------------------------- /src/Renderer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "SDL.h" 4 | 5 | struct Color; 6 | struct Theme; 7 | 8 | #include 9 | #include "Theme.h" 10 | 11 | class Renderer 12 | { 13 | SDL_Window *mWindow; 14 | SDL_Renderer *mRenderer; 15 | SDL_Texture *mFont, *mBackground, *mIntermediateTexture; 16 | int mGuiWidth, mGuiHeight, mFontWidth, mFontHeight; 17 | Theme mTheme; 18 | 19 | bool loadFont(const std::string& path, int charWidth, int charHeight); 20 | bool loadGui(const std::string& path, int width, int height); 21 | 22 | public: 23 | Renderer(); 24 | ~Renderer(); 25 | 26 | bool setTheme(const Theme& theme); 27 | const Theme& getTheme() const; 28 | void clearRect(const SDL_Rect& rect, const Color& color); 29 | void clearRect(const SDL_Rect& rect, const Theme::ColorType& color); 30 | void drawRect(const SDL_Rect& rect, const Color& color); 31 | void drawRect(const SDL_Rect& rect, const Theme::ColorType& color); 32 | void setClip(const SDL_Rect& area); 33 | void unsetClip(); 34 | void setColor(const Color& color); 35 | void renderRect(const SDL_Rect& rect, const Color& color, int index = 0); 36 | void renderBackground(const SDL_Rect& rect); 37 | SDL_Rect getTextRect(const char * text); 38 | void renderText(const SDL_Rect& position, const Color& color, const char * text); 39 | void renderText(const SDL_Rect& position, const Theme::ColorType& color, const char * text); 40 | void renderTextV(const SDL_Rect& position, const Color& color, const char * text, ...) __attribute__((format(printf, 4, 5))); 41 | void renderTextV(const SDL_Rect& position, const Theme::ColorType& color, const char * text, ...) __attribute__((format(printf, 4, 5))); 42 | void renderChar(const SDL_Rect& position, const Color& color, int c); 43 | void renderLine(int x1, int y1, int x2, int y2, const Color& color); 44 | void renderLine(int x1, int y1, int x2, int y2, const Theme::ColorType& color); 45 | void renderPoints(const SDL_Point* points, int count, const Color& color); 46 | void renderPoints(const SDL_Point* points, int count, const Theme::ColorType& color); 47 | void beginRendering(); 48 | void present(); 49 | int getFontWidth() const; 50 | int getFontHeight() const; 51 | SDL_Rect getWindowArea() const; 52 | void scaleEventCoordinates(SDL_Event& event) const; 53 | }; 54 | -------------------------------------------------------------------------------- /src/FileSelector.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Editor.h" 4 | 5 | struct EditorState; 6 | struct Renderer; 7 | struct SDL_Rect; 8 | struct TextEditor; 9 | struct Label; 10 | struct MessageBox; 11 | 12 | #include "GenericSelector.h" 13 | 14 | class FileSelector: public GenericSelector 15 | { 16 | static const int filenameSize = 256; 17 | static const int filterSize = 256; 18 | 19 | enum 20 | { 21 | MessageBoxOverwrite 22 | }; 23 | 24 | char mFilter[filterSize]; 25 | char mFilename[filenameSize]; 26 | Label *mFilenameLabel; 27 | TextEditor *mNameField; 28 | MessageBox *mMessageBox; 29 | bool mCheckOverwrite; 30 | 31 | struct FileItem: public Item { 32 | bool isDirectory; 33 | std::string path; 34 | int size; 35 | 36 | FileItem(bool isDirectory, const char *path, int size); 37 | 38 | // For sorting the listing 39 | static bool directorySort(const Item* a, const Item* b); 40 | static bool checkDirectory(const char *name); 41 | static const char *formatSize(int size); 42 | }; 43 | 44 | virtual void onSelectItem(const Item& item); 45 | virtual void renderItem(Renderer& renderer, const SDL_Rect& area, const Item& item, bool isSelected); 46 | virtual void accept(bool isFinal = false); 47 | virtual void reject(bool isFinal = false); 48 | 49 | static bool fileExists(const char *path); 50 | 51 | public: 52 | FileSelector(EditorState& editorState); 53 | virtual ~FileSelector(); 54 | 55 | /* Set path & populate list 56 | */ 57 | void setPath(const char *path); 58 | 59 | /* Set file extension list (with leading period) 60 | */ 61 | void setFilter(const char *extension); 62 | 63 | /* Read file list from the path set with setPath() 64 | * using the extension filter set by setFilter() 65 | */ 66 | void populate(); 67 | 68 | /* Whether the dialog should check if a file exists 69 | * with the selected filename (for save dialogs) 70 | */ 71 | void setOverwriteCheck(bool state); 72 | 73 | /* After the dialog finishes this will return the path 74 | * and filename to selected (or new) file 75 | */ 76 | const char * getSelectedPath() const; 77 | 78 | virtual bool onEvent(SDL_Event& event); 79 | virtual void onMessageBoxEvent(const Editor& messageBox, int code); 80 | virtual void onModalStatusChange(bool isNowModal); 81 | virtual void onRendererMount(const Renderer& renderer); 82 | }; 83 | -------------------------------------------------------------------------------- /src/MessageManager.cpp: -------------------------------------------------------------------------------- 1 | #include "MessageManager.h" 2 | #include 3 | 4 | MessageManager::Message::Message(int _id, Editor::MessageClass _messageClass, const std::string& _message) 5 | : id(_id), messageClass(_messageClass), text(_message) 6 | { 7 | resetDelay(); 8 | } 9 | 10 | 11 | void MessageManager::Message::resetDelay() 12 | { 13 | delay = MessageAnimationMs * 2 + MessageVisibleMs; 14 | } 15 | 16 | 17 | float MessageManager::Message::getVisibility() const 18 | { 19 | if (delay > MessageAnimationMs && delay < MessageVisibleMs + MessageAnimationMs) 20 | return 1.0; 21 | 22 | if (delay <= MessageAnimationMs) 23 | return static_cast(delay) / MessageAnimationMs; 24 | 25 | if (delay > MessageAnimationMs + MessageVisibleMs) 26 | return 1.0f - static_cast(delay - MessageAnimationMs - MessageVisibleMs) / MessageAnimationMs; 27 | 28 | return 0.0f; 29 | } 30 | 31 | 32 | MessageManager::MessageManager() 33 | : mWasVisible(false), mMessageIDCounter(1) 34 | { 35 | } 36 | 37 | 38 | void MessageManager::update(int ms) 39 | { 40 | if (mMessageQueue.empty()) 41 | { 42 | mWasVisible = false; 43 | return; 44 | } 45 | 46 | mMessageQueue.front().delay -= ms; 47 | 48 | if (mMessageQueue.front().delay <= 0) 49 | { 50 | mMessageQueue.pop_front(); 51 | } 52 | 53 | mWasVisible = true; 54 | } 55 | 56 | 57 | int MessageManager::pushMessage(Editor::MessageClass messageClass, const std::string& message, int messageId) 58 | { 59 | auto foundItem = std::find_if(mMessageQueue.begin(), mMessageQueue.end(), 60 | [&messageId](const Message& message) -> bool { return message.id == messageId; }); 61 | 62 | if (foundItem != mMessageQueue.end()) 63 | { 64 | // Replace old 65 | 66 | foundItem->messageClass = messageClass; 67 | foundItem->text = message; 68 | foundItem->resetDelay(); 69 | return messageId; 70 | } 71 | else 72 | { 73 | // Insert new 74 | 75 | mMessageQueue.push_back(Message(mMessageIDCounter, messageClass, message)); 76 | return mMessageIDCounter++; 77 | } 78 | } 79 | 80 | 81 | const MessageManager::Message* MessageManager::getVisibleMessage() const 82 | { 83 | if (mMessageQueue.empty()) 84 | return NULL; 85 | 86 | return &mMessageQueue.front(); 87 | } 88 | 89 | 90 | bool MessageManager::getWasMessageVisibleLastFrame() const 91 | { 92 | return mWasVisible; 93 | } 94 | -------------------------------------------------------------------------------- /src/PatternRow.cpp: -------------------------------------------------------------------------------- 1 | #include "PatternRow.h" 2 | 3 | PatternRow::PatternRow() 4 | { 5 | clear(); 6 | } 7 | 8 | const char *PatternRow::getNoteName(int noteIdx) 9 | { 10 | if (noteIdx == NoNote) 11 | return "--"; 12 | 13 | static const char *noteNames[] = { 14 | "C-", "C#", "D-", "D#", "E-", "F-", "F#", "G-", "G#", "A-", "A#", "B-" 15 | }; 16 | 17 | return noteNames[noteIdx]; 18 | } 19 | 20 | 21 | void PatternRow::setNoteAndOctave(int noteAndOctave) 22 | { 23 | getNote().setNoteAndOctave(noteAndOctave); 24 | } 25 | 26 | 27 | int PatternRow::getNoteWithOctave() const 28 | { 29 | return getNote().getNoteWithOctave(); 30 | } 31 | 32 | 33 | void PatternRow::setOctave(int octave) 34 | { 35 | getNote().param2 = octave; 36 | } 37 | 38 | 39 | int PatternRow::getOctave() const 40 | { 41 | return getNote().param2; 42 | } 43 | 44 | 45 | int PatternRow::getNoteNr() const 46 | { 47 | if (getNote().effect != 'n') 48 | return NoNote; 49 | 50 | return getNote().param1; 51 | } 52 | 53 | 54 | void PatternRow::clear(int flags) 55 | { 56 | if (flags & FlagNote) 57 | getNote() = EffectParam(); 58 | 59 | if (flags & FlagEffectType) 60 | { 61 | for (int i = 0 ; i < effectParams ; ++i) 62 | { 63 | getEffect(i) = EffectParam(); 64 | } 65 | } 66 | } 67 | 68 | 69 | bool PatternRow::shouldSkipParam1() const 70 | { 71 | return (getNote().effect == 'n'); 72 | } 73 | 74 | 75 | void PatternRow::translateColumnEnum(int columnIndex, int& effectParam, PatternRow::Column& column) 76 | { 77 | if (columnIndex < EffectType) 78 | { 79 | column = static_cast(columnIndex); 80 | effectParam = 0; 81 | } 82 | else 83 | { 84 | // columnIndex >= 3 repeats EffectType, Param1, Param2, EffectType, Param1... 85 | column = static_cast(columnIndex % 3 + EffectType); 86 | effectParam = columnIndex / 3 - 1; 87 | } 88 | } 89 | 90 | 91 | EffectParam& PatternRow::getNote() 92 | { 93 | return effect[0]; 94 | } 95 | 96 | 97 | EffectParam& PatternRow::getEffect(int index) 98 | { 99 | return effect[index + 1]; 100 | } 101 | 102 | 103 | const EffectParam& PatternRow::getNote() const 104 | { 105 | return effect[0]; 106 | } 107 | 108 | 109 | const EffectParam& PatternRow::getEffect(int index) const 110 | { 111 | return effect[index + 1]; 112 | } 113 | 114 | 115 | const EffectParam& PatternRow::getAnyParam(int index) const 116 | { 117 | return effect[index]; 118 | } 119 | -------------------------------------------------------------------------------- /src/ISynth.cpp: -------------------------------------------------------------------------------- 1 | #include "ISynth.h" 2 | #include "IOscillator.h" 3 | #include "SequenceRow.h" 4 | #include "Sample.h" 5 | #include "SDL.h" 6 | #include 7 | 8 | const int ISynth::oscillatorProbeLength; 9 | 10 | ISynth::ISynth() 11 | : mProbePosition(0) 12 | { 13 | mOscillator = new IOscillator*[SequenceRow::maxTracks]; 14 | mPreviousOscillatorOutput = new Sample16[oscillatorProbeLength * SequenceRow::maxTracks]; 15 | mTempBuffer = new Sample16[2048]; 16 | 17 | SDL_memset(mPreviousOscillatorOutput, 0, sizeof(Sample16) * oscillatorProbeLength * SequenceRow::maxTracks); 18 | } 19 | 20 | 21 | ISynth::~ISynth() 22 | { 23 | for (int i = 0 ; i < SequenceRow::maxTracks ; ++i) 24 | delete mOscillator[i]; 25 | 26 | delete[] mOscillator; 27 | delete[] mPreviousOscillatorOutput; 28 | } 29 | 30 | 31 | IOscillator& ISynth::getOscillator(int i) 32 | { 33 | return *mOscillator[i]; 34 | } 35 | 36 | 37 | const Sample16* ISynth::getOscillatorProbe(int oscillator) const 38 | { 39 | return &mPreviousOscillatorOutput[oscillatorProbeLength * oscillator]; 40 | } 41 | 42 | 43 | void ISynth::update(int numSamples) 44 | { 45 | for (int i = 0 ; i < SequenceRow::maxTracks ; ++i) 46 | mOscillator[i]->update(numSamples); 47 | } 48 | 49 | 50 | void ISynth::render(Sample16 *buffer, int numSamples) 51 | { 52 | SDL_memset(buffer, 0, sizeof(Sample16) * numSamples); 53 | 54 | int probeCount = std::min(numSamples, oscillatorProbeLength); 55 | 56 | for (int i = 0 ; i < SequenceRow::maxTracks ; ++i) 57 | { 58 | SDL_memset(mTempBuffer, 0, sizeof(Sample16) * numSamples); 59 | mOscillator[i]->render(mTempBuffer, numSamples); 60 | 61 | for (int p = 0 ; p < numSamples ; ++p) 62 | { 63 | buffer[p].left += mTempBuffer[p].left; 64 | buffer[p].right += mTempBuffer[p].right; 65 | } 66 | 67 | Sample16 *src = mTempBuffer + std::max(0, numSamples - oscillatorProbeLength); 68 | Sample16 *dest = mPreviousOscillatorOutput + oscillatorProbeLength * i; 69 | 70 | for (int p = 0 ; p < probeCount ; ++p) 71 | { 72 | Sample16& sample = dest[(p + mProbePosition) % oscillatorProbeLength]; 73 | sample.left = src->left; 74 | sample.right = src->right; 75 | src++; 76 | } 77 | } 78 | 79 | mProbePosition += probeCount; 80 | } 81 | 82 | 83 | int ISynth::getProbePosition() const 84 | { 85 | return mProbePosition; 86 | } 87 | 88 | 89 | void ISynth::setSampleRate(int rate) 90 | { 91 | for (int i = 0 ; i < SequenceRow::maxTracks ; ++i) 92 | mOscillator[i]->setSampleRate(rate); 93 | } 94 | 95 | 96 | void ISynth::reset() 97 | { 98 | } 99 | -------------------------------------------------------------------------------- /src/Gamepad.cpp: -------------------------------------------------------------------------------- 1 | #include "Gamepad.h" 2 | #include 3 | 4 | typedef struct { int axis; int direction; int idx; int button; } AxisTransTab; 5 | 6 | void Gamepad::initControllers() 7 | { 8 | controllerAxis[0] = 0; 9 | controllerAxis[1] = 0; 10 | 11 | for (int i = 0; i < SDL_NumJoysticks(); ++i) 12 | { 13 | if (SDL_IsGameController(i)) 14 | { 15 | SDL_GameController *newController = SDL_GameControllerOpen(i); 16 | 17 | if (newController) 18 | { 19 | controller.push_back(newController); 20 | } 21 | } 22 | } 23 | 24 | } 25 | 26 | 27 | void Gamepad::deinitControllers() 28 | { 29 | for (auto ctrl : controller) 30 | SDL_GameControllerClose(ctrl); 31 | 32 | controller.clear(); 33 | } 34 | 35 | 36 | bool Gamepad::loadDefinitions(const char *path) 37 | { 38 | return SDL_GameControllerAddMappingsFromFile(path) != -1; 39 | } 40 | 41 | 42 | void Gamepad::translateAxisToDPad(const SDL_Event& event) 43 | { 44 | const AxisTransTab transTab[] = { 45 | { SDL_CONTROLLER_AXIS_LEFTX, -1, 0, SDL_CONTROLLER_BUTTON_DPAD_LEFT }, 46 | { SDL_CONTROLLER_AXIS_LEFTX, 1, 0, SDL_CONTROLLER_BUTTON_DPAD_RIGHT }, 47 | { SDL_CONTROLLER_AXIS_LEFTY, -1, 1, SDL_CONTROLLER_BUTTON_DPAD_UP }, 48 | { SDL_CONTROLLER_AXIS_LEFTY, 1, 1, SDL_CONTROLLER_BUTTON_DPAD_DOWN } 49 | }; 50 | 51 | int i; 52 | 53 | for (i = 0 ; i < 4 ; ++i) 54 | { 55 | const AxisTransTab trans = transTab[i]; 56 | 57 | if (trans.axis == event.caxis.axis) 58 | { 59 | bool pressed = event.caxis.value < 1000 * trans.direction; 60 | bool wasPressed = controllerAxis[trans.idx] < 1000 * trans.direction; 61 | 62 | if (wasPressed && !pressed) 63 | { 64 | SDL_Event event = {0}; 65 | event.type = SDL_CONTROLLERBUTTONUP; 66 | event.cbutton.timestamp = event.caxis.timestamp; 67 | event.cbutton.which = event.caxis.which; 68 | event.cbutton.button = trans.button; 69 | event.cbutton.state = SDL_RELEASED; 70 | 71 | SDL_PushEvent(&event); 72 | } 73 | 74 | if (pressed && !wasPressed) 75 | { 76 | SDL_Event event = {0}; 77 | event.type = SDL_CONTROLLERBUTTONDOWN; 78 | event.cbutton.timestamp = event.caxis.timestamp; 79 | event.cbutton.which = event.caxis.which; 80 | event.cbutton.button = trans.button; 81 | event.cbutton.state = SDL_PRESSED; 82 | 83 | SDL_PushEvent(&event); 84 | } 85 | } 86 | } 87 | 88 | for (i = 0 ; i < 4 ; ++i) 89 | { 90 | const AxisTransTab trans = transTab[i]; 91 | 92 | if (trans.axis == event.caxis.axis) 93 | { 94 | controllerAxis[trans.idx] = event.caxis.value; 95 | break; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/MacroEditor.cpp: -------------------------------------------------------------------------------- 1 | #include "MacroEditor.h" 2 | #include "PatternRow.h" 3 | #include "Song.h" 4 | #include "Pattern.h" 5 | #include "Macro.h" 6 | #include "Color.h" 7 | #include "EditorState.h" 8 | #include "Renderer.h" 9 | #include "CommandOptionSelector.h" 10 | 11 | MacroEditor::MacroEditor(EditorState& editorState, IPlayer& player, Song& song) 12 | : TrackEditor(editorState, editorState.macroEditor, player, song, 1) 13 | { 14 | editorState.macro.addListener(this); 15 | setTriggerNotes(false); 16 | setAddMacroEffect(false); 17 | } 18 | 19 | 20 | MacroEditor::~MacroEditor() 21 | { 22 | 23 | } 24 | 25 | 26 | bool MacroEditor::onEvent(SDL_Event& event) 27 | { 28 | return TrackEditor::onEvent(event); 29 | } 30 | 31 | 32 | PatternRow& MacroEditor::getCurrentPatternRow() 33 | { 34 | Macro& macro = mSong.getMacro(mEditorState.macro); 35 | 36 | return macro.getRow(mTrackEditorState.currentRow); 37 | } 38 | 39 | 40 | PatternRow& MacroEditor::getPatternRow(int track, int row) 41 | { 42 | Macro& macro = mSong.getMacro(mEditorState.macro); 43 | 44 | return macro.getRow(row); 45 | } 46 | 47 | 48 | Pattern& MacroEditor::getCurrentPattern(int track) 49 | { 50 | return mSong.getMacro(mEditorState.macro); 51 | } 52 | 53 | 54 | void MacroEditor::findUnusedTrack(int track) 55 | { 56 | int orig = mEditorState.macro; 57 | 58 | while (!mSong.getMacro(mEditorState.macro).isEmpty()) 59 | { 60 | mEditorState.macro = (mEditorState.macro + 1) % Song::maxMacros; 61 | 62 | // Check if we are back at starting position 63 | 64 | if (orig == mEditorState.macro) 65 | break; 66 | } 67 | } 68 | 69 | 70 | void MacroEditor::onRequestCommandRegistration() 71 | { 72 | registerCommand("Macro", "Mark block start", [this]() { this->setBlockStartToCurrentRow(); }, SDLK_b, KMOD_CTRL); 73 | registerCommand("Macro", "Mark block end", [this]() { this->setBlockEndToCurrentRow(); }, SDLK_e, KMOD_CTRL); 74 | registerCommand("Macro", "Find unused macro", [this]() { this->findCurrentUnusedTrack(); }, SDLK_u, KMOD_CTRL); 75 | registerCommand("Macro", "Kill current macro", [this]() { this->killCurrentTrack(); }, SDLK_k, KMOD_CTRL); 76 | registerCommand("Macro", "Copy macro", [this]() { this->copyCurrentTrack(); }, SDLK_F3); 77 | registerCommand("Macro", "Paste macro", [this]() { this->pasteCurrentTrack(); }, SDLK_F4); 78 | registerCommand("Macro", "Copy macro block", [this]() { this->copyCurrentBlock(); }, SDLK_c, KMOD_CTRL); 79 | registerCommand("Macro", "Paste macro block", [this]() { this->pasteCurrentBlock(); }, SDLK_v, KMOD_CTRL); 80 | registerCommand("Macro", "Set edit skip", [this](int value) { 81 | this->setEditSkip(value); 82 | }, [this](CommandOptionSelector& selector) { 83 | for (int i = 0 ; i <= 16 ; ++i) 84 | selector.addIntItem(i); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /src/Pattern.cpp: -------------------------------------------------------------------------------- 1 | #include "Pattern.h" 2 | #include "PatternRow.h" 3 | #include 4 | 5 | const int Pattern::maxRows; 6 | 7 | Pattern::Pattern() 8 | { 9 | rows = new PatternRow[maxRows]; 10 | } 11 | 12 | 13 | Pattern::~Pattern() 14 | { 15 | delete [] rows; 16 | } 17 | 18 | 19 | PatternRow& Pattern::getRow(int row) 20 | { 21 | return rows[row]; 22 | } 23 | 24 | 25 | void Pattern::clear() 26 | { 27 | for (int i = 0 ; i < maxRows ; ++i) 28 | rows[i].clear(); 29 | } 30 | 31 | 32 | int Pattern::getLastUsedRow() const 33 | { 34 | int last = -1; 35 | 36 | for (int i = 0 ; i < maxRows ; ++i) 37 | { 38 | const PatternRow& row = rows[i]; 39 | bool hasData = false; 40 | 41 | for (int effectParam = 0 ; !hasData && effectParam < PatternRow::effectParams + 1 ; ++effectParam) 42 | { 43 | hasData |= !row.getAnyParam(effectParam).isEmpty(); 44 | } 45 | 46 | if (hasData) 47 | { 48 | last = i; 49 | } 50 | } 51 | 52 | return last + 1; 53 | } 54 | 55 | 56 | int Pattern::getLastMacroUsed() const 57 | { 58 | int last = -1; 59 | 60 | for (int i = 0 ; i < maxRows ; ++i) 61 | { 62 | const PatternRow& row = rows[i]; 63 | 64 | for (int effectParam = 0 ; effectParam < PatternRow::effectParams + 1 ; ++effectParam) 65 | { 66 | const EffectParam& effect = row.getAnyParam(effectParam); 67 | if (effect.effect == 'm') 68 | { 69 | last = effect.getParamsAsByte(); 70 | } 71 | } 72 | } 73 | 74 | return last; 75 | } 76 | 77 | 78 | bool Pattern::isEmpty() const 79 | { 80 | return getLastUsedRow() == 0; 81 | } 82 | 83 | 84 | void Pattern::insertRow(int row, int flags) 85 | { 86 | for (int i = maxRows - 1 ; i > row ; --i) 87 | { 88 | PatternRow& src = rows[i - 1]; 89 | PatternRow& dest = rows[i]; 90 | 91 | if (flags & PatternRow::FlagNote) 92 | { 93 | dest.getNote() = src.getNote(); 94 | } 95 | 96 | if (flags & PatternRow::FlagEffect) 97 | { 98 | for (int effectParam = 0 ; effectParam < PatternRow::effectParams ; ++effectParam) 99 | { 100 | dest.getEffect(effectParam) = src.getEffect(effectParam); 101 | } 102 | } 103 | } 104 | 105 | rows[row].clear(flags); 106 | } 107 | 108 | 109 | void Pattern::deleteRow(int row, int flags) 110 | { 111 | for (int i = row ; i < maxRows - 1 ; ++i) 112 | { 113 | PatternRow& src = rows[i + 1]; 114 | PatternRow& dest = rows[i]; 115 | 116 | if (flags & PatternRow::FlagNote) 117 | { 118 | dest.getNote() = src.getNote(); 119 | } 120 | 121 | if (flags & PatternRow::FlagEffect) 122 | { 123 | for (int effectParam = 0 ; effectParam < PatternRow::effectParams ; ++effectParam) 124 | { 125 | dest.getEffect(effectParam) = src.getEffect(effectParam); 126 | } 127 | } 128 | } 129 | 130 | rows[maxRows - 1].clear(flags); 131 | } 132 | -------------------------------------------------------------------------------- /src/Wave.cpp: -------------------------------------------------------------------------------- 1 | #include "Wave.h" 2 | #include "Oscillator.h" 3 | #include "WaveGen.h" 4 | #include "Random.h" 5 | #include 6 | #include 7 | #include 8 | 9 | Wave::Wave() 10 | : mLength(0), mLevels(0), mData(NULL), mOffset(NULL) 11 | { 12 | } 13 | 14 | 15 | void Wave::init(int length) 16 | { 17 | mLength = length; 18 | int totalSamples = 0; 19 | 20 | mLevels = 0; 21 | 22 | for (int l = length ; l > 0 ; l >>= 1) 23 | mLevels++; 24 | 25 | mOffset = new int[mLevels]; 26 | 27 | for (int level = 0, l = length ; l > 0 ; l >>= 1, level++) 28 | { 29 | mOffset[level] = totalSamples; 30 | totalSamples += l; 31 | } 32 | 33 | mData = new int[totalSamples]; 34 | } 35 | 36 | 37 | Wave::~Wave() 38 | { 39 | delete[] mOffset; 40 | delete[] mData; 41 | } 42 | 43 | 44 | const int* Wave::getData(int level) const 45 | { 46 | return mData + mOffset[level]; 47 | } 48 | 49 | 50 | void Wave::generateMipMapLevel(int level) 51 | { 52 | int length = getLength(level); 53 | int *dest = mData + mOffset[level]; 54 | int *src = mData + mOffset[level - 1]; 55 | 56 | //printf("Level %d: ", level); 57 | 58 | for (int i = 0 ; i < length ; ++i) 59 | { 60 | dest[i] = (src[i * 2] + src[i * 2 + 1]) / 2; 61 | 62 | //printf("%d ", dest[i]); 63 | } 64 | 65 | //printf("\n"); 66 | } 67 | 68 | 69 | void Wave::generateMipMap() 70 | { 71 | for (int i = 1 ; i < mLevels ; ++i) 72 | generateMipMapLevel(i); 73 | } 74 | 75 | 76 | int Wave::getLength(int level) const 77 | { 78 | return mLength >> level; 79 | } 80 | 81 | 82 | int Wave::getLevels() const 83 | { 84 | return mLevels; 85 | } 86 | 87 | 88 | void Wave::generate(int index) 89 | { 90 | if (index < 16) 91 | generateSquare(0.5f + (float)index / 16 * 0.5); 92 | else if (index == 255) 93 | { 94 | generateNoise(); 95 | } 96 | else 97 | { 98 | WaveGen gen; 99 | gen.generate(index, mData, mLength, waveAmplitude / 2); 100 | //generateSquare(0.5f); 101 | } 102 | 103 | generateMipMap(); 104 | } 105 | 106 | 107 | void Wave::generateSquare(float pw) 108 | { 109 | for (int i = 0 ; i < mLength ; ++i) 110 | if (i < mLength * pw) 111 | mData[i] = -waveAmplitude/2; 112 | else 113 | mData[i] = waveAmplitude/2; 114 | 115 | 116 | } 117 | 118 | 119 | void Wave::generateSine() 120 | { 121 | 122 | } 123 | 124 | 125 | 126 | void Wave::generateNoise() 127 | { 128 | int hold = 0; 129 | 130 | Random rnd; 131 | 132 | for (int i = 0 ; i < mLength ; ++i) 133 | { 134 | if ((i & 3) == 0) 135 | { 136 | hold = rnd.rnd(-waveAmplitude / 2, waveAmplitude / 2); 137 | } 138 | 139 | mData[i] = hold; 140 | } 141 | } 142 | 143 | 144 | void Wave::generateSaw() 145 | { 146 | } 147 | 148 | 149 | void Wave::generateTriangle(float step) 150 | { 151 | } 152 | -------------------------------------------------------------------------------- /src/NoteState.cpp: -------------------------------------------------------------------------------- 1 | #include "NoteState.h" 2 | #include "EffectParam.h" 3 | #include "ITrackState.h" 4 | #include "PlayerState.h" 5 | #include "SDL.h" 6 | #include 7 | 8 | NoteState::NoteState() 9 | : frequency(0), slideTarget(0), volume(0) 10 | { 11 | SDL_memset(effectMemory, 0, sizeof(effectMemory)); 12 | } 13 | 14 | 15 | void NoteState::setFrequencyFromNote(int note) 16 | { 17 | static const float ratio = powf(2.0f, 1.0f/12.0f); 18 | frequency = powf(ratio, note - (12 * 3 + 9)); 19 | } 20 | 21 | 22 | void NoteState::setSlideTargetFromNote(int note) 23 | { 24 | static const float ratio = powf(2.0f, 1.0f/12.0f); 25 | slideTarget = powf(ratio, note - (12 * 3 + 9)); 26 | } 27 | 28 | 29 | bool NoteState::handleEffectZeroTick(const EffectParam& effect, ITrackState& trackState, PlayerState& playerState) 30 | { 31 | int asByte = effect.getParamsAsByte(); 32 | 33 | switch (effect.effect) 34 | { 35 | case 'c': 36 | volume = asByte; 37 | 38 | if (volume > ITrackState::maxVolume) 39 | volume = ITrackState::maxVolume; 40 | break; 41 | 42 | case 'f': 43 | if (asByte < 0x20) 44 | playerState.songSpeed = asByte; 45 | else 46 | playerState.songRate = asByte; 47 | break; 48 | 49 | default: 50 | return trackState.handleEffectZeroTick(effect, playerState); 51 | } 52 | 53 | return false; 54 | } 55 | 56 | 57 | void NoteState::handleEffectAnyTick(const EffectParam& effect, ITrackState& trackState, PlayerState& playerState) 58 | { 59 | int param1 = effect.param1; 60 | int param2 = effect.param2; 61 | int asByte = effect.getParamsAsByte(); 62 | 63 | if (asByte != 0) 64 | effectMemory[effect.effect] = asByte; 65 | else 66 | { 67 | asByte = effectMemory[effect.effect]; 68 | param1 = (asByte >> 4) & 0xf; 69 | param2 = asByte & 0xf; 70 | } 71 | 72 | switch (effect.effect) 73 | { 74 | case 'a': 75 | volume -= param2; 76 | 77 | if (volume < 0) 78 | volume = 0; 79 | 80 | volume += param1; 81 | 82 | if (volume > ITrackState::maxVolume) 83 | volume = ITrackState::maxVolume; 84 | 85 | break; 86 | 87 | case 'k': 88 | if (asByte == playerState.tick) 89 | volume = 0; 90 | break; 91 | 92 | case '1': 93 | { 94 | frequency += (float)asByte / slideDivider; 95 | 96 | } 97 | break; 98 | 99 | case '2': 100 | { 101 | frequency -= (float)asByte / slideDivider; 102 | 103 | if (frequency < 0) 104 | frequency = 0; 105 | } 106 | break; 107 | 108 | case '3': 109 | { 110 | if (slideTarget < frequency) 111 | { 112 | frequency -= (float)asByte / slideDivider; 113 | 114 | if (frequency < slideTarget) 115 | frequency = slideTarget; 116 | } 117 | else 118 | { 119 | frequency += (float)asByte / slideDivider; 120 | 121 | if (frequency > slideTarget) 122 | frequency = slideTarget; 123 | } 124 | 125 | 126 | } 127 | break; 128 | 129 | default: 130 | trackState.handleEffectAnyTick(EffectParam(effect.effect, param1, param2), playerState); 131 | break; 132 | } 133 | 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/Emscripten.cpp: -------------------------------------------------------------------------------- 1 | #include "Emscripten.h" 2 | #include "SDL.h" 3 | #include 4 | #include "App.h" 5 | 6 | #ifdef __EMSCRIPTEN__ 7 | 8 | #include "Prototracker.h" 9 | 10 | extern Prototracker *g_prototracker; 11 | 12 | #include 13 | 14 | extern "C" { 15 | 16 | void emDeinit(); 17 | 18 | EMSCRIPTEN_KEEPALIVE 19 | void emFilesSyncedShutdown() 20 | { 21 | SDL_Event event; 22 | event.type = EVERYTHING_DONE; 23 | SDL_PushEvent(&event); 24 | } 25 | 26 | 27 | EMSCRIPTEN_KEEPALIVE 28 | void emFilesSyncedStartup() 29 | { 30 | SDL_Event event; 31 | event.type = EVERYTHING_READY; 32 | SDL_PushEvent(&event); 33 | } 34 | 35 | EMSCRIPTEN_KEEPALIVE 36 | void emSyncFsAndStartup() 37 | { 38 | EM_ASM_({ 39 | var appName = UTF8ToString($0); 40 | console.log("Mounting filesystem"); 41 | var path = '/libsdl'; 42 | FS.mkdir(path); 43 | path+='/'+appName; 44 | FS.mkdir(path); 45 | path+='/'+appName; 46 | FS.mkdir(path); 47 | FS.mkdir('/imported'); 48 | FS.mount(IDBFS, {}, path); 49 | 50 | FS.syncfs(true, function(err) { 51 | assert(!err); 52 | console.log("Filesystem is loaded, starting up"); 53 | Module.ccall('emFilesSyncedStartup', 'v'); 54 | }); 55 | }, APP_NAME); 56 | } 57 | 58 | 59 | EMSCRIPTEN_KEEPALIVE 60 | void emSyncFs() 61 | { 62 | EM_ASM( 63 | console.log("Syncing filesystem"); 64 | 65 | FS.syncfs(false, function(err) { 66 | assert(!err); 67 | console.log("Filesystem synced"); 68 | }); 69 | ); 70 | } 71 | 72 | 73 | EMSCRIPTEN_KEEPALIVE 74 | void emSyncFsAndShutdown() 75 | { 76 | EM_ASM( 77 | console.log("Saving filesystem"); 78 | FS.syncfs(function(err) { 79 | assert(!err); 80 | console.log("Filesystem is saved, shutting down"); 81 | Module.ccall('emFilesSyncedShutdown', 'v'); 82 | }); 83 | ); 84 | } 85 | 86 | 87 | EMSCRIPTEN_KEEPALIVE 88 | const char * emOnBeforeUnload(int eventType, const void *reserved, void *userData) 89 | { 90 | return ""; 91 | } 92 | 93 | 94 | EMSCRIPTEN_KEEPALIVE 95 | void emOnFileImported() 96 | { 97 | SDL_Event event; 98 | event.type = SONG_IMPORTED; 99 | SDL_PushEvent(&event); 100 | } 101 | 102 | 103 | EMSCRIPTEN_KEEPALIVE 104 | void emExportFile() 105 | { 106 | SDL_Event event; 107 | event.type = EXPORT_SONG; 108 | SDL_PushEvent(&event); 109 | } 110 | 111 | 112 | EMSCRIPTEN_KEEPALIVE 113 | void emPlaySong() 114 | { 115 | SDL_Event event; 116 | event.type = PLAY_SONG; 117 | SDL_PushEvent(&event); 118 | } 119 | 120 | 121 | EMSCRIPTEN_KEEPALIVE 122 | void emNewSong() 123 | { 124 | SDL_Event event; 125 | event.type = NEW_SONG; 126 | SDL_PushEvent(&event); 127 | } 128 | 129 | 130 | EMSCRIPTEN_KEEPALIVE 131 | const char * emRequestSong() 132 | { 133 | return g_prototracker->getSongBase64().c_str(); 134 | } 135 | 136 | 137 | EMSCRIPTEN_KEEPALIVE 138 | void emAppReady() 139 | { 140 | emscripten_run_script("if (typeof appReady === \"function\") appReady();"); 141 | } 142 | 143 | EMSCRIPTEN_KEEPALIVE 144 | void emAppShutdown() 145 | { 146 | emscripten_run_script("if (typeof appShutdown === \"function\") appShutdown();"); 147 | } 148 | 149 | } 150 | 151 | #else 152 | 153 | void emSyncFsAndShutdown() {} 154 | void emSyncFsAndStartup() {} 155 | 156 | #endif 157 | -------------------------------------------------------------------------------- /src/MainEditor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Editor.h" 4 | #include 5 | #include 6 | 7 | struct IPlayer; 8 | struct Song; 9 | struct PatternEditor; 10 | struct TextEditor; 11 | struct SequenceRowEditor; 12 | struct Oscilloscope; 13 | struct ISynth; 14 | struct Mixer; 15 | struct FileSelector; 16 | struct Listenable; 17 | struct Theme; 18 | struct MessageManager; 19 | struct MessageDisplayer; 20 | struct TooltipManager; 21 | struct TooltipDisplayer; 22 | struct AudioDeviceSelector; 23 | struct CommandSelector; 24 | struct CommandOptionSelector; 25 | 26 | class MainEditor: public Editor 27 | { 28 | IPlayer& mPlayer; 29 | PlayerState& mPlayerState; 30 | Song& mSong; 31 | ISynth& mSynth; 32 | Mixer& mMixer; 33 | Listenable *mOscillatorsProbePos; 34 | PatternEditor *patternEditor; 35 | SequenceRowEditor *sequenceRowEditor; 36 | TextEditor *songNameEditor; 37 | TextEditor *macroNameEditor; 38 | FileSelector *fileSelector; 39 | AudioDeviceSelector *audioDeviceSelector; 40 | CommandSelector *commandSelector; 41 | CommandOptionSelector *commandOptionSelector; 42 | MessageManager *mMessageManager; 43 | MessageDisplayer *mMessageDisplayer; 44 | TooltipManager *mTooltipManager; 45 | TooltipDisplayer *mTooltipDisplayer; 46 | 47 | int mDragStartX, mDragStartY; 48 | bool mIsDragging; 49 | const CommandDescriptor *mSelectedCommand; 50 | 51 | enum 52 | { 53 | FileSelectionLoad, 54 | FileSelectionSave, 55 | AudioDeviceSelection, 56 | CommandSelection, 57 | CommandOptionSelection, 58 | }; 59 | 60 | void displayLoadDialog(); 61 | void displaySaveDialog(); 62 | void displayAudioDeviceDialog(); 63 | void displayCommandPalette(); 64 | 65 | std::string mBase64Encoded; 66 | 67 | std::string getUserFile(const char *file) const; 68 | void deleteChildren(); 69 | 70 | void startDragging(int x, int y); 71 | void stopDragging(); 72 | void togglePositionFollowing(); 73 | 74 | protected: 75 | virtual void onRequestCommandRegistration(); 76 | 77 | public: 78 | MainEditor(EditorState& editorState, IPlayer& player, PlayerState& playerState, Song& song, ISynth& synth, Mixer& mixer); 79 | virtual ~MainEditor(); 80 | 81 | virtual bool onEvent(SDL_Event& event); 82 | virtual void onDraw(Renderer& renderer, const SDL_Rect& area); 83 | virtual void onFileSelectorEvent(const Editor& fileSelector, bool accept); 84 | virtual void showTooltip(const SDL_Rect& area, const char* message); 85 | virtual int showMessageInner(MessageClass messageClass, int messageId, const char* message); 86 | 87 | void cycleFocus(); 88 | void syncPlayerState(); 89 | 90 | virtual void onUpdate(int ms); 91 | 92 | void setMacro(int index); 93 | void syncSongParameters(const Song& song); 94 | void refreshAll(); 95 | void setPatternLength(int length); 96 | void setOctave(int octave); 97 | 98 | void playSong(); 99 | void playPattern(); 100 | void stopSong(); 101 | void muteTracks(); 102 | void toggleTrackMuting(int track); 103 | void togglePlayStop(); 104 | void toggleEditMode(); 105 | 106 | void setAudioDevice(const char *device); 107 | void displayCommandOptionDialog(const CommandDescriptor& command); 108 | 109 | bool saveSong(const char *path); 110 | bool loadSong(const char *path); 111 | bool exportSong(); 112 | void newSong(); 113 | const std::string& getSongBase64(); 114 | 115 | bool loadState(); 116 | void saveState(); 117 | 118 | bool loadElements(const Theme& theme); 119 | 120 | }; 121 | -------------------------------------------------------------------------------- /src/TextEditor.cpp: -------------------------------------------------------------------------------- 1 | #include "TextEditor.h" 2 | #include "Renderer.h" 3 | #include "Color.h" 4 | #include 5 | 6 | TextEditor::TextEditor(EditorState& editorState) 7 | : Editor(editorState), mCursorPosition(0), mIsEditing(false), mAlwaysShowCursor(false), mSolidBackground(true) 8 | { 9 | } 10 | 11 | 12 | TextEditor::~TextEditor() 13 | { 14 | } 15 | 16 | 17 | void TextEditor::setIsEditing(bool state) 18 | { 19 | mIsEditing = state; 20 | 21 | if (state) 22 | SDL_StartTextInput(); 23 | else 24 | SDL_StopTextInput(); 25 | } 26 | 27 | 28 | bool TextEditor::onEvent(SDL_Event& event) 29 | { 30 | if (mIsEditing) 31 | { 32 | switch (event.type) 33 | { 34 | case SDL_KEYDOWN: 35 | if (event.key.keysym.sym == SDLK_BACKSPACE) 36 | { 37 | handleBackspace(); 38 | return true; 39 | } 40 | else if (event.key.keysym.sym == SDLK_ESCAPE || event.key.keysym.sym == SDLK_RETURN) 41 | { 42 | setIsEditing(false); 43 | setDirty(true); 44 | return true; 45 | } 46 | #ifdef __EMSCRIPTEN__ 47 | // In case Event.preventDefault() eats the SDL_TEXTINPUT event, we manually add the (ASCII) keypresses here 48 | else 49 | { 50 | if (event.key.keysym.sym < 127) 51 | typeText((std::string() + static_cast(event.key.keysym.sym)).c_str()); 52 | 53 | return true; 54 | } 55 | #endif 56 | break; 57 | 58 | case SDL_TEXTINPUT: 59 | typeText(event.text.text); 60 | return true; 61 | } 62 | } 63 | else 64 | { 65 | switch (event.type) 66 | { 67 | case SDL_MOUSEBUTTONDOWN: 68 | setIsEditing(true); 69 | setDirty(true); 70 | return true; 71 | break; 72 | 73 | case SDL_KEYDOWN: 74 | if (event.key.keysym.sym == SDLK_RETURN) 75 | { 76 | setIsEditing(true); 77 | setDirty(true); 78 | return true; 79 | } 80 | break; 81 | } 82 | } 83 | 84 | return false; 85 | } 86 | 87 | 88 | void TextEditor::onDraw(Renderer& renderer, const SDL_Rect& area) 89 | { 90 | setDirty(false); 91 | 92 | if (hasFocus()) 93 | renderer.clearRect(area, Theme::ColorType::TextFocus); 94 | else 95 | { 96 | if (mSolidBackground) 97 | renderer.clearRect(area, Theme::ColorType::TextBackground); 98 | else 99 | renderer.renderBackground(area); 100 | } 101 | 102 | renderer.renderText(area, Theme::ColorType::NormalText, mBuffer); 103 | 104 | if ((hasFocus() && mIsEditing) || mAlwaysShowCursor) 105 | { 106 | SDL_Rect cursor = { area.x + renderer.getFontWidth() * static_cast(strlen(mBuffer)), area.y, 107 | renderer.getFontWidth(), renderer.getFontHeight() }; 108 | renderer.clearRect(cursor, Theme::ColorType::TextCursor); 109 | } 110 | } 111 | 112 | 113 | void TextEditor::setSolidBackground(bool state) 114 | { 115 | mSolidBackground = state; 116 | } 117 | 118 | 119 | void TextEditor::setBuffer(char *buffer, int bufferSize) 120 | { 121 | setDirty(true); 122 | mBuffer = buffer; 123 | mBufferSize = bufferSize; 124 | } 125 | 126 | 127 | void TextEditor::typeText(const char *text) 128 | { 129 | strncat(mBuffer, text, mBufferSize - strlen(mBuffer) - 1); 130 | setDirty(true); 131 | } 132 | 133 | 134 | void TextEditor::handleBackspace() 135 | { 136 | int length = strlen(mBuffer); 137 | 138 | if (length > 0) 139 | { 140 | mBuffer[length - 1] = '\0'; 141 | setDirty(true); 142 | } 143 | 144 | } 145 | 146 | 147 | void TextEditor::setText(const char *text) 148 | { 149 | strncpy(mBuffer, text, mBufferSize); 150 | setDirty(true); 151 | } 152 | 153 | 154 | void TextEditor::setAlwaysShowCursor(bool state) 155 | { 156 | mAlwaysShowCursor = state; 157 | 158 | } 159 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build-linux: 4 | docker: 5 | - image: minextu/sdl2-cross-platform 6 | steps: 7 | - checkout 8 | - run: &init-steps 9 | name: Init 10 | command: | 11 | echo 'export EXECNAME=${CIRCLE_PROJECT_REPONAME}' >> $BASH_ENV 12 | echo 'export BASENAME=${CIRCLE_PROJECT_REPONAME}-$(git describe --tags --always | sed -e 's/^v//')' >> $BASH_ENV 13 | mkdir artifacts 14 | - run: 15 | name: Build (Linux) 16 | command: make linux OUTPUT=${EXECNAME} 17 | - run: 18 | name: Store binary 19 | command: | 20 | zip -r artifacts/${BASENAME}-linux.zip ${EXECNAME} assets 21 | zip -j artifacts/${BASENAME}-linux.zip \ 22 | LICENSE 23 | make -f Makefile.linux clean OUTPUT=${EXECNAME} 24 | - persist_to_workspace: 25 | root: . 26 | paths: 27 | - artifacts 28 | build-win: 29 | docker: 30 | - image: minextu/sdl2-cross-platform 31 | steps: 32 | - checkout 33 | - run: *init-steps 34 | - run: 35 | name: Build (Windows 32-bit) 36 | command: | 37 | PATH="/usr/i686-w64-mingw32/bin:$PATH" 38 | make linux CC=i686-w64-mingw32-g++ OUTPUT=${EXECNAME}.exe 39 | - run: 40 | name: Store binary 41 | command: | 42 | zip -r artifacts/${BASENAME}-win32.zip ${EXECNAME}.exe assets 43 | zip -j artifacts/${BASENAME}-win32.zip \ 44 | LICENSE \ 45 | /usr/i686-w64-mingw32/bin/SDL2.dll \ 46 | /usr/i686-w64-mingw32/bin/SDL2_image.dll \ 47 | /usr/i686-w64-mingw32/bin/libpng16-16.dll \ 48 | /usr/i686-w64-mingw32/bin/zlib1.dll 49 | make -f Makefile.linux clean OUTPUT=${EXECNAME}.exe 50 | - persist_to_workspace: 51 | root: . 52 | paths: 53 | - artifacts 54 | build-macos: 55 | macos: 56 | xcode: 10.2.1 57 | steps: 58 | - checkout 59 | - run: *init-steps 60 | - restore_cache: 61 | keys: 62 | - brew-cache 63 | - run: 64 | name: Install deps 65 | command: brew install sdl2 sdl2_image zip 66 | - save_cache: 67 | key: brew-cache 68 | paths: 69 | - /usr/local/Homebrew 70 | - run: 71 | name: Build (MacOS) 72 | command: make macos 73 | - run: 74 | name: Store binary 75 | command: | 76 | zip -r artifacts/${BASENAME}-macos.zip ${EXECNAME} assets 77 | zip -j artifacts/${BASENAME}-macos.zip \ 78 | LICENSE 79 | - persist_to_workspace: 80 | root: . 81 | paths: 82 | - artifacts 83 | - store_artifacts: 84 | path: ./artifacts 85 | publish-github-release: 86 | docker: 87 | - image: cibuilds/github:0.10 88 | steps: 89 | - checkout 90 | - attach_workspace: 91 | at: . 92 | - run: 93 | name: "Publish Release on GitHub" 94 | command: | 95 | VERSION=$(git describe --tags --always) 96 | ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} \ 97 | -c ${CIRCLE_SHA1} ${VERSION} ./artifacts/ 98 | 99 | workflows: 100 | version: 2 101 | test: 102 | jobs: 103 | - build-linux: 104 | filters: &build-filters 105 | branches: 106 | ignore: master 107 | tags: 108 | ignore: /.*/ 109 | - build-win: 110 | filters: *build-filters 111 | - build-macos: 112 | filters: *build-filters 113 | build-and-release: 114 | jobs: 115 | - build-linux: 116 | filters: &release-filters 117 | branches: 118 | ignore: /.*/ 119 | tags: 120 | only: /^v\d+\.\d+\.\d+$/ 121 | - build-win: 122 | filters: *release-filters 123 | - build-macos: 124 | filters: *release-filters 125 | - publish-github-release: 126 | requires: 127 | - build-linux 128 | - build-win 129 | - build-macos 130 | filters: *release-filters 131 | -------------------------------------------------------------------------------- /src/WaveGen.cpp: -------------------------------------------------------------------------------- 1 | #include "WaveGen.h" 2 | #include "Random.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #ifndef M_PI 8 | # define M_PI 3.14159265358979323846 /* pi */ 9 | #endif 10 | 11 | #define convertBits(value, bit, width) ((value >> bit) & ((1 << width) - 1)) 12 | 13 | WaveGen::WaveGen() 14 | { 15 | } 16 | 17 | 18 | WaveGen::~WaveGen() 19 | { 20 | } 21 | 22 | 23 | void WaveGen::initSeed(int seed) 24 | { 25 | mCarrier = convertBits(seed, carrierBit, carrierWidth); 26 | mModulator = convertBits(seed, modulatorBit, modulatorWidth); 27 | mModulatorRatio = convertBits(seed, modulatorRatioBit, modulatorRatioWidth); 28 | mPulseWidth = convertBits(seed, pwBit, pwWidth); 29 | mBoost = convertBits(seed, boostBit, boostWidth); 30 | 31 | //printf("%03d c=%d m=%d r=%d pw=%d\n", seed, mCarrier, mModulator, mModulatorRatio, mPulseWidth); 32 | } 33 | 34 | float WaveGen::step(float in, int steps) 35 | { 36 | return (float)(int)(in * (steps / 2)) / (steps / 2); 37 | } 38 | 39 | 40 | float WaveGen::sine(float phase) 41 | { 42 | return sin(phase * M_PI * 2.0f); 43 | } 44 | 45 | 46 | float WaveGen::saw(float phase) 47 | { 48 | return fmod(fmod(phase, 1.0f) + 1.0f, 1.0f) * 2.0f - 1.0f; 49 | } 50 | 51 | 52 | float WaveGen::square(float phase, float pw) 53 | { 54 | return fmod(fmod(phase, 1.0f) + 1.0f, 1.0f) < pw ? -1.0f : 1.0f; 55 | } 56 | 57 | 58 | float WaveGen::zSquare(float phase, float pw) 59 | { 60 | return fmod(fmod(phase, 1.0f) + 1.0f, 1.0f) < pw ? 0.0f : 1.0f; 61 | } 62 | 63 | 64 | void WaveGen::generate(int seed, int *data, int length, int amplitude) 65 | { 66 | memset(data, 0, sizeof(int) * length); 67 | 68 | Random rnd; 69 | initSeed(seed); 70 | 71 | float hold = 0; 72 | 73 | for (int _i = 0 ; _i < length ; ++_i) 74 | { 75 | int i = _i; 76 | 77 | const struct { float c, m; } ratio[1 << (modulatorRatioWidth)] = { 78 | {1, 1}, {1, 2}, {1, 3}, {2, 1}, 79 | }; 80 | 81 | float carrier, phase = static_cast(i) / length, modPhase = 0.0f; 82 | float c = ratio[mModulatorRatio].c; 83 | float m = ratio[mModulatorRatio].m; 84 | 85 | const float pulseWidth[1 << (pwWidth)] = {0.5f, 0.333f, 0.25f, 0.125f}; 86 | const float modifier[1 << (pwWidth)] = {0.01f, 0.25, 0.5f, 1.0f}; 87 | const float modifierPhase[1 << (pwWidth)] = {0.01f, 0.25, 0.333f, 0.5f}; 88 | 89 | switch (mModulator) 90 | { 91 | default: 92 | modPhase = 0; 93 | break; 94 | 95 | case 0: 96 | modPhase = sine(phase * m) * modifier[mPulseWidth]; 97 | break; 98 | } 99 | 100 | switch (mCarrier) 101 | { 102 | default: 103 | case 0: 104 | carrier = sine(phase * c + modPhase); 105 | break; 106 | 107 | case 1: 108 | carrier = saw(phase * c + modPhase); 109 | break; 110 | 111 | case 2: 112 | carrier = square(phase * c + modPhase, pulseWidth[mPulseWidth]); 113 | break; 114 | 115 | case 3: 116 | switch (mModulator) 117 | { 118 | default: 119 | case 0: 120 | carrier = sine(fmod(phase * c / 4 + modPhase, 0.25f)); 121 | break; 122 | 123 | case 1: 124 | carrier = step(rnd.rndf() * 2.0f - 1.0f, 16) * 2.0f + square(phase*c*4, pulseWidth[mPulseWidth]) * 2.0f; 125 | break; 126 | 127 | case 2: 128 | carrier = step(rnd.rndf() * 2.0f - 1.0f, 64) * 2.0f + sine(phase*c*4) * 2.0f; 129 | break; 130 | 131 | case 3: 132 | carrier = step(rnd.rndf() * 2.0f - 1.0f, 32) * 2.0f * zSquare(phase*c, pulseWidth[mPulseWidth]) + sine(phase); 133 | break; 134 | } 135 | break; 136 | } 137 | 138 | switch (mModulator) 139 | { 140 | default: 141 | break; 142 | case 1: 143 | carrier *= sine(phase * m + modifierPhase[mPulseWidth % 4]); 144 | break; 145 | 146 | case 2: 147 | carrier *= saw(phase * m + modifierPhase[mPulseWidth % 4]); 148 | break; 149 | 150 | case 3: 151 | carrier *= square(phase * m + modifierPhase[mPulseWidth % 4]); 152 | break; 153 | } 154 | 155 | //carrier += sin(phase) * mBoost / 8; 156 | 157 | data[_i] = std::min(static_cast(amplitude), std::max(-static_cast(amplitude), amplitude * carrier)); 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/Value.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Listenable.h" 4 | 5 | class Value: public Listenable 6 | { 7 | protected: 8 | int mValue; 9 | 10 | public: 11 | int getValue() const { return mValue; } 12 | 13 | Value(): Listenable(), mValue(0) {} 14 | Value(int value): Listenable(), mValue(value) {} 15 | operator int() const { return mValue; } 16 | 17 | inline Value& operator=(int rhs) { if (rhs != mValue) { mValue = rhs; notify(); } return *this; } 18 | inline Value& operator=(const Value& rhs) { if (rhs.getValue() != mValue) { mValue = rhs.getValue(); notify(); } return *this; } 19 | inline Value& operator--() { --mValue; notify(); return *this; } 20 | inline Value& operator++() { ++mValue; notify(); return *this; } 21 | inline Value& operator+=(const Value& rhs) { if (rhs.getValue() != 0) { mValue += rhs.getValue(); notify(); } return *this; } 22 | inline Value& operator-=(const Value& rhs) { if (rhs.getValue() != 0) { mValue -= rhs.getValue(); notify(); } return *this; } 23 | inline Value& operator/=(const Value& rhs) { mValue /= rhs.getValue(); notify(); return *this; } 24 | inline Value& operator%=(const Value& rhs) { mValue %= rhs.getValue(); notify(); return *this; } 25 | 26 | inline int operator+() const { return +mValue; } 27 | inline int operator-() const { return -mValue; } 28 | inline int operator!() const { return !mValue; } 29 | inline int operator~() const { return ~mValue; } 30 | }; 31 | 32 | inline bool operator!=(const Value& lhs, const Value& rhs){ return lhs.getValue() != rhs.getValue(); } 33 | inline bool operator==(const Value& lhs, const Value& rhs){ return lhs.getValue() == rhs.getValue(); } 34 | inline bool operator< (const Value& lhs, const Value& rhs){ return lhs.getValue() < rhs.getValue(); } 35 | inline bool operator> (const Value& lhs, const Value& rhs){ return lhs.getValue() > rhs.getValue(); } 36 | inline bool operator<=(const Value& lhs, const Value& rhs){ return lhs.getValue() <= rhs.getValue(); } 37 | inline bool operator>=(const Value& lhs, const Value& rhs){ return lhs.getValue() >= rhs.getValue(); } 38 | 39 | inline bool operator!=(int lhs, const Value& rhs){ return lhs != rhs.getValue(); } 40 | inline bool operator==(int lhs, const Value& rhs){ return lhs == rhs.getValue(); } 41 | inline bool operator< (int lhs, const Value& rhs){ return lhs < rhs.getValue(); } 42 | inline bool operator> (int lhs, const Value& rhs){ return lhs > rhs.getValue(); } 43 | inline bool operator<=(int lhs, const Value& rhs){ return lhs <= rhs.getValue(); } 44 | inline bool operator>=(int lhs, const Value& rhs){ return lhs >= rhs.getValue(); } 45 | 46 | inline bool operator!=(const Value& lhs, int rhs){ return lhs.getValue() != rhs; } 47 | inline bool operator==(const Value& lhs, int rhs){ return lhs.getValue() == rhs; } 48 | inline bool operator< (const Value& lhs, int rhs){ return lhs.getValue() < rhs; } 49 | inline bool operator> (const Value& lhs, int rhs){ return lhs.getValue() > rhs; } 50 | inline bool operator<=(const Value& lhs, int rhs){ return lhs.getValue() <= rhs; } 51 | inline bool operator>=(const Value& lhs, int rhs){ return lhs.getValue() >= rhs; } 52 | 53 | inline int operator+(int lhs, const Value& rhs){ return lhs + rhs.getValue(); } 54 | inline int operator-(int lhs, const Value& rhs){ return lhs - rhs.getValue(); } 55 | inline int operator*(int lhs, const Value& rhs){ return lhs * rhs.getValue(); } 56 | inline int operator/(int lhs, const Value& rhs){ return lhs / rhs.getValue(); } 57 | inline int operator%(int lhs, const Value& rhs){ return lhs % rhs.getValue(); } 58 | 59 | inline int operator+(const Value& lhs, int rhs){ return lhs.getValue() + rhs; } 60 | inline int operator-(const Value& lhs, int rhs){ return lhs.getValue() - rhs; } 61 | inline int operator*(const Value& lhs, int rhs){ return lhs.getValue() * rhs; } 62 | inline int operator/(const Value& lhs, int rhs){ return lhs.getValue() / rhs; } 63 | inline int operator%(const Value& lhs, int rhs){ return lhs.getValue() % rhs; } 64 | 65 | inline int operator+(const Value& lhs, const Value& rhs){ return lhs.getValue() + rhs.getValue(); } 66 | inline int operator-(const Value& lhs, const Value& rhs){ return lhs.getValue() - rhs.getValue(); } 67 | inline int operator*(const Value& lhs, const Value& rhs){ return lhs.getValue() * rhs.getValue(); } 68 | inline int operator/(const Value& lhs, const Value& rhs){ return lhs.getValue() / rhs.getValue(); } 69 | inline int operator%(const Value& lhs, const Value& rhs){ return lhs.getValue() % rhs.getValue(); } 70 | -------------------------------------------------------------------------------- /src/Oscillator.cpp: -------------------------------------------------------------------------------- 1 | #include "Oscillator.h" 2 | #include 3 | #include 4 | #include "SDL.h" 5 | #include "Sample.h" 6 | #include "WaveStore.h" 7 | #include "SequenceRow.h" 8 | #include "Wave.h" 9 | #include "TrackState.h" 10 | 11 | Oscillator::Oscillator() 12 | : IOscillator(), mWaveStore(NULL), mWave(0), mSpeed(0), mVolume(1.0), mPosition(0) 13 | { 14 | } 15 | 16 | 17 | Oscillator::~Oscillator() 18 | { 19 | } 20 | 21 | 22 | void Oscillator::handleTrackState(ITrackState& trackState) 23 | { 24 | IOscillator::handleTrackState(trackState); 25 | 26 | // Note: We assume the provided reference actually is a TrackState 27 | // We inited it in Player::Player() so we know it will be. 28 | 29 | TrackState& extTrackState = static_cast(trackState); 30 | 31 | if (extTrackState.wave != -1) 32 | { 33 | this->setWave(extTrackState.wave); 34 | extTrackState.wave = -1; 35 | } 36 | 37 | if (extTrackState.queuedWave != -1) 38 | { 39 | this->queueWave(extTrackState.queuedWave); 40 | extTrackState.queuedWave = -1; 41 | } 42 | } 43 | 44 | 45 | void Oscillator::triggerNote() 46 | { 47 | // We do nothing here. Just let the wave run free. 48 | } 49 | 50 | 51 | void Oscillator::setWaveStore(const WaveStore& waveStore) 52 | { 53 | mWaveStore = &waveStore; 54 | } 55 | 56 | 57 | void Oscillator::setPosition(int newPosition) 58 | { 59 | mPosition = newPosition; 60 | } 61 | 62 | 63 | void Oscillator::setWave(int wave) 64 | { 65 | mWave = wave; 66 | mQueuedWave = wave; 67 | } 68 | 69 | 70 | void Oscillator::queueWave(int wave) 71 | { 72 | mQueuedWave = wave; 73 | } 74 | 75 | 76 | void Oscillator::setFrequency(float frequency) 77 | { 78 | mSpeed = frequency * oscillatorResolution * oscillatorLength*2; 79 | } 80 | 81 | 82 | void Oscillator::setVolume(int volume) 83 | { 84 | // This sets to the volume to 1/SequenceRow::maxTracks (25 %) 85 | // to avoid clipping if all tracks output at max signal. 86 | 87 | mVolume = volume * volumeResolution / TrackState::maxVolume / SequenceRow::maxTracks; 88 | } 89 | 90 | 91 | void Oscillator::update(int numSamples) 92 | { 93 | const Wave& wave = mWaveStore->getWave(mWave); 94 | int level = 0; //trunc(log2((float)mSpeed / oscillatorResolution)); 95 | int resolution = oscillatorResolution << level; 96 | 97 | if (level < 0) 98 | level = 0; 99 | else if (level >= wave.getLevels()) 100 | level = wave.getLevels() - 1; 101 | 102 | int waveLengthMaskRes = wave.getLength(level) * resolution - 1; 103 | 104 | mPosition = (mPosition + mSpeed * numSamples); 105 | 106 | // Oscillator looped, set the queued wave 107 | if (mPosition > waveLengthMaskRes) 108 | { 109 | setWave(mQueuedWave); 110 | mPosition &= waveLengthMaskRes; 111 | } 112 | } 113 | 114 | 115 | void Oscillator::render(Sample16 *buffer, int numSamples, int offset) 116 | { 117 | if (!mIsEnabled) 118 | return; 119 | 120 | const Wave* wave = &mWaveStore->getWave(mWave); 121 | 122 | // Sample mipmap level is always 0 since oversampling takes care of aliasing 123 | 124 | int level = 0; //trunc(log2((float)mSpeed / oscillatorResolution)); 125 | 126 | if (level < 0) 127 | level = 0; 128 | else if (level >= wave->getLevels()) 129 | level = wave->getLevels() - 1; 130 | 131 | int oversampleMul = 1 << oscillatorOversample; 132 | 133 | const int *waveData = wave->getData(level); 134 | int position = (mPosition + mSpeed * offset) * oversampleMul; 135 | 136 | /* 137 | * Scale the divider by the level 138 | */ 139 | int resolution = (oscillatorResolution << level) * oversampleMul; 140 | 141 | /* 142 | * We assume that length is always 2^n 143 | */ 144 | int waveLengthMask = wave->getLength(level) - 1; 145 | int waveLengthMaskRes = wave->getLength(level) * resolution - 1; 146 | 147 | for (int i = 0 ; i < numSamples ; ++i) 148 | { 149 | int sample = 0; 150 | 151 | for (int o = 0 ; o < oversampleMul ; ++o) 152 | { 153 | /*int sampleA = waveData[(position / resolution) & waveLengthMask]; 154 | int sampleB = waveData[(position / resolution + 1) & waveLengthMask]; 155 | sample += (sampleB - sampleA) * (position % resolution) / resolution + sampleA;*/ 156 | 157 | sample += waveData[(position / resolution) & waveLengthMask]; 158 | position += mSpeed; 159 | 160 | // Oscillator looped, set the queued wave 161 | if (position > waveLengthMaskRes) 162 | { 163 | wave = &mWaveStore->getWave(mQueuedWave); 164 | waveData = wave->getData(level); 165 | position &= waveLengthMaskRes; 166 | } 167 | } 168 | 169 | sample = sample * mVolume / volumeResolution / oversampleMul; 170 | 171 | buffer[i].left += sample; 172 | buffer[i].right += sample; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/Random.cpp: -------------------------------------------------------------------------------- 1 | #include "Random.h" 2 | 3 | /* ----------------------------------------------------------------------- */ 4 | /* This is the Mersenne Twister by Makoto Matsumoto and Takuji Nishimura */ 5 | /* http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/MT2002/emt19937ar.html */ 6 | /* ----------------------------------------------------------------------- */ 7 | 8 | /* A few very minor changes were made by me - Adam */ 9 | 10 | /* 11 | A C-program for MT19937, with initialization improved 2002/1/26. 12 | Coded by Takuji Nishimura and Makoto Matsumoto. 13 | 14 | Before using, initialize the state by using init_genrand(seed) 15 | or init_by_array(init_key, key_length). 16 | 17 | Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura, 18 | All rights reserved. 19 | 20 | Redistribution and use in source and binary forms, with or without 21 | modification, are permitted provided that the following conditions 22 | are met: 23 | 24 | 1. Redistributions of source code must retain the above copyright 25 | notice, this list of conditions and the following disclaimer. 26 | 27 | 2. Redistributions in binary form must reproduce the above copyright 28 | notice, this list of conditions and the following disclaimer in the 29 | documentation and/or other materials provided with the distribution. 30 | 31 | 3. The names of its contributors may not be used to endorse or promote 32 | products derived from this software without specific prior written 33 | permission. 34 | 35 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 36 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 37 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 38 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 39 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 40 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 41 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 42 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 43 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 44 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 45 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 46 | 47 | 48 | Any feedback is very welcome. 49 | http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html 50 | email: m-mat @ math.sci.hiroshima-u.ac.jp (remove space) 51 | */ 52 | 53 | /* Period parameters */ 54 | 55 | Random::Random() 56 | { 57 | mti = N+1; 58 | seed(0); 59 | } 60 | 61 | /* initializes mt[N] with a seed */ 62 | void Random::seed(unsigned long s) 63 | { 64 | mt[0]= s & 0xffffffffUL; 65 | for (mti=1; mti> 30)) + mti); 68 | /* See Knuth TAOCP Vol2. 3rd Ed. P.106 for multiplier. */ 69 | /* In the previous versions, MSBs of the seed affect */ 70 | /* only MSBs of the array mt[]. */ 71 | /* 2002/01/09 modified by Makoto Matsumoto */ 72 | mt[mti] &= 0xffffffffUL; 73 | /* for >32 bit machines */ 74 | } 75 | } 76 | 77 | /* generates a random number on [0,0xffffffff]-interval */ 78 | unsigned int Random::rndu() 79 | { 80 | unsigned int y; 81 | static unsigned long mag01[2]={0x0UL, MATRIX_A}; 82 | /* mag01[x] = x * MATRIX_A for x=0,1 */ 83 | 84 | if (mti >= N) { /* generate N words at one time */ 85 | int kk; 86 | 87 | for (kk=0;kk> 1) ^ mag01[y & 0x1UL]; 90 | } 91 | for (;kk> 1) ^ mag01[y & 0x1UL]; 94 | } 95 | y = (mt[N-1]&UPPER_MASK)|(mt[0]&LOWER_MASK); 96 | mt[N-1] = mt[M-1] ^ (y >> 1) ^ mag01[y & 0x1UL]; 97 | 98 | mti = 0; 99 | } 100 | 101 | y = mt[mti++]; 102 | 103 | /* Tempering */ 104 | y ^= (y >> 11); 105 | y ^= (y << 7) & 0x9d2c5680UL; 106 | y ^= (y << 15) & 0xefc60000UL; 107 | y ^= (y >> 18); 108 | 109 | return y; 110 | } 111 | 112 | int Random::rnd(int min_val, int max_val) 113 | { 114 | if (min_val==max_val) return min_val; 115 | return (unsigned int)rndu()%(unsigned int)(max_val-min_val+1)+(unsigned int)min_val; 116 | } 117 | 118 | float Random::rndf() { 119 | return (float)rndu()/(float)0xffffffff; 120 | } 121 | 122 | long double Random::rndl() { 123 | return (long double)rndu()/(long double)0xffffffff; 124 | } 125 | -------------------------------------------------------------------------------- /src/CommandOptionSelector.cpp: -------------------------------------------------------------------------------- 1 | #include "CommandOptionSelector.h" 2 | #include "Renderer.h" 3 | #include "Color.h" 4 | #include "SDL.h" 5 | #include "TextEditor.h" 6 | #include "Label.h" 7 | #include "MessageBox.h" 8 | #include "MainEditor.h" 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include "Mixer.h" 16 | #include "Label.h" 17 | 18 | CommandOptionSelector::CommandOptionSelector(EditorState& editorState, const CommandDescriptor& command) 19 | : GenericSelector(editorState), mCommandDescriptor(command) 20 | { 21 | mFilterField = new TextEditor(editorState); 22 | mFilterField->setBuffer(mFilter, sizeof(mFilter)); 23 | mFilterField->setIsEditing(true); 24 | mFilterField->setAlwaysShowCursor(true); 25 | 26 | mFilterLabel = new Label(editorState); 27 | mFilterLabel->setText(">"); 28 | addChild(mFilterLabel, 0, 8, 8, 8); 29 | 30 | strcpy(mFilter, ""); 31 | addChild(mFilterField, 8, 8, 208, 8); 32 | setFocus(mFilterField); 33 | } 34 | 35 | 36 | CommandOptionSelector::~CommandOptionSelector() 37 | { 38 | delete mFilterField; 39 | delete mFilterLabel; 40 | } 41 | 42 | 43 | void CommandOptionSelector::onRendererMount(const Renderer& renderer) 44 | { 45 | GenericSelector::onRendererMount(renderer); 46 | 47 | mFilterLabel->setColor(renderer.getTheme().getColor(Theme::ColorType::ModalTitleText)); 48 | mFilterLabel->setBackground(renderer.getTheme().getColor(Theme::ColorType::ModalTitleBackground)); 49 | 50 | SDL_Rect filterLabelArea = mFilterLabel->getArea(); 51 | filterLabelArea.y = mLabel->getArea().h; 52 | filterLabelArea.w = renderer.getFontWidth(); 53 | filterLabelArea.h = renderer.getFontHeight(); 54 | mFilterLabel->setArea(filterLabelArea); 55 | 56 | SDL_Rect filterArea = mFilterField->getArea(); 57 | filterArea.x = filterLabelArea.x + filterLabelArea.w; 58 | filterArea.y = filterArea.y; 59 | filterArea.w = mThisArea.w - filterArea.x; 60 | filterArea.h = renderer.getFontHeight(); 61 | mFilterField->setArea(filterArea); 62 | } 63 | 64 | 65 | void CommandOptionSelector::accept(bool isFinal) 66 | { 67 | mParent->onFileSelectorEvent(*this, true); 68 | } 69 | 70 | 71 | void CommandOptionSelector::reject(bool isFinal) 72 | { 73 | mParent->onFileSelectorEvent(*this, false); 74 | } 75 | 76 | 77 | void CommandOptionSelector::renderItem(Renderer& renderer, const SDL_Rect& area, const Item& item, bool isSelected) 78 | { 79 | const CommandOption& commandOption = static_cast(item); 80 | Theme::ColorType color = Theme::ColorType::NormalText; 81 | 82 | if (isSelected) 83 | color = Theme::ColorType::SelectedRow; 84 | 85 | renderer.clearRect(area, Theme::ColorType::ModalBackground); 86 | 87 | int width = area.w / 8 - 10; 88 | 89 | renderer.renderTextV(area, color, "%d", commandOption.value); 90 | } 91 | 92 | 93 | bool CommandOptionSelector::caseInsensitiveFind(const char *haystack, const char *needle) 94 | { 95 | // Empty needle, everything matches 96 | 97 | if (*needle == '\0') 98 | return true; 99 | 100 | const char *a = haystack; 101 | 102 | while (*a) 103 | { 104 | const char *prev = a, *b = needle; 105 | 106 | do 107 | { 108 | if (tolower(*b) != tolower(*a)) 109 | break; 110 | 111 | ++b; 112 | ++a; 113 | 114 | if (*b == '\0') 115 | return true; 116 | } 117 | while (*a); 118 | 119 | a = prev; 120 | ++a; 121 | } 122 | 123 | return false; 124 | } 125 | 126 | 127 | void CommandOptionSelector::populate() 128 | { 129 | invalidateParent(); 130 | 131 | clearItems(); 132 | 133 | mCommandDescriptor.option(*this); 134 | 135 | selectItem(0); 136 | } 137 | 138 | 139 | const CommandOptionSelector::CommandOption& CommandOptionSelector::getSelectedOption() const 140 | { 141 | return static_cast(getSelectedItem()); 142 | } 143 | 144 | 145 | CommandOptionSelector::CommandOption::CommandOption(int _value) 146 | : value(_value) 147 | { 148 | } 149 | 150 | 151 | void CommandOptionSelector::onModalStatusChange(bool isNowModal) 152 | { 153 | // Make sure text field will receive SDL_TEXTINPUTs 154 | // and disables them after dialog close 155 | mFilterField->setIsEditing(isNowModal); 156 | 157 | // Reset filter on modal open 158 | strcpy(mFilter, ""); 159 | } 160 | 161 | 162 | bool CommandOptionSelector::onEvent(SDL_Event& event) 163 | { 164 | if (GenericSelector::onEvent(event)) 165 | return true; 166 | 167 | bool filterEvent = mFilterField->onEvent(event); 168 | 169 | if (filterEvent) 170 | { 171 | populate(); 172 | } 173 | 174 | return filterEvent; 175 | } 176 | 177 | 178 | void CommandOptionSelector::addIntItem(int value) 179 | { 180 | char temp[100]; 181 | snprintf(temp, sizeof(temp), "%d", value); 182 | 183 | if (caseInsensitiveFind(temp, mFilter)) 184 | addItem(new CommandOption(value)); 185 | } 186 | -------------------------------------------------------------------------------- /src/ColumnEditor.cpp: -------------------------------------------------------------------------------- 1 | #include "ColumnEditor.h" 2 | #include "EditorState.h" 3 | #include 4 | 5 | ColumnEditor::ColumnEditor(EditorState& editorState, TrackEditorState& trackEditorState, int tracks, int columns) 6 | :Editor(editorState), mTrackEditorState(trackEditorState), maxTracks(tracks), maxRows(256), mColumns(columns), mRowNumberMargin(0), mTrackMargin(0) 7 | { 8 | trackEditorState.currentRow.addListener(this); 9 | trackEditorState.currentTrack.addListener(this); 10 | trackEditorState.currentColumn.addListener(this); 11 | } 12 | 13 | 14 | ColumnEditor::~ColumnEditor() 15 | { 16 | } 17 | 18 | 19 | /* SDL has trouble with AZERTY number keys (they are accessed with the shift key). 20 | * The following routines bypass this by determining the number by using scancodes 21 | * (on 95 % of keyboard layouts the number keys are in the same order and at same 22 | * location). 23 | */ 24 | 25 | int ColumnEditor::getCharFromKey(const SDL_Keysym& sym) const 26 | { 27 | if (sym.sym >= SDLK_a && sym.sym <= SDLK_z) 28 | return sym.sym - SDLK_a + 'a'; 29 | 30 | // Scancodes are ordered 1-9 and then 0 (as on the keyboard) 31 | 32 | if (sym.scancode == SDL_SCANCODE_0) 33 | return '0'; 34 | 35 | if (sym.scancode >= SDL_SCANCODE_1 && sym.scancode <= SDL_SCANCODE_9) 36 | return sym.scancode - SDL_SCANCODE_1 + '1'; 37 | 38 | return -1; 39 | } 40 | 41 | 42 | int ColumnEditor::getHexFromKey(const SDL_Keysym& sym) const 43 | { 44 | if (sym.sym >= SDLK_a && sym.sym <= SDLK_f) 45 | return sym.sym - SDLK_a + 0xa; 46 | 47 | // Scancodes are ordered 1-9 and then 0 (as on the keyboard) 48 | 49 | if (sym.scancode == SDL_SCANCODE_0) 50 | return 0; 51 | 52 | if (sym.scancode >= SDL_SCANCODE_1 && sym.scancode <= SDL_SCANCODE_9) 53 | return sym.scancode - SDL_SCANCODE_1 + 1; 54 | 55 | return -1; 56 | } 57 | 58 | 59 | int ColumnEditor::getNoteFromKey(const SDL_Keysym& sym) const 60 | { 61 | static const struct { SDL_Scancode scancode; int note; } syms[] = 62 | { 63 | {SDL_SCANCODE_Z, 0}, 64 | {SDL_SCANCODE_S, 1}, 65 | {SDL_SCANCODE_X, 2}, 66 | {SDL_SCANCODE_D, 3}, 67 | {SDL_SCANCODE_C, 4}, 68 | {SDL_SCANCODE_V, 5}, 69 | {SDL_SCANCODE_G, 6}, 70 | {SDL_SCANCODE_B, 7}, 71 | {SDL_SCANCODE_H, 8}, 72 | {SDL_SCANCODE_N, 9}, 73 | {SDL_SCANCODE_J, 10}, 74 | {SDL_SCANCODE_M, 11}, 75 | {SDL_SCANCODE_COMMA, 12}, 76 | {SDL_SCANCODE_L, 13}, 77 | {SDL_SCANCODE_PERIOD, 14}, 78 | 79 | {SDL_SCANCODE_Q, 12}, 80 | {SDL_SCANCODE_2, 13}, 81 | {SDL_SCANCODE_W, 14}, 82 | {SDL_SCANCODE_3, 15}, 83 | {SDL_SCANCODE_E, 16}, 84 | {SDL_SCANCODE_R, 17}, 85 | {SDL_SCANCODE_5, 18}, 86 | {SDL_SCANCODE_T, 19}, 87 | {SDL_SCANCODE_6, 20}, 88 | {SDL_SCANCODE_Y, 21}, 89 | {SDL_SCANCODE_7, 22}, 90 | {SDL_SCANCODE_U, 23}, 91 | {SDL_SCANCODE_I, 24}, 92 | {SDL_SCANCODE_9, 25}, 93 | {SDL_SCANCODE_O, 26}, 94 | {SDL_SCANCODE_0, 27}, 95 | {SDL_SCANCODE_P, 28}, 96 | {SDL_SCANCODE_P, -1} 97 | }; 98 | 99 | for (int i = 0 ; syms[i].note != -1 ; ++i) 100 | if (syms[i].scancode == sym.scancode) 101 | return syms[i].note; 102 | 103 | return -1; 104 | } 105 | 106 | 107 | void ColumnEditor::setMaxRows(int rows) 108 | { 109 | maxRows = rows; 110 | 111 | if (mTrackEditorState.currentRow >= rows) 112 | mTrackEditorState.currentRow = rows - 1; 113 | } 114 | 115 | 116 | void ColumnEditor::scrollView(int d, bool wrap) 117 | { 118 | mTrackEditorState.currentRow += d; 119 | 120 | if (wrap) 121 | { 122 | mTrackEditorState.currentRow = (mTrackEditorState.currentRow + maxRows) % maxRows; 123 | } 124 | else 125 | { 126 | if (mTrackEditorState.currentRow < 0) 127 | mTrackEditorState.currentRow = 0; 128 | else if (mTrackEditorState.currentRow >= maxRows) 129 | mTrackEditorState.currentRow = maxRows - 1; 130 | } 131 | } 132 | 133 | 134 | void ColumnEditor::changeColumn(int d) 135 | { 136 | if (d < 0) 137 | { 138 | --mTrackEditorState.currentColumn; 139 | 140 | if (mTrackEditorState.currentColumn < 0) 141 | { 142 | mTrackEditorState.currentColumn = mColumns - 1; 143 | changeTrack(-1); 144 | } 145 | } 146 | else 147 | { 148 | ++mTrackEditorState.currentColumn; 149 | 150 | if (mTrackEditorState.currentColumn >= mColumns) 151 | { 152 | mTrackEditorState.currentColumn = 0; 153 | changeTrack(1); 154 | } 155 | } 156 | } 157 | 158 | 159 | void ColumnEditor::changeTrack(int d) 160 | { 161 | int currentTrack = mTrackEditorState.currentTrack; 162 | currentTrack += d; 163 | 164 | if (currentTrack < 0) 165 | { 166 | currentTrack = 0; 167 | mTrackEditorState.currentColumn = 0; 168 | } 169 | 170 | if (currentTrack > maxTracks - 1) 171 | { 172 | currentTrack = maxTracks - 1; 173 | mTrackEditorState.currentColumn = mColumns - 1; 174 | } 175 | 176 | mTrackEditorState.currentTrack = currentTrack; 177 | } 178 | 179 | 180 | void ColumnEditor::setRowNumberMargin(int margin) 181 | { 182 | mRowNumberMargin = margin; 183 | } 184 | 185 | 186 | void ColumnEditor::setTrackMargin(int margin) 187 | { 188 | mTrackMargin = margin; 189 | } 190 | 191 | 192 | bool ColumnEditor::isRowActive(int track, int row) const 193 | { 194 | return false; 195 | } 196 | -------------------------------------------------------------------------------- /src/Editor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "SDL.h" 4 | #include "Listener.h" 5 | #include 6 | #include 7 | 8 | struct PlayerState; 9 | struct Renderer; 10 | struct EditorState; 11 | struct CommandOptionSelector; 12 | struct MainEditor; 13 | /* 14 | 15 | The Editor class is the base class for all GUI elements. 16 | 17 | */ 18 | 19 | class Editor: public Listener 20 | { 21 | public: 22 | static const int replacePreviousMessage = -1; 23 | static const int modalMargin = 16; 24 | 25 | enum MessageClass 26 | { 27 | MessageInfo, 28 | MessageError 29 | }; 30 | 31 | typedef std::function Command; 32 | typedef std::function CommandWithOption; 33 | typedef std::function CommandOptionFunc; 34 | 35 | struct CommandDescriptor { 36 | char context[200], name[200]; 37 | Command func; 38 | CommandWithOption funcWithOption; 39 | CommandOptionFunc option; 40 | int sym, mod; 41 | 42 | CommandDescriptor(const char *context, const char *name, Command func, int sym = -1, int mod = 0); 43 | CommandDescriptor(const char *context, const char *name, CommandWithOption func, CommandOptionFunc option, int sym = -1, int mod = 0); 44 | }; 45 | 46 | struct EditorChild { 47 | Editor *editor; 48 | SDL_Rect area; 49 | EditorChild(Editor *editor, const SDL_Rect& area); 50 | }; 51 | 52 | private: 53 | Editor *mFocus; 54 | 55 | void drawModal(Renderer& renderer); 56 | virtual void onDraw(Renderer& renderer, const SDL_Rect& area) = 0; 57 | void drawChildren(Renderer& renderer, const SDL_Rect& area); 58 | void childAreaChanged(Editor *changedChild); 59 | 60 | protected: 61 | Editor *mModal; 62 | EditorState& mEditorState; 63 | bool mIsDirty, mRedraw; 64 | Editor *mParent; 65 | std::vector mChildren; 66 | SDL_Rect mThisArea; 67 | bool mWantsFocus; 68 | int mPopupMessageId; 69 | std::vector mCommands; 70 | bool mMounted; 71 | 72 | void removeFocus(); 73 | void setModal(Editor *modal); 74 | 75 | void drawCoveredChildren(Renderer& renderer, const SDL_Rect& area, const SDL_Rect& childArea, int maxIndex); 76 | 77 | void invalidateAll(); 78 | void invalidateParent(); 79 | 80 | bool shouldRedrawBackground() const; 81 | virtual void onAreaChanged(const SDL_Rect& area); 82 | 83 | /* Actual rendering of the message */ 84 | virtual int showMessageInner(MessageClass messageClass, int messageId, const char* message); 85 | 86 | // Register commands only here so that the Editor is added as a child and the registration 87 | // is propagated 88 | virtual void onRequestCommandRegistration(); 89 | 90 | bool registerCommand(const char *context, const char *commandName, Command command, int sym = -1, int mod = 0); 91 | bool registerCommand(const char *context, const char *commandName, CommandWithOption command, CommandOptionFunc option, int sym = -1, int mod = 0); 92 | 93 | public: 94 | Editor(EditorState& editorState, bool wantFocus = true); 95 | virtual ~Editor(); 96 | 97 | void addChild(Editor *child, int x, int y, int w, int h); 98 | 99 | void setDirty(bool dirty); 100 | virtual void onFileSelectorEvent(const Editor& fileSelector, bool accept); 101 | virtual void onMessageBoxEvent(const Editor& messageBox, int code); 102 | virtual void onListenableChange(Listenable *listenable); 103 | virtual void onLoaded(); 104 | 105 | // When setting/unsetting as a modal 106 | virtual void onModalStatusChange(bool isNowModal); 107 | 108 | // When the Editor is rendered the first time 109 | virtual void onRendererMount(const Renderer& renderer); 110 | virtual bool isDirty() const; 111 | bool isFocusable() const; 112 | bool hasDirty() const; 113 | void setFocus(Editor *editor); 114 | Editor * getFocus(); 115 | bool hasFocus(); 116 | 117 | // Tell Editor its own top-left corner (absolute) 118 | void setArea(const SDL_Rect& area); 119 | 120 | const SDL_Rect& getArea() const; 121 | 122 | /** 123 | * Messages 124 | */ 125 | 126 | /** 127 | * Tooltip - set in SDL_MOUSEMOTION 128 | */ 129 | virtual void showTooltip(const SDL_Rect& area, const char* message); 130 | void showTooltipV(const SDL_Rect& area, const char* message, ...) __attribute__((format(printf, 3, 4))); 131 | 132 | /** 133 | * Status popup 134 | */ 135 | int showMessage(MessageClass messageClass, int messageId, const char* message); 136 | int showMessage(MessageClass messageClass, const char* message); 137 | int showMessageV(MessageClass messageClass, const char* message, ...) __attribute__((format(printf, 3, 4))); 138 | int showMessageV(MessageClass messageClass, int messageId, const char* message, ...) __attribute__((format(printf, 4, 5))); 139 | 140 | void draw(Renderer& renderer, const SDL_Rect& area); 141 | virtual void onUpdate(int ms); 142 | void update(int ms); 143 | 144 | /* onEvent() should return false if the event was not consumed 145 | * so that the parent Editor knows to process it. 146 | */ 147 | virtual bool onEvent(SDL_Event& event); 148 | 149 | bool handleCommandShortcuts(MainEditor& mainEditor, const SDL_Event& event); 150 | const std::vector& getCommands() const; 151 | std::vector getChildCommands() const; 152 | 153 | /** 154 | * Helper members 155 | */ 156 | static bool pointInRect(const SDL_Point& point, const SDL_Rect& rect); 157 | static bool intersectRect(const SDL_Rect& a, const SDL_Rect& b, SDL_Rect& result); 158 | }; 159 | -------------------------------------------------------------------------------- /src/EditorState.cpp: -------------------------------------------------------------------------------- 1 | #include "EditorState.h" 2 | #include "FileSection.h" 3 | #include 4 | #include 5 | 6 | EditorState::EditorState() 7 | : macro(0), octave(4), followPlayPosition(true) 8 | { 9 | 10 | } 11 | 12 | 13 | TrackEditorState::TrackEditorState() 14 | : currentRow(0), currentTrack(0), currentColumn(0), editSkip(1), blockStart(-1), blockEnd(-1) 15 | { 16 | 17 | } 18 | 19 | 20 | FileSection * EditorState::pack() 21 | { 22 | FileSection * state = FileSection::createSection("STAT"); 23 | state->writeByte(0); 24 | state->writeDword(macro); 25 | state->writeDword(octave); 26 | state->writeDword(editMode); 27 | state->writeDword(followPlayPosition); 28 | 29 | /*state->writeDword(copyBuffer.getLength()); 30 | state->writePattern(copyBuffer);*/ 31 | 32 | FileSection *trState = sequenceEditor.pack(); 33 | state->writeSection(*trState); 34 | delete trState; 35 | 36 | trState = patternEditor.pack(); 37 | state->writeSection(*trState); 38 | delete trState; 39 | 40 | trState = macroEditor.pack(); 41 | state->writeSection(*trState); 42 | delete trState; 43 | 44 | state->writeString(audioDevice.c_str()); 45 | 46 | return state; 47 | } 48 | 49 | 50 | bool EditorState::unpack(const FileSection& section) 51 | { 52 | if (strcmp(section.getName(), "STAT") != 0) 53 | return false; 54 | 55 | int offset = 0; 56 | 57 | unsigned int version = section.readByte(offset); 58 | 59 | if (version == FileSection::invalidRead) 60 | return false; 61 | 62 | unsigned int macroNr = section.readDword(offset); 63 | 64 | if (macroNr == FileSection::invalidRead) 65 | return false; 66 | 67 | unsigned int octaveNr = section.readDword(offset); 68 | 69 | if (octaveNr == FileSection::invalidRead) 70 | return false; 71 | 72 | unsigned int editModeNr = section.readDword(offset); 73 | 74 | if (editModeNr == FileSection::invalidRead) 75 | return false; 76 | 77 | unsigned int followNr = section.readDword(offset); 78 | 79 | if (followNr == FileSection::invalidRead) 80 | return false; 81 | 82 | /*unsigned int copyBufNr = section.readDword(offset); 83 | 84 | if (copyBufNr == FileSection::invalidRead) 85 | return false; 86 | 87 | if (!section.readPattern(copyBuffer, offset)) 88 | return false; 89 | 90 | //copyBuffer.setLength(copyBufNr);*/ 91 | 92 | FileSection *trState = section.readSection(offset); 93 | 94 | if (!trState) 95 | return false; 96 | 97 | if (!sequenceEditor.unpack(*trState)) 98 | { 99 | delete trState; 100 | return false; 101 | } 102 | 103 | delete trState; 104 | 105 | trState = section.readSection(offset); 106 | 107 | if (!trState) 108 | return false; 109 | 110 | if (!patternEditor.unpack(*trState)) 111 | { 112 | delete trState; 113 | return false; 114 | } 115 | 116 | delete trState; 117 | 118 | trState = section.readSection(offset); 119 | 120 | if (!trState) 121 | return false; 122 | 123 | if (!macroEditor.unpack(*trState)) 124 | { 125 | delete trState; 126 | return false; 127 | } 128 | 129 | delete trState; 130 | 131 | const char *tempAudioDevice = section.readString(offset); 132 | 133 | if (!tempAudioDevice) 134 | return false; 135 | 136 | macro = macroNr; 137 | octave = octaveNr; 138 | editMode = editModeNr; 139 | followPlayPosition = followNr; 140 | audioDevice = tempAudioDevice; 141 | 142 | return true; 143 | } 144 | 145 | 146 | FileSection * TrackEditorState::pack() 147 | { 148 | FileSection * state = FileSection::createSection("TSTA"); 149 | state->writeByte(0); 150 | state->writeDword(currentRow); 151 | state->writeDword(currentTrack); 152 | state->writeDword(currentColumn); 153 | state->writeDword(editSkip); 154 | state->writeDword(blockStart + 1); 155 | state->writeDword(blockEnd + 1); 156 | 157 | return state; 158 | } 159 | 160 | 161 | bool TrackEditorState::unpack(const FileSection& section) 162 | { 163 | if (strcmp(section.getName(), "TSTA") != 0) 164 | return false; 165 | 166 | int offset = 0; 167 | 168 | unsigned int version = section.readByte(offset); 169 | 170 | if (version == FileSection::invalidRead) 171 | return false; 172 | 173 | unsigned int currentRowNr = section.readDword(offset); 174 | 175 | if (currentRowNr == FileSection::invalidRead) 176 | return false; 177 | 178 | unsigned int currentTrackNr = section.readDword(offset); 179 | 180 | if (currentTrackNr == FileSection::invalidRead) 181 | return false; 182 | 183 | unsigned int currentColumnNr = section.readDword(offset); 184 | 185 | if (currentColumnNr == FileSection::invalidRead) 186 | return false; 187 | 188 | unsigned int editSkipNr = section.readDword(offset); 189 | 190 | if (editSkipNr == FileSection::invalidRead) 191 | return false; 192 | 193 | unsigned int blockStartNr = section.readDword(offset); 194 | 195 | if (blockStartNr == FileSection::invalidRead) 196 | return false; 197 | 198 | unsigned int blockEndNr = section.readDword(offset); 199 | 200 | if (blockEndNr == FileSection::invalidRead) 201 | return false; 202 | 203 | currentRow = currentRowNr; 204 | currentTrack = currentTrackNr; 205 | currentColumn = currentColumnNr; 206 | 207 | editSkip = editSkipNr; 208 | blockStart = static_cast(blockStartNr) - 1; 209 | blockEnd = static_cast(blockEndNr) - 1; 210 | 211 | return true; 212 | } 213 | 214 | 215 | void EditorState::reset() 216 | { 217 | sequenceEditor = TrackEditorState(); 218 | patternEditor = TrackEditorState(); 219 | macroEditor = TrackEditorState(); 220 | 221 | copyBuffer.clear(); 222 | 223 | followPlayPosition = true; 224 | 225 | macro = 0; 226 | octave = 4; 227 | editMode = 0; 228 | audioDevice = ""; 229 | } 230 | -------------------------------------------------------------------------------- /src/PatternEditor.cpp: -------------------------------------------------------------------------------- 1 | #include "PatternEditor.h" 2 | #include "PatternRow.h" 3 | #include "Sequence.h" 4 | #include "SequenceRow.h" 5 | #include "EditorState.h" 6 | #include "CommandOptionSelector.h" 7 | #include "Song.h" 8 | #include "Pattern.h" 9 | #include "Renderer.h" 10 | #include "Color.h" 11 | #include "PlayerState.h" 12 | #include "IPlayer.h" 13 | #include "SDL.h" 14 | 15 | PatternEditor::PatternEditor(EditorState& editorState, IPlayer& player, Song& song) 16 | : TrackEditor(editorState, editorState.patternEditor, player, song, SequenceRow::maxTracks) 17 | { 18 | editorState.sequenceEditor.currentRow.addListener(this); 19 | } 20 | 21 | 22 | PatternEditor::~PatternEditor() 23 | { 24 | } 25 | 26 | 27 | void PatternEditor::setPattern(int track, int pattern) 28 | { 29 | if (pattern < 0) 30 | pattern = 0; 31 | 32 | if (pattern >= Song::maxPatterns) 33 | pattern = Song::maxPatterns - 1; 34 | 35 | getCurrentSequenceRow().pattern[track] = pattern; 36 | 37 | /* 38 | * Trigger sequence display update 39 | */ 40 | 41 | mEditorState.sequenceEditor.currentRow.notify(); 42 | 43 | setDirty(true); 44 | } 45 | 46 | 47 | void PatternEditor::setCurrentPattern(int pattern) 48 | { 49 | setPattern(mTrackEditorState.currentTrack, pattern); 50 | } 51 | 52 | 53 | bool PatternEditor::onEvent(SDL_Event& event) 54 | { 55 | switch (event.type) 56 | { 57 | case SDL_KEYDOWN: 58 | if (event.key.keysym.mod & KMOD_SHIFT) 59 | { 60 | switch (event.key.keysym.sym) 61 | { 62 | case SDLK_LEFT: 63 | setSequenceRow(mEditorState.sequenceEditor.currentRow - 1); 64 | return true; 65 | 66 | case SDLK_RIGHT: 67 | setSequenceRow(mEditorState.sequenceEditor.currentRow + 1); 68 | return true; 69 | } 70 | } 71 | 72 | break; 73 | } 74 | 75 | return TrackEditor::onEvent(event); 76 | } 77 | 78 | 79 | PatternRow& PatternEditor::getPatternRow(int track, int row) 80 | { 81 | Pattern& pattern = mSong.getPattern(track, mSong.getSequence().getRow(mEditorState.sequenceEditor.currentRow).pattern[track]); 82 | return pattern.getRow(row); 83 | } 84 | 85 | 86 | PatternRow& PatternEditor::getCurrentPatternRow() 87 | { 88 | SequenceRow seqRow = mSong.getSequence().getRow(mEditorState.sequenceEditor.currentRow); 89 | Pattern& pattern = mSong.getPattern(mTrackEditorState.currentTrack, seqRow.pattern[mTrackEditorState.currentTrack]); 90 | 91 | return pattern.getRow(mTrackEditorState.currentRow); 92 | } 93 | 94 | 95 | SequenceRow& PatternEditor::getCurrentSequenceRow() 96 | { 97 | return mSong.getSequence().getRow(mEditorState.sequenceEditor.currentRow); 98 | } 99 | 100 | 101 | void PatternEditor::setPatternRow(int row) 102 | { 103 | mTrackEditorState.currentRow = row; 104 | 105 | setDirty(true); 106 | } 107 | 108 | 109 | void PatternEditor::setSequenceRow(int row) 110 | { 111 | if (row < 0) 112 | row = 0; 113 | 114 | if (row >= mSong.getSequenceLength()) 115 | row = mSong.getSequenceLength() - 1; 116 | 117 | mEditorState.sequenceEditor.currentRow = row; 118 | 119 | setDirty(true); 120 | } 121 | 122 | 123 | Pattern& PatternEditor::getCurrentPattern(int track) 124 | { 125 | SequenceRow seqRow = mSong.getSequence().getRow(mEditorState.sequenceEditor.currentRow); 126 | Pattern& pattern = mSong.getPattern(track, seqRow.pattern[mTrackEditorState.currentTrack]); 127 | 128 | return pattern; 129 | } 130 | 131 | 132 | void PatternEditor::findUnusedTrack(int track) 133 | { 134 | SequenceRow& seqRow = mSong.getSequence().getRow(mEditorState.sequenceEditor.currentRow); 135 | int orig = seqRow.pattern[track]; 136 | 137 | while (!mSong.getPattern(track, seqRow.pattern[track]).isEmpty()) 138 | { 139 | seqRow.pattern[track] = (seqRow.pattern[track] + 1) % Song::maxPatterns; 140 | 141 | // Check if we are back at starting position 142 | 143 | if (orig == seqRow.pattern[track]) 144 | break; 145 | } 146 | 147 | mEditorState.sequenceEditor.currentRow.notify(); 148 | } 149 | 150 | 151 | bool PatternEditor::isRowActive(int track, int row) const 152 | { 153 | const PlayerState& playerState = mPlayer.getPlayerState(); 154 | const SequenceRow& playRow = mSong.getSequence().getRow(playerState.sequenceRow); 155 | const SequenceRow& seqRow = mSong.getSequence().getRow(mEditorState.sequenceEditor.currentRow); 156 | return playerState.isPlaying() && playerState.patternRow == row && playRow.pattern[track] == seqRow.pattern[track]; 157 | } 158 | 159 | 160 | void PatternEditor::onRequestCommandRegistration() 161 | { 162 | registerCommand("Pattern", "Next pattern", [this]() { 163 | this->setCurrentPattern(this->getCurrentSequenceRow().pattern[this->mTrackEditorState.currentTrack] + 1); 164 | }, SDLK_UP, KMOD_SHIFT); 165 | registerCommand("Pattern", "Previous pattern", [this]() { 166 | this->setCurrentPattern(this->getCurrentSequenceRow().pattern[this->mTrackEditorState.currentTrack] - 1); 167 | }, SDLK_DOWN, KMOD_SHIFT); 168 | registerCommand("Pattern", "Next sequence row", [this]() { 169 | this->setSequenceRow(mEditorState.sequenceEditor.currentRow + 1); 170 | }, SDLK_RIGHT, KMOD_SHIFT); 171 | registerCommand("Pattern", "Previous sequence row", [this]() { 172 | this->setSequenceRow(mEditorState.sequenceEditor.currentRow - 1); 173 | }, SDLK_LEFT, KMOD_SHIFT); 174 | 175 | registerCommand("Pattern", "Mark block start", [this]() { this->setBlockStartToCurrentRow(); }, SDLK_b, KMOD_CTRL); 176 | registerCommand("Pattern", "Mark block end", [this]() { this->setBlockEndToCurrentRow(); }, SDLK_e, KMOD_CTRL); 177 | registerCommand("Pattern", "Find unused pattern", [this]() { this->findCurrentUnusedTrack(); }, SDLK_u, KMOD_CTRL); 178 | registerCommand("Pattern", "Kill current pattern", [this]() { this->killCurrentTrack(); }, SDLK_k, KMOD_CTRL); 179 | registerCommand("Pattern", "Copy pattern", [this]() { this->copyCurrentTrack(); }, SDLK_F3); 180 | registerCommand("Pattern", "Paste pattern", [this]() { this->pasteCurrentTrack(); }, SDLK_F4); 181 | registerCommand("Pattern", "Copy pattern block", [this]() { this->copyCurrentBlock(); }, SDLK_c, KMOD_CTRL); 182 | registerCommand("Pattern", "Paste pattern block", [this]() { this->pasteCurrentBlock(); }, SDLK_v, KMOD_CTRL); 183 | registerCommand("Pattern", "Set edit skip", [this](int value) { 184 | this->setEditSkip(value); 185 | }, [this](CommandOptionSelector& selector) { 186 | for (int i = 0 ; i <= 16 ; ++i) 187 | selector.addIntItem(i); 188 | }); 189 | } 190 | -------------------------------------------------------------------------------- /src/CommandSelector.cpp: -------------------------------------------------------------------------------- 1 | #include "CommandSelector.h" 2 | #include "Renderer.h" 3 | #include "Color.h" 4 | #include "SDL.h" 5 | #include "TextEditor.h" 6 | #include "Label.h" 7 | #include "MessageBox.h" 8 | #include "MainEditor.h" 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include "Mixer.h" 16 | #include "Label.h" 17 | 18 | CommandSelector::CommandSelector(EditorState& editorState, const MainEditor& mainEditor) 19 | : GenericSelector(editorState), mMainEditor(mainEditor) 20 | { 21 | mFilterField = new TextEditor(editorState); 22 | mFilterField->setBuffer(mFilter, sizeof(mFilter)); 23 | mFilterField->setIsEditing(true); 24 | mFilterField->setAlwaysShowCursor(true); 25 | 26 | mFilterLabel = new Label(editorState); 27 | mFilterLabel->setText(">"); 28 | addChild(mFilterLabel, 0, 8, 8, 8); 29 | 30 | strcpy(mFilter, ""); 31 | addChild(mFilterField, 8, 8, 208, 8); 32 | setFocus(mFilterField); 33 | } 34 | 35 | 36 | CommandSelector::~CommandSelector() 37 | { 38 | delete mFilterField; 39 | delete mFilterLabel; 40 | } 41 | 42 | 43 | void CommandSelector::onRendererMount(const Renderer& renderer) 44 | { 45 | GenericSelector::onRendererMount(renderer); 46 | 47 | mFilterLabel->setColor(renderer.getTheme().getColor(Theme::ColorType::ModalTitleText)); 48 | mFilterLabel->setBackground(renderer.getTheme().getColor(Theme::ColorType::ModalTitleBackground)); 49 | 50 | SDL_Rect filterLabelArea = mFilterLabel->getArea(); 51 | filterLabelArea.y = mLabel->getArea().h; 52 | filterLabelArea.w = renderer.getFontWidth(); 53 | filterLabelArea.h = renderer.getFontHeight(); 54 | mFilterLabel->setArea(filterLabelArea); 55 | 56 | SDL_Rect filterArea = mFilterField->getArea(); 57 | filterArea.x = filterLabelArea.x + filterLabelArea.w; 58 | filterArea.y = filterLabelArea.y; 59 | filterArea.w = mThisArea.w - filterArea.x; 60 | filterArea.h = renderer.getFontHeight(); 61 | mFilterField->setArea(filterArea); 62 | } 63 | 64 | 65 | void CommandSelector::accept(bool isFinal) 66 | { 67 | mParent->onFileSelectorEvent(*this, true); 68 | } 69 | 70 | 71 | void CommandSelector::reject(bool isFinal) 72 | { 73 | mParent->onFileSelectorEvent(*this, false); 74 | } 75 | 76 | 77 | void CommandSelector::renderItem(Renderer& renderer, const SDL_Rect& area, const Item& item, bool isSelected) 78 | { 79 | const CommandItem& commandItem = static_cast(item); 80 | Theme::ColorType color = Theme::ColorType::NormalText; 81 | 82 | if (isSelected) 83 | color = Theme::ColorType::SelectedRow; 84 | 85 | renderer.clearRect(area, Theme::ColorType::ModalBackground); 86 | 87 | renderer.renderTextV(area, color, "%s: %s", commandItem.command.context, commandItem.command.name); 88 | 89 | if (commandItem.command.sym != -1) 90 | { 91 | const char *keyName = SDL_GetKeyName(commandItem.command.sym); 92 | SDL_Rect textRect = renderer.getTextRect(keyName); 93 | SDL_Rect shortcutArea = { area.x + area.w - textRect.w, area.y, textRect.w, textRect.h }; 94 | renderer.clearRect(shortcutArea, Theme::ColorType::CommandShortcutBackground); 95 | renderer.renderText(shortcutArea, Theme::ColorType::CommandShortcut, keyName); 96 | 97 | if (commandItem.command.mod != 0) 98 | { 99 | static const struct { const char *name; int mod; } modMap[] = { 100 | { "CTRL", KMOD_CTRL }, 101 | { "ALT", KMOD_ALT }, 102 | { "SHIFT", KMOD_SHIFT }, 103 | { NULL, 0 }, 104 | }; 105 | 106 | int left = shortcutArea.x; 107 | 108 | for (int i = 0 ; modMap[i].name ; ++i) 109 | { 110 | if (commandItem.command.mod & modMap[i].mod) 111 | { 112 | const char *keyName = modMap[i].name; 113 | SDL_Rect textRect = renderer.getTextRect(keyName); 114 | SDL_Rect modifierArea = { left - textRect.w - 4, area.y, textRect.w, textRect.h }; 115 | renderer.clearRect(modifierArea, Theme::ColorType::CommandShortcutBackground); 116 | renderer.renderText(modifierArea, Theme::ColorType::CommandShortcut, keyName); 117 | left = modifierArea.x; 118 | } 119 | } 120 | } 121 | } 122 | } 123 | 124 | 125 | bool CommandSelector::caseInsensitiveFind(const char *haystack, const char *needle) 126 | { 127 | // Empty needle, everything matches 128 | 129 | if (*needle == '\0') 130 | return true; 131 | 132 | const char *a = haystack; 133 | 134 | while (*a) 135 | { 136 | const char *prev = a, *b = needle; 137 | 138 | do 139 | { 140 | if (tolower(*b) != tolower(*a)) 141 | break; 142 | 143 | ++b; 144 | ++a; 145 | 146 | if (*b == '\0') 147 | return true; 148 | } 149 | while (*a); 150 | 151 | a = prev; 152 | ++a; 153 | } 154 | 155 | return false; 156 | } 157 | 158 | 159 | void CommandSelector::populate() 160 | { 161 | invalidateParent(); 162 | 163 | clearItems(); 164 | 165 | for (auto command : mMainEditor.getChildCommands()) 166 | { 167 | if (caseInsensitiveFind(command->context, mFilter) || 168 | caseInsensitiveFind(command->name, mFilter)) 169 | addItem(new CommandItem(*command)); 170 | } 171 | 172 | sortItems(CommandItem::sort); 173 | selectItem(0); 174 | } 175 | 176 | 177 | const Editor::CommandDescriptor& CommandSelector::getSelectedCommand() const 178 | { 179 | return static_cast(getSelectedItem()).command; 180 | } 181 | 182 | 183 | CommandSelector::CommandItem::CommandItem(const CommandDescriptor& descriptor) 184 | : command(descriptor) 185 | { 186 | } 187 | 188 | 189 | bool CommandSelector::CommandItem::sort(const CommandSelector::Item* ga, const CommandSelector::Item* gb) 190 | { 191 | const auto& a = static_cast(*ga); 192 | const auto& b = static_cast(*gb); 193 | int cResult = strcmp(a.command.context, b.command.context); 194 | 195 | if (cResult == 0) 196 | return strcmp(a.command.name, b.command.name) < 0; 197 | 198 | return cResult < 0; 199 | } 200 | 201 | 202 | void CommandSelector::onModalStatusChange(bool isNowModal) 203 | { 204 | // Make sure text field will receive SDL_TEXTINPUTs 205 | // and disables them after dialog close 206 | mFilterField->setIsEditing(isNowModal); 207 | 208 | // Reset filter on modal open 209 | strcpy(mFilter, ""); 210 | } 211 | 212 | 213 | bool CommandSelector::onEvent(SDL_Event& event) 214 | { 215 | if (GenericSelector::onEvent(event)) 216 | return true; 217 | 218 | bool filterEvent = mFilterField->onEvent(event); 219 | 220 | if (filterEvent) 221 | { 222 | populate(); 223 | } 224 | 225 | return filterEvent; 226 | } 227 | -------------------------------------------------------------------------------- /src/Prototracker.cpp: -------------------------------------------------------------------------------- 1 | #include "App.h" 2 | #include "Prototracker.h" 3 | #include "Mixer.h" 4 | #include "Song.h" 5 | #include "Synth.h" 6 | #include "Player.h" 7 | #include "EditorState.h" 8 | #include "Gamepad.h" 9 | #include "Renderer.h" 10 | #include "MainEditor.h" 11 | #include "Emscripten.h" 12 | #include "Debug.h" 13 | 14 | #ifdef __EMSCRIPTEN__ 15 | #include 16 | #endif 17 | 18 | Prototracker::Prototracker() 19 | : mReady(false) 20 | { 21 | 22 | } 23 | 24 | Prototracker::~Prototracker() 25 | { 26 | } 27 | 28 | bool Prototracker::init() 29 | { 30 | mEditorState = new EditorState(); 31 | mSong = new Song(); 32 | mPlayer = new Player(*mSong); 33 | mGamepad = new Gamepad(); 34 | mSynth = new Synth(); 35 | mMixer = new Mixer(*mPlayer, *mSynth); 36 | mRenderer = new Renderer(); 37 | 38 | mMainEditor = new MainEditor(*mEditorState, *mPlayer, mPlayer->getPlayerState(), *mSong, *mSynth, *mMixer); 39 | 40 | if (!initRenderer()) { 41 | return false; 42 | } 43 | 44 | #ifndef __EMSCRIPTEN__ 45 | initEditor(); 46 | #endif 47 | 48 | return true; 49 | } 50 | 51 | 52 | void Prototracker::deinit() 53 | { 54 | mMixer->stopThread(); 55 | 56 | delete mMainEditor; 57 | delete mMixer; 58 | delete mPlayer; 59 | delete mSong; 60 | delete mEditorState; 61 | delete mGamepad; 62 | delete mRenderer; 63 | delete mSynth; 64 | } 65 | 66 | bool Prototracker::initRenderer() 67 | { 68 | Theme theme; 69 | 70 | if (!theme.load("assets/elements")) 71 | { 72 | return false; 73 | } 74 | 75 | if (!mRenderer->setTheme(theme)) 76 | { 77 | return false; 78 | } 79 | 80 | SDL_Rect area = {0, 0, theme.getWidth(), theme.getHeight()}; 81 | mMainEditor->setArea(area); 82 | 83 | if (!mMainEditor->loadElements(theme)) 84 | { 85 | return false; 86 | } 87 | 88 | mPreviousTick = SDL_GetTicks(); 89 | return true; 90 | } 91 | 92 | 93 | void Prototracker::initEditor() 94 | { 95 | // Emscripten needs an absolute path to filesystem root 96 | #ifdef __EMSCRIPTEN__ 97 | const char *gamepadPath = "/assets/gamecontrollerdb.txt"; 98 | const char *songPath = "/assets/dub.song"; 99 | #else 100 | const char *gamepadPath = "assets/gamecontrollerdb.txt"; 101 | const char *songPath = "assets/dub.song"; 102 | #endif 103 | 104 | mGamepad->loadDefinitions(gamepadPath); 105 | mGamepad->initControllers(); 106 | mReady = true; 107 | 108 | if (!mMainEditor->loadState()) 109 | mMainEditor->loadSong(songPath); 110 | 111 | // Try to use the device from the config or autodetect if not set 112 | 113 | mMainEditor->setAudioDevice(mEditorState->audioDevice.c_str()); 114 | } 115 | 116 | /** 117 | * Returns true until quit is signaled 118 | */ 119 | 120 | bool Prototracker::handleEvents() 121 | { 122 | bool done = false; 123 | SDL_Event event; 124 | 125 | while (SDL_PollEvent(&event)) 126 | { 127 | #ifdef __EMSCRIPTEN__ 128 | if (event.type == NEW_SONG) 129 | { 130 | mMainEditor->newSong(); 131 | } 132 | else if (event.type == PLAY_SONG) 133 | { 134 | mMainEditor->togglePlayStop(); 135 | } 136 | else if (event.type == SONG_IMPORTED) 137 | { 138 | mMainEditor->loadSong("/imported/imported-song.song"); 139 | } 140 | else if (event.type == EXPORT_SONG) 141 | { 142 | mMainEditor->exportSong(); 143 | } 144 | else if (event.type == EVERYTHING_READY) 145 | { 146 | initEditor(); 147 | emAppReady(); 148 | } 149 | else if (event.type == EVERYTHING_DONE) 150 | { 151 | debug("Received shutdown event"); 152 | mMainEditor->saveState(); 153 | done = true; 154 | emscripten_cancel_main_loop(); 155 | mMixer->stopThread(); 156 | emAppShutdown(); 157 | exit(0); 158 | } 159 | else 160 | #endif 161 | if (event.type == SDL_APP_WILLENTERBACKGROUND) 162 | { 163 | mMainEditor->saveState(); 164 | } 165 | else if (event.type == SDL_QUIT || event.type == SDL_APP_TERMINATING) 166 | { 167 | done = true; 168 | mMainEditor->saveState(); 169 | } 170 | else if (event.type == SDL_CONTROLLERAXISMOTION) 171 | { 172 | mGamepad->translateAxisToDPad(event); 173 | } 174 | else if (event.type == SDL_CONTROLLERDEVICEADDED || event.type == SDL_CONTROLLERDEVICEREMOVED || event.type == SDL_CONTROLLERDEVICEREMAPPED) 175 | { 176 | /* We just reinit all controllers instead of initializing a specific one */ 177 | 178 | #if SDL_VERSION_ATLEAST(2,0,4) && !defined(__EMSCRIPTEN__) 179 | if (event.type == SDL_CONTROLLERDEVICEADDED) 180 | mMainEditor->showMessageV(Editor::MessageInfo, "Plugged in %s", SDL_JoystickNameForIndex(event.cdevice.which)); 181 | else 182 | mMainEditor->showMessageV(Editor::MessageInfo, "Unplugged %s", SDL_GameControllerName(SDL_GameControllerFromInstanceID(event.cdevice.which))); 183 | #endif 184 | 185 | mGamepad->deinitControllers(); 186 | mGamepad->initControllers(); 187 | 188 | 189 | } 190 | #if SDL_VERSION_ATLEAST(2,0,4) 191 | #ifndef __EMSCRIPTEN__ 192 | // TODO: Connection events mess up emscripten audio timing because this event happens 193 | // before actual ready state. 194 | else if (event.type == SDL_AUDIODEVICEADDED) 195 | { 196 | mMainEditor->showMessageV(Editor::MessageInfo, "%s connected.", 197 | SDL_GetAudioDeviceName(event.adevice.which, 0)); 198 | 199 | if (!mMixer->getCurrentDeviceName()) 200 | { 201 | mMainEditor->setAudioDevice(SDL_GetAudioDeviceName(event.adevice.which, 0)); 202 | } 203 | } 204 | else if (event.type == SDL_AUDIODEVICEREMOVED) 205 | { 206 | if (mMixer->getCurrentDeviceID() == event.adevice.which) 207 | { 208 | mEditorState->audioDevice = ""; 209 | mMainEditor->showMessageV(Editor::MessageInfo, "%s disconnected.", 210 | mMixer->getCurrentDeviceName()); 211 | mMixer->stopThread(); 212 | } 213 | } 214 | #endif 215 | #endif 216 | else 217 | { 218 | mPlayer->lock(); 219 | mRenderer->scaleEventCoordinates(event); 220 | mMainEditor->onEvent(event); 221 | mPlayer->unlock(); 222 | } 223 | } 224 | 225 | if (mReady) 226 | { 227 | mPlayer->lock(); 228 | mMainEditor->syncPlayerState(); 229 | mMainEditor->syncSongParameters(*mSong); 230 | 231 | Uint32 ticks = SDL_GetTicks() - mPreviousTick; 232 | 233 | if (ticks > 0) 234 | { 235 | mMainEditor->update(ticks); 236 | mPreviousTick += ticks; 237 | } 238 | 239 | if (mMainEditor->isDirty()) 240 | { 241 | mPlayer->unlock(); 242 | mRenderer->beginRendering(); 243 | mMainEditor->draw(*mRenderer, mRenderer->getWindowArea()); 244 | #ifndef __EMSCRIPTEN__ 245 | mRenderer->present(); 246 | #endif 247 | } 248 | else 249 | { 250 | mPlayer->unlock(); 251 | #ifndef __EMSCRIPTEN__ 252 | SDL_Delay(2); 253 | #endif 254 | } 255 | 256 | #ifdef __EMSCRIPTEN__ 257 | mRenderer->present(); 258 | #endif 259 | } 260 | 261 | return !done; 262 | } 263 | 264 | 265 | std::string Prototracker::getSongBase64() const 266 | { 267 | return mMainEditor->getSongBase64(); 268 | } 269 | -------------------------------------------------------------------------------- /src/Theme.cpp: -------------------------------------------------------------------------------- 1 | #include "Debug.h" 2 | #include "Theme.h" 3 | #include "Color.h" 4 | #include 5 | #include 6 | #include "SDL.h" 7 | #include "SDL_rwops.h" 8 | 9 | Theme::Theme() 10 | { 11 | mFontWidth = 8; 12 | mFontHeight = 8; 13 | mWidth = 480; 14 | mHeight = 360; 15 | mBasePath = "assets/"; 16 | mBackgroundPath = mBasePath+"gui.png"; 17 | mFontPath = mBasePath+"font.png"; 18 | } 19 | 20 | 21 | bool Theme::load(const std::string& path) 22 | { 23 | mPath = path; 24 | 25 | if (!loadDefinition(path)) 26 | { 27 | SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Could not load theme", ("Could not load "+path+". Perhaps you need to set the working directory?").c_str(), NULL); 28 | return false; 29 | } 30 | 31 | return true; 32 | } 33 | 34 | 35 | const std::string& Theme::getName() const 36 | { 37 | return mName; 38 | } 39 | 40 | 41 | const std::string& Theme::getPath() const 42 | { 43 | return mPath; 44 | } 45 | 46 | const std::string& Theme::getFontPath() const 47 | { 48 | return mFontPath; 49 | } 50 | 51 | const std::string& Theme::getBackgroundPath() const 52 | { 53 | return mBackgroundPath; 54 | } 55 | 56 | 57 | int Theme::getFontWidth() const 58 | { 59 | return mFontWidth; 60 | } 61 | 62 | 63 | int Theme::getFontHeight() const 64 | { 65 | return mFontHeight; 66 | } 67 | 68 | int Theme::getWidth() const 69 | { 70 | return mWidth; 71 | } 72 | 73 | 74 | int Theme::getHeight() const 75 | { 76 | return mHeight; 77 | } 78 | 79 | 80 | const Theme::Element& Theme::getElement(int index) const 81 | { 82 | return mElement[index]; 83 | } 84 | 85 | 86 | int Theme::getElementCount() const 87 | { 88 | return mElement.size(); 89 | } 90 | 91 | 92 | static char *rwgets(char *buf, int count, SDL_RWops *rw) 93 | { 94 | int i; 95 | 96 | buf[count - 1] = '\0'; 97 | 98 | for (i = 0; i < count - 1; i++) 99 | { 100 | if (SDL_RWread(rw, buf + i, 1, 1) != 1) 101 | { 102 | if (i == 0) 103 | { 104 | return NULL; 105 | } 106 | 107 | break; 108 | } 109 | 110 | if (buf[i] == '\n') 111 | { 112 | break; 113 | } 114 | } 115 | 116 | buf[i] = '\0'; 117 | 118 | return buf; 119 | } 120 | 121 | 122 | bool Theme::loadDefinition(const std::string& path) 123 | { 124 | SDL_RWops *rw = SDL_RWFromFile(path.c_str(), "r"); 125 | 126 | if (!rw) 127 | return false; 128 | 129 | int lineCounter = 0; 130 | 131 | static struct { ColorType type; const char *name; } colors[] = { 132 | { ColorType::CurrentRow, "CurrentRow" }, 133 | { ColorType::BlockMarker, "BlockMarker" }, 134 | { ColorType::EditCursor, "EditCursor" }, 135 | { ColorType::NonEditCursor, "NonEditCursor" }, 136 | { ColorType::RowCounter, "RowCounter" }, 137 | { ColorType::SelectedRow, "SelectedRow" }, 138 | { ColorType::ModalBackground, "ModalBackground" }, 139 | { ColorType::ModalBorder, "ModalBorder" }, 140 | { ColorType::ModalTitleBackground, "ModalTitleBackground" }, 141 | { ColorType::ModalTitleText, "ModalTitleText" }, 142 | { ColorType::NormalText, "NormalText" }, 143 | { ColorType::ScrollBar, "ScrollBar" }, 144 | { ColorType::PlayHead, "PlayHead" }, 145 | { ColorType::TextCursor, "TextCursor" }, 146 | { ColorType::TextBackground, "TextBackground" }, 147 | { ColorType::TextFocus, "TextFocus" }, 148 | { ColorType::OscilloscopeColor, "Oscilloscope" }, 149 | { ColorType::CommandShortcut, "CommandShortcut" }, 150 | { ColorType::CommandShortcutBackground, "CommandShortcutBackground" }, 151 | { ColorType::MutedOscilloscopeColor, "MutedOscilloscope" }, 152 | }; 153 | 154 | static struct { ElementType type; const char *name; } elements[] = { 155 | { ElementType::PatternEditor, "PatternEditor" }, 156 | { ElementType::SequenceEditor, "SequenceEditor" }, 157 | { ElementType::MacroEditor, "MacroEditor" }, 158 | { ElementType::Oscilloscope, "Oscilloscope"}, 159 | { ElementType::SongName, "SongName"}, 160 | { ElementType::MacroName, "MacroName"}, 161 | { ElementType::MacroNumber, "MacroNumber"}, 162 | { ElementType::SequencePosition, "SequencePosition"}, 163 | { ElementType::SequenceLength, "SequenceLength"}, 164 | { ElementType::PatternLength, "PatternLength"}, 165 | { ElementType::OctaveNumber, "OctaveNumber"}, 166 | { ElementType::TouchRegion, "TouchRegion"}, 167 | }; 168 | 169 | while (true) 170 | { 171 | ++lineCounter; 172 | 173 | char line[500]; 174 | if (rwgets(line, sizeof(line), rw) == NULL) 175 | break; 176 | 177 | // Check if comment 178 | 179 | if (line[0] == '#') 180 | continue; 181 | 182 | char elementName[20], path[100], strParameters[10][50] = {0}; 183 | int parameters[10] = {0}; 184 | 185 | if (sscanf(line, "%19s \"%99[^\"]\" %d %d", elementName, path, ¶meters[0], ¶meters[1]) >= 4 && strcmp("Font", elementName) == 0) 186 | { 187 | mFontPath = mBasePath + path; 188 | mFontWidth = parameters[0]; 189 | mFontHeight = parameters[1]; 190 | } 191 | else if (sscanf(line, "%19s \"%99[^\"]\" %d %d %d", elementName, path, ¶meters[0], ¶meters[1], ¶meters[2]) >= 4 && 192 | (strcmp("GUI", elementName) == 0 || strcmp("Color", elementName) == 0)) 193 | { 194 | if (strcmp(elementName, "Color") == 0) 195 | { 196 | ColorType colorType = NumColors; 197 | for (auto color : colors) 198 | { 199 | if (strcmp(color.name, path) == 0) 200 | { 201 | colorType = color.type; 202 | break; 203 | } 204 | } 205 | 206 | if (colorType < NumColors) 207 | { 208 | mColors[colorType] = Color(parameters[0], parameters[1], parameters[2]); 209 | } 210 | else 211 | { 212 | debug("Unknown color %s", path); 213 | } 214 | } 215 | else 216 | { 217 | mBackgroundPath = mBasePath + path; 218 | mWidth = parameters[0]; 219 | mHeight = parameters[1]; 220 | } 221 | } 222 | else if ((sscanf(line, "%19s %d %d %d %d \"%50[^\"]\" \"%50[^\"]\"", elementName, ¶meters[0], ¶meters[1], ¶meters[2], ¶meters[3], strParameters[0], strParameters[1]) >= 5 223 | && strcmp("TouchRegion", elementName) == 0) 224 | ||sscanf(line, "%19s %d %d %d %d %d %d", elementName, ¶meters[0], ¶meters[1], ¶meters[2], ¶meters[3], ¶meters[4], ¶meters[5]) >= 5) 225 | { 226 | Element element; 227 | element.type = Theme::Unknown; 228 | 229 | for (auto elementDef : elements) 230 | { 231 | if (strcmp(elementDef.name, elementName) == 0) 232 | { 233 | element.type = elementDef.type; 234 | break; 235 | } 236 | } 237 | 238 | if (element.type != Theme::Unknown) 239 | { 240 | memcpy(element.parameters, parameters, sizeof(element.parameters)); 241 | memcpy(element.strParameters, strParameters, sizeof(element.strParameters)); 242 | mElement.push_back(element); 243 | } 244 | else 245 | { 246 | debug("Unknown element %s on line %d", elementName, lineCounter); 247 | } 248 | } 249 | else 250 | { 251 | debug("Weirdness on line %d", lineCounter); 252 | } 253 | } 254 | 255 | SDL_RWclose(rw); 256 | 257 | return true; 258 | } 259 | 260 | 261 | const Color& Theme::getColor(ColorType type) const 262 | { 263 | return mColors[type]; 264 | } 265 | -------------------------------------------------------------------------------- /src/FileSelector.cpp: -------------------------------------------------------------------------------- 1 | #include "FileSelector.h" 2 | #include "Renderer.h" 3 | #include "Color.h" 4 | #include "SDL.h" 5 | #include "TextEditor.h" 6 | #include "Label.h" 7 | #include "MessageBox.h" 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | FileSelector::FileSelector(EditorState& editorState) 15 | : GenericSelector(editorState), mCheckOverwrite(false) 16 | { 17 | mNameField = new TextEditor(editorState); 18 | mNameField->setBuffer(mFilename, filenameSize); 19 | mNameField->setIsEditing(true); 20 | mNameField->setAlwaysShowCursor(true); 21 | mFilenameLabel = new Label(editorState); 22 | mFilenameLabel->setText("FILENAME:"); 23 | addChild(mFilenameLabel, 0, 8, 8*9, 8); 24 | addChild(mNameField, 8*9, 8, 208, 8); 25 | strcpy(mFilename, ""); 26 | strcpy(mFilter, ""); 27 | setFocus(mNameField); 28 | mMessageBox = new MessageBox(editorState); 29 | } 30 | 31 | 32 | FileSelector::~FileSelector() 33 | { 34 | delete mNameField; 35 | delete mFilenameLabel; 36 | delete mMessageBox; 37 | } 38 | 39 | 40 | void FileSelector::onRendererMount(const Renderer& renderer) 41 | { 42 | GenericSelector::onRendererMount(renderer); 43 | 44 | mFilenameLabel->setColor(renderer.getTheme().getColor(Theme::ColorType::ModalTitleText)); 45 | mFilenameLabel->setBackground(renderer.getTheme().getColor(Theme::ColorType::ModalTitleBackground)); 46 | 47 | SDL_Rect filenameLabelArea = mFilenameLabel->getArea(); 48 | filenameLabelArea.y = mLabel->getArea().h; 49 | filenameLabelArea.w = renderer.getFontWidth() * 9; 50 | filenameLabelArea.h = renderer.getFontHeight(); 51 | mFilenameLabel->setArea(filenameLabelArea); 52 | 53 | SDL_Rect filenameArea = mNameField->getArea(); 54 | filenameArea.x = filenameLabelArea.x + filenameLabelArea.w; 55 | filenameArea.y = filenameLabelArea.y; 56 | filenameArea.w = mThisArea.w - filenameArea.x; 57 | filenameArea.h = renderer.getFontHeight(); 58 | mNameField->setArea(filenameArea); 59 | } 60 | 61 | 62 | bool FileSelector::fileExists(const char *path) 63 | { 64 | FILE *f = fopen(path, "r"); 65 | 66 | if (!f) 67 | return false; 68 | 69 | fclose(f); 70 | 71 | return true; 72 | } 73 | 74 | 75 | void FileSelector::accept(bool isFinal) 76 | { 77 | if (isFinal) 78 | mParent->onFileSelectorEvent(*this, true); 79 | else 80 | { 81 | if (FileItem::checkDirectory(getSelectedPath())) 82 | { 83 | setPath(getSelectedPath()); 84 | return; 85 | } 86 | 87 | if (mCheckOverwrite && fileExists(getSelectedPath())) 88 | { 89 | mMessageBox->setTitle("Overwrite y/n"); 90 | mMessageBox->setId(MessageBoxOverwrite); 91 | setModal(mMessageBox); 92 | } 93 | else 94 | mParent->onFileSelectorEvent(*this, true); 95 | } 96 | } 97 | 98 | 99 | void FileSelector::onMessageBoxEvent(const Editor& _messageBox, int code) 100 | { 101 | const MessageBox& messageBox = reinterpret_cast(_messageBox); 102 | 103 | if (messageBox.getId() == MessageBoxOverwrite) 104 | { 105 | if (code == 1) 106 | { 107 | accept(true); 108 | } 109 | else 110 | { 111 | } 112 | } 113 | 114 | setModal(NULL); 115 | } 116 | 117 | 118 | void FileSelector::reject(bool isFinal) 119 | { 120 | mParent->onFileSelectorEvent(*this, false); 121 | } 122 | 123 | 124 | void FileSelector::renderItem(Renderer& renderer, const SDL_Rect& area, const Item& item, bool isSelected) 125 | { 126 | const FileItem& fileItem = static_cast(item); 127 | Theme::ColorType color = Theme::ColorType::NormalText; 128 | 129 | if (isSelected) 130 | color = Theme::ColorType::SelectedRow; 131 | 132 | renderer.clearRect(area, Theme::ColorType::ModalBackground); 133 | 134 | int width = area.w / renderer.getFontWidth() - 10; 135 | 136 | if (fileItem.isDirectory) 137 | renderer.renderTextV(area, color, "<%*s> DIR", -width, fileItem.path.c_str()); 138 | else 139 | renderer.renderTextV(area, color, "%*s %9s", -width, fileItem.path.c_str(), FileItem::formatSize(fileItem.size)); 140 | } 141 | 142 | 143 | void FileSelector::setPath(const char *path) 144 | { 145 | chdir(path); 146 | setDirty(true); 147 | populate(); 148 | } 149 | 150 | 151 | void FileSelector::populate() 152 | { 153 | invalidateParent(); 154 | 155 | clearItems(); 156 | 157 | DIR *directory = opendir("."); 158 | 159 | if (!directory) 160 | return; 161 | 162 | while (true) 163 | { 164 | struct dirent *entry; 165 | struct stat statBuf; 166 | 167 | entry = readdir(directory); 168 | 169 | if (entry == NULL) 170 | break; 171 | 172 | if (!stat(entry->d_name, &statBuf) && ((statBuf.st_mode & S_IFDIR) || 173 | (strlen(entry->d_name) > strlen(mFilter) && strcmp(entry->d_name + strlen(entry->d_name) - strlen(mFilter), mFilter) == 0))) 174 | { 175 | addItem(new FileItem(statBuf.st_mode & S_IFDIR, entry->d_name, statBuf.st_size)); 176 | } 177 | } 178 | 179 | closedir(directory); 180 | 181 | sortItems(FileItem::directorySort); 182 | 183 | selectItem(0); 184 | } 185 | 186 | 187 | const char * FileSelector::getSelectedPath() const 188 | { 189 | return mFilename; 190 | } 191 | 192 | 193 | void FileSelector::setFilter(const char *extension) 194 | { 195 | strncpy(mFilter, extension, filterSize); 196 | } 197 | 198 | 199 | void FileSelector::setOverwriteCheck(bool state) 200 | { 201 | mCheckOverwrite = state; 202 | } 203 | 204 | 205 | FileSelector::FileItem::FileItem(bool _isDirectory, const char *_path, int _size) 206 | : isDirectory(_isDirectory), path(_path), size(_size) 207 | { 208 | } 209 | 210 | 211 | bool FileSelector::FileItem::directorySort(const FileSelector::Item* ga, const FileSelector::Item* gb) 212 | { 213 | const FileItem& a = static_cast(*ga); 214 | const FileItem& b = static_cast(*gb); 215 | 216 | // Directories first 217 | 218 | if (a.isDirectory && !b.isDirectory) 219 | return true; 220 | 221 | // Then in alphabetical order if BOTH a & b are either directories or files 222 | 223 | if (a.isDirectory == b.isDirectory && strcmp(a.path.c_str(), b.path.c_str()) < 0) 224 | return true; 225 | 226 | return false; 227 | } 228 | 229 | 230 | bool FileSelector::FileItem::checkDirectory(const char *name) 231 | { 232 | struct stat statBuf; 233 | 234 | if (!stat(name, &statBuf)) 235 | { 236 | return (statBuf.st_mode & S_IFDIR) == S_IFDIR; 237 | } 238 | 239 | return false; 240 | } 241 | 242 | 243 | const char *FileSelector::FileItem::formatSize(int size) 244 | { 245 | static char buffer[50]; 246 | 247 | const struct { int divisor; const char *unit; } units[] = { 248 | {1, "B"}, 249 | {1024, "k"}, 250 | {1024 * 1024, "M"}, 251 | {1024 * 1024 * 1024, "G"}, 252 | {0, NULL} 253 | }; 254 | 255 | int divisor = 0; 256 | const char *unit = NULL; 257 | 258 | for (int i = 0 ; units[i].divisor > 0 ; ++i) 259 | { 260 | if (size >= units[i].divisor) 261 | { 262 | divisor = units[i].divisor; 263 | unit = units[i].unit; 264 | } 265 | } 266 | 267 | snprintf(buffer, sizeof(buffer), "%d%s", size / divisor, unit); 268 | 269 | return buffer; 270 | } 271 | 272 | 273 | void FileSelector::onSelectItem(const Item& item) 274 | { 275 | const FileItem& fileItem = static_cast(item); 276 | strncpy(mFilename, fileItem.path.c_str(), filenameSize); 277 | mNameField->setText(mFilename); 278 | } 279 | 280 | 281 | bool FileSelector::onEvent(SDL_Event& event) 282 | { 283 | return GenericSelector::onEvent(event) || mNameField->onEvent(event); 284 | } 285 | 286 | 287 | void FileSelector::onModalStatusChange(bool isNowModal) 288 | { 289 | // Make sure text field will receive SDL_TEXTINPUTs 290 | // and disables them after dialog close 291 | mNameField->setIsEditing(isNowModal); 292 | } 293 | --------------------------------------------------------------------------------