├── src ├── unittest │ ├── åäö.txt │ ├── テスト.txt │ ├── main.cpp │ ├── test_misc.cpp │ ├── test_vgm.cpp │ ├── test_conf.cpp │ ├── test_input.cpp │ ├── test_riff.cpp │ ├── test_song.cpp │ └── test_track.cpp ├── conf.h ├── core.h ├── stringf.h ├── stringf.cpp ├── riff.h ├── util.h ├── driver.h ├── mml_input.h ├── optimizer.h ├── driver.cpp ├── song.h ├── conf.cpp ├── wave.h ├── vgm.h ├── input.h ├── mmlc.cpp ├── platform │ ├── mdslink.cpp │ ├── md.h │ └── mdsdrv.h ├── riff.cpp ├── player.h ├── track.h ├── input.cpp ├── vgm.cpp ├── song.cpp ├── mml_input.cpp └── wave.cpp ├── .gitignore ├── sample ├── pcm │ ├── bd_17k5.wav │ ├── esd_17k5.wav │ ├── hhc_17k5.wav │ ├── hho_17k5.wav │ ├── sd_17k5.wav │ ├── crash_17k5.wav │ ├── ride_17k5.wav │ ├── tom1h_17k5.wav │ ├── tom1l_17k5.wav │ ├── tom1m_17k5.wav │ ├── tom2h_17k5.wav │ ├── tom2l_17k5.wav │ ├── tom2m_17k5.wav │ └── bd_crash_17k5.wav ├── sand_light.mml ├── midnight.mml ├── idk.mml ├── junkers_high.mml └── passport.mml ├── README.md ├── CMakeLists.txt └── Makefile /src/unittest/åäö.txt: -------------------------------------------------------------------------------- 1 | my filename contains Latin1 characters 2 | -------------------------------------------------------------------------------- /src/unittest/テスト.txt: -------------------------------------------------------------------------------- 1 | my filename contains Unicode characters 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/* 2 | /obj/* 3 | /lib/* 4 | doxygen 5 | *.vgm 6 | *.mds 7 | ctrmml 8 | -------------------------------------------------------------------------------- /sample/pcm/bd_17k5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superctr/ctrmml/HEAD/sample/pcm/bd_17k5.wav -------------------------------------------------------------------------------- /sample/pcm/esd_17k5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superctr/ctrmml/HEAD/sample/pcm/esd_17k5.wav -------------------------------------------------------------------------------- /sample/pcm/hhc_17k5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superctr/ctrmml/HEAD/sample/pcm/hhc_17k5.wav -------------------------------------------------------------------------------- /sample/pcm/hho_17k5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superctr/ctrmml/HEAD/sample/pcm/hho_17k5.wav -------------------------------------------------------------------------------- /sample/pcm/sd_17k5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superctr/ctrmml/HEAD/sample/pcm/sd_17k5.wav -------------------------------------------------------------------------------- /sample/pcm/crash_17k5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superctr/ctrmml/HEAD/sample/pcm/crash_17k5.wav -------------------------------------------------------------------------------- /sample/pcm/ride_17k5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superctr/ctrmml/HEAD/sample/pcm/ride_17k5.wav -------------------------------------------------------------------------------- /sample/pcm/tom1h_17k5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superctr/ctrmml/HEAD/sample/pcm/tom1h_17k5.wav -------------------------------------------------------------------------------- /sample/pcm/tom1l_17k5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superctr/ctrmml/HEAD/sample/pcm/tom1l_17k5.wav -------------------------------------------------------------------------------- /sample/pcm/tom1m_17k5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superctr/ctrmml/HEAD/sample/pcm/tom1m_17k5.wav -------------------------------------------------------------------------------- /sample/pcm/tom2h_17k5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superctr/ctrmml/HEAD/sample/pcm/tom2h_17k5.wav -------------------------------------------------------------------------------- /sample/pcm/tom2l_17k5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superctr/ctrmml/HEAD/sample/pcm/tom2l_17k5.wav -------------------------------------------------------------------------------- /sample/pcm/tom2m_17k5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superctr/ctrmml/HEAD/sample/pcm/tom2m_17k5.wav -------------------------------------------------------------------------------- /sample/pcm/bd_crash_17k5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superctr/ctrmml/HEAD/sample/pcm/bd_crash_17k5.wav -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ctrmml 2 | ====== 3 | This is still in development. Compatibility with future versions is not guaranteed. 4 | 5 | ## Building 6 | make -j5 7 | 8 | #### Running unit tests 9 | make test -j5 10 | 11 | ## Usage 12 | ctrmml 13 | 14 | ## MML reference 15 | See `mml_ref.md` for command reference 16 | -------------------------------------------------------------------------------- /src/unittest/main.cpp: -------------------------------------------------------------------------------- 1 | // http://cppunit.sourceforge.net/doc/cvs/cppunit_cookbook.html 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | int main(int argc, char **argv) 9 | { 10 | CppUnit::TextTestRunner runner; 11 | CppUnit::TestFactoryRegistry ®istry = CppUnit::TestFactoryRegistry::getRegistry(); 12 | CppUnit::BriefTestProgressListener listener; 13 | runner.eventManager().addListener(&listener); 14 | runner.addTest(registry.makeTest()); 15 | bool run_ok = runner.run("", false,true,false); 16 | return !run_ok; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/conf.h: -------------------------------------------------------------------------------- 1 | /*! \file src/conf.h 2 | * \brief Configuration file handling. 3 | * 4 | * Handles a simple hierarchial configuration file format. 5 | */ 6 | #ifndef CONF_H 7 | #define CONF_H 8 | #include 9 | #include 10 | #include 11 | 12 | class Conf; 13 | 14 | //! Configuration object. 15 | class Conf 16 | { 17 | public: 18 | Conf(); 19 | Conf(const std::string& key); 20 | Conf(const std::string& key, const std::vector& subkeys); 21 | 22 | Conf& get_subkey(const std::string& key); 23 | static Conf from_string(const char* str); 24 | 25 | std::vector subkeys; 26 | std::string key; 27 | 28 | private: 29 | const char* parse_token(const char* head); 30 | }; 31 | #endif 32 | -------------------------------------------------------------------------------- /src/core.h: -------------------------------------------------------------------------------- 1 | /*! \file src/core.h 2 | * \brief ctrmml core include file. 3 | * 4 | * Includes forward declarations of the core classes and 5 | * container data structures used by ctrmml. 6 | */ 7 | #ifndef CORE_H 8 | #define CORE_H 9 | 10 | #define CTRMML_VERSION "0.3" 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | class Track; 17 | class Song; 18 | class Input; 19 | class InputRef; 20 | class VGM_Writer; 21 | class VGM_Interface; 22 | class Player; 23 | class Driver; 24 | class Platform; 25 | 26 | typedef std::vector Tag; 27 | typedef std::map Tag_Map; 28 | typedef std::map Track_Map; 29 | typedef std::shared_ptr InputRefPtr; 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /src/stringf.h: -------------------------------------------------------------------------------- 1 | //! \file stringf.h 2 | //! String formatting helper functions. 3 | #ifndef STRINGF_H 4 | #define STRINGF_H 5 | #include 6 | 7 | //! Return a printf-formatted std::string. 8 | /*! 9 | * \param format Format. 10 | * \param ... Data parameters. 11 | */ 12 | std::string stringf(const char* format, ...); 13 | 14 | //! Case-insensitive string comparison. 15 | /*! 16 | * Return true if strings are equal 17 | */ 18 | bool iequal(const std::string &s1, const std::string &s2); 19 | 20 | 21 | #ifdef _WIN32 22 | #include 23 | #include 24 | std::vector get_native_filename(const std::string &s, unsigned int cp = 65001, int max = 1024); 25 | #else 26 | inline std::string get_native_filename(const std::string &s, unsigned int cp = 0, unsigned int max = 0) 27 | { 28 | return s; 29 | } 30 | #endif 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /src/unittest/test_misc.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "../stringf.h" 5 | 6 | class Misc_Test : public CppUnit::TestFixture 7 | { 8 | CPPUNIT_TEST_SUITE(Misc_Test); 9 | CPPUNIT_TEST(test_open_test_file_latin1); 10 | CPPUNIT_TEST(test_open_test_file_unicode); 11 | CPPUNIT_TEST_SUITE_END(); 12 | public: 13 | void setUp() 14 | { 15 | } 16 | void tearDown() 17 | { 18 | } 19 | void test_open_test_file_latin1() 20 | { 21 | std::ifstream inputfile = std::ifstream(get_native_filename(u8"src/unittest/åäö.txt").data()); 22 | if(!inputfile) 23 | CPPUNIT_FAIL("file couldn't be opened"); 24 | std::string str; 25 | std::getline(inputfile, str); 26 | CPPUNIT_ASSERT_EQUAL(std::string("my filename contains Latin1 characters"), str); 27 | } 28 | void test_open_test_file_unicode() 29 | { 30 | std::ifstream inputfile = std::ifstream(get_native_filename(u8"src/unittest/テスト.txt").data()); 31 | if(!inputfile) 32 | CPPUNIT_FAIL("file couldn't be opened"); 33 | std::string str; 34 | std::getline(inputfile, str); 35 | CPPUNIT_ASSERT_EQUAL(std::string("my filename contains Unicode characters"), str); 36 | } 37 | }; 38 | 39 | CPPUNIT_TEST_SUITE_REGISTRATION(Misc_Test); 40 | -------------------------------------------------------------------------------- /src/unittest/test_vgm.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "../vgm.h" 4 | 5 | class VGM_Writer_Test : public CppUnit::TestFixture 6 | { 7 | CPPUNIT_TEST_SUITE(VGM_Writer_Test); 8 | CPPUNIT_TEST(test_vgm_output); 9 | CPPUNIT_TEST_SUITE_END(); 10 | public: 11 | void setUp() 12 | { 13 | } 14 | void tearDown() 15 | { 16 | } 17 | // VGM output will need to be verified manually as few asserts have been made on the output 18 | // right now we verify that allocations won't fail 19 | void test_vgm_output() 20 | { 21 | auto vgm = VGM_Writer("", 0x51, 0x80); 22 | vgm.poke32(0x2C, 7670454); // YM2612 23 | vgm.poke32(0x0C, 3579575); // SegaPSG 24 | vgm.poke16(0x28, 0x0009); 25 | vgm.poke8(0x2A, 0x10); 26 | vgm.poke8(0x2B, 0x03); 27 | vgm.write(0x50, 0, 0, 0x9f); // Mute PSG 28 | vgm.write(0x50, 0, 0, 0xbf); 29 | vgm.write(0x50, 0, 0, 0xdf); 30 | vgm.write(0x50, 0, 0, 0xff); 31 | vgm.write(0x52, 0, 0x2b, 0x80); // FM dac enable 32 | for(int i=0; i<1000000; i++) 33 | { 34 | // write a sawtooth waveform 35 | vgm.write(0x52, 0, 0x2a, (i & 0xff)); // FM dac data 36 | vgm.delay(1.5); 37 | } 38 | vgm.stop(); 39 | vgm.write_tag(); 40 | // verify sample count (1.5*1000000) 41 | CPPUNIT_ASSERT_EQUAL((uint32_t) 1500000, vgm.peek32(0x18)); 42 | } 43 | }; 44 | 45 | CPPUNIT_TEST_SUITE_REGISTRATION(VGM_Writer_Test); 46 | 47 | -------------------------------------------------------------------------------- /src/stringf.cpp: -------------------------------------------------------------------------------- 1 | #include "stringf.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #ifdef _WIN32 8 | #include 9 | #include 10 | //! Wrapper to get the native filename on windows (needed for unicode support) 11 | std::vector get_native_filename(const std::string &s, unsigned int cp, int max) 12 | { 13 | std::vector wcs (max, 0); 14 | wchar_t* ptr = wcs.data(); 15 | int l = MultiByteToWideChar(cp,0,s.c_str(),-1,ptr,max-1); 16 | if(l == 0) 17 | { 18 | throw std::runtime_error("get_native_filename"); 19 | } 20 | return wcs; 21 | } 22 | #else 23 | #endif 24 | 25 | std::string stringf(const char* format, ...) 26 | { 27 | using namespace std; 28 | char* buf; 29 | va_list arg1,arg2; 30 | va_start(arg1,format); 31 | va_copy(arg2,arg1); 32 | int size = vsnprintf(NULL,0,format,arg2) + 1; 33 | buf = new char[size]; 34 | vsnprintf(buf,size,format,arg1); 35 | std::string out = std::string(buf); 36 | delete[] buf; 37 | va_end(arg1); 38 | va_end(arg2); 39 | return out; 40 | } 41 | 42 | class iequal_class 43 | { 44 | public: 45 | bool operator() (int c1, int c2) const 46 | { 47 | return std::tolower(c1) == std::tolower(c2); 48 | } 49 | }; 50 | 51 | bool iequal(const std::string &s1, const std::string &s2) 52 | { 53 | return s1.size() == s2.size() && std::equal(s1.begin(), s1.end(), s2.begin(), iequal_class()); 54 | } 55 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # TODO: set static build flags on windows - best option? 2 | 3 | cmake_minimum_required(VERSION 3.10) 4 | project(ctrmml) 5 | 6 | find_package(PkgConfig REQUIRED) 7 | pkg_check_modules(CPPUNIT cppunit) 8 | 9 | add_library(ctrmml 10 | src/track.cpp 11 | src/song.cpp 12 | src/input.cpp 13 | src/mml_input.cpp 14 | src/player.cpp 15 | src/stringf.cpp 16 | src/vgm.cpp 17 | src/driver.cpp 18 | src/wave.cpp 19 | src/riff.cpp 20 | src/conf.cpp 21 | src/optimizer.cpp 22 | src/platform/md.cpp 23 | src/platform/mdsdrv.cpp) 24 | target_include_directories(ctrmml PUBLIC src) 25 | 26 | add_executable(mmlc src/mmlc.cpp) 27 | target_link_libraries(mmlc ctrmml) 28 | 29 | add_executable(mdslink src/platform/mdslink.cpp) 30 | target_link_libraries(mdslink ctrmml) 31 | 32 | if(CPPUNIT_FOUND) 33 | add_executable(ctrmml_unittest 34 | src/unittest/test_track.cpp 35 | src/unittest/test_song.cpp 36 | src/unittest/test_input.cpp 37 | src/unittest/test_mml_input.cpp 38 | src/unittest/test_player.cpp 39 | src/unittest/test_vgm.cpp 40 | src/unittest/test_riff.cpp 41 | src/unittest/test_conf.cpp 42 | src/unittest/test_mdsdrv.cpp 43 | src/unittest/test_misc.cpp 44 | src/unittest/main.cpp) 45 | target_link_libraries(ctrmml_unittest ctrmml) 46 | target_link_libraries(ctrmml_unittest ${CPPUNIT_LIBRARIES}) 47 | enable_testing() 48 | add_test(NAME run_ctrmml_unittest COMMAND ctrmml_unittest WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) 49 | endif() 50 | -------------------------------------------------------------------------------- /src/riff.h: -------------------------------------------------------------------------------- 1 | //! \file riff.h 2 | #ifndef RIFF_H 3 | #define RIFF_H 4 | #include "core.h" 5 | #include 6 | 7 | //! Macro to convert a string "ABCD" to literal 'DCBA' 8 | #define FOURCC(code) (((*(uint32_t*)code & 0xff000000)>>24)\ 9 | |((*(uint32_t*)code & 0x00ff0000)>>8)\ 10 | |((*(uint32_t*)code & 0x0000ff00)<<8)\ 11 | |((*(uint32_t*)code & 0x000000ff)<<24)) 12 | 13 | //! RIFF (Resource Interchange File Format) I/O class 14 | class RIFF 15 | { 16 | public: 17 | const static uint32_t TYPE_RIFF = 0x52494646; 18 | const static uint32_t TYPE_LIST = 0x4C495354; 19 | const static uint32_t ID_NONE = 0x20202020; 20 | 21 | RIFF(uint32_t chunk_type); 22 | RIFF(uint32_t chunk_type, const std::vector& initial_data); 23 | RIFF(uint32_t chunk_type, uint32_t id); 24 | RIFF(uint32_t chunk_type, uint32_t id, const std::vector& initial_data); 25 | RIFF(const std::vector& initial_data); 26 | 27 | void rewind(); 28 | bool at_end() const; 29 | 30 | //void set_type(uint32_t type); 31 | uint32_t get_type() const; 32 | void set_id(uint32_t id); 33 | uint32_t get_id() const; 34 | //uint32_t get_size() const; 35 | 36 | void add_chunk(const class RIFF& new_chunk); 37 | void add_data(std::vector& new_data); 38 | 39 | std::vector& get_data(); 40 | std::vector get_chunk(); 41 | 42 | std::vector to_bytes() const; 43 | 44 | private: 45 | uint32_t size; 46 | uint32_t position; 47 | uint32_t type; 48 | std::vector data; 49 | }; 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | //! \file util.h 2 | #ifndef UTIL_H 3 | #define UTIL_H 4 | 5 | //! Write 32-bit little endian integer to a vector. 6 | static inline void write_le32(std::vector& data, uint32_t pos, uint32_t new_data) 7 | { 8 | if(data.size() < pos+4) 9 | data.resize(pos+4); 10 | data[pos] = new_data; 11 | data[pos+1] = new_data>>8; 12 | data[pos+2] = new_data>>16; 13 | data[pos+3] = new_data>>24; 14 | } 15 | 16 | //! Write 32-bit big endian integer to a vector. 17 | static inline void write_be32(std::vector& data, uint32_t pos, uint32_t new_data) 18 | { 19 | if(data.size() < pos+4) 20 | data.resize(pos+4); 21 | data[pos+3] = new_data; 22 | data[pos+2] = new_data>>8; 23 | data[pos+1] = new_data>>16; 24 | data[pos] = new_data>>24; 25 | } 26 | 27 | //! Write 16-bit big endian integer to a vector. 28 | static inline void write_be16(std::vector& data, uint32_t pos, uint32_t new_data) 29 | { 30 | if(data.size() < pos+2) 31 | data.resize(pos+2); 32 | data[pos+1] = new_data; 33 | data[pos+0] = new_data>>8; 34 | } 35 | 36 | //! Read 32-bit little endian integer to a vector. 37 | static inline uint32_t read_le32(const std::vector& data, uint32_t pos) 38 | { 39 | return data.at(pos) | (data.at(pos+1)<<8) | (data.at(pos+2)<<16) | (data.at(pos+3)<<24); 40 | } 41 | 42 | //! Read 32-bit big endian integer to a vector. 43 | static inline uint32_t read_be32(const std::vector& data, uint32_t pos) 44 | { 45 | return data.at(pos+3) | (data.at(pos+2)<<8) | (data.at(pos+1)<<16) | (data.at(pos+0)<<24); 46 | } 47 | 48 | #endif // UTIL_H 49 | -------------------------------------------------------------------------------- /src/driver.h: -------------------------------------------------------------------------------- 1 | /*! \file driver.h 2 | * \brief Sound driver base. 3 | * 4 | * A sound driver handles playback of Song files, either real-time 5 | * using sound chip interfaces or emulator (such as libvgm), or 6 | * just conversion to VGM file. 7 | */ 8 | #ifndef DRIVER_H 9 | #define DRIVER_H 10 | #include "core.h" 11 | 12 | //! Sound driver base class. 13 | /*! 14 | * \todo this will provide an abstraction between derived 15 | * classes and the VGM file or sound chip interfaces provided by 16 | * the calling functions. 17 | * 18 | * By using the interfaces `_w` to write to sound chip registers, 19 | * a derived class can support both real-time playback and VGM logging. 20 | */ 21 | class Driver 22 | { 23 | public: 24 | Driver(unsigned int rate, VGM_Interface* vgm); 25 | 26 | // TODO: split into load_song() and play_song()? 27 | //! Play a new song 28 | virtual void play_song(Song& song) = 0; 29 | //! Reset the sound driver, silencing sound chips and 30 | //! allowing for playback to be restarted or a new 31 | //! song to be played. 32 | virtual void reset() = 0; 33 | //! Skip a specified number of ticks 34 | virtual void skip_ticks(unsigned int ticks) = 0; 35 | //! Play a tick and return the delta until the next. 36 | virtual double play_step() = 0; 37 | //! Return false if song has finished playback, true otherwise. 38 | virtual bool is_playing() = 0; 39 | //! Get the number of player ticks (playing time). 40 | virtual uint32_t get_player_ticks() = 0; 41 | //! Get the number of times the song has looped. 42 | virtual int get_loop_count() = 0; 43 | 44 | unsigned int get_rate(); 45 | 46 | protected: 47 | // VGM low-level 48 | void write(uint8_t command, uint16_t port, uint16_t reg, uint16_t data); 49 | void set_loop(); 50 | 51 | // VGM write helpers 52 | void ym2612_w(uint8_t port, uint8_t reg, uint8_t ch, uint8_t op, uint16_t data); 53 | void sn76489_w(uint8_t reg, uint8_t ch, uint16_t data); 54 | 55 | private: 56 | VGM_Interface* vgm; 57 | double delta; 58 | unsigned int rate; 59 | }; 60 | 61 | #endif 62 | 63 | -------------------------------------------------------------------------------- /src/mml_input.h: -------------------------------------------------------------------------------- 1 | /*! \file src/mml_input.h 2 | * \brief MML (Music Macro Language) parser 3 | * 4 | * For more info about the MML dialect used here, see 5 | * the [MML reference](mml_ref.md). 6 | */ 7 | #ifndef MML_INPUT_H 8 | #define MML_INPUT_H 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include "input.h" 14 | #include "track.h" 15 | 16 | //! MML (Music Macro Language) parser class 17 | /*! 18 | * For more info about the MML dialect used here, see 19 | * the [MML reference](mml_ref.md). 20 | * 21 | * \todo it should be possible to derive this in order to 22 | * better support different MML dialects. 23 | */ 24 | class MML_Input: public Line_Input 25 | { 26 | public: 27 | typedef std::map Track_Position_Map; 28 | 29 | MML_Input(Song* song); 30 | ~MML_Input(); 31 | 32 | Track_Position_Map get_track_map(); 33 | 34 | private: 35 | // MML read helpers 36 | unsigned int read_duration(); 37 | int read_parameter(int default_parameter); 38 | int expect_parameter(); 39 | int expect_signed(); 40 | int read_note(int c); // c is the first character 41 | void platform_exclusive(); 42 | 43 | // Wrappers that provide error/warning messages 44 | // or other functions 45 | void mml_slur(); 46 | void mml_reverse_rest(int duration); 47 | void mml_grace(); 48 | void mml_transpose(); 49 | void mml_echo(); 50 | void event_relative(Event::Type type, Event::Type subtype); 51 | void conditional_block_begin(); 52 | void conditional_block_end(int c); 53 | 54 | // MML command parsers. Returns 0 and increments position if parsing succeeds. 55 | // the idea is that these can be swapped out for different MML dialects or platforms. 56 | bool mml_basic(); // Notes, length, octave, etc. 57 | bool mml_control(); // Loop control 58 | bool mml_envelope(); // Instrument, volume, envelope etc. 59 | 60 | // Parsers for various parts of the MML file 61 | void parse_mml_track(); 62 | void parse_mml(); 63 | void parse_tag(); 64 | 65 | // Convert track id from character 66 | int get_track_id(); 67 | 68 | // Virtual function override from Line_Input 69 | void parse_line(); 70 | 71 | std::string tag_key; 72 | Track* track; 73 | uint16_t track_id; 74 | uint16_t track_offset; 75 | std::vector track_list; 76 | void (MML_Input::*last_cmd)(); 77 | bool conditional_block; 78 | }; 79 | #endif 80 | 81 | -------------------------------------------------------------------------------- /src/optimizer.h: -------------------------------------------------------------------------------- 1 | /*! \file src/optimizer.h 2 | * \brief Track optimizer 3 | */ 4 | #ifndef OPTIMIZER_H 5 | #define OPTIMIZER_H 6 | #include "core.h" 7 | #include "track.h" 8 | #include 9 | #include 10 | 11 | class Optimizer; 12 | 13 | /*! Track optimizer 14 | */ 15 | class Optimizer 16 | { 17 | public: 18 | struct Stack_Analyzer 19 | { 20 | // returns drum mode status 21 | int analyze_track(Song&, Track& track, Optimizer& optimizer, int drum_mode); 22 | bool parsing = false; // avoid infinite nesting 23 | 24 | int16_t base_usage = 0; // set by calling tracks if subroutine 25 | int16_t max_usage = 0; 26 | std::vector event_list; 27 | }; 28 | 29 | struct Match 30 | { 31 | uint16_t track_id = 0; 32 | uint32_t position = 0; 33 | 34 | //we can only find one loop match anyway 35 | uint32_t loop_position = 0; 36 | uint32_t loop_length = 0; 37 | 38 | uint32_t sub_length = 0; 39 | uint32_t sub_repeats = 0; 40 | int32_t sub_score = 0; 41 | 42 | inline int32_t loop_score() 43 | { 44 | return loop_length - 2; 45 | } 46 | inline int32_t best_score() 47 | { 48 | int32_t score = loop_score(); 49 | return (sub_score > score) ? sub_score : score; 50 | } 51 | // internally used to determine the best score 52 | // 53 | }; 54 | 55 | Optimizer(Song& song, int verbose = 0); 56 | 57 | void optimize(); 58 | void analyze_stack(); 59 | 60 | unsigned int find_match_length(uint32_t src_track, uint32_t src_start, uint32_t dst_track, uint32_t dst_start, unsigned int* loop_length = nullptr); 61 | Match find_match(uint32_t src_track, uint32_t src_start); 62 | void find_best_match(); 63 | void apply_match(); 64 | 65 | void find_subroutines(); 66 | void replace_with_subroutine(std::vector& event_list, uint32_t track_id, uint32_t position, uint32_t length); 67 | void add_event(std::vector& event_list, uint32_t track_id, uint32_t position, Event::Type type, int16_t param, std::shared_ptr& reference); 68 | 69 | void print_progress(int track_id); 70 | 71 | static const int max_stack_depth; 72 | static const int min_sub_score; 73 | static const int min_loop_score; 74 | static const int max_sub_stack; 75 | static const int max_loop_stack; 76 | 77 | int16_t sub_id; 78 | int pass; 79 | 80 | int verbose; 81 | int min_score; 82 | int last_score; 83 | int top_score; 84 | 85 | Song* song; 86 | Match best_match; 87 | std::map stack_analyzer; 88 | }; 89 | 90 | #endif 91 | -------------------------------------------------------------------------------- /src/driver.cpp: -------------------------------------------------------------------------------- 1 | #include "driver.h" 2 | #include "vgm.h" 3 | 4 | #define DEBUG_FM(fmt,...) { } 5 | #define DEBUG_PSG(fmt,...) { } 6 | //#define DEBUG_FM(fmt,...) { printf(fmt, __VA_ARGS__); } 7 | //#define DEBUG_PSG(fmt,...) { printf(fmt, __VA_ARGS__); } 8 | 9 | Driver::Driver(unsigned int rate, VGM_Interface* vgm) 10 | : vgm(vgm) 11 | , delta(0) 12 | , rate(rate) 13 | { 14 | } 15 | 16 | unsigned int Driver::get_rate() 17 | { 18 | return rate; 19 | } 20 | 21 | void Driver::write(uint8_t command, uint16_t port, uint16_t reg, uint16_t data) 22 | { 23 | if(vgm) 24 | vgm->write(command, port, reg, data); 25 | } 26 | 27 | void Driver::set_loop() 28 | { 29 | if(vgm) 30 | vgm->set_loop(); 31 | } 32 | 33 | void Driver::ym2612_w(uint8_t port, uint8_t reg, uint8_t ch, uint8_t op, uint16_t data) 34 | { 35 | if(reg == 0x28) 36 | { 37 | data <<= 4; 38 | data += ch | (port << 2); 39 | DEBUG_FM("opn-keyon port %d reg %02x data %02x (ch %d op %d)\n", port, reg, data, ch, op); 40 | write(0x52, 0, reg, data); 41 | } 42 | else if(reg >= 0x30 && reg < 0xa0) 43 | { 44 | reg += op*4; 45 | reg += ch; 46 | DEBUG_FM("opn-param port %d reg %02x data %02x (ch %d op %d)\n", port, reg, data, ch, op); 47 | write(0x52, port, reg, data); 48 | } 49 | else if(reg >= 0xa0 && reg < 0xb0) // ch3 operator freq 50 | { 51 | reg += ch; 52 | // would use a lookup table in 68k code 53 | if(reg >= 0xa8 && op == 0) 54 | reg += 1; 55 | else if(reg >= 0xa8 && op == 1) 56 | reg += 2; 57 | else if(reg >= 0xa8 && op == 2) 58 | reg += 0; 59 | else if(reg >= 0xa8 && op == 3) 60 | reg = 0xa2; 61 | DEBUG_FM("opn-fnum port %d reg %02x data %04x (ch %d op %d)\n", port, reg, data, ch, op); 62 | write(0x52, port, reg+4, data>>8); 63 | write(0x52, port, reg, data&0xff); 64 | } 65 | else if(reg >= 0xb0) 66 | { 67 | reg += ch; 68 | DEBUG_FM("opn-param port %d reg %02x data %02x (ch %d op %d)\n", port, reg, data, ch, op); 69 | write(0x52, port, reg, data); 70 | } 71 | else 72 | { 73 | DEBUG_FM("opn-param port %d reg %02x data %02x\n", port, reg, data); 74 | write(0x52, 0, reg, data); // port0 only 75 | } 76 | } 77 | 78 | void Driver::sn76489_w(uint8_t reg, uint8_t ch, uint16_t data) 79 | { 80 | uint8_t cmd1, cmd2; 81 | if(reg == 0) // frequency 82 | { 83 | data &= 0x3ff; 84 | cmd1 = (data & 0x0f) | (ch << 5) | 0x80; 85 | cmd2 = data >> 4; 86 | DEBUG_PSG("psg %02x,%02x (ch %d freq %04x)\n", cmd1, cmd2, ch, data); 87 | // don't send second byte if noise 88 | write(0x50, 0, 0, cmd1); 89 | if(ch < 3) 90 | write(0x50, 0, 0, cmd2); 91 | } 92 | else if(reg == 1) // volume 93 | { 94 | cmd1 = (data & 0x0f) | (ch << 5) | 0x90; 95 | DEBUG_PSG("psg %02x (ch %d vol %04x)\n", cmd1, ch, data); 96 | write(0x50, 0, 0, cmd1); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/song.h: -------------------------------------------------------------------------------- 1 | //! \file song.h 2 | #ifndef SONG_H 3 | #define SONG_H 4 | #include 5 | #include 6 | #include 7 | 8 | #include "core.h" 9 | 10 | //! Song class. 11 | /*! 12 | * The song consists of a track map and a tag map. 13 | * 14 | * The tracks represent channels or individual phrases and contain sequence data. Track IDs (keys) are uint16_t. 15 | * 16 | * The tags represent song metadata (for example title and author) as well as envelopes and other platform-specific data 17 | * that cannot be easily represented in a portable way. Tags consists of a string vector and tag map keys are also strings. 18 | * The # prefix is used for song metadata, @ for instruments or envelope data. 19 | * 20 | * The 'cmd_' prefix is special and used for platform-specific events. Use the register_platform_command() and 21 | * get_platform_command() to set and retrieve these tags. 22 | */ 23 | class Song 24 | { 25 | public: 26 | Song(); 27 | virtual ~Song(); 28 | 29 | Tag_Map& get_tag_map(); 30 | void add_tag(const std::string& key, std::string value); 31 | void add_tag_list(const std::string &key, const std::string &value); 32 | void set_tag(const std::string& key, std::string value); 33 | bool check_tag(const std::string& key) const; 34 | Tag& get_tag(const std::string& key); 35 | Tag& get_or_make_tag(const std::string& key); 36 | const std::string& get_tag_front(const std::string& key) const; 37 | std::string get_tag_front_safe(const std::string& key) const; 38 | 39 | int16_t register_platform_command(int16_t param, const std::string& value); 40 | Tag& get_platform_command(int16_t param); 41 | Tag& get_tag_order_list(); 42 | 43 | Track& get_track(uint16_t id); 44 | Track& make_track(uint16_t id); 45 | 46 | Track_Map& get_track_map(); 47 | 48 | uint16_t get_ppqn() const; 49 | void set_ppqn(uint16_t new_ppqn); 50 | 51 | bool set_platform(const std::string& key); 52 | const Platform* get_platform() const; 53 | 54 | private: 55 | Tag_Map tag_map; 56 | Track_Map track_map; 57 | uint16_t ppqn; 58 | int16_t platform_command_index; 59 | 60 | Platform* platform; 61 | }; 62 | 63 | //! Platform base class 64 | /*! 65 | * This contains platform-specific functions that can convert or create objects that work with 66 | * Songs. 67 | */ 68 | class Platform 69 | { 70 | public: 71 | typedef std::vector> Format_List; 72 | 73 | virtual ~Platform() 74 | { 75 | } 76 | 77 | virtual std::shared_ptr get_driver(unsigned int rate, VGM_Interface* vgm_interface) const; 78 | virtual const Format_List& get_export_formats() const; 79 | virtual std::vector get_export_data(Song& song, int format) const; 80 | protected: 81 | virtual std::vector vgm_export(Song& song, unsigned int max_seconds = 3600, unsigned int num_loops = 1) const; 82 | }; 83 | 84 | #endif 85 | 86 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRC = src 2 | OBJ = obj 3 | OBJ_BASE := $(OBJ) 4 | LIBCTRMML = lib/libctrmml 5 | 6 | CFLAGS = -Wall --std=c++14 7 | LDFLAGS = 8 | 9 | ifneq ($(RELEASE),1) 10 | ifeq ($(ASAN),1) 11 | CFLAGS += -fsanitize=address -O1 -fno-omit-frame-pointer 12 | LDFLAGS += -fsanitize=address 13 | endif 14 | ifeq ($(COVERAGE),1) 15 | CFLAGS += --coverage 16 | LDFLAGS += --coverage 17 | OBJ := $(OBJ)/coverage 18 | endif 19 | CFLAGS += -g 20 | LIBCTRMML := $(LIBCTRMML)_debug.a 21 | else 22 | OBJ := $(OBJ)/release 23 | CFLAGS += -O2 -DNDEBUG 24 | LDFLAGS += -s 25 | LIBCTRMML := $(LIBCTRMML).a 26 | endif 27 | 28 | LDFLAGS_TEST = -lcppunit 29 | ifeq ($(OS),Windows_NT) 30 | LDFLAGS += -static-libgcc -static-libstdc++ -Wl,-Bstatic -lstdc++ -lpthread -Wl,-Bdynamic 31 | endif 32 | 33 | CORE_OBJS = \ 34 | $(OBJ)/track.o \ 35 | $(OBJ)/song.o \ 36 | $(OBJ)/input.o \ 37 | $(OBJ)/mml_input.o \ 38 | $(OBJ)/player.o \ 39 | $(OBJ)/stringf.o \ 40 | $(OBJ)/vgm.o \ 41 | $(OBJ)/driver.o \ 42 | $(OBJ)/wave.o \ 43 | $(OBJ)/riff.o \ 44 | $(OBJ)/conf.o \ 45 | $(OBJ)/optimizer.o \ 46 | $(OBJ)/platform/md.o \ 47 | $(OBJ)/platform/mdsdrv.o 48 | 49 | MMLC_OBJS = \ 50 | $(CORE_OBJS) \ 51 | $(OBJ)/mmlc.o 52 | 53 | MDSLINK_OBJS = \ 54 | $(CORE_OBJS) \ 55 | $(OBJ)/platform/mdslink.o 56 | 57 | UNITTEST_OBJS = \ 58 | $(CORE_OBJS) \ 59 | $(OBJ)/unittest/test_track.o \ 60 | $(OBJ)/unittest/test_song.o \ 61 | $(OBJ)/unittest/test_input.o \ 62 | $(OBJ)/unittest/test_mml_input.o \ 63 | $(OBJ)/unittest/test_player.o \ 64 | $(OBJ)/unittest/test_vgm.o \ 65 | $(OBJ)/unittest/test_riff.o \ 66 | $(OBJ)/unittest/test_conf.o \ 67 | $(OBJ)/unittest/test_mdsdrv.o \ 68 | $(OBJ)/unittest/test_misc.o \ 69 | $(OBJ)/unittest/main.o 70 | 71 | SAMPLE_MML = \ 72 | sample/idk.vgm \ 73 | sample/junkers_high.vgm \ 74 | sample/midnight.vgm \ 75 | sample/passport.vgm \ 76 | sample/sand_light.vgm 77 | 78 | $(OBJ)/%.o: $(SRC)/%.cpp 79 | @mkdir -p $(@D) 80 | $(CXX) $(CFLAGS) -MMD -c $< -o $@ 81 | 82 | sample/%.vgm: sample/%.mml mmlc 83 | ./mmlc $< 84 | 85 | all: mmlc mdslink test 86 | 87 | lib: $(LIBCTRMML) 88 | 89 | $(LIBCTRMML): $(CORE_OBJS) 90 | @mkdir -p $(@D) 91 | $(AR) -q $@ $(CORE_OBJS) 92 | 93 | mmlc: $(MMLC_OBJS) 94 | $(CXX) $(MMLC_OBJS) $(LDFLAGS) -o $@ 95 | 96 | mdslink: $(MDSLINK_OBJS) 97 | $(CXX) $(MDSLINK_OBJS) $(LDFLAGS) -o $@ 98 | 99 | unittest: $(UNITTEST_OBJS) 100 | $(CXX) $(UNITTEST_OBJS) $(LDFLAGS) $(LDFLAGS_TEST) -o $@ 101 | 102 | clean: 103 | rm -rf $(OBJ_BASE) 104 | 105 | doc: 106 | doxygen Doxyfile 107 | 108 | sample_mml: mmlc $(SAMPLE_MML) 109 | 110 | cleandoc: 111 | rm -rf doxygen 112 | 113 | test: unittest 114 | ./unittest 115 | 116 | check: test 117 | 118 | .PHONY: all lib test check clean doc cleandoc sample_mml 119 | 120 | -include $(OBJ)/*.d $(OBJ)/unittest/*.d $(OBJ)/platform/*.d 121 | -------------------------------------------------------------------------------- /src/unittest/test_conf.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "../conf.h" 4 | 5 | class Conf_Test : public CppUnit::TestFixture 6 | { 7 | CPPUNIT_TEST_SUITE(Conf_Test); 8 | CPPUNIT_TEST(test_conf_parse1); 9 | CPPUNIT_TEST(test_conf_parse2); 10 | CPPUNIT_TEST(test_conf_parse3); 11 | CPPUNIT_TEST(test_conf_parse4); 12 | CPPUNIT_TEST(test_conf_parse5); 13 | CPPUNIT_TEST_SUITE_END(); 14 | public: 15 | void setUp() 16 | { 17 | } 18 | void tearDown() 19 | { 20 | } 21 | void test_conf_parse1() 22 | { 23 | auto conf = Conf::from_string("test { some stuff }"); 24 | CPPUNIT_ASSERT_EQUAL((int)1, (int)conf.subkeys.size()); 25 | CPPUNIT_ASSERT_EQUAL(std::string("test"), conf.subkeys[0].key); 26 | CPPUNIT_ASSERT_EQUAL((int)2, (int)conf.subkeys[0].subkeys.size()); 27 | CPPUNIT_ASSERT_EQUAL(std::string("some"), conf.subkeys[0].subkeys[0].key); 28 | CPPUNIT_ASSERT_EQUAL(std::string("stuff"), conf.subkeys[0].subkeys[1].key); 29 | } 30 | 31 | void test_conf_parse2() 32 | { 33 | auto conf = Conf::from_string("foo: bar baz"); 34 | CPPUNIT_ASSERT_EQUAL((int)2, (int)conf.subkeys.size()); 35 | CPPUNIT_ASSERT_EQUAL(std::string("foo"), conf.subkeys[0].key); 36 | CPPUNIT_ASSERT_EQUAL((int)1, (int)conf.subkeys[0].subkeys.size()); 37 | CPPUNIT_ASSERT_EQUAL(std::string("bar"), conf.subkeys[0].subkeys[0].key); 38 | CPPUNIT_ASSERT_EQUAL(std::string("baz"), conf.subkeys[1].key); 39 | } 40 | 41 | void test_conf_parse3() 42 | { 43 | auto conf = Conf::from_string("\"escaped string\": \"another escaped string\" \"i love them\""); 44 | CPPUNIT_ASSERT_EQUAL((int)2, (int)conf.subkeys.size()); 45 | CPPUNIT_ASSERT_EQUAL(std::string("escaped string"), conf.subkeys[0].key); 46 | CPPUNIT_ASSERT_EQUAL((int)1, (int)conf.subkeys[0].subkeys.size()); 47 | CPPUNIT_ASSERT_EQUAL(std::string("another escaped string"), conf.subkeys[0].subkeys[0].key); 48 | CPPUNIT_ASSERT_EQUAL(std::string("i love them"), conf.subkeys[1].key); 49 | } 50 | 51 | void test_conf_parse4() 52 | { 53 | // the ':' creates an extra level and i think it's intentional 54 | auto conf = Conf::from_string("\"more testing\": {strange,love}"); 55 | CPPUNIT_ASSERT_EQUAL((int)1, (int)conf.subkeys.size()); 56 | CPPUNIT_ASSERT_EQUAL(std::string("more testing"), conf.subkeys[0].key); 57 | CPPUNIT_ASSERT_EQUAL((int)1, (int)conf.subkeys[0].subkeys.size()); 58 | CPPUNIT_ASSERT_EQUAL((int)2, (int)conf.subkeys[0].subkeys[0].subkeys.size()); 59 | CPPUNIT_ASSERT_EQUAL(std::string("strange"), conf.subkeys[0].subkeys[0].subkeys[0].key); 60 | CPPUNIT_ASSERT_EQUAL(std::string("love"), conf.subkeys[0].subkeys[0].subkeys[1].key); 61 | } 62 | 63 | void test_conf_parse5() 64 | { 65 | auto conf = Conf::from_string("foo bar; baz"); 66 | CPPUNIT_ASSERT_EQUAL((int)2, (int)conf.subkeys.size()); 67 | CPPUNIT_ASSERT_EQUAL(std::string("foo"), conf.subkeys[0].key); 68 | CPPUNIT_ASSERT_EQUAL(std::string("bar"), conf.subkeys[1].key); 69 | } 70 | }; 71 | 72 | CPPUNIT_TEST_SUITE_REGISTRATION(Conf_Test); 73 | 74 | -------------------------------------------------------------------------------- /src/conf.cpp: -------------------------------------------------------------------------------- 1 | #include "conf.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | Conf::Conf() 8 | : subkeys() 9 | , key() 10 | { 11 | } 12 | 13 | Conf::Conf(const std::string& key) 14 | : subkeys() 15 | , key(key) 16 | { 17 | } 18 | 19 | Conf::Conf(const std::string& key, const std::vector& subkeys) 20 | : subkeys(subkeys) 21 | , key(key) 22 | { 23 | } 24 | 25 | //! Parse configuration from string. 26 | /*! 27 | * This function must process one token at a time, and returns after each 28 | * key, or at the end of the string if there are no more tokens. 29 | */ 30 | const char* Conf::parse_token(const char* head) 31 | { 32 | std::string k (""); 33 | bool read_key = false; 34 | while(*head && (*head != '}')) 35 | { 36 | // break sequences 37 | const char* tail = strpbrk(head, " \t\r\n\":,;{}"); 38 | if(!tail) 39 | tail = head+strlen(head); 40 | 41 | if(tail > head) 42 | { 43 | // read tag 44 | if(read_key) 45 | break; 46 | read_key = true; 47 | k = std::string(head, tail-head); 48 | head = tail; 49 | } 50 | else if(*head == '"') 51 | { 52 | // read quote-enclosed tag 53 | if(read_key) 54 | break; 55 | read_key = true; 56 | while(*++head) 57 | { 58 | char c = *head; 59 | if(c == '\\') 60 | { 61 | c = *++head; 62 | if(c == 'n') 63 | k += "\n"; 64 | else if(c == '\t') 65 | k += "\t"; 66 | else if(c != '\r') 67 | k.push_back(c); 68 | continue; 69 | } 70 | else if(*head == '"') 71 | { 72 | head++; 73 | break; 74 | } 75 | k.push_back(c); 76 | } 77 | } 78 | else if(*head == ':') 79 | { 80 | // assign subkey 81 | Conf subkey(k); 82 | head = subkey.parse_token(++head); 83 | subkeys.push_back(subkey); 84 | return head; 85 | } 86 | else if(*head == ',') 87 | { 88 | // end of key (use to write empty keys) 89 | subkeys.push_back(Conf(k)); 90 | return head+1; 91 | } 92 | else if(*head == ';') 93 | { 94 | // comment 95 | while((*head != '\n' && *head != '\r') && *++head); 96 | } 97 | else if(*head == '{') 98 | { 99 | // begin list of subkeys 100 | Conf subkey(k); 101 | ++head; 102 | while(*head && *head != '}') 103 | head = subkey.parse_token(head); 104 | if(*head++ != '}') 105 | throw std::runtime_error("missing }"); 106 | subkeys.push_back(subkey); 107 | return head; 108 | } 109 | else if(*head != '}') 110 | { 111 | // whitespace 112 | // end of list of subkeys 113 | while(*++head && isspace(*head)); 114 | } 115 | } 116 | if(read_key) 117 | subkeys.push_back(Conf(k)); 118 | return head; 119 | } 120 | 121 | //! Create a Conf from a string. 122 | /*! Syntax is as follows: 123 | * 124 | * level0 { 125 | * level1 { level2 level2 level2 } 126 | * level1: level2 127 | * level1 128 | * level1, 129 | * , 130 | * } 131 | * 132 | */ 133 | Conf Conf::from_string(const char* str) 134 | { 135 | Conf conf(""); 136 | while(*str) 137 | str = conf.parse_token(str); 138 | return conf; 139 | } 140 | -------------------------------------------------------------------------------- /src/wave.h: -------------------------------------------------------------------------------- 1 | //! \file wave.h 2 | #ifndef WAVE_H 3 | #define WAVE_H 4 | #include "core.h" 5 | #include 6 | #include 7 | #include 8 | 9 | //! Wave file 10 | class Wave_File 11 | { 12 | friend class Wave_Bank; 13 | public: 14 | Wave_File(uint16_t channels=0, uint32_t rate=0, uint16_t bits=0); 15 | Wave_File(const std::string& filename); 16 | 17 | virtual ~Wave_File(); 18 | 19 | // Methods to modify waveforms 20 | int read(const std::string& filename); 21 | void add_sample(const int16_t* sample, int count); 22 | 23 | //int save(const std::string& filename); 24 | 25 | private: 26 | int load_file(const std::string& filename, uint8_t** buffer, uint32_t* filesize); 27 | uint32_t parse_chunk(const uint8_t* fdata); 28 | 29 | uint16_t channels; 30 | uint16_t stype; 31 | uint16_t sbits; 32 | uint32_t srate; 33 | uint32_t slength; 34 | uint16_t step; 35 | 36 | bool use_smpl_chunk; 37 | int32_t transpose; 38 | uint32_t lstart; 39 | uint32_t lend; 40 | 41 | // data[channel][n] 42 | std::vector> data; 43 | }; 44 | 45 | //! Base wave rom bank 46 | class Wave_Bank 47 | { 48 | public: 49 | //! Aggregate sample header class. 50 | struct Sample 51 | { 52 | uint32_t position; 53 | uint32_t start; 54 | uint32_t size; 55 | uint32_t loop_start; 56 | uint32_t loop_end; 57 | uint32_t rate; 58 | int32_t transpose; 59 | uint32_t flags; 60 | 61 | // aggregate type - no constructor 62 | void from_bytes(std::vector input); 63 | std::vector to_bytes() const; 64 | }; 65 | 66 | Wave_Bank(unsigned long max_size, unsigned long bank_size = 0); 67 | virtual ~Wave_Bank(); 68 | 69 | // Helper methods 70 | void set_include_paths(const Tag& tag); 71 | 72 | // Methods to modify wave ROM memory 73 | unsigned int add_sample(const Tag& tag); 74 | unsigned int add_sample(Sample header, const std::vector& sample); 75 | 76 | // Methods to get wave ROM memory 77 | const std::vector& get_sample_headers(); 78 | const std::vector& get_rom_data(); 79 | unsigned int get_free_bytes(); 80 | unsigned int get_total_gap(); 81 | unsigned int get_largest_gap(); 82 | const std::string& get_error(); 83 | 84 | protected: 85 | struct Gap 86 | { 87 | unsigned long start; 88 | unsigned long end; 89 | }; 90 | 91 | static const uint32_t NO_FIT = (uint32_t)-1; 92 | 93 | unsigned int find_gap(const Sample& header, uint32_t& gap_start) const; 94 | virtual std::vector encode_sample(const std::string& encoding_type, const std::vector& input); 95 | virtual uint32_t fit_sample(const Sample& header, uint32_t start, uint32_t end) const; 96 | virtual int find_duplicate(const Sample& header, const std::vector& sample) const; 97 | 98 | unsigned long max_size; 99 | unsigned long current_size; 100 | unsigned long bank_size; 101 | 102 | Tag include_paths; 103 | std::vector rom_data; 104 | std::vector gaps; 105 | std::vector samples; 106 | std::string error_message; 107 | }; 108 | 109 | #endif 110 | -------------------------------------------------------------------------------- /sample/sand_light.mml: -------------------------------------------------------------------------------- 1 | #title Sand light 2 | #game Cyber Sled 3 | #composer Shinji Hosoe 4 | #composerj 細江 慎治 5 | #programmer ctr 6 | #comment Mucom88 version by ctr 2019-01-28. ctrmml version 2019-05-08 7 | 8 | #platform megadrive 9 | 10 | @1 fm 3 7 ;namco bass 11 | 27 14 0 7 3 34 0 8 0 0 12 | 31 10 0 8 6 53 0 2 0 0 13 | 31 19 0 5 6 15 0 0 0 0 14 | 31 6 0 9 14 0 0 0 0 0 15 | 16 | @2 fm 4 0 ;2op lead 17 | 20 5 0 1 1 9 0 4 7 0 18 | 31 8 4 7 2 0 0 4 0 0 19 | 20 5 0 1 1 9 0 1 7 0 20 | 31 8 4 7 2 127 0 1 0 0 21 | 22 | @3 fm 3 5 ;distgtr2 23 | 18 0 7 1 0 60 0 10 3 0 24 | 25 8 0 7 2 5 0 0 3 0 25 | 20 0 0 1 0 14 0 2 3 0 26 | 29 8 4 7 2 0 0 1 7 0 27 | 28 | @4 fm 3 5 ;distgtr_mute2 29 | 18 0 14 1 0 60 0 10 3 0 30 | 25 0 5 7 0 5 0 0 3 0 31 | 20 0 0 1 0 14 0 2 3 0 32 | 29 16 15 7 1 0 0 2 7 0 33 | 34 | @5 fm 4 0 ;2op piano 35 | 20 0 0 1 0 30 0 4 3 0 36 | 31 8 4 7 2 0 0 4 0 0 37 | 20 0 0 1 0 30 0 4 6 0 38 | 31 8 4 7 2 10 0 4 3 0 39 | 40 | @6 fm 4 0 ;2op piano (poly) 41 | 20 0 0 1 0 30 0 4 3 0 42 | 31 8 4 7 2 0 0 4 0 0 43 | 20 0 0 1 0 30 0 4 3 0 44 | 31 8 4 7 2 0 0 4 0 0 45 | 46 | @10 psg 15>11 / 10>0:15 47 | @11 psg 15>14 13>6:100 / 0 48 | @12 psg 15>12 11>6:20 / 6>0:75 49 | 50 | ; pcm drum mode 51 | @30 pcm "pcm/bd_17k5.wav" 52 | @31 pcm "pcm/sd_17k5.wav" 53 | @32 pcm "pcm/crash_17k5.wav" 54 | @33 pcm "pcm/bd_crash_17k5.wav" 55 | @34 pcm "pcm/esd_17k5.wav" 56 | @36 pcm "pcm/tom2m_17k5.wav" 57 | @37 pcm "pcm/tom2h_17k5.wav" 58 | 59 | *30 @30c ;D30a 60 | *31 @31c ;D30b 61 | *32 @32c ;D30c 62 | *33 @33c ;D30d 63 | *34 @34c ;D30e 64 | *36 @36c ;D30g 65 | *37 @37c ;D30h 66 | 67 | ; psg drum mode 68 | @40 psg 12>0:4 69 | @41 psg 8>0:4 70 | @42 psg 12>10:3 9>6:6 5>0:20 71 | 72 | *40 @40o9b ;D40a 73 | *41 @41o9b ;D40b 74 | *42 @42o9b ;D40c 75 | 76 | ; polyphonic lead 77 | @54 2op 2 5 5 4 4 0 ; n+4 78 | @55 2op 2 4 4 3 3 5 ; n+5 79 | @56 2op 2 7 7 5 5 -4 ; n+6 80 | @57 2op 2 6 6 4 4 0 ; n+7 81 | @58 2op 2 8 8 5 5 -4 ; n+8 82 | 83 | @64 2op 6 5 5 4 4 0 ; n+4 84 | @65 2op 6 4 4 3 3 5 ; n+5 85 | @67 2op 6 6 6 4 4 0 ; n+7 86 | 87 | A t115 88 | ABCDEGHJ r8. 89 | F l16 D30 bee 90 | J 'mode 1' 91 | 92 | ; 5/8 93 | A l16 L @1v14o3 [f+8.f+r4.f+8.f+r8.>>e&f+<d+re&f+<&b | f@4ff@3fr4. f8.fr@4f32f32fr@3d+&f]4 97 | E l1 L p1 @5v6o4 [f+^4f^4]4 98 | GH l16 L @10o5v11k0 {/(2K20r8} [[d+[dddc+&e&c+>Q5d+<Q5g+c8Q5ccQ8c&dc+&g+ | Q5bbrQ8a+&beQ5e32e32Q5fQ5f+c} | l1{a+/c+} | {b/d+<} 109 | F [d^^ab^^^^a^ ^a^b^/a^a^ | d^^^aaaab^a^]2 | d^^ab^^^^l32bbl16bbbbbb 110 | J [cabcbbc^cab cbbab/ac^b | bbaac^ac^aaa]2 | cabcbbc^^l32bbl16bbbbbb 111 | 112 | ; 7/8 113 | A [[[f8rf&d+&f/a+f8>fd+cd+g+gfr8 | fffd+r8 | [&fcd+&ffd+r8]2]2 114 | B l8 [@55f2.r g2.r d2.r c2.r | d2.r g2.r c2.r c2.r]2 115 | C l8 [@58g2.r@57d+2.rd+2.rd2.r | d+2.rd+2.rd+2.r@56d+2.r]2 116 | D @5v7o4 [p2ffrfrrffrfrrcp1cp3ddcl8dp1dp3dc16ccp1cp3cp1c16p3c4 | c(c)ddcl8d(d)dc16cc(c)c(c16)c4 | 6 | #include 7 | 8 | //! Structure for song tags 9 | struct VGM_Tag 10 | { 11 | std::string title; 12 | std::string author; 13 | std::string creator; 14 | std::string date; 15 | std::string system; 16 | std::string notes; 17 | std::string game; 18 | std::string title_j; 19 | std::string author_j; 20 | std::string system_j; 21 | std::string game_j; 22 | }; 23 | 24 | //! Abstract class for sound chip interfaces 25 | class VGM_Interface 26 | { 27 | public: 28 | //! Write a VGM command 29 | virtual void write(uint8_t command, uint16_t port, uint16_t reg, uint16_t data) = 0; 30 | //! Set up a DAC stream 31 | virtual void dac_setup(uint8_t sid, uint8_t chip_id, uint32_t port, uint32_t reg, uint8_t db_id) = 0; 32 | //! Start a DAC stream 33 | virtual void dac_start(uint8_t sid, uint32_t start, uint32_t length, uint32_t freq) = 0; 34 | //! Stop a DAC stream 35 | virtual void dac_stop(uint8_t sid) = 0; 36 | 37 | // TODO: these should be replaced with appropriate functions to enable sound chips / set attributes 38 | //! Set a 32-bit attribute (VGM header) 39 | virtual void poke32(uint32_t offset, uint32_t data) = 0; 40 | //! Set a 16-bit attribute (VGM header) 41 | virtual void poke16(uint32_t offset, uint16_t data) = 0; 42 | //! Set an 8-bit attribute (VGM header) 43 | virtual void poke8(uint32_t offset, uint8_t data) = 0; 44 | 45 | //! Set the loop position. 46 | /*! 47 | * Included to facilitate easier VGM logging. 48 | */ 49 | virtual void set_loop(); 50 | 51 | //! Indicate that playback or logging should be stopped. 52 | virtual void stop(); 53 | 54 | //! Add a datablock 55 | virtual void datablock( 56 | uint8_t dbtype, 57 | uint32_t dbsize, 58 | const uint8_t* db, 59 | uint32_t maxsize, 60 | uint32_t mask = 0xffffffff, 61 | uint32_t flags = 0, 62 | uint32_t offset = 0) = 0; 63 | }; 64 | 65 | //! Writes VGM files. 66 | class VGM_Writer : public VGM_Interface 67 | { 68 | public: 69 | VGM_Writer(const char* filename, int version = 0x61, int header_size = 0x80); 70 | virtual ~VGM_Writer(); 71 | 72 | // Methods to write VGM register events 73 | void write(uint8_t command, uint16_t port, uint16_t reg, uint16_t data) override; 74 | void dac_setup(uint8_t sid, uint8_t chip_id, uint32_t port, uint32_t reg, uint8_t db_id) override; 75 | void dac_start(uint8_t sid, uint32_t start, uint32_t length, uint32_t freq) override; 76 | void dac_stop(uint8_t sid) override; 77 | 78 | // Methods to write VGM meta events 79 | void set_loop(); 80 | void datablock(uint8_t dbtype, 81 | uint32_t dbsize, 82 | const uint8_t* db, 83 | uint32_t maxsize, 84 | uint32_t mask = 0xffffffff, 85 | uint32_t flags = 0, 86 | uint32_t offset = 0) override; 87 | 88 | // Methods to write VGM control events 89 | void delay(double count); 90 | void stop(); 91 | 92 | // Methods to write VGM header 93 | void poke32(uint32_t offset, uint32_t data) override; 94 | void poke16(uint32_t offset, uint16_t data) override; 95 | void poke8(uint32_t offset, uint8_t data) override; 96 | 97 | // Methods to write VGM footer (tag data) 98 | void write_tag(const VGM_Tag& tag = {}); 99 | 100 | // Methods to get VGM state 101 | uint32_t get_position() const; 102 | uint32_t get_sample_count() const; 103 | uint32_t get_loop_sample() const; 104 | uint32_t peek32(uint32_t offset) const; 105 | uint16_t peek16(uint32_t offset) const; 106 | uint8_t peek8(uint32_t offset) const; 107 | 108 | // Methods to get the VGM buffer (instead of writing to file) 109 | std::vector get_buffer(); 110 | 111 | private: 112 | static const uint32_t initial_buffer_alloc = 100000; 113 | 114 | void my_memcpy(void* src, int size); 115 | void add_datablockcmd(uint8_t dtype, uint32_t size, uint32_t romsize, uint32_t offset); 116 | void add_delay(); 117 | void add_gd3(const char* s); 118 | void reserve(uint32_t bytes); 119 | 120 | std::string filename; 121 | bool completed; 122 | uint8_t* buffer; 123 | uint8_t* buffer_pos; 124 | uint32_t buffer_alloc; 125 | double curr_delay; 126 | uint32_t sample_count; 127 | uint32_t loop_sample; 128 | }; 129 | 130 | #endif 131 | -------------------------------------------------------------------------------- /src/input.h: -------------------------------------------------------------------------------- 1 | /*! \file src/input.h 2 | * \brief Input file formats base 3 | * 4 | * Includes base classes for input file formats. 5 | */ 6 | #ifndef INPUT_H 7 | #define INPUT_H 8 | #include 9 | #include 10 | #include 11 | #include "core.h" 12 | 13 | //! Exception class for input file errors 14 | /*! 15 | * This exception is thrown whenever an error occurs while reading 16 | * or parsing an Input file. 17 | * 18 | * It should not be thrown directly, but rather by using the 19 | * Input::parse_error() function. 20 | * 21 | * \see Input::parse_error() 22 | */ 23 | class InputError : public std::exception 24 | { 25 | public: 26 | InputError(std::shared_ptr ref, const char* message); 27 | const char* what(); 28 | inline std::shared_ptr get_reference() { return reference; } 29 | 30 | private: 31 | std::shared_ptr reference; 32 | char buf[200]; 33 | }; 34 | 35 | //! Reference to input data 36 | /*! 37 | * The reference includes the filename, and if applicable, line and 38 | * column numbers as well as the contents of the line. 39 | * 40 | * \todo this class could also be abstracted or extended to 41 | * better support tracker file formats. 42 | */ 43 | class InputRef 44 | { 45 | public: 46 | InputRef(const std::string& filename = "", const std::string& line = "", int line_no = 0, int column = 0); 47 | 48 | const std::string& get_filename() const; 49 | const unsigned int& get_line() const; 50 | const unsigned int& get_column() const; 51 | const std::string& get_line_contents() const; 52 | 53 | private: 54 | std::string filename; 55 | std::string line_contents; 56 | unsigned int line; 57 | unsigned int column; 58 | }; 59 | 60 | std::ostream& operator<<(std::ostream& os, const class InputRef& ref); 61 | 62 | //! Abstract input file format class. 63 | /*! 64 | * The general purpose of this class (and derived) is to convert 65 | * files to Song objects. 66 | * 67 | * A binary file format might inherit directly from the Input class. 68 | * Text-based formats such as MML can use Line_Input that provides 69 | * helper functions for reading text lines. 70 | */ 71 | class Input 72 | { 73 | public: 74 | Input(Song* song); 75 | virtual ~Input(); 76 | 77 | void open_file(const std::string& filename); 78 | static Input& get_input(const std::string& filename); // Get appropriate input type based on the filename 79 | 80 | protected: 81 | Song& get_song(); 82 | const std::string& get_filename(); 83 | virtual std::shared_ptr get_reference(); 84 | 85 | void parse_error(const char* msg); 86 | void parse_warning(const char* msg); 87 | void include_file(const std::string filename); 88 | 89 | //! Used by derived classes to open and parse a file. 90 | virtual void parse_file() = 0; 91 | 92 | private: 93 | Song* song; 94 | std::string filename; 95 | }; 96 | 97 | //! Line buffer interface 98 | /*! 99 | * This provides a stdio-style interface to a buffer as if it was a file. 100 | */ 101 | class Line_Buffer 102 | { 103 | friend class Line_Input_Test; // needs access to internal variables. 104 | 105 | public: 106 | Line_Buffer(std::string line, unsigned int column = -1); 107 | Line_Buffer(const class Line_Buffer& original); 108 | virtual ~Line_Buffer(); 109 | 110 | int get(); 111 | int get_token(); 112 | int get_num(); 113 | std::string get_line(); 114 | void unget(int c = 0); 115 | unsigned long tell(); 116 | void seek(unsigned long pos); 117 | 118 | protected: 119 | std::shared_ptr buffer; // current line used by get/unget functions, etc. 120 | void set_buffer(std::string line, unsigned int new_column = 0); 121 | 122 | unsigned int column; 123 | }; 124 | 125 | //! Abstract class for text line-based input formats (such as MML) 126 | /*! 127 | * Reads the input files, one line at a time, parsing them using 128 | * the virtual function parse_line(). 129 | * 130 | * To help with parsing, a C stdio-style interface to lines of texts 131 | * is provided. (see Line_Buffer) 132 | * 133 | * Because data is stored in an internal buffer and the position 134 | * is kept track of, a call to get_reference() (and thus parse_error()) 135 | * will create an InputRef with the correct line number and column. 136 | */ 137 | class Line_Input: public Input, protected Line_Buffer 138 | { 139 | friend class Line_Input_Test; // needs access to internal variables. 140 | 141 | public: 142 | Line_Input(Song* song); 143 | virtual ~Line_Input(); 144 | 145 | void read_line(const std::string& input_line, int line_number = -1); 146 | 147 | protected: 148 | std::shared_ptr get_reference(); 149 | 150 | //! Used by derived classes to read the input lines. 151 | virtual void parse_line() = 0; 152 | 153 | private: 154 | void parse_file(); 155 | 156 | unsigned int line; 157 | }; 158 | 159 | #endif 160 | 161 | -------------------------------------------------------------------------------- /src/unittest/test_input.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "../input.h" 4 | 5 | class Line_Input_Test : public CppUnit::TestFixture, private Line_Input 6 | { 7 | CPPUNIT_TEST_SUITE(Line_Input_Test); 8 | CPPUNIT_TEST(test_get); 9 | CPPUNIT_TEST(test_get_zero_at_eol); 10 | CPPUNIT_TEST(test_get_token); 11 | CPPUNIT_TEST(test_unget); 12 | CPPUNIT_TEST(test_unget_at_eol); 13 | CPPUNIT_TEST_EXCEPTION(test_unget_too_many, std::out_of_range); 14 | CPPUNIT_TEST(test_get_num); 15 | CPPUNIT_TEST(test_get_num_increment); 16 | CPPUNIT_TEST(test_get_num_multiple); 17 | CPPUNIT_TEST(test_get_num_sign); 18 | CPPUNIT_TEST(test_get_num_hex); 19 | CPPUNIT_TEST(test_get_line); 20 | CPPUNIT_TEST_EXCEPTION(test_get_num_eol, std::invalid_argument); 21 | CPPUNIT_TEST_EXCEPTION(test_get_num_nan, std::invalid_argument); 22 | CPPUNIT_TEST(test_get_num_nan_increment); 23 | CPPUNIT_TEST(test_inputref); 24 | CPPUNIT_TEST_SUITE_END(); 25 | public: 26 | Line_Input_Test() : Line_Input(0) {} 27 | void setUp() 28 | { 29 | set_buffer(""); 30 | line = 0; 31 | column = 0; 32 | } 33 | void tearDown() 34 | { 35 | } 36 | void test_get() 37 | { 38 | set_buffer("hello"); 39 | CPPUNIT_ASSERT_EQUAL((int)'h', get()); 40 | CPPUNIT_ASSERT_EQUAL((int)'e', get()); 41 | CPPUNIT_ASSERT_EQUAL((int)'l', get()); 42 | CPPUNIT_ASSERT_EQUAL((int)'l', get()); 43 | CPPUNIT_ASSERT_EQUAL((int)'o', get()); 44 | } 45 | void test_get_zero_at_eol() 46 | { 47 | set_buffer("hi"); 48 | CPPUNIT_ASSERT_EQUAL((int)'h', get()); 49 | CPPUNIT_ASSERT_EQUAL((int)'i', get()); 50 | CPPUNIT_ASSERT_EQUAL((int)0, get()); 51 | CPPUNIT_ASSERT_EQUAL((int)0, get()); 52 | CPPUNIT_ASSERT_EQUAL((int)0, get()); 53 | } 54 | void test_get_token() 55 | { 56 | set_buffer("\t hello"); 57 | CPPUNIT_ASSERT_EQUAL((int)'h', get_token()); 58 | CPPUNIT_ASSERT_EQUAL((int)'e', get_token()); 59 | CPPUNIT_ASSERT_EQUAL((int)'l', get_token()); 60 | CPPUNIT_ASSERT_EQUAL((int)'l', get_token()); 61 | CPPUNIT_ASSERT_EQUAL((int)'o', get_token()); 62 | } 63 | void test_unget() 64 | { 65 | set_buffer("hello"); 66 | int c = get(); 67 | unget(c); 68 | test_get(); 69 | } 70 | void test_unget_at_eol() 71 | { 72 | set_buffer("hi"); 73 | get(); 74 | get(); 75 | CPPUNIT_ASSERT_EQUAL((int)0, get()); // = 0 76 | unget(); // should not cause extra write 77 | CPPUNIT_ASSERT_EQUAL(std::string("hi"), *buffer); 78 | CPPUNIT_ASSERT_EQUAL((unsigned int)2, column); 79 | } 80 | void test_unget_too_many() 81 | { 82 | set_buffer("hi"); 83 | get(); 84 | unget(); 85 | unget(); // should throw std::out_of_range 86 | } 87 | void test_get_num() 88 | { 89 | set_buffer("123"); 90 | CPPUNIT_ASSERT_EQUAL((int)123, get_num()); 91 | } 92 | void test_get_num_increment() 93 | { 94 | set_buffer("123 456"); 95 | get_num(); 96 | CPPUNIT_ASSERT_EQUAL((unsigned int)3, column); 97 | } 98 | void test_get_num_multiple() 99 | { 100 | set_buffer("123 456 789 1012345 678"); 101 | CPPUNIT_ASSERT_EQUAL((int)123, get_num()); 102 | CPPUNIT_ASSERT_EQUAL((int)456, get_num()); 103 | CPPUNIT_ASSERT_EQUAL((int)789, get_num()); 104 | CPPUNIT_ASSERT_EQUAL((int)1012345, get_num()); 105 | CPPUNIT_ASSERT_EQUAL((int)678, get_num()); 106 | } 107 | void test_get_num_sign() 108 | { 109 | set_buffer("-123 +123 -123 +123"); 110 | CPPUNIT_ASSERT_EQUAL((int)-123, get_num()); 111 | CPPUNIT_ASSERT_EQUAL((int)123, get_num()); 112 | CPPUNIT_ASSERT_EQUAL((int)-123, get_num()); 113 | CPPUNIT_ASSERT_EQUAL((int)123, get_num()); 114 | } 115 | void test_get_num_hex() 116 | { 117 | set_buffer("$12 x2345"); 118 | CPPUNIT_ASSERT_EQUAL((int)0x12, get_num()); 119 | CPPUNIT_ASSERT_EQUAL((int)0x2345, get_num()); 120 | } 121 | void test_get_num_eol() 122 | { 123 | set_buffer("123"); 124 | get_num(); 125 | get_num(); // should throw std::invalid_argument 126 | } 127 | void test_get_num_nan() 128 | { 129 | set_buffer("Skitrumpa"); 130 | get_num(); // should throw std::invalid_argument 131 | } 132 | // check that column is unchanged if exception is thrown 133 | void test_get_num_nan_increment() 134 | { 135 | set_buffer("Skitrumpa"); 136 | try {get_num();} // should throw std::invalid_argument 137 | catch(std::invalid_argument&) 138 | { 139 | } 140 | CPPUNIT_ASSERT_EQUAL((unsigned int)0, column); 141 | } 142 | void test_get_line() 143 | { 144 | set_buffer("0123456789"); 145 | CPPUNIT_ASSERT_EQUAL(std::string("0123456789"), get_line()); 146 | seek(3); 147 | CPPUNIT_ASSERT_EQUAL(std::string("3456789"), get_line()); 148 | } 149 | void parse_line() 150 | { 151 | // Dummy function 152 | } 153 | void test_inputref() 154 | { 155 | set_buffer("Skitrumpa"); 156 | std::shared_ptr ptr = get_reference(); 157 | get(); 158 | get(); // increment column a few times 159 | CPPUNIT_ASSERT_EQUAL((unsigned int)0,ptr->get_column()); 160 | ptr = get_reference(); 161 | CPPUNIT_ASSERT_EQUAL((unsigned int)2,ptr->get_column()); 162 | } 163 | }; 164 | 165 | CPPUNIT_TEST_SUITE_REGISTRATION(Line_Input_Test); 166 | 167 | -------------------------------------------------------------------------------- /sample/midnight.mml: -------------------------------------------------------------------------------- 1 | #title midnight 2019 2 | #composer ctr 3 | #platform megadrive 4 | 5 | ;=== melody === 6 | @1 fm 2 7 ;ians bajsbas 7 | 31 17 0 0 6 11 2 0 0 0 8 | 31 15 0 15 15 11 0 6 2 0 9 | 31 13 0 5 2 38 0 2 4 0 10 | 31 17 0 11 1 0 0 1 0 0 11 | 12 | @4 psg 11>15:3 15>12:20 / 15>0:20 13 | 14 | @9 fm 4 7; FM3 Square + PAD 15 | 31 0 0 0 0 26 0 8 0 0 16 | 20 10 0 15 7 0 0 4 0 0 17 | 31 9 7 1 3 15 2 4 3 0 18 | 10 0 4 7 0 0 0 4 7 0 19 | 20 | @10 fm 4 0; Dual PAD 21 | 31 9 7 1 3 15 2 6 3 0 22 | 10 9 4 7 3 0 0 6 7 0 23 | 31 9 7 1 3 15 2 4 3 0 24 | 10 0 4 7 0 0 0 4 7 0 25 | 26 | @11 fm 2 7 ;Dist square lead 27 | 17 0 0 1 0 30 0 8 3 0 28 | 31 8 4 7 2 11 0 4 7 0 29 | 20 0 0 1 0 42 0 12 3 0 30 | 31 10 4 7 4 0 0 12 0 0 31 | ; 20 0 0 1 0 42 0 8 3 0 ;mult was 12 32 | ; 31 10 4 7 4 0 0 10 0 0 ;mult was 12 33 | 34 | @12 fm 2 7 ;Dist square lead 35 | 17 0 0 1 0 30 0 8 3 0 36 | 31 8 4 7 2 11 0 4 7 0 37 | 20 0 0 1 0 42 0 8 3 0 ;mult was 12 38 | 31 10 4 7 4 0 0 10 0 0 ;mult was 12 39 | 40 | @13 2op 10 8 8 5 5 -4 ; n+6; Dual PAD 41 | 42 | @14 fm 4 7; FM3 Square + PAD 43 | 31 0 0 0 0 26 0 8 0 0 44 | 20 10 0 15 7 4 0 4 0 0 45 | 31 9 7 1 3 13 2 2 3 0 46 | 25 0 4 7 0 0 0 2 7 0 47 | 48 | @15 fm 4 0; Dual PAD2 49 | 31 9 7 1 3 13 2 6 3 0 50 | 25 9 4 7 3 0 0 6 7 0 51 | 31 9 7 1 3 13 2 4 3 0 52 | 25 0 4 7 0 0 0 4 7 0 53 | @16 2op 15 8 8 5 5 -4 ; n+6; Dual PAD2 54 | @17 2op 15 10 10 6 6 -7 ; n+9; Dual PAD2 55 | 56 | ; pcm drum mode 57 | @30 pcm "pcm/bd_17k5.wav" 58 | @31 pcm "pcm/sd_17k5.wav" 59 | @32 pcm "pcm/crash_17k5.wav" 60 | @33 pcm "pcm/bd_crash_17k5.wav" 61 | @34 pcm "pcm/esd_17k5.wav" 62 | @36 pcm "pcm/tom2m_17k5.wav" 63 | @37 pcm "pcm/tom2h_17k5.wav" 64 | 65 | *30 p3@30c ;D30a 66 | *31 p3@31c ;D30b 67 | *32 p3@32c ;D30c 68 | *33 p3@33c ;D30d 69 | *34 p3@34c ;D30e 70 | *36 p1@36c ;D30g 71 | *37 p2@37c ;D30h 72 | 73 | ;=== psg drums === 74 | @40 psg 13>0:4 ; D40a closed hat 75 | *40 @40M0o9b 76 | @41 psg 9>0:4 ; D40b closed hat (muted) 77 | *41 @41M0o9b 78 | @42 psg 13>11:3 10>6:6 5>0:20 ; D40c open hat 79 | *42 @42M0o9b 80 | @43 psg 12>6:3 5>1:15 1>0:20 ; D40d ride 81 | *43 @43M0o9d 82 | @44 psg 12>0:4 ; D40e closed hat (muted2) 83 | *44 @41M0o9b 84 | @M45 0>5:10 5>4:11 85 | @45 psg 14>11:3 15>0:40 86 | *45 @45M45o8g ;D40f Crashing the party 87 | @46 psg 13>11:3 11>0:130 88 | *46 @46M0o8b ;D40g loooong cymbal 89 | @47 psg 0>11:150 / 0 90 | *47 @47M0o8b ;D40h reverse pls 91 | 92 | 93 | 94 | A t128 95 | 96 | ABCDEFGHIJ L 97 | 98 | ;--bass--; 99 | A @1o3v10 100 | ;--lead-- 101 | B p1 @11o2v10 K20 102 | C 'fm3 0011' @9v10o2 r16 103 | E p2 @12o2v9K40 104 | ;--pad-- 105 | I 'fm3 1100' v9o2 106 | D @10 v9o2 107 | ;--drums-- 108 | F D30 109 | J 'mode 1'D40v15 110 | ;--arps-- 111 | G @4o3v10 112 | H @4o3v9K20 113 | 114 | A l16 [ggrggrfgrgfrgb-gf ggrggrfrg8b-8>c8d8< ggrggrfgrgfrgb-gf ggrggrfrg8f8d4]12 115 | F l4 [aaaa]24 [a/bab]24 l16b^a^^ba^b^bb 116 | J l16 [arbbarbr]96 117 | BCE l1 [r]8 118 | DI l1 [r]8 119 | 120 | GH l16 o2g (2r)2 [o3d(2o2g)2 o3f(2o3d)2 o2g(2o3f)2 121 | GH o3d(2o2g)2 o3g(2o3d)2 o2g(2o3g)2 o3d(2o2g)2 122 | GH o3f(2o3d)2 o2g(2o3f)2 o3d(2o2g)2 o3b-(2o3d)2 123 | GH o3d(2o3b-)2 o3f(2o3d)2 o4c(2o3f)2 o3b-(2o4c)2 /o2g(2o3b-)2 ]24 124 | 125 | BCE l8 [g2.>df2./^dfg4ad2.agfgfd4 l4g.d.c.f.dcd.cagfl4g.d.c.f.dcd.d.c.f.df 126 | 127 | BC l4 gr2.[r1]7 df2.dfd {/R16} [l16gddgd/dgdfdcd]2 b-gfgfdc c d8cdfcd8cfdc d8cdfdfg8fgfb-gb- >c8dcdfdfgb-gfgb->c 128 | BC l4 d2.d8f.dc8d.d8c8c.d.c.f.df 129 | E v6l8 [o2p1g>p2dp1fp1dp2gp2dp1fp1dp2b-p1dp2f>p1cc^/ c^]2 c/^ [r]8 ]2 @13d2e2 132 | I l1 [[b-^ >d ^ e^ /e-^]2 e-/^ [r]8 ]2 f2g2 133 | 134 | ; --part2-- 135 | 136 | A l8 o3[gfgf16b-b-g16>ccccd.g4dgc.f4e16d16ec]2 f.b-4a16b-16>cc8d8.c8.f8.d8f8g4b-8g8^4.gb->d4ccdrd rr4e4er]2 143 | I v7l16 [ o4d8.dr8drr2 r8c8r4. c4cr d8.dr8drr2 r8 frf rr4g4gr]2 144 | 145 | F l16 d^^^bh^ha^aab^g^ [^^aab^g^ahh^b^g^/ a^g^bh^ha^aab^g^]4 146 | J l16 [arbb]32 147 | 148 | A l16o3 ggrggrfgrgfrgb-gf ggrggrfrg8b-8>c8d8 < ggrggrfgrgfrgb-gf ggrggrfrg8f8d4 149 | BC l1 grrr 150 | D l4 12 | #include 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | void print_usage(const char* exename) 20 | { 21 | std::cout << "ctrmml Music Compiler, version " CTRMML_VERSION "\n"; 22 | std::cout << "(C) 2019-2022 Ian Karlsson.\n"; 23 | std::cout << "Licensed under GPLv2, see COPYING for details.\n\n"; 24 | std::cout << "Usage: " << exename << " [options] \n"; 25 | std::cout << "Options:\n"; 26 | std::cout << "\t--output / -o : Set output filename\n"; 27 | std::cout << "\t--format / -f : Set output file format\n"; 28 | std::cout << "\t--optimize / -O : Optimize music data (Experimental!)\n"; 29 | } 30 | 31 | std::string get_extension(const char* input_filename) 32 | { 33 | static char str[256]; 34 | char *last_dot; 35 | strncpy(str,input_filename, 256); 36 | last_dot = strrchr(str, '.'); 37 | if(last_dot && *last_dot == '.') 38 | return last_dot + 1; 39 | else 40 | return ""; 41 | } 42 | 43 | std::string output_filename(const char* input_filename, const char* extension) 44 | { 45 | static char str[256]; 46 | char *last_dot; 47 | strncpy(str,input_filename, 256); 48 | last_dot = strrchr(str, '.'); 49 | if(last_dot) 50 | *last_dot = 0; 51 | strncat(last_dot, ".", 256); 52 | strncat(last_dot, extension, 256); 53 | return str; 54 | } 55 | 56 | void validate_song(Song& song) 57 | { 58 | auto validator = Song_Validator(song); 59 | for(auto it = validator.get_track_map().begin(); it != validator.get_track_map().end(); it++) 60 | { 61 | std::cout << stringf("Track%3d:%7d", it->first, it->second.get_play_time()); 62 | if(auto length = it->second.get_loop_length()) 63 | std::cout << stringf(" (loop %7d)", length); 64 | std::cout << "\n"; 65 | } 66 | } 67 | 68 | Song convert_file(const char* filename) 69 | { 70 | Song song; 71 | MML_Input input = MML_Input(&song); 72 | input.open_file(filename); 73 | validate_song(song); 74 | return song; 75 | } 76 | 77 | int main(int argc, char* argv[]) 78 | { 79 | std::string in_filename = ""; 80 | std::string out_filename = ""; 81 | std::string format = ""; 82 | bool optimize = false; 83 | bool verbose = false; 84 | 85 | for(int arg = 1, default_arguments = 0; arg < argc; arg++) 86 | { 87 | if((!strcmp(argv[arg], "-o") || !strcmp(argv[arg], "--output")) && arg < argc) 88 | out_filename = argv[++arg]; 89 | else if((!strcmp(argv[arg], "-f") || !strcmp(argv[arg], "--format")) && arg < argc) 90 | format = argv[++arg]; 91 | else if(!strcmp(argv[arg], "-O") || !strcmp(argv[arg], "--optimize")) 92 | optimize = true; 93 | else if(!strcmp(argv[arg], "-v")) 94 | verbose = true; 95 | else if(!strcmp(argv[arg], "-h") || !strcmp(argv[arg], "--help")) 96 | { 97 | print_usage(argv[0]); 98 | return -1; 99 | } 100 | else if(default_arguments < 1) 101 | { 102 | default_arguments++; 103 | in_filename = argv[arg]; 104 | } 105 | } 106 | 107 | if(!in_filename.size()) 108 | { 109 | print_usage(argv[0]); 110 | std::cerr << "no input specified\n"; 111 | return -1; 112 | } 113 | 114 | try 115 | { 116 | // Parse MML 117 | Song song = convert_file(in_filename.c_str()); 118 | 119 | // Get available formats 120 | unsigned int format_id = 0; 121 | auto format_list = song.get_platform()->get_export_formats(); 122 | 123 | // Find matching format 124 | for(auto&& i : format_list) 125 | { 126 | if(format == "") 127 | format = i.first; 128 | if(iequal(i.first, format)) 129 | break; 130 | format_id ++; 131 | } 132 | 133 | // No available format 134 | if(format_id == format_list.size()) 135 | { 136 | std::cerr << "Format not available!\n"; 137 | if(format_list.size()) 138 | { 139 | std::cerr << "\nAvailable formats:\n"; 140 | for(auto&& i : format_list) 141 | std::cerr << "\t'" << i.first << "': " << i.second << "\n"; 142 | } 143 | return -1; 144 | } 145 | 146 | // Generate output filename if not already specified 147 | if(!out_filename.size()) 148 | out_filename = output_filename(in_filename.c_str(), format.c_str()); 149 | 150 | // Optimize data 151 | if(optimize) 152 | { 153 | Optimizer opt(song, 1 + verbose); 154 | opt.optimize(); 155 | printf("\n"); 156 | } 157 | 158 | // Export data 159 | std::vector bytes = song.get_platform()->get_export_data(song, format_id); 160 | 161 | // Write to file 162 | if(bytes.size()) 163 | { 164 | std::ofstream out(out_filename, std::ios::binary); 165 | out.write((char*)bytes.data(), bytes.size()); 166 | std::cout << "Wrote " << bytes.size() << " bytes to " << out_filename << "\n"; 167 | } 168 | } 169 | catch (InputError& error) 170 | { 171 | std::cerr << error.what() << "\n"; 172 | return -1; 173 | } 174 | #ifdef NDEBUG 175 | catch (std::logic_error& error) // in case get_driver is not found 176 | { 177 | std::cerr << error.what() << "\n"; 178 | return -1; 179 | } 180 | #endif 181 | } 182 | 183 | -------------------------------------------------------------------------------- /sample/idk.mml: -------------------------------------------------------------------------------- 1 | #title Idk 2 | #composer ctr 3 | #date 2019-05-03 4 | #platform megadrive 5 | 6 | @1 fm 3 0 ;bass 7 | 31 0 19 5 0 23 0 0 0 0 8 | 31 6 0 4 3 19 0 0 0 0 9 | 31 15 0 5 4 38 0 4 0 0 10 | 31 27 0 11 1 0 0 1 0 0 11 | 12 | @3 fm 5 5 ;synpad 13 | 21 0 0 3 0 23 0 2 7 0 14 | 28 14 0 6 1 0 0 2 0 0 15 | 17 0 0 15 0 19 0 2 4 0 16 | 31 15 0 15 3 15 0 4 1 0 17 | 18 | @5 psg 15>11:5 / 10>0:20 19 | 20 | ;==================================================================== 21 | ; Drums (drum mode D30) 22 | ;==================================================================== 23 | @30 pcm "pcm/bd_17k5.wav" 24 | *30 @30p3c ; D30a kick 25 | 26 | @31 pcm "pcm/sd_17k5.wav" 27 | *31 @31p3c ; D30b snare 28 | 29 | @32 pcm "pcm/tom2h_17k5.wav" 30 | *32 @32p2c ; D30c tom 31 | 32 | @33 pcm "pcm/tom2l_17k5.wav" 33 | *33 @33p1c ; D30d tom2 34 | 35 | ;==================================================================== 36 | ; PSG drums (drum mode D40) 37 | ;==================================================================== 38 | @40 psg 13>0:4 ; D40a closed hat 39 | *40 @40o9b 40 | @41 psg 9>0:4 ; D40b closed hat (muted) 41 | *41 @41o9b 42 | @42 psg 13>11:3 10>6:6 5>0:20 ; D40c open hat 43 | *42 @42o9b 44 | @43 psg 12>6:3 5>1:15 1>0:20 ; D40d ride 45 | *43 @43o9d 46 | @44 psg 12>0:4 ; D40e closed hat (muted2) 47 | *44 @41o9b 48 | 49 | ;==================================================================== 50 | ; Pitch envelopes 51 | ;==================================================================== 52 | @M1 0:20 V0:0.6:3 53 | @M2 -0.8>0:6 0 54 | 55 | ;==================================================================== 56 | ; Intro 57 | ;==================================================================== 58 | A t140 59 | ABCD @3 v6Q7 {p2)/p3/p1)/p3} 60 | E @1 v12 61 | F v14D30 62 | GH @5 v15 63 | H (2r16K40 64 | J 'mode 1'v15D40 65 | 66 | ;ABCDFGH r1 67 | ;J l4aaa l8aa 68 | 69 | ABCD o4 l4.{c/e/g/b}l4{c/e-/g/b-}{>f ^8.^8. f^8 a ^8.^8. a2 ^ ^8a8 ^2^8 ^^8 e1 79 | C L l1 o4c^>c^< l4c. ^>c8.^8. c ^8^8 c2^8 ^^8 e^< l4e. ^ ^8.e8. d2 ^>d8^8 e2^8 ^e8 c1 81 | E L l16 [o3d^d^ad^>c^ccd c^ca. ^.c.c. ^ 89 | D l4 o4e.e.^ e. ^. ^ e.e.^>e. ^.d.ga1 >c.a. ^.c.c. ^]2 100 | D [[o4e.e.^ e. ^. ^ e.e.^/>e. ^.e8^^d 101 | E [[o3d^d^ad^>c^ccd c^ccd2^8f4e8ega2^8>c4c.c. ^ 109 | C k-2 o4e.e.^ e. ^. ^ e.e.^>e. ^.c^ccd c^cd2./c8d8 113 | H l1 [d2^8c4d8g2./f8e-8]2 f8g8 114 | 115 | A k0 o4c.^.^>c. c. ^e. ^ ]2 116 | B k0 o4e.e.^ e. ^. ^ f.^.^ f. ^.>f ]2 117 | C k0 o4f.^.^ f. ^. ^ a.^.^ a. ^.>a ]2 118 | D k0 o4a.a.^ a. ^. a>c.c.^ c. c. ^ ]2 119 | E l16k0 [o3d^d^ad^>c^ccd c^ca. ^.c.c. ^ k0]2 128 | C [o4c.c.^>c.c. ^ k-2 o4e.e.^ e. ^. ^ e.e.^>e. ^.e. ^.c^ccd c^cd.ga1 >c.c.fg2.f8g8 b-.a.fg1 132 | GH d.ga2.g8.a16 >c.e1 d.c^ccd c^c 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | void print_usage(const char* exename) 15 | { 16 | std::cout << "mdslink - MDSDRV MML Compiler and Linker\n"; 17 | std::cout << "(C) 2019-2022 ian karlsson\n\n"; 18 | std::cout << "Usage: " << exename << " [options] \n\n"; 19 | std::cout << "Options:\n"; 20 | std::cout << "\t-o : Specify output filenames\n"; 21 | std::cout << "\t-i : Specify ASM headers\n"; 22 | std::cout << "\t-h : Specify C headers\n"; 23 | std::cout << "Note:\n"; 24 | std::cout << "\tInput files can be in .mml or .mds format\n\n"; 25 | std::cout << "MDSDRV version " << MDSDRV_SEQ_VERSION_MAJOR << "." << MDSDRV_SEQ_VERSION_MINOR << " "; 26 | std::cout << "(minimum compatible version " << MDSDRV_MIN_SEQ_VERSION_MAJOR << "." << MDSDRV_MIN_SEQ_VERSION_MINOR << ")\n\n"; 27 | } 28 | 29 | std::string get_extension(const std::string& input_filename) 30 | { 31 | auto pos = input_filename.rfind("."); 32 | if(pos != std::string::npos) 33 | return input_filename.substr(pos); 34 | else 35 | return ""; 36 | } 37 | 38 | // isolate filename from the path 39 | std::string get_filename(const std::string& input_filename) 40 | { 41 | auto spos = input_filename.rfind("/"); 42 | #ifdef _WIN32 43 | // handle windows backslash 44 | auto spos2 = input_filename.rfind("\\"); 45 | if((spos2 != std::string::npos && spos2 > spos) || spos == std::string::npos) 46 | spos = spos2; 47 | #endif 48 | auto epos = input_filename.rfind("."); 49 | if(spos != std::string::npos) 50 | return input_filename.substr(spos+1, epos-spos-1); 51 | else 52 | return input_filename.substr(0, epos); 53 | } 54 | 55 | Song convert_file(const char* filename) 56 | { 57 | Song song; 58 | MML_Input input = MML_Input(&song); 59 | input.open_file(filename); 60 | auto validator = Song_Validator(song); 61 | for(auto it = validator.get_track_map().begin(); it != validator.get_track_map().end(); it++) 62 | { 63 | std::cout << stringf("Track%3d:%7d", it->first, it->second.get_play_time()); 64 | if(auto length = it->second.get_loop_length()) 65 | std::cout << stringf(" (loop %7d)", length); 66 | std::cout << "\n"; 67 | } 68 | return song; 69 | } 70 | 71 | int main(int argc, char* argv[]) 72 | { 73 | auto input = std::vector(); 74 | std::string seq_filename = "mdsseq.bin"; 75 | std::string pcm_filename = "mdspcm.bin"; 76 | std::string c_header_filename = ""; 77 | std::string asm_header_filename = ""; 78 | 79 | for(int arg = 1; arg < argc; arg++) 80 | { 81 | if((!strcmp(argv[arg], "-o") || !strcmp(argv[arg], "--output")) && (arg+1) < argc) 82 | { 83 | seq_filename = argv[++arg]; 84 | pcm_filename = argv[++arg]; 85 | } 86 | else if((!strcmp(argv[arg], "-h") || !strcmp(argv[arg], "--c-header")) && arg < argc) 87 | c_header_filename = argv[++arg]; 88 | else if((!strcmp(argv[arg], "-i") || !strcmp(argv[arg], "--asm-header")) && arg < argc) 89 | asm_header_filename = argv[++arg]; 90 | else 91 | input.push_back(argv[arg]); 92 | } 93 | 94 | if(!input.size()) 95 | { 96 | print_usage(argv[0]); 97 | std::cerr << "no input specified\n"; 98 | return -1; 99 | } 100 | 101 | try 102 | { 103 | auto linker = MDSDRV_Linker(); 104 | for(auto it = input.begin(); it != input.end(); it++) 105 | { 106 | auto extension = get_extension(it->c_str()); 107 | RIFF mds = RIFF(0); 108 | printf("[%ld/%ld] %s\n", 1+it-input.begin(), input.size(), it->c_str()); 109 | if(iequal(extension, ".mds")) 110 | { 111 | if (std::ifstream in{*it, std::ios::binary | std::ios::ate}) 112 | { 113 | auto size = in.tellg(); 114 | auto data = std::vector(size, 0); 115 | in.seekg(0); 116 | if(in.read((char*)&data[0], size)) 117 | { 118 | mds = RIFF(data); 119 | } 120 | else 121 | { 122 | throw InputError(nullptr, stringf("Couldn't read %s", it->c_str()).c_str()); 123 | } 124 | } 125 | else 126 | { 127 | throw InputError(nullptr, stringf("Couldn't open %s", it->c_str()).c_str()); 128 | } 129 | } 130 | else 131 | { 132 | auto song = convert_file(it->c_str()); 133 | auto converter = MDSDRV_Converter(song); 134 | mds = converter.get_mds(); 135 | } 136 | // pass to linker 137 | linker.add_song(mds, get_filename(*it)); 138 | } 139 | if(seq_filename.size()) 140 | { 141 | printf("writing %s ...\n", seq_filename.c_str()); 142 | auto bytes = linker.get_seq_data(); 143 | std::ofstream out(seq_filename, std::ios::binary); 144 | out.write((char*)bytes.data(), bytes.size()); 145 | } 146 | if(pcm_filename.size()) 147 | { 148 | printf("writing %s ...\n", pcm_filename.c_str()); 149 | auto bytes = linker.get_pcm_data(); 150 | std::ofstream out(pcm_filename, std::ios::binary); 151 | out.write((char*)bytes.data(), bytes.size()); 152 | std::cout << linker.get_statistics(); 153 | } 154 | if(asm_header_filename.size()) 155 | { 156 | printf("writing %s ...\n", asm_header_filename.c_str()); 157 | auto bytes = linker.get_asm_header(); 158 | std::ofstream out(asm_header_filename); 159 | out.write((char*)bytes.data(), bytes.size()); 160 | } 161 | if(c_header_filename.size()) 162 | { 163 | printf("writing %s ...\n", c_header_filename.c_str()); 164 | auto bytes = linker.get_c_header(); 165 | std::ofstream out(c_header_filename); 166 | out.write((char*)bytes.data(), bytes.size()); 167 | } 168 | return 0; 169 | } 170 | catch (InputError& error) 171 | { 172 | std::cerr << error.what() << "\n"; 173 | return -1; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/riff.cpp: -------------------------------------------------------------------------------- 1 | #include "riff.h" 2 | #include "util.h" 3 | #include 4 | 5 | //! Create new RIFF. 6 | RIFF::RIFF(uint32_t chunk_type) 7 | : type(chunk_type) 8 | , data() 9 | { 10 | if(type == TYPE_RIFF || type == TYPE_LIST) 11 | { 12 | write_be32(data,0,ID_NONE); 13 | } 14 | rewind(); 15 | } 16 | 17 | //! Create new RIFF with predefined data. 18 | RIFF::RIFF(uint32_t chunk_type, const std::vector& initial_data) 19 | : type(chunk_type) 20 | , data() 21 | { 22 | if(type == TYPE_RIFF || type == TYPE_LIST) 23 | { 24 | write_be32(data,0,ID_NONE); 25 | } 26 | data.insert(data.end(),initial_data.begin(),initial_data.end()); 27 | rewind(); 28 | } 29 | 30 | //! Create new RIFF with predefined ID. 31 | RIFF::RIFF(uint32_t chunk_type, uint32_t id) 32 | : type(chunk_type) 33 | , data() 34 | { 35 | write_be32(data,0,id); 36 | rewind(); 37 | } 38 | 39 | //! Create new RIFF with predefined ID and data. 40 | RIFF::RIFF(uint32_t chunk_type, uint32_t id, const std::vector& initial_data) 41 | : type(chunk_type) 42 | , data() 43 | { 44 | write_be32(data,0,id); 45 | data.insert(data.end(),initial_data.begin(),initial_data.end()); 46 | rewind(); 47 | } 48 | 49 | //! Create new RIFF from a chunk 50 | /*! 51 | * \exception std::out_of_range too small. 52 | */ 53 | RIFF::RIFF(const std::vector& initial_data) 54 | : type() 55 | , data() 56 | { 57 | if(initial_data.size() < 8) 58 | throw std::out_of_range("RIFF::RIFF"); 59 | type = read_be32(initial_data,0); 60 | uint32_t size = read_le32(initial_data,4); 61 | uint32_t actual_size = initial_data.size() - 8; 62 | if(size > actual_size) // Handle incorrect data size 63 | size = actual_size; 64 | data.insert(data.end(),initial_data.begin()+8,initial_data.begin()+8+size); 65 | rewind(); 66 | } 67 | 68 | //! Rewinds the chunk counter. 69 | void RIFF::rewind() 70 | { 71 | if(type == TYPE_RIFF || type == TYPE_LIST) 72 | position = 4; 73 | else 74 | position = 0; 75 | } 76 | 77 | //! Returns true if the chunk counter points at the end. 78 | bool RIFF::at_end() const 79 | { 80 | // at the end of an even or uneven sized chunk 81 | if(position >= data.size()) 82 | return true; 83 | // at the end of an even sized list with an alignment byte 84 | // added but counted into the size (shouldn't normally happen...) 85 | else if((position & 1) && position >= data.size() - 1) 86 | return true; 87 | else 88 | return false; 89 | } 90 | 91 | //! Get the RIFF type. 92 | uint32_t RIFF::get_type() const 93 | { 94 | return type; 95 | } 96 | 97 | //! Set the ID (RIFF or LIST type only). 98 | /*! 99 | * \exception std::invalid_argument if not applicable to this RIFF type. 100 | */ 101 | void RIFF::set_id(uint32_t id) 102 | { 103 | if(type == TYPE_RIFF || type == TYPE_LIST) 104 | return write_be32(data,0,id); 105 | else 106 | throw std::invalid_argument("RIFF::set_id()"); 107 | } 108 | 109 | //! Get the ID (RIFF or LIST type only). 110 | /*! 111 | * \exception std::invalid_argument if not applicable to this RIFF type. 112 | */ 113 | uint32_t RIFF::get_id() const 114 | { 115 | if(type == TYPE_RIFF || type == TYPE_LIST) 116 | return read_be32(data,0); 117 | else 118 | throw std::invalid_argument("RIFF::get_id()"); 119 | } 120 | 121 | //! Add a chunk (RIFF or LIST type only). 122 | /*! 123 | * \exception std::invalid_argument if not applicable to this RIFF type. 124 | */ 125 | void RIFF::add_chunk(const class RIFF& new_chunk) 126 | { 127 | // If data size is uneven, insert an aligment byte. 128 | if(data.size() & 1) 129 | data.push_back(0); 130 | 131 | if(type == TYPE_RIFF || type == TYPE_LIST) 132 | { 133 | auto new_data = new_chunk.data; 134 | write_be32(data,data.size(),new_chunk.get_type()); 135 | write_le32(data,data.size(),new_data.size()); 136 | data.insert(data.end(),new_data.begin(),new_data.end()); 137 | } 138 | else 139 | { 140 | throw std::invalid_argument("RIFF::add_chunk()"); 141 | } 142 | } 143 | 144 | //! Add data. (non-RIFF or LIST types only) 145 | /*! 146 | * \exception std::invalid_argument if not applicable to this RIFF type. 147 | */ 148 | void RIFF::add_data(std::vector& new_data) 149 | { 150 | if(type == TYPE_RIFF || type == TYPE_LIST) 151 | { 152 | throw std::invalid_argument("RIFF::add_data()"); 153 | } 154 | else 155 | { 156 | data.insert(data.end(),new_data.begin(),new_data.end()); 157 | } 158 | } 159 | 160 | //! Get chunk data. 161 | /*! Returns a non-const reference to the internal data vector, 162 | * means that the data can be modified, but you should not make a 163 | * pointer of this data or keep the reference for a long time as 164 | * it can easily be corrupted by resizing the data vector (e.g. by 165 | * add_chunk() or add_data()) 166 | */ 167 | std::vector& RIFF::get_data() 168 | { 169 | return data; 170 | } 171 | 172 | //! Return a vector containing the next chunk and increments the position. 173 | std::vector RIFF::get_chunk() 174 | { 175 | // has to be a list type chunk. 176 | if(type != TYPE_RIFF && type != TYPE_LIST) 177 | throw std::invalid_argument("RIFF::get_chunk()"); 178 | 179 | // If data size is uneven, insert an aligment byte. 180 | if(position & 1) 181 | position++; 182 | 183 | std::vector chunk_data; 184 | write_le32(chunk_data,0,read_le32(data,position)); // could be optimized 185 | position += 4; 186 | uint32_t size = read_le32(data,position); 187 | write_be32(chunk_data,4,size); 188 | position += 4; 189 | if(position+size > data.size()) // Handle incorrect data size 190 | size = data.size() - position; 191 | chunk_data.insert(chunk_data.end(),data.begin()+position,data.begin()+position+size); 192 | position += size; 193 | 194 | return chunk_data; 195 | } 196 | 197 | //! Convert the RIFF object to a byte vector. 198 | std::vector RIFF::to_bytes() const 199 | { 200 | std::vector chunk_data; 201 | write_be32(chunk_data,0,type); 202 | write_le32(chunk_data,4,data.size()); 203 | chunk_data.insert(chunk_data.end(),data.begin(),data.end()); 204 | // Add alignment byte if needed. 205 | if(data.size() & 1) 206 | chunk_data.push_back(0); 207 | return chunk_data; 208 | } 209 | -------------------------------------------------------------------------------- /sample/junkers_high.mml: -------------------------------------------------------------------------------- 1 | #title Hot Fire (Junker's High Edit) / Title 2 | #game OutRun 2019 3 | #composer Shigeki Sako / ctr 4 | #programmer ctr 5 | #date 2019-05-17~18 6 | 7 | #platform megadrive 8 | #option noextpitch 9 | 10 | ;==== Instrument section ===== 11 | @3 fm 3 5 ;distgtr2 12 | 18 0 7 1 0 60 0 10 3 0 13 | 25 8 0 7 2 5 0 0 3 0 14 | 20 0 0 1 0 14 0 2 3 0 15 | 29 8 4 7 2 0 0 1 7 0 16 | 17 | @4 fm 3 5 ;distgtr_mute2 18 | 18 0 14 1 0 60 0 10 3 0 19 | 25 0 5 7 0 5 0 0 3 0 20 | 20 0 0 1 0 14 0 2 3 0 21 | 29 16 15 7 1 0 0 2 7 0 22 | 23 | @5 fm 4 0 ;2op piano 24 | 20 0 0 1 0 30 0 4 3 0 25 | 31 8 4 7 2 0 0 4 0 0 26 | 20 0 0 1 0 30 0 4 6 0 27 | 31 8 4 7 2 10 0 4 3 0 28 | 29 | @7 fm 3 0 ;bass 30 | 31 0 19 5 0 23 0 0 0 0 31 | 31 6 0 4 3 19 0 0 0 0 32 | 31 15 0 5 4 38 0 4 0 0 33 | 31 27 0 11 1 0 0 1 0 0 34 | 35 | @8 fm 5 5 ;synpad 36 | 21 0 0 3 0 23 0 2 7 0 37 | 28 14 0 6 1 0 0 2 0 0 38 | 17 0 0 15 0 19 0 2 4 0 39 | 31 15 0 15 3 15 0 4 1 0 40 | 41 | @9 fm 5 7 ;trumpet 42 | 22 0 0 0 0 25 0 2 1 0 43 | 31 16 0 8 2 0 0 4 0 0 44 | 31 14 0 8 2 0 0 2 7 0 45 | 31 16 0 13 2 0 0 2 3 0 46 | 47 | @10 psg 15>11:5 / 10>0:20 48 | @11 psg 15>12:7 / 12>0:20 49 | 50 | @30 pcm "pcm/bd_17k5.wav" 51 | *30 @30p3c ; D30a kick 52 | 53 | @31 pcm "pcm/sd_17k5.wav" 54 | *31 @31p3c ; D30b snare 55 | 56 | @32 pcm "pcm/tom2h_17k5.wav" 57 | *32 @32p2c ; D30c tom 58 | *33 @32p3c ; D30d tom 59 | *34 @32p1c ; D30e tom 60 | 61 | @33 pcm "pcm/tom2l_17k5.wav" 62 | *35 @33p1c ; D30f tom2 63 | 64 | @40 psg 13>0:4 ; D40a closed hat 65 | *40 @40o9b 66 | @41 psg 9>0:4 ; D40b closed hat (muted) 67 | *41 @41o9b 68 | @42 psg 13>11:3 10>6:6 5>0:20 ; D40c open hat 69 | *42 @42o9b 70 | @43 psg 12>6:3 5>1:15 1>0:20 ; D40d ride 71 | *43 @43o9d 72 | @44 psg 12>0:4 ; D40e closed hat (muted2) 73 | *44 @41o9b 74 | 75 | @M1 -0.4>0:2 0:10 V0:0.8:3 76 | @M2 0:10 V0:2:3 77 | 78 | @M5 -2>0:5 0 79 | @M6 12>0:10 80 | @M7 0:10 0>-12:20 81 | 82 | ;==== Subroutines === 83 | *100 l16o4 [aga>c8ce-dcdcc8ce-dcM5g8M0ed+d< aga>c8ce-dcd8ce8.d8.c8cccc[@3c@4cc@3c@4cc@3c@4c]2 [@3d@4dd@3d@4dd@3d@4d]2 @3e@4ee@3e@4ee@3e@4e c2.cde8 d8.cd8 /edcd8cc8eeddccc8eaeaeaea< p3ff>cfcfcfcf< p3gg>dgdgdgdg< p3gg>cgcgcgcg<]2 119 | F l16 [a^b^a^b^a^b/a^ab^]8 abbbb 120 | H [r1]8 121 | J l16 [abc^abc^abc^/bbc^]8 bbbb 122 | 123 | A l8 o3 [ffff gggg aaaa aa/gg]2 aa 124 | BC l16 o4 @3f@4ff@3f@4ff@3f@4f @3g@4gg@3g@4gg@3g@4g [@3a@4aa@3a@4aa@3a@4a]2 >@3c@4cc@3c@4cc@3c@4c <@3b@4bb@3b@4bb@3b@4b [@3a@4a/a@3a@4aa@3a@4a]2 @3g@4g@3a8 r8 125 | DE @9v11M1 {/(3K10r8} l16 o4 a4.ab>c8c8c4.cde8dc8.c2.} r4 126 | G l8 @11v15o3 [rfrfrgrg aaaa g8. a16r4]2 127 | H l8 @11v15o3 [rararbrb>ccccc16r4]2 128 | F l16 [a^b^a^b^a^b/a^ab^]4 acdeb 129 | J l16 [abc^abc^abc^/bbc^]4 bbbb 130 | 131 | A o3 l8 a.g.a4>de-d c.de-d16e-16 c.de-d c.cc.c.cdr/ced (2r8} 150 | F l8 [a.a.aa.a.aa.a.ab4/r4] d16c8. 151 | J l16 [[abcb]6 abbbabbb]2 152 | 153 | A l16 [[a8>aa<]4 [a-8>a-a-<]4 [g8>gg<]4 [f+8>f+f+<]4 [f8>ff<]4 [g8>gg<]4 [a8>aa<]4 a8r2.^8]2 154 | BG l1 o4 ee dd cd e {/e8r2.^8)2} 155 | C l1 o4 aa-gf+ fg a a8r2.^8 156 | H l1 o4 ccc c8r2.^8 157 | DE l8 o4 a2>e.d.ge2 r4.d16c16 d2d.c.c16cd2c16ce.d.ge2 r4.e16g16 a2>c.cd2g2a16g16a8^2. / l16 (2 >edcdccc c8r2.^8 168 | 169 | ABCDEFGHJ ]2 170 | DE l4o5defg 171 | 172 | ;===== part 4 ==== 173 | A l16 [[a8>aa<]4 [a-8>a-a-<]4 [g8>gg<]4 [f+8>f+f+<]4 [f8>ff<]4 [g8>gg<]4 [a8>aa<]4 / c8>cc< d8>dd< f8>ff< a-8>a-a-< ]2 a8r2.^8 174 | B l16 o4 [aga>c8ce-dcdcc8ce-dcM5g8M0ed+d< aga>c8ce-dcd8c / e8.d8.c8c8@4cc <@3b8@4bb @3a8@4aa @3a-8@4a-a- @3 *100 175 | DE l8 l8a2a.e.a a-2a-.e.a- g2^.a.g f+2.d8e8 f2^.e.f g2^.e.g a16g16a8^2. l4 >cc/d/e2 l8e.d.c l16dccc / l4cc8r2.^8 180 | 181 | F l16 [[a^b^a^b^a^b/a^ab^]4 abbbb]3 [a^b^a^b^a^ba^a/b^]3 bb edcfdcfdcfdcedbb 182 | J l16 [[abc^abc^abc^/bbc^]4 bbbb]3 [abc^abc^abc^bbc^]3 r1 183 | 184 | A @7v10p3 185 | BC @3v10p3 {/(3p1r8K20} 186 | DE @8v9p3 {/(1p2} 187 | G @10v14M0 188 | 189 | ;===== repris ==== 190 | A l8 o3 [aaaaaaaa aaaaaaaa aaaaaaaa f.f.fg4e4]2 191 | BC l16 o4 *100 192 | G l8 o4 [rcccc.c.c c.c.cc.c.c rcccc.c.c c.c.c]2 193 | D l8 o4 [raaaa.a.a a.a.aa.a.a raaaa.a.a a.a.ag4a-4]2 194 | E l8 o5 [reeee-.e-.e- e.e.e-e.e.e- reeee-.e-.e- f.f.fd4e4]2 195 | H [r1]8 196 | F l16 [[a^b^a^b^a^ba^ab^]3 c8.d8.e8f8bbbbbb]2 197 | J l16 [[abc^abc^abc^bbc^]3 d8.d8.d8a8bbbbbb]2 198 | 199 | ;===== outro ==== 200 | A f.f.fg4e4 201 | BC l16o5 e8.d8.c8e} l8 {c/a/e} 212 | F fedcedcedcedcedc b2 213 | -------------------------------------------------------------------------------- /src/player.h: -------------------------------------------------------------------------------- 1 | /*! \file src/player.h 2 | * \brief Track player class 3 | * 4 | * Includes base classes for the Track player. 5 | * 6 | * \see Basic_Player 7 | */ 8 | #ifndef PLAYER_H 9 | #define PLAYER_H 10 | #include "core.h" 11 | #include "track.h" 12 | #include 13 | #include 14 | 15 | //! Player stack frame. 16 | struct Player_Stack 17 | { 18 | //! Defines the type of stack frame. 19 | enum Type { 20 | LOOP = 0, 21 | JUMP = 1, 22 | DRUM_MODE = 2, 23 | MAX_STACK_TYPE = 3 24 | } type; 25 | //! Referenced track. 26 | Track* track; 27 | //! Event position 28 | int position; 29 | //! If \ref LOOP, points to the end position of the loop. 30 | //! May not be filled in until the loop has iterated once. 31 | int end_position; 32 | //! If \ref LOOP, remaining loop count. 33 | int loop_count; 34 | }; 35 | 36 | //! Abstract basic track player. 37 | /*! 38 | * The player class is used to iterate Track events, handling basic 39 | * track events such as looping and jumping to subroutines. 40 | * 41 | * All events are forwarded to the derived classes with the 42 | * event_hook(), loop_hook() and end_hook() virtual functions. 43 | * 44 | * At the end of the track, when an Event::END is encountered, 45 | * loop_hook() is called. Depending on the return value, the track 46 | * is stopped and end_hook() is called. 47 | * 48 | * Typical usage of Basic_Player is to call step_event() until 49 | * is_enabled() returns False. 50 | * 51 | * \see Player 52 | */ 53 | class Basic_Player 54 | { 55 | friend Player; // needed to access position 56 | friend class Player_Test; 57 | 58 | public: 59 | Basic_Player(Song& song, Track& track); 60 | virtual ~Basic_Player(); 61 | 62 | void step_event(); 63 | void reset_loop_count(); 64 | 65 | bool is_enabled() const; 66 | bool is_inside_loop() const; 67 | bool is_inside_jump() const; 68 | unsigned int get_play_time() const; 69 | unsigned int get_loop_play_time() const; 70 | int get_loop_count() const; 71 | const Event& get_event() const; 72 | 73 | virtual std::vector> get_references(); 74 | 75 | protected: 76 | void disable(); 77 | void stack_push(const Player_Stack& stack_obj); 78 | Player_Stack& stack_top(Player_Stack::Type type); 79 | Player_Stack stack_pop(Player_Stack::Type type); 80 | Player_Stack::Type get_stack_type(); 81 | unsigned int get_stack_depth(Player_Stack::Type type); 82 | 83 | Song* get_song(); 84 | 85 | void error(const char* message) const; 86 | 87 | //! Called at every event. 88 | virtual void event_hook() = 0; 89 | //! Called at the loop position. Return 1 to continue loop, 0 to end playback (end_hook will be called) 90 | virtual bool loop_hook() = 0; 91 | //! Called at the end position. 92 | virtual void end_hook() = 0; 93 | 94 | //! Current event. 95 | Event event; 96 | //! Pointer to current event in the track. 97 | Event *track_event; 98 | //! Current reference 99 | std::shared_ptr reference; 100 | //! Playing time 101 | unsigned int play_time; 102 | //! Playing time at loop point 103 | int loop_play_time; 104 | //! Keyon time from current event 105 | unsigned int on_time; 106 | //! Keyoff time from current event 107 | unsigned int off_time; 108 | 109 | private: 110 | void stack_underflow(int type); 111 | 112 | Song* song; 113 | Track* track; 114 | bool enabled; 115 | int position; 116 | int loop_position; 117 | int loop_reset_position; // Position to increment the loop count 118 | int loop_count; 119 | int loop_reset_count; 120 | std::stack stack; 121 | unsigned int stack_depth[Player_Stack::MAX_STACK_TYPE]; 122 | unsigned int max_stack_depth; 123 | // # of loops in the stack where the loop count is 0. 124 | unsigned int loop_begin_depth; 125 | }; 126 | 127 | //! Generic track player. 128 | /*! 129 | * This handles the channel events using an internal track state 130 | * array. Drum mode events are also handled here. 131 | * 132 | * The event is then passed to write_event(). 133 | * 134 | * This player also keeps track of note and rest durations (in ticks) 135 | * and so if play_tick() is called at a regular interval, playback 136 | * of multiple tracks can be synchronized. 137 | * 138 | * skip_ticks() can be used to skip events. Because 139 | * events are not sent to write_event() during this time, the derived 140 | * write_event() should mainly check for Event::NOTE and Event::REST types 141 | * (other Event types in special cases) and get the state of the channel 142 | * variables using the \c get_flag and \c get_var (and platform event 143 | * equivalents) respectively. 144 | * 145 | * The Player class is intended for actual playback. For conversion, 146 | * directly inheriting Basic_Player might be a better idea. 147 | * 148 | * \see Basic_Player 149 | */ 150 | class Player : public Basic_Player 151 | { 152 | friend class Player_Test; 153 | 154 | public: 155 | Player(Song& song, Track& track); 156 | virtual ~Player(); 157 | 158 | void skip_ticks(unsigned int ticks); 159 | void play_tick(); 160 | 161 | bool coarse_volume_flag() const; 162 | bool bpm_flag() const; 163 | int16_t get_platform_var(int type) const; 164 | int16_t get_var(Event::Type type) const; 165 | 166 | void set_var(Event::Type type, int16_t val); 167 | void set_coarse_volume_flag(bool state); 168 | 169 | void platform_update(const Tag& tag); 170 | 171 | protected: 172 | bool get_platform_flag(unsigned int type) const; 173 | void clear_platform_flag(unsigned int type); 174 | bool get_update_flag(Event::Type type) const; 175 | void set_update_flag(Event::Type type); 176 | void clear_update_flag(Event::Type type); 177 | int16_t get_last_note() const; 178 | virtual uint32_t parse_platform_event(const Tag& tag, int16_t* platform_state); 179 | virtual void write_event(); 180 | 181 | private: 182 | void handle_drum_mode(); 183 | void handle_event(); 184 | virtual void event_hook() override; 185 | virtual bool loop_hook() override; 186 | virtual void end_hook() override; 187 | 188 | int16_t last_note; 189 | bool skip_flag; 190 | int note_count; 191 | int rest_count; 192 | int16_t platform_state[Event::CHANNEL_CMD_COUNT]; 193 | uint32_t platform_update_mask; 194 | int16_t track_state[Event::CHANNEL_CMD_COUNT]; 195 | uint32_t track_update_mask; 196 | 197 | }; 198 | 199 | //! Track validator 200 | /*! 201 | * The track validator is used to perform a "sanity check" of a Track, 202 | * detecting playback errors while also calculating the play and loop 203 | * duration. 204 | */ 205 | class Track_Validator : public Basic_Player 206 | { 207 | public: 208 | Track_Validator(Song& song, Track& track); 209 | 210 | unsigned int get_loop_length() const; 211 | 212 | private: 213 | void event_hook() override; 214 | bool loop_hook() override; 215 | void end_hook() override; 216 | 217 | unsigned int loop_time; 218 | }; 219 | 220 | //! Song validator 221 | /*! 222 | * Validates all tracks in a song using Track_Validator. 223 | */ 224 | class Song_Validator 225 | { 226 | public: 227 | Song_Validator(Song& song); 228 | 229 | const std::map& get_track_map() const; 230 | 231 | private: 232 | std::map track_map; 233 | }; 234 | 235 | #endif 236 | -------------------------------------------------------------------------------- /src/unittest/test_riff.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "../riff.h" 4 | 5 | class RIFF_Test : public CppUnit::TestFixture 6 | { 7 | CPPUNIT_TEST_SUITE(RIFF_Test); 8 | CPPUNIT_TEST(test_riff_initialize); 9 | CPPUNIT_TEST(test_riff_add_data); 10 | CPPUNIT_TEST(test_riff_add_chunk); 11 | CPPUNIT_TEST(test_riff_add_chunk_alignment); 12 | CPPUNIT_TEST(test_riff_encode_decode); 13 | CPPUNIT_TEST_SUITE_END(); 14 | public: 15 | void setUp() 16 | { 17 | } 18 | void tearDown() 19 | { 20 | } 21 | void test_riff_initialize() 22 | { 23 | // empty RIFF chunk 24 | { 25 | std::vector data_block = { 26 | 'R','I','F','F',4,0,0,0,' ',' ',' ',' ' 27 | }; 28 | auto riff = RIFF(RIFF::TYPE_RIFF); 29 | CPPUNIT_ASSERT_MESSAGE("RIFF(chunk_type)", riff.to_bytes() == data_block); 30 | } 31 | // empty RIFF chunk with ID 32 | { 33 | std::vector data_block = { 34 | 'R','I','F','F',4,0,0,0,'R','I','F','F' 35 | }; 36 | auto riff = RIFF(RIFF::TYPE_RIFF, RIFF::TYPE_RIFF); 37 | CPPUNIT_ASSERT_MESSAGE("RIFF(chunk_type,id)", riff.to_bytes() == data_block); 38 | } 39 | // empty RIFF chunk with predefined data 40 | { 41 | std::vector initial_data = { 42 | 65,66,67,68,4,0,0,0,69,70,71,72 43 | }; 44 | std::vector data_block = { 45 | 'R','I','F','F',16,0,0,0,' ',' ',' ',' ',65,66,67,68,4,0,0,0,69,70,71,72 46 | }; 47 | auto riff = RIFF(RIFF::TYPE_RIFF, initial_data); 48 | CPPUNIT_ASSERT_MESSAGE("RIFF(chunk_type,initial_data)", riff.to_bytes() == data_block); 49 | } 50 | // empty RIFF chunk with ID and predefined data 51 | { 52 | std::vector initial_data = { 53 | 65,66,67,68,4,0,0,0,69,70,71,72 54 | }; 55 | std::vector data_block = { 56 | 'L','I','S','T',16,0,0,0,'R','I','F','F',65,66,67,68,4,0,0,0,69,70,71,72 57 | }; 58 | auto riff = RIFF(RIFF::TYPE_LIST, RIFF::TYPE_RIFF, initial_data); 59 | CPPUNIT_ASSERT_MESSAGE("RIFF(chunk_type,id,initial_data)", riff.to_bytes() == data_block); 60 | } 61 | // empty non-RIFF chunk 62 | { 63 | std::vector data_block = { 64 | 65,66,67,68,0,0,0,0 65 | }; 66 | auto riff = RIFF(0x41424344); 67 | CPPUNIT_ASSERT_MESSAGE("RIFF(0x41424344)", riff.to_bytes() == data_block); 68 | } 69 | // empty non-RIFF chunk with predefined data 70 | { 71 | std::vector initial_data = { 72 | 65,66,67,68,4,0,0,0,69,70,71 73 | }; 74 | std::vector data_block = { 75 | 65,66,67,68,11,0,0,0,65,66,67,68,4,0,0,0,69,70,71,0 76 | }; 77 | auto riff = RIFF(0x41424344, initial_data); 78 | CPPUNIT_ASSERT_MESSAGE("RIFF(chunk_type,initial_data)", riff.to_bytes() == data_block); 79 | } 80 | } 81 | void test_riff_add_data() 82 | { 83 | std::vector added_data = { 84 | 65,66,67,68,4,0,0,0,69,70,71,72 85 | }; 86 | std::vector data_block = { 87 | 65,66,67,68,12,0,0,0,65,66,67,68,4,0,0,0,69,70,71,72 88 | }; 89 | auto riff = RIFF(0x41424344); 90 | riff.add_data(added_data); 91 | CPPUNIT_ASSERT_MESSAGE("RIFF(chunk_type,initial_data)", riff.to_bytes() == data_block); 92 | auto riff2 = RIFF(RIFF::TYPE_RIFF); 93 | CPPUNIT_ASSERT_THROW(riff2.add_data(added_data), std::invalid_argument); 94 | } 95 | void test_riff_add_chunk() 96 | { 97 | std::vector data_block = { 98 | 'R','I','F','F',16,0,0,0,' ',' ',' ',' ',65,66,67,68,4,0,0,0,69,70,71,72 99 | }; 100 | auto riff = RIFF(RIFF::TYPE_RIFF); 101 | auto new_chunk = RIFF(0x41424344,0x45464748); 102 | riff.add_chunk(new_chunk); 103 | CPPUNIT_ASSERT_MESSAGE("RIFF(chunk_type,initial_data)", riff.to_bytes() == data_block); 104 | auto riff2 = RIFF(0x41424344); 105 | CPPUNIT_ASSERT_THROW(riff2.add_chunk(new_chunk), std::invalid_argument); 106 | } 107 | void test_riff_add_chunk_alignment() 108 | { 109 | std::vector added_data = { 110 | 1,2,3 111 | }; 112 | // it's not clear if the size of the RIFF chunk should include the padding byte 113 | // but that is what I assume. 114 | std::vector data_block = { 115 | 'R','I','F','F',27,0,0,0,' ',' ',' ',' ',65,66,67,68,3,0,0,0,1,2,3,0, 116 | 65,66,67,69,3,0,0,0,1,2,3,0 117 | }; 118 | auto riff = RIFF(RIFF::TYPE_RIFF); 119 | riff.add_chunk(RIFF(0x41424344,added_data)); 120 | riff.add_chunk(RIFF(0x41424345,added_data)); 121 | CPPUNIT_ASSERT_MESSAGE("RIFF(chunk_type,initial_data)", riff.to_bytes() == data_block); 122 | } 123 | // dirty way to convert a string to vector container 124 | std::vector str_to_vector(const char* str) 125 | { 126 | std::vector vec = {}; 127 | while(*str) 128 | vec.push_back(*str++); 129 | return vec; 130 | } 131 | void test_riff_encode_decode() 132 | { 133 | auto added_data = str_to_vector("some bytes!"); 134 | auto added_data2 = str_to_vector("even more bytes..."); 135 | auto added_data3 = str_to_vector("first element of a list"); 136 | auto added_data4 = str_to_vector("second element of a list"); 137 | auto added_data5 = str_to_vector("one final element"); 138 | auto riff = RIFF(RIFF::TYPE_RIFF); 139 | riff.set_id(0x41424343); 140 | riff.add_chunk(RIFF(0x41424344,added_data)); 141 | riff.add_chunk(RIFF(0x41424345,added_data2)); 142 | auto list = RIFF(RIFF::TYPE_LIST); 143 | list.set_id(0x41424346); 144 | list.add_chunk(RIFF(0x41424347,added_data3)); 145 | list.add_chunk(RIFF(0x41424348,added_data4)); 146 | riff.add_chunk(list); 147 | riff.add_chunk(RIFF(0x41424349,added_data5)); 148 | // create a new RIFF object 149 | auto vec = riff.to_bytes(); 150 | riff = RIFF(vec); 151 | CPPUNIT_ASSERT_EQUAL((uint32_t)RIFF::TYPE_RIFF, riff.get_type()); 152 | CPPUNIT_ASSERT_EQUAL((uint32_t)0x41424343, riff.get_id()); 153 | // check first chunk 154 | CPPUNIT_ASSERT_EQUAL(false, riff.at_end()); 155 | auto chunk = RIFF(riff.get_chunk()); 156 | CPPUNIT_ASSERT_EQUAL((uint32_t)0x41424344, chunk.get_type()); 157 | CPPUNIT_ASSERT_MESSAGE("mismatch", added_data == chunk.get_data()); 158 | // check second chunk 159 | CPPUNIT_ASSERT_EQUAL(false, riff.at_end()); 160 | chunk = RIFF(riff.get_chunk()); 161 | CPPUNIT_ASSERT_EQUAL((uint32_t)0x41424345, chunk.get_type()); 162 | CPPUNIT_ASSERT_MESSAGE("mismatch", added_data2 == chunk.get_data()); 163 | // check third chunk 164 | CPPUNIT_ASSERT_EQUAL(false, riff.at_end()); 165 | chunk = RIFF(riff.get_chunk()); 166 | CPPUNIT_ASSERT_EQUAL((uint32_t)RIFF::TYPE_LIST, chunk.get_type()); 167 | CPPUNIT_ASSERT_EQUAL((uint32_t)0x41424346, chunk.get_id()); 168 | // check first sub chunk 169 | CPPUNIT_ASSERT_EQUAL(false, chunk.at_end()); 170 | auto subchunk = RIFF(chunk.get_chunk()); 171 | CPPUNIT_ASSERT_EQUAL((uint32_t)0x41424347, subchunk.get_type()); 172 | CPPUNIT_ASSERT_MESSAGE("mismatch", added_data3 == subchunk.get_data()); 173 | // check second sub chunk 174 | CPPUNIT_ASSERT_EQUAL(false, chunk.at_end()); 175 | subchunk = RIFF(chunk.get_chunk()); 176 | CPPUNIT_ASSERT_EQUAL((uint32_t)0x41424348, subchunk.get_type()); 177 | CPPUNIT_ASSERT_MESSAGE("mismatch", added_data4 == subchunk.get_data()); 178 | // at the end of subchunk 179 | CPPUNIT_ASSERT_EQUAL(true, chunk.at_end()); 180 | // check fifth chunk 181 | CPPUNIT_ASSERT_EQUAL(false, riff.at_end()); 182 | chunk = RIFF(riff.get_chunk()); 183 | CPPUNIT_ASSERT_EQUAL((uint32_t)0x41424349, chunk.get_type()); 184 | CPPUNIT_ASSERT_MESSAGE("mismatch", added_data5 == chunk.get_data()); 185 | CPPUNIT_ASSERT_EQUAL(true, riff.at_end()); 186 | } 187 | }; 188 | 189 | CPPUNIT_TEST_SUITE_REGISTRATION(RIFF_Test); 190 | 191 | -------------------------------------------------------------------------------- /src/track.h: -------------------------------------------------------------------------------- 1 | /*! \file src/track.h 2 | * \brief Song events and tracks. 3 | * 4 | * A Song consists of multiple tracks, those in turn consist of 5 | * Events. Events are similar to MIDI messages. 6 | */ 7 | #ifndef TRACK_H 8 | #define TRACK_H 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include "core.h" 14 | 15 | //! Track event. 16 | /*! 17 | * A track event is analogous to a MIDI message. 18 | * 19 | * The length of the event is defined with \ref on_time and \ref off_time. 20 | * 21 | * There are multiple types of events. An event of type \ref REST can 22 | * have an `off_time` only. \ref NOTE and \ref TIE events can have an 23 | * `on_time` and `off_time`. 24 | * The total length of the event is the sum of those lengths. 25 | * 26 | * All other kinds of events are immediate and both the `on_time` and 27 | * `off_time` must be 0. 28 | */ 29 | struct Event 30 | { 31 | //! Event types 32 | /*! 33 | * The individual types have been grouped in the source code, 34 | * but does not show in Doxygen documentation right now. Just 35 | * view the source code for more clear documentation. 36 | */ 37 | enum Type { 38 | // Basic events 39 | NOP = 0, //!< Does nothing and ignores all parameters. 40 | REST, //!< Key off. Reads \ref off_time. 41 | NOTE, //!< Key on, \ref param defines note, Reads \ref on_time and \ref off_time. 42 | TIE, //!< Extends the previous note or rest. \ref Reads on_time and \ref off_time. 43 | 44 | // Track events 45 | LOOP_START, //!< Start of a loop block. 46 | LOOP_BREAK, //!< At the last iteration, skip to the end of the loop block. 47 | LOOP_END, //!< End of the loop block. \ref param defines loop count 48 | SEGNO, //!< Set the track loop position. 49 | JUMP, //!< Jump to a track. \ref param specifies the track number. Previous position is stored in stack. 50 | END, //!< Jump to the stack position, alternatively the loop position, alternatively stops the track. 51 | SLUR, //!< Indicates that the next note is legato. Normally this means that the envelope and sample should not be restarted. 52 | PLATFORM, //!< Platform-specific commands, the parameter is associated with a tag from the song data. 53 | 54 | // Special channel events. 55 | // These affect the same memory as another command. 56 | TRANSPOSE_REL, //!< Relative transpose. 57 | VOL, //!< Coarse volume. \ref param must be between 0-15, normally corresponding to -2dB per step. 58 | VOL_REL, //!< Relative coarse volume. 59 | VOL_FINE_REL, //!< Relative fine volume. Platform-specific. 60 | TEMPO_BPM, //!< Set tempo in quarter notes per minute 61 | 62 | // Channel events 63 | INS, //!< Set instrument 64 | TRANSPOSE, //!< Set transpose 65 | DETUNE, //!< Set detune 66 | VOL_FINE, //!< Fine volume. Platform-specific. 67 | PAN, //!< Set panning. Signed parameter. 68 | VOL_ENVELOPE, //!< Set volume envelope ID. 69 | PITCH_ENVELOPE, //!< Set pitch envelope ID. 70 | PAN_ENVELOPE, //!< Set pan envelope ID. 71 | PORTAMENTO, //!< Set pitch envelope ID. 72 | DRUM_MODE, //!< Set drum mode. 73 | TEMPO, //!< Set platform-specific tempo. 74 | 75 | // Special defines. 76 | // These are not actual events but rather used to know the number 77 | // of events and size of track state memory. 78 | CMD_COUNT, //!< Command ID count. 79 | CHANNEL_CMD = INS, //!< first channel cmd ID 80 | CHANNEL_CMD_COUNT = CMD_COUNT - CHANNEL_CMD, //!< channel command count 81 | INVALID = -1, //!< represents an invalid or unknown command. 82 | }; 83 | 84 | //! The event type. 85 | Event::Type type; 86 | //! Optional parameter. 87 | int16_t param; 88 | //! Key-on time (for \ref NOTE and \ref TIE types only) 89 | uint16_t on_time; 90 | //! Key-off time (for \ref NOTE, \ref REST and \ref TIE types only) 91 | uint16_t off_time; 92 | //! Set by a Player to help look up the play time of an event. 93 | uint32_t play_time; 94 | //! Pointer to an input file reference. 95 | std::shared_ptr reference; 96 | }; 97 | 98 | //! Track structure. 99 | /*! 100 | * Each song consists of an indeterminate number of tracks which are 101 | * used for channels as well as individual phrases (subroutines) 102 | * that may be referenced by a channel. 103 | * 104 | * The functions of this class facilitate easier insertion of events to a track. 105 | */ 106 | class Track 107 | { 108 | public: 109 | //! Default octave setting. 110 | static const int DEFAULT_OCTAVE = 5; 111 | //! Default measure length (whole note duration). 112 | static const uint16_t DEFAULT_MEASURE_LEN = 96; 113 | //! Default quantize dividend. 114 | static const uint16_t DEFAULT_QUANTIZE = 8; 115 | //! Default quantize divisor. 116 | static const uint16_t DEFAULT_QUANTIZE_PARTS = 8; 117 | //! Maximum size of the echo buffer 118 | static const uint16_t ECHO_BUFFER_SIZE = 10; 119 | 120 | Track(uint16_t ppqn = DEFAULT_MEASURE_LEN/4); 121 | 122 | // Methods that add Events 123 | void add_event(Event& new_event); 124 | void add_event(Event::Type type, int16_t param = 0, uint16_t on_time = 0, uint16_t off_time = 0); 125 | void add_note(int note, uint16_t duration = 0); 126 | int add_tie(uint16_t duration = 0); 127 | void add_rest(uint16_t duration = 0); 128 | int add_slur(); 129 | void add_echo(uint16_t duration); 130 | 131 | // Methods that modify previous Events 132 | void reverse_rest(uint16_t duration = 0); 133 | 134 | // Methods that modify following Events 135 | void set_reference(const std::shared_ptr& ref); 136 | void set_octave(int param); 137 | void change_octave(int param); 138 | void set_duration(uint16_t duration); 139 | int set_quantize(uint16_t param, uint16_t parts = 8); 140 | void set_early_release(uint16_t param); 141 | void set_drum_mode(uint16_t param); 142 | void set_echo(uint16_t delay, int16_t volume); 143 | void clear_echo_buffer(); 144 | 145 | // Methods to retrieve Events 146 | std::vector& get_events(); 147 | Event& get_event(unsigned long position); 148 | unsigned long get_event_count() const; 149 | 150 | // Methods that set Track state 151 | void set_measure_len(uint16_t param); 152 | void set_shuffle(int16_t param); 153 | void set_key_signature(const char* key); 154 | void modify_key_signature(char note, int8_t modifier); 155 | 156 | // Methods to get Track state 157 | bool is_enabled() const; 158 | bool in_drum_mode() const; 159 | uint16_t get_duration(uint16_t duration = 0) const; 160 | uint16_t get_measure_len() const; 161 | int16_t get_shuffle() const; 162 | int8_t get_key_signature(char note); 163 | uint16_t get_echo_delay() const; 164 | int16_t get_echo_volume() const; 165 | 166 | private: 167 | void enable(); 168 | uint16_t on_time(uint16_t duration) const; 169 | uint16_t off_time(uint16_t duration) const; 170 | uint16_t add_shuffle(uint16_t duration) const; 171 | void push_echo_note(uint16_t note); 172 | 173 | bool enabled; 174 | uint16_t drum_mode; 175 | uint8_t ch; 176 | std::vector events; 177 | int last_note_pos; // last event id that was a note 178 | int octave; 179 | uint16_t measure_len; 180 | uint16_t default_duration; // default duration 181 | uint16_t quantize; 182 | uint16_t quantize_parts; 183 | uint16_t early_release; 184 | int16_t shuffle; 185 | uint8_t sharp_mask; 186 | uint8_t flat_mask; 187 | uint16_t echo_delay; 188 | int16_t echo_volume; 189 | std::deque echo_buffer; 190 | std::shared_ptr reference; 191 | }; 192 | #endif 193 | 194 | -------------------------------------------------------------------------------- /src/input.cpp: -------------------------------------------------------------------------------- 1 | #include "input.h" 2 | #include "song.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | //! Creates an InputError exception. 10 | /*! 11 | * \param ref a reference pointing at the error. If this is nullptr, a generic 12 | * error message is generated. 13 | */ 14 | InputError::InputError(std::shared_ptr ref, const char* message) 15 | : reference(ref) 16 | { 17 | if(ref == nullptr) 18 | { 19 | std::strncpy(buf,message,200); 20 | } 21 | else 22 | { 23 | std::snprintf(buf,200,"%s:%d:%d: %s", ref->get_filename().c_str(), ref->get_line()+1, ref->get_column()+1, message); 24 | } 25 | } 26 | 27 | //! Return exception information. 28 | const char* InputError::what() 29 | { 30 | return buf; 31 | } 32 | 33 | //============================================================================= 34 | 35 | //! Creates an InputRef. 36 | InputRef::InputRef(const std::string &fn, const std::string &ln, int lno, int col) 37 | : filename(fn), line_contents(ln), line(lno), column(col) 38 | { 39 | } 40 | 41 | //! Return the file name 42 | const std::string& InputRef::get_filename() const 43 | { 44 | return filename; 45 | } 46 | 47 | //! Return the line number 48 | const unsigned int& InputRef::get_line() const 49 | { 50 | return line; 51 | } 52 | 53 | //! Return the column number 54 | const unsigned int& InputRef::get_column() const 55 | { 56 | return column; 57 | } 58 | 59 | //! Return the contents of the line. 60 | const std::string& InputRef::get_line_contents() const 61 | { 62 | return line_contents; 63 | } 64 | 65 | //! Print a formatted InputRef. 66 | std::ostream& operator<<(std::ostream& os, const class InputRef& ref) 67 | { 68 | os << ref.get_filename() << ":" << ref.get_line()+1 << ":" << ref.get_column(); 69 | return os; 70 | } 71 | 72 | //============================================================================= 73 | 74 | //! Creates an Input. 75 | Input::Input(Song* song) 76 | : song(song), filename("") 77 | { 78 | } 79 | 80 | Input::~Input() 81 | { 82 | } 83 | 84 | //! Open a file and parse it. 85 | /*! 86 | * This adds the filepath to the include_path tag, 87 | * sets the filename and calls parse_file(). 88 | * 89 | * \exception InputError in case of a read or parse error. 90 | */ 91 | void Input::open_file(const std::string& fn) 92 | { 93 | int path_break = fn.find_last_of("/\\"); 94 | if(path_break != -1) 95 | song->add_tag("include_path", fn.substr(0, path_break + 1)); 96 | filename = fn; 97 | parse_file(); 98 | } 99 | 100 | //! Get the target Song object 101 | Song& Input::get_song() 102 | { 103 | return *song; 104 | } 105 | 106 | //! Get current filename 107 | const std::string &Input::get_filename() 108 | { 109 | return filename; 110 | } 111 | 112 | //! Get an InputRef. 113 | /*! 114 | * This can be overridden by derived classes to support column/file 115 | * numbers where this is relevant. 116 | */ 117 | std::shared_ptr Input::get_reference() 118 | { 119 | InputRef r = InputRef(filename); 120 | return std::make_shared(r); 121 | } 122 | 123 | //! Throw an InputError. 124 | void Input::parse_error(const char* msg) 125 | { 126 | throw InputError(get_reference(), msg); 127 | } 128 | 129 | //! Raise a parse warning. 130 | /*! 131 | * \todo this should be added to a warning buffer... 132 | */ 133 | void Input::parse_warning(const char* msg) 134 | { 135 | std::cerr << *get_reference() << ": " << msg << "\n"; 136 | std::cerr << get_reference()->get_line_contents() << std::endl; 137 | } 138 | 139 | //============================================================================= 140 | 141 | //! Creates a Line_Buffer 142 | Line_Buffer::Line_Buffer(std::string line, unsigned int column) 143 | : column(column) 144 | { 145 | buffer = std::make_shared(line); 146 | } 147 | 148 | //! Duplicates a Line_Buffer 149 | Line_Buffer::Line_Buffer(const class Line_Buffer& original) 150 | : buffer(original.buffer) 151 | , column(original.column) 152 | { 153 | } 154 | 155 | //! Line_Buffer destructor 156 | Line_Buffer::~Line_Buffer() 157 | { 158 | } 159 | 160 | //! Get the next character from the buffer 161 | /*! 162 | * Also increments the buffer position. 163 | * 164 | * \retval 0 if at the end of the current buffer. The column number 165 | * will still be incremented. 166 | */ 167 | int Line_Buffer::get() 168 | { 169 | if(column >= buffer->size()) 170 | { 171 | column++; 172 | return 0; 173 | } 174 | return (*buffer)[column++]; 175 | } 176 | 177 | //! Get the next non-blank character from the buffer. 178 | /*! 179 | * Blank characters are skipped until the next non-blank character is 180 | * found. 181 | */ 182 | int Line_Buffer::get_token() 183 | { 184 | int c; 185 | do c = get(); 186 | while (std::isblank(c)); 187 | return c; 188 | } 189 | 190 | //! Get a number from the buffer. 191 | /*! 192 | * Blank characters are skipped until the next number is found. 193 | * A `$` or `x` prefix indicates hexadecimal number. 194 | * 195 | * \exception std::invalid_argument if no number could be read. 196 | */ 197 | int Line_Buffer::get_num() 198 | { 199 | int base = 10; 200 | int c = get_token(); 201 | if(c == '$' || c == 'x') 202 | base = 16; 203 | else 204 | unget(c); 205 | if(column == buffer->size()) 206 | throw std::invalid_argument("expected number"); 207 | const char* ptr = buffer->c_str() + column; 208 | const char* endptr = ptr; 209 | int ret = strtol(ptr, (char**)&endptr, base); 210 | if(ptr == endptr) 211 | throw std::invalid_argument("expected number"); 212 | column += endptr - ptr; 213 | return ret; 214 | } 215 | 216 | //! Return a substring starting from the current position. 217 | std::string Line_Buffer::get_line() 218 | { 219 | return std::string(*buffer, column); 220 | } 221 | 222 | //! Put back the character to the buffer, decrementing the buffer position. 223 | /*! 224 | * The buffer position is decremented by this call. 225 | * 226 | * \param c character to put back. If this is 0, no character will be put 227 | * back and the buffer contents are unchanged. Effectively doing 228 | * the same as a seek(tell-1); 229 | * 230 | * \exception std::out_of_range If the buffer position is already at 0. 231 | */ 232 | void Line_Buffer::unget(int c) 233 | { 234 | if(column == 0) 235 | throw std::out_of_range("unget too many"); 236 | if(c == 0) 237 | { 238 | column--; 239 | return; 240 | } 241 | (*buffer)[--column] = c; 242 | } 243 | 244 | //! Get current buffer position. 245 | unsigned long Line_Buffer::tell() 246 | { 247 | return column; 248 | } 249 | 250 | //! Set the buffer position. 251 | void Line_Buffer::seek(unsigned long pos) 252 | { 253 | column = pos; 254 | } 255 | 256 | //! Set the contents of the buffer and reset the position. 257 | void Line_Buffer::set_buffer(std::string line, unsigned int new_column) 258 | { 259 | buffer = std::make_shared(line); 260 | if(new_column >= 0) 261 | column = new_column; 262 | } 263 | 264 | //============================================================================= 265 | 266 | //! Creates a Line_Input. 267 | Line_Input::Line_Input(Song* song) 268 | : Input(song), Line_Buffer("", 0), line(0) 269 | { 270 | } 271 | 272 | Line_Input::~Line_Input() 273 | { 274 | } 275 | 276 | //! Open file and parse lines. 277 | void Line_Input::parse_file() 278 | { 279 | std::ifstream inputfile = std::ifstream(get_filename()); 280 | buffer = std::make_shared(""); 281 | if(!inputfile) 282 | parse_error("failed to open file"); 283 | line = 0; 284 | for(std::string str; std::getline(inputfile, str);) 285 | { 286 | read_line(str); 287 | line++; 288 | } 289 | } 290 | 291 | std::shared_ptr Line_Input::get_reference() 292 | { 293 | InputRef r = InputRef(get_filename(), *buffer, line, column); 294 | return std::make_shared(r); 295 | } 296 | 297 | //! Read a single input line and parse it. 298 | /*! 299 | * Optionally also set the line number. 300 | */ 301 | void Line_Input::read_line(const std::string& input_line, int line_number) 302 | { 303 | if (line_number >= 0) 304 | line = line_number; 305 | column = 0; 306 | buffer = std::make_shared(input_line); 307 | parse_line(); 308 | } 309 | -------------------------------------------------------------------------------- /sample/passport.mml: -------------------------------------------------------------------------------- 1 | #title PASSPORT.MML 2 | #composer ??? 3 | #author ctr 4 | #platform megadrive 5 | #comment from Windows 95 6 | 7 | ;----------------------------------------------------- 8 | ; FM voice data (from MUCOM88) 9 | @2 fm 10 | 0 7 11 | 31 18 0 6 2 36 0 10 3 0 12 | 31 14 4 6 2 45 0 0 3 0 13 | 31 10 4 6 2 18 1 0 3 0 14 | 31 10 3 6 2 0 1 0 3 0 15 | @8 fm 16 | 3 7 17 | 31 8 2 2 14 40 3 6 0 0 18 | 19 6 0 2 15 51 1 6 3 0 19 | 15 10 0 2 2 38 3 3 6 0 20 | 14 3 0 3 0 0 2 3 3 0 -7 21 | @28 fm 22 | 2 7 23 | 28 4 3 7 1 33 2 1 6 0 24 | 27 9 1 2 0 71 3 12 3 0 25 | 28 4 3 6 0 49 2 4 1 0 26 | 26 3 0 5 10 0 3 1 3 0 27 | @109 fm 28 | 4 7 29 | 31 0 0 0 0 33 0 8 7 0 30 | 18 15 1 7 3 16 0 8 7 0 31 | 31 0 0 0 0 24 0 4 3 0 32 | 31 15 1 7 3 7 0 4 3 0 33 | @112 fm 34 | 2 7 35 | 31 15 7 8 2 33 0 0 6 0 36 | 20 16 6 7 4 18 2 7 6 0 37 | 31 5 6 7 1 52 0 0 6 0 38 | 31 8 7 7 5 0 0 1 6 0 39 | @59 fm 40 | 3 7 41 | 31 21 19 6 2 0 0 15 2 0 42 | 31 21 12 6 2 35 0 8 2 0 43 | 31 21 13 6 3 32 0 7 3 0 44 | 31 19 16 9 2 0 0 2 3 0 45 | @51 fm 46 | 4 0 47 | 22 5 6 4 6 35 1 3 0 0 48 | 25 12 6 5 1 10 2 3 3 0 49 | 20 7 6 4 6 28 1 8 6 0 50 | 25 12 6 5 6 0 2 3 6 0 51 | ;---------------------------------------------------------- 52 | ; PSG voice data 53 | @10 psg 15>12:4 / 11>7:6 6>0:36 54 | @11 psg 15>13:5 / 10>0:15 55 | ;---------------------------------------------------------- 56 | ; PCM voice data 57 | @20 pcm "pcm/bd_17k5.wav" 58 | @21 pcm "pcm/sd_17k5.wav" 59 | @22 pcm "pcm/bd_crash_17k5.wav" 60 | @23 pcm "pcm/tom2l_17k5.wav" 61 | @24 pcm "pcm/tom2m_17k5.wav" 62 | @25 pcm "pcm/tom2h_17k5.wav" 63 | *20 @20c ;D20a 64 | *21 @21c ;D20b 65 | *22 @22c ;D20c 66 | *23 @23c ;D20d 67 | *24 @24c ;D20e 68 | *25 @25c ;D20f 69 | ;---------------------------------------------------------- 70 | ; PSG noise data 71 | @40 psg 12>0:4 72 | @41 psg 8>0:4 73 | @42 psg 12>10:3 9>6:6 5>0:20 74 | *40 @40o9b ;D40a 75 | *41 @41o9b ;D40b 76 | *42 @42o9b ;D40c 77 | ;---------------------------------------------------------- 78 | 79 | A t175 80 | 81 | A @2v12 82 | B @8v7 83 | CDE @109v12 84 | F D20 85 | GH @10 v14 86 | H K20 87 | J D40 'mode 1' v13 88 | 89 | ; bar0 90 | A l16 o4[c4.cr8.c4.c8r4.(4gr<)4ar >(4gr<)4b-r >(4gr8.c p1d p1c p1c4.c8r4.(4gr<)4a8r8b-8r4gr]2 >(4gr<)4ar >(4gr<)4b-r >(4gr8.c4.c2(4gr<)4ar>(4gr<)4b-r>(4gr<)4br>(4gr)4 c4.cr8.c4.c2(4gr<)4ar >(4gr<)4b-r >(4gr8.)4c8^1 106 | *103 l4 o3f. d.e-^1 d.f. d.e-^2c e- d.f8^1 107 | *104 l4 o3a. f.g ^1 f. d2 c8^1 a. f.g ^2e-g f. d2 g8^1 108 | *105 v10@11o3d8^2. 109 | A [*100]3 110 | C [*102]3 111 | D [*103]3 112 | E [*104]3 113 | F l4 c.[a8ba2.ba a.a8b./a8^2b2a.]2 ae8ab2 114 | 115 | ; bar24 116 | @M1 0:20 V0:1:3 117 | @M2 -2>0:20 0 118 | @M3 2>0:25 0 119 | @M4 -2>0:7 0 120 | @M5 1>0:35 0 121 | *106 l16 @10o4M2g4.>&M1crr4&M1crr4cre-8 r8 e-8fr8.c8 crcrdre-rfr 138 | B l16 M6v10p3 r32e-32f8.^2.^2.r8 g-32g16.^8 fr8. e-8.r fr8. c8.^2r e-r8. e-32f16. ^1^2.r8 g32b-16.^8 gr8. f4e- r8. c8.^4re-r8. g8e-rf8& 139 | C l2 r2r8 p1@28v12o4g g r4. [r1]2 p2r2 >b-^2 r2 [r1]2 140 | D l2 r2r4 @28v10o4a-a-r4 [r1]2 r2r4 >g ^4 r2 [r1]2 141 | E l2 r2r4.p2@28v12o5e-d r8 [r1]2 p1r2r4. c ^8 r2 [r1]2 142 | G l8 M1@10v10q7 o4 ccc l16 v14q0 e-r8. grf4^1 v10q7 l8 [ccccccc e-c f c g-f e-c /ccccc l16 v14q0 cre-r8.frr4fr l8 v10q7 ce-cc]2 143 | H l8 M1@10v10q7 o3 ggg l16 v14q0 a-r8.>crc4^1 v10q7cd-ccre-rgrfre-rcrf8g8cre-r f8^1^2.r8 b-32>c16.^8 b-4e-8g 4g8 cc e-c f c g-f e-c cccc l16 v14q0 cr8.e-r8.[f8g8e-r/r8]2 f8r8 e-rfrgrb-rgrb-r>cr cre-rcrc8^1 v10q7cd-ccr cre-rcrcrr2.r8c(3c(3c(2c M0 l1[r]3 v7 *101 159 | C @109v12p3 [*102]2 160 | D @109v12 [*103]2 161 | E @109v12p3 [*104]2 162 | G l16 v15 *106 o3c8e-8 &M5d4M0 *109 v15 o4 (2cr)2 163 | H l16 v12 r8 *106 o3c8e-8 &M5d8M0 k-12 *109 k0 v12 o4cr(2cr)2 164 | F l4 c[b8a.b/ aba8a8b a]4 a8b8b8e8a8b8b 165 | J l8 [[ab]10 ac [ab]15 ac [ab]5]2 166 | 167 | ; bar64 168 | *110 l16 v14o4g8^8.r b-r8. >cr8. e-8c8^8.r c4 0:20 0 176 | A l16 o4[crc8r8 cre-4 o4crc8r8 cr0:40 0 188 | @M9 3>0:15 0 189 | @M10 0>-0.4:25 -0.4>0:25 0:10 0>0.5:40 190 | B l2 @59 o5v12p3 [g4.g8]7 g4 @8v6l8gb- 191 | C l16 v12o2crc8r8 cre-8r8 e-8a-8r8 a-8>d-8r8 d-r 200 | A k0 *111 o5c4r1 201 | B k36*111 o5c16r8.r1 202 | C k24*111 o5c4r1 203 | D k24*111 o5c4r1 204 | E k12*111 o5c4r1 205 | G k12*111 o5c4r1 206 | H k0 *111 o5c4r1 207 | F l4 a8a8b ab ab8a4a8b a8a8bab. a8 f16e16d8 b8 f16e16d8 b8 c4r1 208 | J l8 [a4]12 a8c4. a8c4. a4r1 -------------------------------------------------------------------------------- /src/platform/md.h: -------------------------------------------------------------------------------- 1 | //! \file platform/md.h 2 | #ifndef PLATFORM_MD_H 3 | #define PLATFORM_MD_H 4 | #include "../core.h" 5 | #include "../player.h" 6 | #include "../driver.h" 7 | #include "../vgm.h" 8 | #include "mdsdrv.h" 9 | #include 10 | #include 11 | 12 | class MD_Channel; 13 | class MD_MacroTrack; 14 | class MD_Driver; 15 | 16 | //! Megadrive abstract channel 17 | class MD_Channel : public Player 18 | { 19 | friend MD_MacroTrack; 20 | 21 | public: 22 | MD_Channel(MD_Driver& driver, int id); 23 | void update(int seq_ticks); 24 | void seek(int ticks); 25 | 26 | protected: 27 | enum 28 | { 29 | EVENT_CHANNEL_MODE = 0, 30 | EVENT_LFO = 1, 31 | EVENT_LFO_DELAY = 2, 32 | EVENT_LFO_CONFIG = 3, 33 | EVENT_FM3 = 4, 34 | EVENT_WRITE_ADDR = 5, 35 | EVENT_WRITE_DATA = 6, 36 | EVENT_TL_MODIFY = 7, 37 | }; 38 | 39 | uint8_t write_fm_operator(int idx, int bank, int id, const std::vector& idata); 40 | void write_fm_4op(int bank, int id); 41 | uint16_t get_fm_pitch(uint16_t pitch) const; 42 | uint16_t get_psg_pitch(uint16_t pitch) const; 43 | uint8_t get_psg_volume(uint16_t volume) const; 44 | 45 | virtual void v_set_ins() = 0; 46 | virtual void v_set_vol() = 0; 47 | virtual void v_set_pan() = 0; 48 | virtual void v_key_on() = 0; 49 | virtual void v_key_off() = 0; 50 | virtual void v_set_pitch() = 0; 51 | virtual void v_set_type() = 0; 52 | virtual void v_update_envelope() = 0; 53 | 54 | MD_Driver* driver; 55 | int channel_id; 56 | bool pcm_channel_enable; 57 | bool pcm_channel_valid; 58 | int pcm_channel_id; 59 | 60 | bool slur_flag; //!< Flag to disable key on for the next note 61 | bool key_on_flag; 62 | // Portamento 63 | uint16_t note_pitch; //< Target pitch for portamento 64 | uint16_t porta_value; //!< Current pitch (256 'cents' per semitone) 65 | uint16_t last_pitch; //!< Last pitch, used to optimize register writes 66 | // Pitch envelopes 67 | const std::vector* pitch_env_data; 68 | bool pitch_env_extend; 69 | uint16_t pitch_env_value; //!< Pitch envelope value 70 | uint8_t pitch_env_delay; 71 | uint8_t pitch_env_pos; 72 | // Target pitch 73 | uint16_t pitch; //!< pitch with portamento and envelope calculated 74 | int8_t ins_transpose; //!< Instrument transpose (for FM 2op). compiled files should have this already 'cooked' 75 | uint8_t con; //!< FM connection 76 | uint8_t tl[4]; // also used for Ch3 mode 77 | // Macro track 78 | std::unique_ptr macro_track; 79 | bool macro_carry; 80 | 81 | private: 82 | uint32_t parse_platform_event(const Tag& tag, int16_t* platform_state) override; 83 | void write_event() override; 84 | 85 | void update_tempo(); 86 | void update_state(); 87 | void update_pitch(); 88 | 89 | void key_on(); 90 | void key_off(); 91 | void set_pitch(); 92 | void set_vol(); 93 | void set_ins(); 94 | 95 | void set_pitch_fm3(); 96 | void set_vol_fm3(); 97 | 98 | void key_on_pcm(); 99 | void key_off_pcm(); 100 | 101 | void reset_macro_track(); 102 | }; 103 | 104 | //! Megadrive macro track 105 | class MD_MacroTrack : public Basic_Player 106 | { 107 | public: 108 | MD_MacroTrack(MD_Channel& channel, Song& song, Track& track); 109 | void update(); 110 | 111 | private: 112 | void event_hook() override; 113 | bool loop_hook() override; 114 | void end_hook() override; 115 | 116 | MD_Channel& channel; 117 | int loop_count; 118 | }; 119 | 120 | //! Megadrive FM channel 121 | class MD_FM : public MD_Channel 122 | { 123 | public: 124 | MD_FM(MD_Driver& driver, int track_id, int channel_id); 125 | 126 | private: 127 | void v_set_ins() override; 128 | void v_set_vol() override; 129 | void v_set_pan() override; 130 | void v_key_on() override; 131 | void v_key_off() override; 132 | void v_set_pitch() override; 133 | void v_set_type() override; 134 | void v_update_envelope() override; 135 | 136 | enum 137 | { 138 | NORMAL = 0, 139 | FM3_2OP = 1 140 | } type; 141 | uint8_t bank : 1; //!< YM2612 port id. 142 | uint8_t id : 2; //!< YM2612 channel id. 143 | uint8_t pan_lfo; //!< FM panning & lfo parameters 144 | }; 145 | 146 | //! Megadrive abstract PSG channel 147 | class MD_PSG : public MD_Channel 148 | { 149 | public: 150 | MD_PSG(MD_Driver& driver, int track_id, int channel_id); 151 | 152 | protected: 153 | void set_envelope(std::vector* idata); 154 | void v_key_on() override; 155 | void v_key_off() override; 156 | void v_set_pan() override; 157 | void v_update_envelope() override; 158 | 159 | //! Channel index 160 | int id; 161 | const std::vector* env_data; //!< Pointer to envelope data 162 | bool env_keyoff; //!< Envelope key off flag 163 | uint8_t env_pos; //!< Envelope position 164 | uint8_t env_delay; //!< Envelope delay and current volume 165 | }; 166 | 167 | //! Megadrive PSG melodic channel 168 | class MD_PSGMelody : public MD_PSG 169 | { 170 | public: 171 | MD_PSGMelody(MD_Driver& driver, int track_id, int channel_id); 172 | private: 173 | enum 174 | { 175 | NORMAL = 0, 176 | FM3 = 1 177 | } type; 178 | 179 | void v_set_ins() override; 180 | void v_set_vol() override; 181 | void v_set_pitch() override; 182 | void v_set_type() override; 183 | }; 184 | 185 | //! Megadrive PSG noise channel 186 | class MD_PSGNoise : public MD_PSG 187 | { 188 | public: 189 | MD_PSGNoise(MD_Driver& driver, int track_id, int channel_id); 190 | 191 | private: 192 | enum 193 | { 194 | NORMAL = 0, 195 | PSG3_WHITE = 1, 196 | PSG3_PERIODIC = 2 197 | } type; 198 | 199 | void v_set_ins() override; 200 | void v_set_vol() override; 201 | void v_set_pitch() override; 202 | void v_set_type() override; 203 | }; 204 | 205 | //! Megadrive dummy channel 206 | class MD_Dummy : public MD_Channel 207 | { 208 | public: 209 | MD_Dummy(MD_Driver& driver, int track_id, int channel_id); 210 | 211 | private: 212 | int id; 213 | 214 | void v_set_ins() override; 215 | void v_set_vol() override; 216 | void v_set_pan() override; 217 | void v_key_on() override; 218 | void v_key_off() override; 219 | void v_set_pitch() override; 220 | void v_set_type() override; 221 | void v_update_envelope() override; 222 | }; 223 | 224 | struct MD_PCMChannel 225 | { 226 | bool enabled; 227 | int8_t pitch; 228 | uint32_t start; 229 | uint32_t length; 230 | uint32_t position; 231 | uint8_t volume; 232 | uint8_t phase; 233 | uint8_t count; 234 | 235 | inline int update_phase() 236 | { 237 | int out = phase >> 7; 238 | phase = (phase << 1) | out; 239 | return out; 240 | } 241 | }; 242 | 243 | //! MDSDRV PCM emulator 244 | class MD_PCMDriver 245 | { 246 | public: 247 | MD_PCMDriver(MD_Driver& driver); 248 | 249 | double set_mode(int data); // returns sample rate 250 | uint8_t set_ins(int channel, int data); 251 | void set_vol(int channel, int data); 252 | void set_pitch(int channel, int data); 253 | void key_on(int channel); 254 | void key_off(int channel); 255 | 256 | void update(); 257 | 258 | protected: 259 | MD_Driver* driver; 260 | MD_PCMChannel channels[3]; 261 | 262 | int mode; 263 | 264 | int8_t mix_channel(int16_t accumulator, int channel); 265 | 266 | static bool tables_initialized; 267 | static int8_t vol_table[16][256]; 268 | static const uint8_t pitch_table[2][8]; 269 | }; 270 | 271 | //! Megadrive sound driver 272 | class MD_Driver : public Driver 273 | { 274 | friend MD_Channel; 275 | friend MD_FM; 276 | friend MD_PSG; 277 | friend MD_PSGMelody; 278 | friend MD_PSGNoise; 279 | friend MD_PCMDriver; 280 | public: 281 | MD_Driver(unsigned int rate, VGM_Interface* vgm_interface, int pcm_mode = 0, bool is_pal = false); 282 | 283 | void play_song(Song& song); 284 | void reset(); 285 | void skip_ticks(unsigned int ticks); 286 | bool is_playing(); 287 | int get_loop_count(); 288 | double play_step(); 289 | uint32_t get_player_ticks(); 290 | 291 | private: 292 | uint8_t bpm_to_delta(uint16_t bpm); 293 | void seq_update(); 294 | void reset_loop_count(); 295 | 296 | MDSDRV_Data data; 297 | MD_PCMDriver pcm; 298 | Song* song; 299 | VGM_Interface* vgm; 300 | 301 | std::vector> channels; 302 | int pcm_mode; 303 | double seq_rate; 304 | double pcm_rate; 305 | double seq_delta; 306 | double pcm_delta; 307 | double seq_counter; 308 | double pcm_counter; 309 | 310 | uint8_t tempo_delta; 311 | uint8_t tempo_counter; 312 | uint32_t ticks; 313 | 314 | uint8_t fm3_mask; 315 | uint8_t fm3_con; 316 | uint8_t fm3_tl[4]; 317 | 318 | int last_pcm_channel; 319 | 320 | bool loop_trigger; 321 | }; 322 | 323 | #endif 324 | 325 | -------------------------------------------------------------------------------- /src/unittest/test_song.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "../song.h" 4 | #include "../track.h" 5 | 6 | class Song_Test : public CppUnit::TestFixture 7 | { 8 | CPPUNIT_TEST_SUITE(Song_Test); 9 | CPPUNIT_TEST(test_set_tag); 10 | CPPUNIT_TEST(test_add_tag); 11 | CPPUNIT_TEST(test_add_tag_trim_trailing_spaces); 12 | CPPUNIT_TEST(test_add_tag_list); 13 | CPPUNIT_TEST(test_add_tag_list_enclosed); 14 | CPPUNIT_TEST(test_add_tag_list_enclosed_escape); 15 | CPPUNIT_TEST(test_add_tag_list_comma_separation); 16 | CPPUNIT_TEST(test_add_tag_list_successive_calls); 17 | CPPUNIT_TEST(test_add_tag_list_semicolon); 18 | CPPUNIT_TEST(test_add_tag_list_space_semicolon); 19 | CPPUNIT_TEST(test_add_tag_list_enclosed_semicolon); 20 | CPPUNIT_TEST(test_get_track); 21 | CPPUNIT_TEST_EXCEPTION(test_get_invalid_track, std::out_of_range); 22 | CPPUNIT_TEST(test_get_track_map); 23 | CPPUNIT_TEST(test_set_platform_command); 24 | CPPUNIT_TEST(test_get_platform_command); 25 | CPPUNIT_TEST(test_get_tag_order_list); 26 | CPPUNIT_TEST_SUITE_END(); 27 | private: 28 | Song *song; 29 | Tag *tag; 30 | public: 31 | void setUp() 32 | { 33 | song = new Song(); 34 | tag = nullptr; 35 | } 36 | void tearDown() 37 | { 38 | delete song; 39 | } 40 | // create tag and verify that it exists 41 | void test_set_tag() 42 | { 43 | song->set_tag("Tag1", "first value in tag1"); 44 | tag = &song->get_tag("Tag1"); 45 | CPPUNIT_ASSERT_EQUAL(std::string("first value in tag1"), tag->at(0)); 46 | song->set_tag("Tag1", "overwritten value in tag1"); 47 | CPPUNIT_ASSERT_EQUAL(std::string("overwritten value in tag1"), tag->at(0)); 48 | } 49 | // create tag and verify that it exists 50 | void test_add_tag() 51 | { 52 | song->add_tag("Tag1", "first value in tag1"); 53 | song->add_tag("Tag1", "second value in tag1"); 54 | song->add_tag("Tag2", "Value2"); 55 | 56 | // Test two different methods of retrieving the value. 57 | tag = &song->get_tag("Tag1"); 58 | CPPUNIT_ASSERT_EQUAL(std::string("first value in tag1"), tag->at(0)); 59 | CPPUNIT_ASSERT_EQUAL(std::string("second value in tag1"), tag->at(1)); 60 | CPPUNIT_ASSERT_EQUAL(std::string("Value2"), song->get_tag_front("Tag2")); 61 | } 62 | // create tag with spaces and verify spaces are removed 63 | void test_add_tag_trim_trailing_spaces() 64 | { 65 | song->add_tag("Tag1", "trailing spaces "); 66 | CPPUNIT_ASSERT_EQUAL(std::string("trailing spaces"), song->get_tag_front("Tag1")); 67 | } 68 | // Space separated values 69 | void test_add_tag_list() 70 | { 71 | song->add_tag_list("Tag1", "my tag list"); 72 | tag = &song->get_tag("Tag1"); 73 | CPPUNIT_ASSERT_EQUAL(std::string("my"), tag->at(0)); 74 | CPPUNIT_ASSERT_EQUAL(std::string("tag"), tag->at(1)); 75 | CPPUNIT_ASSERT_EQUAL(std::string("list"), tag->at(2)); 76 | CPPUNIT_ASSERT(tag->size() == 3); 77 | } 78 | // Test double-quote-enclosed tags 79 | void test_add_tag_list_enclosed() 80 | { 81 | song->add_tag_list("Tag1", "\" One enclosed tag with leading and trailing spaces \" \"Another enclosed tag\", \"and a final one\""); 82 | tag = &song->get_tag("Tag1"); 83 | CPPUNIT_ASSERT_EQUAL(std::string(" One enclosed tag with leading and trailing spaces "), tag->at(0)); 84 | CPPUNIT_ASSERT_EQUAL(std::string("Another enclosed tag"), tag->at(1)); 85 | CPPUNIT_ASSERT_EQUAL(std::string("and a final one"), tag->at(2)); 86 | CPPUNIT_ASSERT(tag->size() == 3); 87 | } 88 | // Escaped \" sequence in double-quote-enclosed tags 89 | void test_add_tag_list_enclosed_escape() 90 | { 91 | song->add_tag_list("Tag1", "\"Enclosed tag with an \\\"escaped\\\" quote mark\""); 92 | CPPUNIT_ASSERT_EQUAL(std::string("Enclosed tag with an \"escaped\" quote mark"), song->get_tag_front("Tag1")); 93 | CPPUNIT_ASSERT(song->get_tag("Tag1").size() == 1); 94 | } 95 | // Separating values with commas 96 | void test_add_tag_list_comma_separation() 97 | { 98 | song->add_tag_list("Tag1", "first, second,, fourth, , sixth"); 99 | tag = &song->get_tag("Tag1"); 100 | CPPUNIT_ASSERT_EQUAL(std::string("first"), tag->at(0)); 101 | CPPUNIT_ASSERT_EQUAL(std::string("second"), tag->at(1)); 102 | CPPUNIT_ASSERT_EQUAL(std::string(""), tag->at(2)); 103 | CPPUNIT_ASSERT_EQUAL(std::string("fourth"), tag->at(3)); 104 | CPPUNIT_ASSERT_EQUAL(std::string(""), tag->at(4)); 105 | CPPUNIT_ASSERT_EQUAL(std::string("sixth"), tag->at(5)); 106 | CPPUNIT_ASSERT(tag->size() == 6); 107 | } 108 | // Successive calls should add more values to the tag 109 | void test_add_tag_list_successive_calls() 110 | { 111 | song->add_tag_list("Tag1", "first, second"); 112 | song->add_tag_list("Tag1", "third, fourth"); 113 | tag = &song->get_tag("Tag1"); 114 | CPPUNIT_ASSERT_EQUAL(std::string("first"), tag->at(0)); 115 | CPPUNIT_ASSERT_EQUAL(std::string("second"), tag->at(1)); 116 | CPPUNIT_ASSERT_EQUAL(std::string("third"), tag->at(2)); 117 | CPPUNIT_ASSERT_EQUAL(std::string("fourth"), tag->at(3)); 118 | CPPUNIT_ASSERT(tag->size() == 4); 119 | } 120 | // No more tags should be added after a semicolon is read 121 | void test_add_tag_list_semicolon() 122 | { 123 | song->add_tag_list("Tag1", "first, second; This is a comment"); 124 | tag = &song->get_tag("Tag1"); 125 | CPPUNIT_ASSERT_EQUAL(std::string("first"), tag->at(0)); 126 | CPPUNIT_ASSERT_EQUAL(std::string("second"), tag->at(1)); 127 | CPPUNIT_ASSERT(tag->size() == 2); 128 | } 129 | // Test that no extra tags are added even if spaces or commas are 130 | // added after the last tag. 131 | void test_add_tag_list_space_semicolon() 132 | { 133 | song->add_tag_list("Tag1", "first, second ; This is a comment"); 134 | song->add_tag_list("Tag2", "first, second, ; This is a comment"); 135 | tag = &song->get_tag("Tag1"); 136 | CPPUNIT_ASSERT_EQUAL(std::string("first"), tag->at(0)); 137 | CPPUNIT_ASSERT_EQUAL(std::string("second"), tag->at(1)); 138 | CPPUNIT_ASSERT(tag->size() == 2); 139 | tag = &song->get_tag("Tag2"); 140 | CPPUNIT_ASSERT_EQUAL(std::string("first"), tag->at(0)); 141 | CPPUNIT_ASSERT_EQUAL(std::string("second"), tag->at(1)); 142 | CPPUNIT_ASSERT(tag->size() == 2); 143 | } 144 | // Semicolons are allowed inside enclosed tags. 145 | void test_add_tag_list_enclosed_semicolon() 146 | { 147 | song->add_tag_list("Tag1", "\";_;\""); 148 | tag = &song->get_tag("Tag1"); 149 | CPPUNIT_ASSERT_EQUAL(std::string(";_;"), tag->at(0)); 150 | CPPUNIT_ASSERT(tag->size() == 1); 151 | } 152 | // Track addressing 153 | void test_get_track_map() 154 | { 155 | Track &t1 = song->get_track_map()[0]; 156 | Track &t2 = song->get_track_map()[12]; 157 | t1.add_note(12); 158 | t2.add_note(45); 159 | t2.add_note(78); 160 | CPPUNIT_ASSERT_EQUAL((unsigned long) 1, t1.get_event_count()); 161 | CPPUNIT_ASSERT_EQUAL((unsigned long) 2, t2.get_event_count()); 162 | } 163 | void test_get_track() 164 | { 165 | Track &t1 = song->get_track_map()[0]; 166 | Track &t2 = song->get_track(0); 167 | t1.add_note(12); 168 | t1.add_note(45); 169 | t1.add_note(78); 170 | CPPUNIT_ASSERT_EQUAL((unsigned long) 3, t1.get_event_count()); 171 | CPPUNIT_ASSERT_EQUAL((unsigned long) 3, t2.get_event_count()); 172 | } 173 | void test_get_invalid_track() 174 | { 175 | // Doesn't exist, we should get std::out_of_range 176 | Track &t = song->get_track(0); 177 | t.add_note(12); // should not get this far 178 | } 179 | // Platform commands 180 | void test_set_platform_command() 181 | { 182 | CPPUNIT_ASSERT_EQUAL((int16_t)-32768, song->register_platform_command(-1, "first")); 183 | CPPUNIT_ASSERT_EQUAL((int16_t)123, song->register_platform_command(123, "second")); 184 | CPPUNIT_ASSERT_EQUAL((int16_t)-32767, song->register_platform_command(-1, "third")); 185 | 186 | } 187 | void test_get_platform_command() 188 | { 189 | int16_t param1 = song->register_platform_command(-1, "first"); 190 | int16_t param2 = song->register_platform_command(123, "second"); 191 | int16_t param3 = song->register_platform_command(-1, "third"); 192 | CPPUNIT_ASSERT_EQUAL(std::string("first"), song->get_platform_command(param1).at(0)); 193 | CPPUNIT_ASSERT_EQUAL(std::string("second"), song->get_platform_command(param2).at(0)); 194 | CPPUNIT_ASSERT_EQUAL(std::string("third"), song->get_platform_command(param3).at(0)); 195 | } 196 | void test_get_tag_order_list() 197 | { 198 | song->add_tag("Tag1", "first"); 199 | song->add_tag("Tag3", "second"); 200 | song->add_tag("Tag2", "third"); 201 | 202 | tag = &song->get_tag_order_list(); 203 | CPPUNIT_ASSERT_EQUAL(std::string("Tag1"), tag->at(0)); 204 | CPPUNIT_ASSERT_EQUAL(std::string("Tag3"), tag->at(1)); 205 | CPPUNIT_ASSERT_EQUAL(std::string("Tag2"), tag->at(2)); 206 | } 207 | }; 208 | 209 | CPPUNIT_TEST_SUITE_REGISTRATION(Song_Test); 210 | 211 | -------------------------------------------------------------------------------- /src/platform/mdsdrv.h: -------------------------------------------------------------------------------- 1 | //! \file platform/mdsdrv.h 2 | #ifndef PLATFORM_MDSDRV_COMMON_H 3 | #define PLATFORM_MDSDRV_COMMON_H 4 | #include "../core.h" 5 | #include "../song.h" 6 | #include "../wave.h" 7 | #include "../track.h" 8 | #include "../player.h" 9 | #include "../riff.h" 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | class MDSDRV_Data; 18 | struct MDSDRV_Event; 19 | class MDSDRV_Track_Writer; 20 | class MDSDRV_Converter; 21 | class MDSDRV_Linker; 22 | class MDSDRV_Platform; 23 | 24 | // Current sequence version 25 | #define MDSDRV_SEQ_VERSION_MAJOR 0 26 | #define MDSDRV_SEQ_VERSION_MINOR 6 27 | 28 | // Minimum compatible sequence version 29 | #define MDSDRV_MIN_SEQ_VERSION_MAJOR 0 30 | #define MDSDRV_MIN_SEQ_VERSION_MINOR 2 31 | 32 | // PCM sampling rate 33 | #define MDSDRV_PCM_RATE 17500 34 | 35 | //! helper functions for MDSDRV 36 | uint8_t MDSDRV_get_register(const std::string& str); 37 | 38 | //! MDSDRV data bank 39 | class MDSDRV_Data 40 | { 41 | friend MDSDRV_Track_Writer; 42 | friend MDSDRV_Converter; 43 | friend MDSDRV_Linker; 44 | friend class MD_PCMDriver; 45 | friend class MD_Driver; 46 | friend class MD_Channel; 47 | friend class MD_PSG; 48 | friend class MD_PSGMelody; 49 | friend class MD_PSGNoise; 50 | 51 | public: 52 | enum InstrumentType 53 | { 54 | INS_UNDEFINED = 0, 55 | INS_PSG = 1, 56 | INS_FM = 2, 57 | INS_PCM = 3 58 | }; 59 | 60 | MDSDRV_Data(); 61 | 62 | void read_song(Song& song); 63 | void add_instrument(uint16_t id, const Tag& tag); 64 | void add_pitch_envelope(uint16_t id, const Tag& tag); 65 | void add_extended_pitch_envelope(uint16_t id, const Tag& tag); 66 | 67 | private: 68 | static const int data_count_max = 256; 69 | 70 | void add_ins_fm_4op(uint16_t id, const Tag& tag); 71 | void add_ins_fm_2op(uint16_t id, const Tag& tag); 72 | void add_ins_psg(uint16_t id, const Tag& tag); 73 | void add_ins_pcm(uint16_t id, const Tag& tag); 74 | 75 | void add_pitch_node(const char* s, bool extend, std::vector* env_data); 76 | void add_pitch_vibrato(const char* s, bool extend, std::vector* env_data); 77 | 78 | int add_unique_data(const std::vector& data); 79 | std::string dump_data(uint16_t id, uint16_t mapped_id); // debug function 80 | 81 | //! Allow extended pitch envelopes 82 | bool use_extended_pitch; 83 | 84 | //! Data bank, holds all instrument and envelope data 85 | std::vector> data_bank; 86 | //! Waverom bank, holds PCM samples. 87 | Wave_Bank wave_rom; 88 | //! Maps the current song instruments to data_bank entries. 89 | std::map envelope_map; 90 | //! Maps the PCM instruments to a wave_rom header. 91 | std::map wave_map; 92 | //! Maps the current song instrument to transpose settings (for FM 2op only) 93 | std::map ins_transpose; 94 | //! Specify the instrument types of the defined song instruments. 95 | std::map ins_type; 96 | //! Maps the current song pitch envelopes to data_bank entries. 97 | std::map pitch_map; 98 | //! Specify the instrument types of the defined pitch envelopes. 99 | std::set pitch_extend; 100 | //! Diagnostic message 101 | std::string message; 102 | }; 103 | 104 | //! MDSDRV sequence event 105 | struct MDSDRV_Event 106 | { 107 | enum Type { 108 | CARRY = 0x7e, // carry event for macro track 109 | SEGNO = 0x7f, // virtual "segno" event 110 | 111 | REST = 0x80, 112 | TIE, 113 | NOTE, 114 | SLR = 0xe0, // slur (legato) 115 | INS, // instrument 116 | VOL, // volume 117 | VOLM, // volume modulate 118 | TRS, // transpose 119 | TRSM, // transpose modulate 120 | DTN, // detune 121 | PTA, // portamento 122 | PEG, // pitch envelope 123 | PAN, // panning enable 124 | LFO, // lfo ams/pms 125 | MTAB, // macro table 126 | FLG, // channel flags 127 | FMCREG, // channel register write 128 | FMTL, // tl write 129 | FMTLM, // tl modulate 130 | PCM, // PCM instrument 131 | PCMRATE, // PCM rate 132 | PCMMODE, // PCM mixing mode 133 | JUMP = 0xf5, // jump 134 | FMREG, // global FM register write 135 | DMFINISH, // drum mode subroutine: play note and exit 136 | COMM, // communication variable byte 137 | TEMPO, // tempo 138 | LP, // loop start 139 | LPF, // loop finish 140 | LPB, // loop break 141 | LPBL, // loop break (long jump) 142 | PAT, // pattern/subroutine 143 | FINISH // normal subroutine exit (0xff) 144 | }; 145 | 146 | // explicit constructor needed since type is not enum Type 147 | inline MDSDRV_Event(uint8_t type, uint16_t arg) 148 | : type(type) 149 | , arg(arg) 150 | {} 151 | 152 | uint8_t type; 153 | uint16_t arg; 154 | }; 155 | 156 | //! Track writer. 157 | class MDSDRV_Track_Writer : public Basic_Player 158 | { 159 | public: 160 | MDSDRV_Track_Writer(MDSDRV_Converter& mdsdrv, 161 | int id, 162 | bool in_drum_mode, 163 | bool drum_mode_enabled, 164 | std::vector& converted_events); 165 | 166 | private: 167 | void event_hook() override; 168 | bool loop_hook() override; 169 | void end_hook() override; 170 | 171 | void parse_platform_event(const Tag& tag); 172 | uint8_t bpm_to_delta(uint16_t bpm); 173 | void check_instrument(int16_t param); 174 | 175 | MDSDRV_Converter& mdsdrv; 176 | std::vector& converted_events; 177 | bool in_drum_mode; //! set while executing a drum mode routine 178 | bool drum_mode_enabled; //! set to >0 to make note events call drum mode routines 179 | bool in_loop; 180 | uint16_t rest_time; 181 | int track_id; 182 | }; 183 | 184 | //! MDSDRV sequence converter 185 | class MDSDRV_Converter 186 | { 187 | friend MDSDRV_Track_Writer; 188 | friend class MDSDRV_Converter_Test; 189 | public: 190 | MDSDRV_Converter(Song& song); 191 | 192 | RIFF get_mds(); 193 | 194 | private: 195 | void parse_track(int track_id); 196 | std::vector convert_track(const std::vector& event_list); 197 | std::vector convert_macro_track(const std::vector& event_list); 198 | int get_subroutine(int track_id, bool in_drum_mode, bool drum_mode_enabled); 199 | int get_macro_track(int track_id); 200 | int get_envelope(int mapped_id); 201 | 202 | inline uint32_t get_data_id(int envelope_id) { return subroutine_list.size() + macro_track_list.size() + envelope_id; } 203 | 204 | Song* song; 205 | MDSDRV_Data data; 206 | //! Map of used data from the data bank. 207 | std::map used_data_map; // Maps event parameter to envelope_id 208 | std::map subroutine_map; // Maps event parameter to track_id 209 | std::map macro_track_map; // Maps event parameter to track_id 210 | std::vector> subroutine_list; 211 | std::vector> macro_track_list; 212 | std::map> track_list; 213 | std::vector sequence_data; 214 | uint16_t sequence_base; 215 | 216 | }; 217 | 218 | //! MDSDRV data linker 219 | class MDSDRV_Linker 220 | { 221 | typedef std::map String_Counter; 222 | struct Seq_Data { 223 | std::string filename; 224 | std::vector data; 225 | std::vector> patch_table; 226 | }; 227 | 228 | public: 229 | MDSDRV_Linker(); 230 | 231 | void add_song(RIFF& mds, const std::string& filename = ""); 232 | unsigned int get_seq_count() const; 233 | std::string get_seq_data_asm(); 234 | std::vector get_seq_data(); 235 | std::vector get_pcm_data(); 236 | std::string get_statistics(); 237 | 238 | std::string get_asm_header() const; 239 | std::string get_c_header() const; 240 | 241 | private: 242 | int add_unique_data(const std::vector& data); 243 | int find_unique_data(const std::vector& data) const; 244 | std::vector get_pcm_header(const Wave_Bank::Sample& sample) const; 245 | void check_version(uint8_t major, uint8_t minor); 246 | 247 | std::string keyify_string(const std::string& input) const; 248 | std::string unique_string(const std::string& input, String_Counter& map) const; 249 | 250 | std::vector> data_bank; 251 | std::vector data_offset; 252 | std::map> seq_bank; 253 | Wave_Bank wave_rom; 254 | }; 255 | 256 | class MDSDRV_Platform : public Platform 257 | { 258 | public: 259 | MDSDRV_Platform(int pcm_mode); 260 | 261 | std::shared_ptr get_driver(unsigned int rate, VGM_Interface* vgm_interface) const; 262 | const Platform::Format_List& get_export_formats() const; 263 | std::vector get_export_data(Song& song, int format) const; 264 | 265 | private: 266 | int pcm_mode; 267 | }; 268 | 269 | #endif 270 | -------------------------------------------------------------------------------- /src/vgm.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #if defined(_WIN32) 11 | #include 12 | #else 13 | #include 14 | #include 15 | #endif 16 | 17 | #include 18 | #include 19 | 20 | #include "vgm.h" 21 | 22 | void VGM_Interface::set_loop() 23 | { 24 | } 25 | 26 | void VGM_Interface::stop() 27 | { 28 | } 29 | 30 | //===================================================================== 31 | 32 | //! Constructs a VGM_Writer. 33 | /*! 34 | * \param filename If blank, no file is written to the disk. 35 | * \param version Minor part of the VGM file version. Major version 0x1 is always written. 36 | * \param header_size the size of the VGM file header. 37 | */ 38 | VGM_Writer::VGM_Writer(const char* filename, int version, int header_size) 39 | : filename(filename), 40 | completed(0), 41 | curr_delay(0), 42 | sample_count(0), 43 | loop_sample(0) 44 | { 45 | // create initial buffer 46 | buffer = (uint8_t*) std::calloc(initial_buffer_alloc, sizeof(uint8_t)); 47 | if(!buffer) 48 | throw std::bad_alloc(); 49 | buffer_pos = buffer; 50 | buffer_alloc = initial_buffer_alloc; 51 | 52 | // vgm magic 53 | my_memcpy((uint32_t*)"Vgm ", 4); 54 | buffer_pos += 4; 55 | *buffer_pos++ = version; 56 | *buffer_pos++ = 0x01; 57 | 58 | // header offset 59 | poke32(0x34, header_size - 0x34); 60 | buffer_pos = buffer + header_size; 61 | } 62 | 63 | //! VGM_Writer destructor 64 | VGM_Writer::~VGM_Writer() 65 | { 66 | poke32(0x04, get_position() - 4); 67 | if(filename.size() && completed) 68 | { 69 | std::cout << "Writing " << filename << "...\n"; 70 | auto output = std::ofstream(filename, std::ios::binary); 71 | output.write((char*)buffer, get_position()); 72 | std::free(buffer); 73 | } 74 | } 75 | 76 | //! Write a command. 77 | void VGM_Writer::write(uint8_t command, uint16_t port, uint16_t reg, uint16_t data) 78 | { 79 | reserve(100); 80 | add_delay(); 81 | 82 | if(command == 0xe1) // C352 83 | { 84 | *buffer_pos++ = command; 85 | *buffer_pos++ = reg>>8; 86 | *buffer_pos++ = reg&0xff; 87 | *buffer_pos++ = data>>8; 88 | *buffer_pos++ = data&0xff; 89 | } 90 | else if(command > 0x50 && command < 0x60) // yamaha chips 91 | { 92 | *buffer_pos++ = command + port; 93 | *buffer_pos++ = reg; 94 | *buffer_pos++ = data; 95 | } 96 | else if(command == 0x50 || command == 0x30) // PSG 97 | { 98 | *buffer_pos++ = command; 99 | *buffer_pos++ = data; 100 | } 101 | else // following is for D0-D6 commands... 102 | { 103 | *buffer_pos++ = command; 104 | *buffer_pos++ = port; 105 | *buffer_pos++ = (reg&0xff); 106 | *buffer_pos++ = (data&0xff); 107 | } 108 | } 109 | 110 | //! DAC stream setup 111 | void VGM_Writer::dac_setup(uint8_t sid, uint8_t chip_id, uint32_t port, uint32_t reg, uint8_t db_id) 112 | { 113 | reserve(100); 114 | add_delay(); 115 | *buffer_pos++ = 0x90; 116 | *buffer_pos++ = sid; 117 | *buffer_pos++ = chip_id; 118 | *buffer_pos++ = port; 119 | *buffer_pos++ = reg; 120 | *buffer_pos++ = 0x91; 121 | *buffer_pos++ = sid; 122 | *buffer_pos++ = db_id; 123 | *buffer_pos++ = 1; 124 | *buffer_pos++ = 0; 125 | } 126 | 127 | //! DAC stream playback 128 | void VGM_Writer::dac_start(uint8_t sid, uint32_t start, uint32_t length, uint32_t freq) 129 | { 130 | reserve(100); 131 | add_delay(); 132 | *buffer_pos++ = 0x92; 133 | *buffer_pos++ = sid; 134 | *buffer_pos++ = freq & 0xff; 135 | *buffer_pos++ = freq >> 8; 136 | *buffer_pos++ = freq >> 16; 137 | *buffer_pos++ = freq >> 24; 138 | *buffer_pos++ = 0x93; 139 | *buffer_pos++ = sid; 140 | *buffer_pos++ = start & 0xff; 141 | *buffer_pos++ = start >> 8; 142 | *buffer_pos++ = start >> 16; 143 | *buffer_pos++ = start >> 24; 144 | *buffer_pos++ = 0x01; 145 | *buffer_pos++ = length & 0xff; 146 | *buffer_pos++ = length >> 8; 147 | *buffer_pos++ = length >> 16; 148 | *buffer_pos++ = length >> 24; 149 | } 150 | 151 | //! DAC stream playback 152 | void VGM_Writer::dac_stop(uint8_t sid) 153 | { 154 | reserve(100); 155 | add_delay(); 156 | *buffer_pos++ = 0x94; 157 | *buffer_pos++ = sid; 158 | } 159 | 160 | //! Sets the loop point 161 | void VGM_Writer::set_loop() 162 | { 163 | add_delay(); 164 | loop_sample = sample_count; 165 | poke32(0x1c, get_position()-0x1c); 166 | } 167 | 168 | //! Adds a datablock. 169 | /*! 170 | * NOTE: mask argument is currently ignored. So leave it -1. 171 | */ 172 | void VGM_Writer::datablock(uint8_t dbtype, uint32_t dbsize, const uint8_t* db, uint32_t maxsize, uint32_t mask, uint32_t flags, uint32_t offset) 173 | { 174 | reserve(dbsize + 100); 175 | add_delay(); 176 | add_datablockcmd(dbtype, dbsize | flags, maxsize, offset); 177 | for(uint32_t i = 0; i < dbsize; i++) 178 | { 179 | *buffer_pos++ = *db++; 180 | } 181 | } 182 | 183 | //! Adds a delay 184 | void VGM_Writer::delay(double count) 185 | { 186 | curr_delay += count; 187 | } 188 | 189 | //! Add a VGM stop command (0x66) 190 | void VGM_Writer::stop() 191 | { 192 | add_delay(); 193 | *buffer_pos++ = 0x66; 194 | poke32(0x18, sample_count); 195 | if(loop_sample) 196 | poke32(0x20, sample_count - loop_sample); 197 | completed = 1; 198 | } 199 | 200 | //! Write a long to the vgm buffer 201 | void VGM_Writer::poke32(uint32_t offset, uint32_t data) 202 | { 203 | *(uint32_t*)(buffer+offset) = data; 204 | } 205 | 206 | //! Write a short to the vgm buffer 207 | void VGM_Writer::poke16(uint32_t offset, uint16_t data) 208 | { 209 | *(uint16_t*)(buffer+offset) = data; 210 | } 211 | 212 | //! Write a char to the vgm buffer 213 | void VGM_Writer::poke8(uint32_t offset, uint8_t data) 214 | { 215 | *(uint8_t*)(buffer+offset) = data; 216 | } 217 | 218 | //! Write GD3 tags. Only call this after calling VGM_Writer::stop(). 219 | void VGM_Writer::write_tag(const VGM_Tag& tag) 220 | { 221 | reserve(2000); 222 | std::time_t t; 223 | std::time(&t); 224 | std::tm* tm = std::localtime(&t); 225 | char ts[32]; 226 | std::strftime(ts,32,"%Y-%m-%d %H:%M:%S",tm); 227 | std::string tracknotes = "ctrmml (built " __DATE__ " " __TIME__ ")"; 228 | poke32(0x14, get_position()-0x14); 229 | my_memcpy((uint8_t*)"Gd3 \x00\x01\x00\x00", 8); 230 | uint32_t len_s = get_position(); 231 | buffer_pos += 4; 232 | 233 | add_gd3(tag.title.c_str()); // Track name 234 | add_gd3(tag.title_j.c_str()); // Track name (native) 235 | add_gd3(tag.game.c_str()); // Game name 236 | add_gd3(tag.game_j.c_str()); // Game name (native) 237 | add_gd3(tag.system.c_str()); // System name 238 | add_gd3(tag.system_j.c_str()); // System name (native) 239 | add_gd3(tag.author.c_str()); // Author name 240 | add_gd3(tag.author_j.c_str()); // Author name (native) 241 | if(tag.date.size()) 242 | add_gd3(tag.date.c_str()); // Date 243 | else 244 | add_gd3(ts); 245 | add_gd3(tag.creator.c_str()); // Pack author 246 | if(tag.notes.size()) 247 | add_gd3(tag.notes.c_str()); // Notes 248 | else 249 | add_gd3(tracknotes.c_str()); 250 | 251 | poke32(len_s, get_position()-len_s-4); // length 252 | } 253 | 254 | //! Gets the current buffer position 255 | uint32_t VGM_Writer::get_position() const 256 | { 257 | return buffer_pos - buffer; 258 | } 259 | 260 | //! Gets the current sample position 261 | uint32_t VGM_Writer::get_sample_count() const 262 | { 263 | return sample_count; 264 | } 265 | 266 | //! Gets the sample count at the loop position 267 | uint32_t VGM_Writer::get_loop_sample() const 268 | { 269 | return loop_sample; 270 | } 271 | 272 | //! Return a long from the vgm buffer 273 | uint32_t VGM_Writer::peek32(uint32_t offset) const 274 | { 275 | return *(uint32_t*)(buffer+offset); 276 | } 277 | 278 | //! Return a short from the vgm buffer 279 | uint16_t VGM_Writer::peek16(uint32_t offset) const 280 | { 281 | return *(uint16_t*)(buffer+offset); 282 | } 283 | 284 | //! Return a char from the vgm buffer 285 | uint8_t VGM_Writer::peek8(uint32_t offset) const 286 | { 287 | return *(uint8_t*)(buffer+offset); 288 | } 289 | 290 | //! Get the VGM buffer. 291 | std::vector VGM_Writer::get_buffer() 292 | { 293 | if(completed) 294 | poke32(0x04, get_position() - 4); 295 | 296 | return std::vector(buffer, buffer + get_position()); 297 | } 298 | 299 | void VGM_Writer::my_memcpy(void* src, int size) 300 | { 301 | std::memcpy(buffer_pos,src,size); 302 | buffer_pos += size; 303 | } 304 | 305 | void VGM_Writer::add_datablockcmd(uint8_t dtype, uint32_t size, uint32_t romsize, uint32_t offset) 306 | { 307 | *buffer_pos++ = 0x67; 308 | *buffer_pos++ = 0x66; 309 | *buffer_pos++ = dtype; 310 | size += 8; 311 | my_memcpy((uint32_t*)&size,4); 312 | my_memcpy((uint32_t*)&romsize,4); 313 | my_memcpy((uint32_t*)&offset,4); 314 | } 315 | 316 | void VGM_Writer::add_delay() 317 | { 318 | if(curr_delay >= 1) 319 | { 320 | int delay = std::floor(curr_delay); 321 | curr_delay -= delay; 322 | 323 | sample_count += delay; 324 | int commandcount = delay/65535; 325 | uint16_t finalcommand = delay%65535; 326 | 327 | while(commandcount) 328 | { 329 | *buffer_pos++ = 0x61; 330 | *buffer_pos++ = 0xff; 331 | *buffer_pos++ = 0xff; 332 | commandcount--; 333 | } 334 | 335 | if(finalcommand > 16) 336 | { 337 | *buffer_pos++ = 0x61; 338 | my_memcpy(&finalcommand,2); 339 | } 340 | else if(finalcommand > 0) 341 | { 342 | *buffer_pos++ = 0x70 + finalcommand-1; 343 | } 344 | } 345 | } 346 | 347 | void VGM_Writer::add_gd3(const char* s) 348 | { 349 | long l; 350 | long max=256; 351 | #if defined(_WIN32) 352 | l = MultiByteToWideChar(CP_UTF8,0,s,-1,(wchar_t*)buffer_pos,max); 353 | if(l == 0) 354 | { 355 | std::cerr << "Something very bad happened. MultiByteToWideChar failed\n"; 356 | buffer_pos += 2; 357 | } 358 | else 359 | { 360 | buffer_pos += l * sizeof(wchar_t); 361 | if(l == max) 362 | buffer_pos += 2; 363 | } 364 | #else 365 | std::u16string u16_conv = 366 | std::wstring_convert, char16_t>{}.from_bytes(s); 367 | const char16_t* source = u16_conv.data(); 368 | while(*source != 0 && max != 0) { 369 | *(char16_t*)buffer_pos = *source; 370 | buffer_pos += 2; 371 | source++; 372 | max--; 373 | } 374 | buffer_pos += 2; // 0x00, 0x00 double null-terminator 375 | #endif 376 | } 377 | 378 | void VGM_Writer::reserve(uint32_t bytes) 379 | { 380 | // resize buffer if needed 381 | while((buffer_alloc - get_position()) < bytes) 382 | { 383 | uint8_t* temp; 384 | temp = (uint8_t*)realloc(buffer,buffer_alloc*2); 385 | if(temp) 386 | { 387 | buffer_alloc *= 2; 388 | buffer_pos = temp+get_position(); 389 | buffer = temp; 390 | } 391 | else 392 | { 393 | throw std::bad_alloc(); 394 | } 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /src/song.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "song.h" 7 | #include "vgm.h" 8 | #include "driver.h" 9 | #include "track.h" 10 | #include "stringf.h" 11 | #include "platform/mdsdrv.h" 12 | 13 | //! Constructs a Song. 14 | Song::Song() 15 | : tag_map() 16 | , track_map() 17 | , ppqn(24) 18 | , platform_command_index(-32768) 19 | { 20 | platform = new MDSDRV_Platform(0); 21 | } 22 | 23 | //! Destructs a Song 24 | Song::~Song() 25 | { 26 | if(platform) 27 | delete platform; 28 | } 29 | 30 | //! Get a reference to the tag map. 31 | Tag_Map& Song::get_tag_map() 32 | { 33 | return tag_map; 34 | } 35 | 36 | //! Check if the tag with the specified key is present 37 | /*! 38 | * \exception std::out_of_range if not found 39 | */ 40 | bool Song::check_tag(const std::string& key) const 41 | { 42 | return tag_map.find(key) != tag_map.end(); 43 | } 44 | 45 | //! Gets the tag with the specified key. 46 | /*! 47 | * \exception std::out_of_range if not found 48 | */ 49 | Tag& Song::get_tag(const std::string& key) 50 | { 51 | return tag_map.at(key); 52 | } 53 | 54 | //! Gets the tag with the specified key, otherwise creates a new tag. 55 | /*! 56 | * If a new tag is created, an entry is added to the 'tag_order' tag. 57 | */ 58 | Tag& Song::get_or_make_tag(const std::string& key) 59 | { 60 | auto lookup = tag_map.find(key); 61 | if(lookup != tag_map.end()) 62 | { 63 | return lookup->second; 64 | } 65 | else 66 | { 67 | tag_map["tag_order"].push_back(key); 68 | return tag_map[key]; 69 | } 70 | } 71 | 72 | //! Gets the tag order list. 73 | Tag& Song::get_tag_order_list() 74 | { 75 | return tag_map["tag_order"]; 76 | } 77 | 78 | //! Gets the first value of the tag with the specified key. 79 | /*! 80 | * \exception std::out_of_range if not found 81 | */ 82 | const std::string& Song::get_tag_front(const std::string& key) const 83 | { 84 | return tag_map.at(key).front(); 85 | } 86 | 87 | //! Gets the first value of the tag with the specified key. Should not throw exceptions. 88 | /*! 89 | * If the tag is not present or empty, an empty string ("") is returned instead. 90 | */ 91 | std::string Song::get_tag_front_safe(const std::string& key) const 92 | { 93 | if(tag_map.count(key) && tag_map.at(key).size()) 94 | return tag_map.at(key).front(); 95 | else 96 | return ""; 97 | } 98 | 99 | //! Appends a value to the tag with the specified key. 100 | /*! 101 | * Appends the value as a new item to the tag. 102 | * 103 | * \param key Tag key. If the key is not found, a new tag is created. 104 | * \param value String that is added to the tag. 105 | * \see set_tag() add_tag_list() 106 | */ 107 | void Song::add_tag(const std::string& key, std::string value) 108 | { 109 | // delete trailing spaces 110 | while(!value.empty() && isspace(value.back())) 111 | value.pop_back(); 112 | get_or_make_tag(key).push_back(value); 113 | } 114 | 115 | //! Set the value to the tag with the specified key. 116 | /*! 117 | * This function is used to write song metadata, such as title, 118 | * platform, ppqn, etc. 119 | * 120 | * Overwrites all previous values that have been written to a tag 121 | * and creates an item containing the whole string. 122 | * 123 | * \param key Tag key. If the key is not found, a new tag is created. 124 | * \param value String that is written to the tag. 125 | * \see add_tag() add_tag_list() 126 | */ 127 | void Song::set_tag(const std::string& key, std::string value) 128 | { 129 | // delete trailing spaces 130 | while(!value.empty() && isspace(value.back())) 131 | value.pop_back(); 132 | Tag& tag = get_or_make_tag(key); 133 | tag.clear(); 134 | tag.push_back(value); 135 | } 136 | 137 | //! Add double-quote-enclosed string 138 | /*! 139 | * \param key Tag key. If the key is not found, a new tag is created. 140 | * \param s First character of the key, not including the first '"'. 141 | */ 142 | static char* add_tag_enclosed(Tag *tag, char* s) 143 | { 144 | // process string first 145 | char *head = s, *tail = s; 146 | while(*head) 147 | { 148 | if(*head == '\\' && *++head) 149 | { 150 | if(*head == 'n') 151 | *head = '\n'; 152 | else if(*head == 't') 153 | *head = '\t'; 154 | } 155 | else if(*head == '"') 156 | { 157 | head++; 158 | break; 159 | } 160 | *tail++ = *head++; 161 | } 162 | *tail++ = 0; 163 | tag->push_back(s); 164 | return head; 165 | } 166 | 167 | //! Append multiple values to the tag with the specified key. 168 | /*! 169 | * \p value contains a string containing an array of values separated 170 | * by spaces or commas. Each value is added as an item in the tag. 171 | * 172 | * A value can be enclosed with quotes to support spaces. 173 | * 174 | * A comma with no preceeding non-space character inserts a blank item. 175 | * 176 | * \param key Tag key. If the key is not found, a new tag is created. 177 | * \param value List of values to add. The list can be separated by 178 | * commas OR spaces, but comma always forces a value to 179 | * be added. 180 | * 181 | * \see add_tag() set_tag() 182 | */ 183 | void Song::add_tag_list(const std::string& key, const std::string& value) 184 | { 185 | Tag *tag = &get_or_make_tag(key); 186 | char *str = strdup(value.c_str()); 187 | char *s = str; 188 | int last_char = 0; 189 | while(*s) 190 | { 191 | char* nexts = strpbrk(s, " \t\r\n\",;"); 192 | char c; 193 | if(nexts == NULL) 194 | { 195 | // full string 196 | tag->push_back(s); 197 | break; 198 | } 199 | c = *nexts; 200 | if(c == '\"') 201 | { 202 | // enclosed string 203 | nexts = add_tag_enclosed(tag, s+1); 204 | last_char = c; 205 | } 206 | else 207 | { 208 | // separator 209 | *nexts++ = 0; 210 | if(strlen(s)) 211 | { 212 | tag->push_back(s); 213 | last_char = c; 214 | } 215 | else if(c == ',') // empty , block adds an empty tag 216 | { 217 | if(last_char == c) 218 | tag->push_back(s); 219 | else 220 | last_char = c; 221 | } 222 | if(c == ';') 223 | break; 224 | } 225 | s = nexts; 226 | } 227 | free(str); 228 | } 229 | 230 | //! Registers a platform command. 231 | /*! 232 | * The command can later be retrieved with get_platform_command(). 233 | * 234 | * \param param Command id. Set to -1 to use a sequential id. 235 | * \param value Command arguments. 236 | * Parsed by Player::parse_platfrom_event() 237 | * \return The tag's command id. 238 | * 239 | * \see Player::parse_platform_event() 240 | */ 241 | int16_t Song::register_platform_command(int16_t param, const std::string& value) 242 | { 243 | if(param == -1) 244 | param = platform_command_index++; 245 | add_tag_list(stringf("cmd_%d", param), value); 246 | return param; 247 | } 248 | 249 | //! Gets the registered platform command with the specified id. 250 | /*! 251 | * \param param Command id. 252 | * \return Reference to the tag with the specified id. 253 | * \exception std::out_of_range if not found 254 | */ 255 | Tag& Song::get_platform_command(int16_t param) 256 | { 257 | return tag_map.at(stringf("cmd_%d", param)); 258 | } 259 | 260 | //! Get a reference to the track map. 261 | Track_Map& Song::get_track_map() 262 | { 263 | return track_map; 264 | } 265 | 266 | //! Get a reference to the track with the specified id. 267 | /*! 268 | * \exception std::out_of_range if not found. Use make_track() to create track if needed. 269 | */ 270 | Track& Song::get_track(uint16_t id) 271 | { 272 | return track_map.at(id); 273 | } 274 | 275 | //! Get a reference to the track with the specified id. 276 | /*! 277 | * If the track is not found, a new one is created. 278 | */ 279 | Track& Song::make_track(uint16_t id) 280 | { 281 | if(!track_map.count(id)) 282 | return track_map.emplace(id, Track(ppqn)).first->second; 283 | else 284 | return track_map.at(id); 285 | } 286 | 287 | //! Gets the global Pulses per quarter note (PPQN) setting. 288 | uint16_t Song::get_ppqn() const 289 | { 290 | return ppqn; 291 | } 292 | 293 | //! Sets the global Pulses per quarter note (PPQN) setting. 294 | void Song::set_ppqn(uint16_t new_ppqn) 295 | { 296 | ppqn = new_ppqn; 297 | } 298 | 299 | //! Gets the platform 300 | const Platform* Song::get_platform() const 301 | { 302 | return platform; 303 | } 304 | 305 | //! Sets the platform 306 | /*! 307 | * \return 1 on failure 308 | * \return 0 on success 309 | */ 310 | bool Song::set_platform(const std::string& key) 311 | { 312 | if(iequal(key, "megadrive")) 313 | { 314 | delete platform; 315 | platform = new MDSDRV_Platform(0); 316 | } 317 | else if(iequal(key, "mdsdrv")) 318 | { 319 | delete platform; 320 | platform = new MDSDRV_Platform(2); 321 | } 322 | //type = key; 323 | return 0; 324 | } 325 | 326 | std::shared_ptr Platform::get_driver(unsigned int rate, VGM_Interface* vgm_interface) const 327 | { 328 | throw std::logic_error("No available driver"); 329 | } 330 | 331 | const Platform::Format_List& Platform::get_export_formats() const 332 | { 333 | static const Platform::Format_List out = {{"vgm", "VGM"}}; 334 | return out; 335 | } 336 | 337 | std::vector Platform::get_export_data(Song& song, int format) const 338 | { 339 | if(format == 0) 340 | { 341 | return vgm_export(song); 342 | } 343 | else 344 | { 345 | throw std::logic_error("no such exporter"); 346 | } 347 | } 348 | 349 | static inline std::string safe_get_tag(Song& song, const std::string& tagname) 350 | { 351 | if(song.get_tag_map()[tagname].size()) 352 | return song.get_tag_map()[tagname].front(); 353 | else 354 | return ""; 355 | } 356 | 357 | static inline VGM_Tag get_tags(Song& song) 358 | { 359 | VGM_Tag tag; 360 | Tag_Map tag_map = song.get_tag_map(); 361 | 362 | tag.title = safe_get_tag(song,"#title"); 363 | tag.title_j = safe_get_tag(song,"#titlej"); 364 | tag.author = safe_get_tag(song,"#composer"); 365 | tag.author_j = safe_get_tag(song,"#composerj"); 366 | tag.system = safe_get_tag(song,"#system"); 367 | tag.system_j = safe_get_tag(song,"#systemj"); 368 | tag.game = safe_get_tag(song,"#game"); 369 | tag.game_j = safe_get_tag(song,"#gamej"); 370 | tag.creator = safe_get_tag(song,"#programmer"); 371 | tag.notes = safe_get_tag(song,"#comment"); 372 | tag.date = safe_get_tag(song,"#vgmdate"); 373 | 374 | // Fallback tags 375 | if(!tag.author.size()) 376 | tag.creator = safe_get_tag(song,"#author"); 377 | if(!tag.creator.size()) 378 | tag.creator = safe_get_tag(song,"#programer"); 379 | if(!tag.creator.size()) 380 | tag.creator = tag.author; 381 | 382 | return tag; 383 | } 384 | 385 | std::vector Platform::vgm_export(Song& song, unsigned int max_seconds, unsigned int num_loops) const 386 | { 387 | VGM_Writer vgm("", 0x61, 0x100); 388 | auto driver = song.get_platform()->get_driver(44100, &vgm); 389 | unsigned long max_time = max_seconds * 44100; 390 | driver->play_song(song); 391 | double elapsed_time = 0; 392 | double delta = 0; 393 | bool looped_or_finished = 0; 394 | while(elapsed_time < max_time) 395 | { 396 | vgm.delay(delta); 397 | delta = driver->play_step(); 398 | elapsed_time += delta; 399 | if(!driver->is_playing() || driver->get_loop_count() >= (int)num_loops) 400 | { 401 | looped_or_finished = 1; 402 | break; 403 | } 404 | } 405 | if(!looped_or_finished) 406 | vgm.delay(max_time-elapsed_time); 407 | vgm.stop(); 408 | vgm.write_tag(get_tags(song)); 409 | return vgm.get_buffer(); 410 | } 411 | -------------------------------------------------------------------------------- /src/mml_input.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "mml_input.h" 5 | #include "song.h" 6 | #include "track.h" 7 | #include "stringf.h" 8 | 9 | unsigned int MML_Input::read_duration() 10 | { 11 | int duration = 0, dot; 12 | try 13 | { 14 | int c = get(); 15 | if(c == ':') 16 | { 17 | duration = get_num(); 18 | } 19 | else 20 | { 21 | unget(c); 22 | int div = get_num(); 23 | if(div < 1) 24 | parse_error("illegal duration"); 25 | duration = track->get_measure_len() / div; 26 | } 27 | if(duration < 0) 28 | parse_error("illegal duration"); 29 | } 30 | catch(std::invalid_argument&) 31 | { 32 | duration = track->get_duration(); 33 | } 34 | dot = duration>>1; 35 | while(1) 36 | { 37 | if(get() == '.') 38 | { 39 | duration += dot; 40 | dot >>= 1; 41 | } 42 | else 43 | { 44 | unget(); 45 | break; 46 | } 47 | } 48 | return (unsigned)duration; 49 | } 50 | 51 | int MML_Input::read_parameter(int default_parameter) 52 | { 53 | try 54 | { 55 | return get_num(); 56 | } 57 | catch(std::invalid_argument&) 58 | { 59 | return default_parameter; 60 | } 61 | } 62 | 63 | int MML_Input::expect_parameter() 64 | { 65 | try 66 | { 67 | return get_num(); 68 | } 69 | catch(std::invalid_argument&) 70 | { 71 | parse_error("missing parameter"); 72 | return 0; 73 | } 74 | } 75 | 76 | int MML_Input::expect_signed() 77 | { 78 | // is this function necessary anymore? 79 | return expect_parameter(); 80 | } 81 | 82 | int MML_Input::read_note(int c) 83 | { 84 | // would be nice to support key signatures... 85 | static const int note_values[8] = {9,11,0,2,4,5,7,11}; // abcdefgh 86 | int8_t val = c - 'a'; 87 | int8_t sig = 0; 88 | // linear if in drum mode 89 | if(!track->in_drum_mode()) 90 | { 91 | val = note_values[val & 7]; 92 | sig = track->get_key_signature(c); 93 | } 94 | // sharps/flats 95 | c = get(); 96 | if(c == '+') 97 | sig = 1; 98 | else if(c == '-') 99 | sig = -1; 100 | else if(c == '=') 101 | sig = 0; 102 | else 103 | unget(c); 104 | return val + sig; 105 | } 106 | 107 | //! Platform-exclusive messages (' ...') 108 | void MML_Input::platform_exclusive() 109 | { 110 | std::string str = ""; 111 | char c = get(); 112 | while(c && c != '\'') 113 | { 114 | str.push_back(c); 115 | c = get(); 116 | } 117 | if(c != '\'') 118 | parse_error("unterminated platform-exclusive message"); 119 | int16_t param = get_song().register_platform_command(-1, str); 120 | track->add_event(Event::PLATFORM, param); 121 | } 122 | 123 | void MML_Input::mml_slur() 124 | { 125 | if(track->add_slur()) 126 | parse_warning("slur may not affect articulation of previous note"); 127 | } 128 | 129 | void MML_Input::mml_reverse_rest(int duration) 130 | { 131 | try 132 | { 133 | track->reverse_rest(duration); 134 | } 135 | catch (std::domain_error&) 136 | { 137 | parse_error("unable to backtrack"); 138 | } 139 | catch (std::length_error&) 140 | { 141 | parse_error("previous note is not long enough"); 142 | } 143 | } 144 | 145 | void MML_Input::mml_grace() 146 | { 147 | int c = read_note(get_token()); 148 | int duration = read_duration(); 149 | mml_reverse_rest(duration); 150 | track->add_note(c, duration); 151 | } 152 | 153 | void MML_Input::mml_transpose() 154 | { 155 | int c = get_token(); 156 | if(c == '_') 157 | { 158 | track->add_event(Event::TRANSPOSE_REL, expect_signed()); 159 | } 160 | else if(c == '{') 161 | { 162 | try 163 | { 164 | // Read key signature 165 | std::string str = ""; 166 | do 167 | { 168 | c = get(); 169 | if(c && c != '}' && !std::isspace(c)) 170 | str.push_back(c); 171 | } 172 | while(c && c != '}'); 173 | track->set_key_signature(str.c_str()); 174 | } 175 | catch(std::invalid_argument&) 176 | { 177 | parse_error("invalid key signature"); 178 | } 179 | } 180 | else 181 | { 182 | unget(); 183 | track->add_event(Event::TRANSPOSE, expect_signed()); 184 | } 185 | } 186 | 187 | //! mucom88 style echo command 188 | void MML_Input::mml_echo() 189 | { 190 | int c = get_token(); 191 | if(c == '=') 192 | { 193 | int16_t delay = expect_parameter(); 194 | if(delay < 0) 195 | delay = -delay; 196 | else 197 | track->clear_echo_buffer(); 198 | if(get_token() != ',') 199 | parse_error("expected ','"); 200 | uint16_t volume = expect_parameter(); 201 | track->set_echo(delay, volume); 202 | } 203 | else 204 | { 205 | unget(); 206 | track->add_echo(read_duration()); 207 | } 208 | } 209 | 210 | //! combination command that allows for two Event::Type depending on 211 | //! if a sign prefix is found. 212 | void MML_Input::event_relative(Event::Type type, Event::Type subtype) 213 | { 214 | int c = get_token(); 215 | if(c == '+' || c == '-') 216 | type = subtype; 217 | if(c != '+') 218 | unget(); 219 | if(type == Event::INVALID) 220 | parse_error("parameter must be relative (+ or - prefix)"); 221 | track->add_event(type, expect_parameter()); 222 | } 223 | 224 | // Basic MML command parser. Commands defined here are the ones 225 | // unlikely to change in different MML dialects. 226 | bool MML_Input::mml_basic() 227 | { 228 | int c = get_token(); 229 | if(c >= 'a' && c <= 'h') 230 | { 231 | c = read_note(c); 232 | track->add_note(c, read_duration()); 233 | } 234 | else if(c == 'r') 235 | track->add_rest(read_duration()); 236 | else if(c == '^') 237 | track->add_tie(read_duration()); 238 | else if(c == '&') 239 | mml_slur(); 240 | else if(c == 'o') 241 | track->set_octave(expect_parameter() - 1); 242 | else if(c == '<') 243 | track->change_octave(-1); 244 | else if(c == '>') 245 | track->change_octave(1); 246 | else if(c == 'l') 247 | track->set_duration(read_duration()); 248 | else if(c == 'Q') 249 | track->set_quantize(expect_parameter()); 250 | else if(c == 'q') 251 | track->set_early_release(expect_parameter()); 252 | else if(c == 'R') 253 | mml_reverse_rest(read_duration()); 254 | else if(c == '~') 255 | mml_grace(); 256 | else if(c == 'C') 257 | track->set_measure_len(expect_parameter()); 258 | else if(c == 's') 259 | track->set_shuffle(expect_signed()); 260 | else if(c == '\\') 261 | mml_echo(); 262 | else 263 | { 264 | unget(c); 265 | return true; 266 | } 267 | return false; 268 | } 269 | 270 | bool MML_Input::mml_control() 271 | { 272 | int c = get_token(); 273 | if(c == '[') 274 | track->add_event(Event::LOOP_START); 275 | else if(c == '/') 276 | track->add_event(Event::LOOP_BREAK); 277 | else if(c == ']') 278 | track->add_event(Event::LOOP_END, read_parameter(2)); 279 | else if(c == 'L') 280 | track->add_event(Event::SEGNO); 281 | else if(c == '*') 282 | track->add_event(Event::JUMP, expect_parameter()); 283 | else if(c == '\'') 284 | platform_exclusive(); 285 | else 286 | { 287 | unget(c); 288 | return true; 289 | } 290 | return false; 291 | } 292 | 293 | // Dialect specific MML commands 294 | bool MML_Input::mml_envelope() 295 | { 296 | int c = get_token(); 297 | if(c == '@') 298 | track->add_event(Event::INS, expect_parameter()); 299 | else if(c == '_') 300 | mml_transpose(); 301 | else if(c == 'k') // TODO: ktype command to set compile-time transpose? 302 | mml_transpose(); 303 | else if(c == 'K') 304 | track->add_event(Event::DETUNE, expect_signed()); 305 | else if(c == 'v') 306 | track->add_event(Event::VOL, expect_parameter()); 307 | else if(c == '(') 308 | track->add_event(Event::VOL_REL, -read_parameter(1)); 309 | else if(c == ')') 310 | track->add_event(Event::VOL_REL, read_parameter(1)); 311 | else if(c == 'V') 312 | event_relative(Event::VOL_FINE, Event::VOL_FINE_REL); 313 | else if(c == 'p') 314 | track->add_event(Event::PAN, expect_signed()); 315 | else if(c == 'E') 316 | track->add_event(Event::VOL_ENVELOPE, expect_parameter()); 317 | else if(c == 'M') 318 | track->add_event(Event::PITCH_ENVELOPE, expect_parameter()); 319 | else if(c == 'P') 320 | track->add_event(Event::PAN_ENVELOPE, expect_parameter()); 321 | else if(c == 'G') 322 | track->add_event(Event::PORTAMENTO, expect_parameter()); 323 | else if(c == 'D') 324 | track->set_drum_mode(expect_parameter()); 325 | else if(c == 't') 326 | track->add_event(Event::TEMPO_BPM, expect_parameter()); 327 | else if(c == 'T') 328 | track->add_event(Event::TEMPO, expect_parameter()); 329 | else 330 | { 331 | unget(c); 332 | return true; 333 | } 334 | return false; 335 | } 336 | 337 | void MML_Input::conditional_block_begin() 338 | { 339 | int c; 340 | int offset = track_offset; 341 | conditional_block = 1; 342 | while(offset) 343 | { 344 | do c = get_token(); 345 | while(c != 0 && c != '/' && c != ';'); 346 | if(c != '/') 347 | parse_error("unterminated conditonal block"); 348 | c = 0; 349 | offset--; 350 | } 351 | } 352 | 353 | void MML_Input::conditional_block_end(int c) 354 | { 355 | while(c != 0 && c != '}' && c != ';') 356 | c = get_token(); 357 | if(c != '}') 358 | parse_error("unterminated conditional block"); 359 | conditional_block = 0; 360 | } 361 | 362 | void MML_Input::parse_mml_track() 363 | { 364 | int c; 365 | while(1) 366 | { 367 | // could this be rewritten using a command map? 368 | // it would be nice to at least split the MML commands 369 | // into a few functions... 370 | c = get_token(); 371 | if(c == '|') // Separator 372 | continue; 373 | else if(c == ';') // Comment 374 | break; 375 | else if((c == '/' || c == '}') && conditional_block) 376 | conditional_block_end(c); 377 | else if(c == '{' && !conditional_block) 378 | conditional_block_begin(); 379 | else if(c == '%') 380 | track->add_event(Event::PLATFORM, expect_parameter()); 381 | else if(c == 0) 382 | return; 383 | else 384 | { 385 | unget(c); 386 | // Set reference 387 | track->set_reference(get_reference()); 388 | // Here i can read a list of command handlers and call them 389 | // until one returns false 390 | if(mml_basic() == false) 391 | continue; 392 | else if(mml_control() == false) 393 | continue; 394 | else if(mml_envelope() == false) 395 | continue; 396 | parse_error("unknown MML command"); 397 | } 398 | } 399 | } 400 | 401 | void MML_Input::parse_mml() 402 | { 403 | unsigned long col = tell(); 404 | for(unsigned int i = 0; i < track_list.size(); i++) 405 | { 406 | seek(col); 407 | track_id = track_list[i]; 408 | track_offset = i; 409 | track = &get_song().make_track(track_id); 410 | conditional_block = false; 411 | parse_mml_track(); 412 | if(conditional_block) 413 | parse_error("unterminated conditional block"); 414 | } 415 | } 416 | 417 | void MML_Input::parse_tag() 418 | { 419 | //std::cout << "parse_tag() "<= 'A' && c <= 'Z') 442 | return c - 'A'; 443 | else if(std::isdigit(c)) 444 | return c - '0' + 26; 445 | else if(c == '*') 446 | return get_num(); 447 | // No match 448 | unget(c); 449 | return -1; 450 | } 451 | 452 | MML_Input::MML_Input(Song* song) 453 | : Line_Input(song), 454 | track_id(0), 455 | track_offset(0), 456 | track_list(0), 457 | last_cmd(nullptr), 458 | conditional_block(0) 459 | { 460 | // Perhaps the initial state of mml_input should be track A. 461 | // Or maybe it can be initialized by a previous MML_Input during 462 | // an "include" command. 463 | } 464 | 465 | MML_Input::~MML_Input() 466 | { 467 | } 468 | 469 | //! Get a list of tracks that were affected by the previous read_line() 470 | MML_Input::Track_Position_Map MML_Input::get_track_map() 471 | { 472 | Track_Position_Map out = {}; 473 | if(last_cmd == &MML_Input::parse_mml) 474 | { 475 | for(auto && i : track_list) 476 | { 477 | try 478 | { 479 | out[i] = get_song().get_track(i).get_event_count(); 480 | } 481 | catch(std::out_of_range& except) 482 | { 483 | // track doesn't exist; no events added 484 | } 485 | } 486 | } 487 | return out; 488 | } 489 | 490 | void MML_Input::parse_line() 491 | { 492 | int c = get_track_id(); 493 | if(c != -1) 494 | { 495 | // Read track list 496 | track_list.clear(); 497 | do 498 | { 499 | track_list.push_back(c); 500 | c = get_track_id(); 501 | } 502 | while (c != -1); 503 | last_cmd = &MML_Input::parse_mml; 504 | } 505 | else 506 | { 507 | c = get(); 508 | if(c == '#' || c == '@') 509 | { 510 | // This could maybe be more efficient 511 | tag_key.clear(); 512 | do 513 | { 514 | tag_key.push_back(std::tolower(c)); 515 | c = get(); 516 | } 517 | while (c != 0 && !std::isspace(c)); 518 | last_cmd = &MML_Input::parse_tag; 519 | } 520 | else if(c == ';') 521 | { 522 | // Comment 523 | return; 524 | } 525 | else if(!std::isblank(c)) 526 | { 527 | // Not at the end of the line 528 | if(c != 0) 529 | { 530 | parse_error("Expected track or tag identifier"); 531 | } 532 | // At the end of the line, we can stop parsing 533 | return; 534 | } 535 | unget(c); 536 | } 537 | 538 | c = get(); 539 | if(std::isblank(c)) 540 | { 541 | // Skip non blank characters 542 | c = get_token(); 543 | unget(c); 544 | if(c == 0) 545 | return; 546 | if(last_cmd != nullptr) 547 | { 548 | (this->*last_cmd)(); 549 | return; 550 | } 551 | } 552 | return; 553 | } 554 | 555 | -------------------------------------------------------------------------------- /src/unittest/test_track.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "../track.h" 4 | 5 | class Track_Test : public CppUnit::TestFixture 6 | { 7 | CPPUNIT_TEST_SUITE(Track_Test); 8 | CPPUNIT_TEST(test_add_notes); 9 | CPPUNIT_TEST(test_illegal_quantize); 10 | CPPUNIT_TEST(test_add_note_quantize); 11 | CPPUNIT_TEST(test_add_note_early_release); 12 | CPPUNIT_TEST(test_add_note_default_duration); 13 | CPPUNIT_TEST(test_add_note_octave_change); 14 | CPPUNIT_TEST(test_tie_extend_duration); 15 | CPPUNIT_TEST(test_tie_extend_add_tie); 16 | CPPUNIT_TEST(test_tie_extend_add_rest); 17 | CPPUNIT_TEST(test_tie_only_add_tie); 18 | CPPUNIT_TEST(test_tie_only_add_tie2); 19 | CPPUNIT_TEST(test_slur); 20 | CPPUNIT_TEST(test_slur_impossible); 21 | CPPUNIT_TEST(test_reverse_rest_shorten_on_time); 22 | CPPUNIT_TEST(test_reverse_rest_shorten_off_time); 23 | CPPUNIT_TEST_EXCEPTION(test_reverse_rest_overflow, std::length_error); 24 | CPPUNIT_TEST_EXCEPTION(test_reverse_rest_impossible, std::domain_error); 25 | CPPUNIT_TEST(test_get_event_count); 26 | CPPUNIT_TEST(test_key_signature); 27 | CPPUNIT_TEST(test_shuffle); 28 | CPPUNIT_TEST_SUITE_END(); 29 | private: 30 | Track *track; 31 | public: 32 | void setUp() 33 | { 34 | track = new Track(); 35 | } 36 | void tearDown() 37 | { 38 | delete track; 39 | } 40 | // Add notes and verify that they are present in the event list. 41 | void test_add_notes() 42 | { 43 | int i; 44 | std::vector::iterator it; 45 | for(i=0; i<10; i++) 46 | track->add_note(i, 24); 47 | for(i=0, it = track->get_events().begin(); 48 | it != track->get_events().end(); i++, it++) 49 | { 50 | CPPUNIT_ASSERT_EQUAL((int16_t) (i + 12*5),it->param); 51 | CPPUNIT_ASSERT_EQUAL((uint16_t) 24,it->on_time); 52 | } 53 | // whatever 54 | } 55 | // test illegal quantize values 56 | void test_illegal_quantize() 57 | { 58 | CPPUNIT_ASSERT(track->set_quantize(12,8) == -1); 59 | CPPUNIT_ASSERT(track->set_quantize(20) == -1); 60 | CPPUNIT_ASSERT(track->set_quantize(0,0) == -1); 61 | CPPUNIT_ASSERT(track->set_quantize(8) == 0); 62 | CPPUNIT_ASSERT(track->set_quantize(10,20) == 0); 63 | CPPUNIT_ASSERT(track->set_quantize(0) == 0); // special case 64 | } 65 | // Set quantize and add notes and verify that durations are correct. 66 | void test_add_note_quantize() 67 | { 68 | track->set_quantize(6); 69 | track->add_note(1, 24); 70 | track->set_quantize(2); 71 | track->add_note(1, 24); 72 | track->set_quantize(8); 73 | track->add_note(1, 24); 74 | track->set_quantize(4); 75 | track->add_note(1, 24); 76 | track->set_early_release(0); // should cancel 77 | track->add_note(1, 24); 78 | CPPUNIT_ASSERT_EQUAL((uint16_t)18, track->get_event(0).on_time); 79 | CPPUNIT_ASSERT_EQUAL((uint16_t)6, track->get_event(0).off_time); 80 | CPPUNIT_ASSERT_EQUAL((uint16_t)6, track->get_event(1).on_time); 81 | CPPUNIT_ASSERT_EQUAL((uint16_t)18, track->get_event(1).off_time); 82 | CPPUNIT_ASSERT_EQUAL((uint16_t)24, track->get_event(2).on_time); 83 | CPPUNIT_ASSERT_EQUAL((uint16_t)0, track->get_event(2).off_time); 84 | CPPUNIT_ASSERT_EQUAL((uint16_t)12, track->get_event(3).on_time); 85 | CPPUNIT_ASSERT_EQUAL((uint16_t)12, track->get_event(3).off_time); 86 | CPPUNIT_ASSERT_EQUAL((uint16_t)24, track->get_event(4).on_time); 87 | CPPUNIT_ASSERT_EQUAL((uint16_t)0, track->get_event(4).off_time); 88 | } 89 | // Set early release and add notes and verify that durations are correct. 90 | void test_add_note_early_release() 91 | { 92 | track->set_early_release(6); 93 | track->add_note(1, 24); 94 | track->set_early_release(2); 95 | track->add_note(1, 24); 96 | track->set_early_release(8); 97 | track->add_note(1, 24); 98 | track->set_early_release(4); 99 | track->add_note(1, 24); 100 | track->set_early_release(35); 101 | track->add_note(1, 24); 102 | track->set_quantize(8); // should cancel 103 | track->add_note(1, 24); 104 | CPPUNIT_ASSERT_EQUAL((uint16_t)18, track->get_event(0).on_time); 105 | CPPUNIT_ASSERT_EQUAL((uint16_t)6, track->get_event(0).off_time); 106 | CPPUNIT_ASSERT_EQUAL((uint16_t)22, track->get_event(1).on_time); 107 | CPPUNIT_ASSERT_EQUAL((uint16_t)2, track->get_event(1).off_time); 108 | CPPUNIT_ASSERT_EQUAL((uint16_t)16, track->get_event(2).on_time); 109 | CPPUNIT_ASSERT_EQUAL((uint16_t)8, track->get_event(2).off_time); 110 | CPPUNIT_ASSERT_EQUAL((uint16_t)20, track->get_event(3).on_time); 111 | CPPUNIT_ASSERT_EQUAL((uint16_t)4, track->get_event(3).off_time); 112 | CPPUNIT_ASSERT_EQUAL((uint16_t)1, track->get_event(4).on_time); 113 | CPPUNIT_ASSERT_EQUAL((uint16_t)23, track->get_event(4).off_time); 114 | CPPUNIT_ASSERT_EQUAL((uint16_t)24, track->get_event(5).on_time); 115 | CPPUNIT_ASSERT_EQUAL((uint16_t)0, track->get_event(5).off_time); 116 | } 117 | // Set default duration, add notes and verify that durations are correct 118 | void test_add_note_default_duration() 119 | { 120 | track->set_duration(16); 121 | track->add_note(1); 122 | track->set_duration(50); 123 | track->add_note(1); 124 | CPPUNIT_ASSERT_EQUAL((uint16_t)16, track->get_event(0).on_time); 125 | CPPUNIT_ASSERT_EQUAL((uint16_t)50, track->get_event(1).on_time); 126 | } 127 | // Add notes with various octave changes and verify that notes are correct. 128 | void test_add_note_octave_change() 129 | { 130 | track->set_octave(0); 131 | track->add_note(0); 132 | CPPUNIT_ASSERT_EQUAL((int16_t)0, track->get_event(0).param); 133 | track->set_octave(1); 134 | track->add_note(0); 135 | CPPUNIT_ASSERT_EQUAL((int16_t)12, track->get_event(1).param); 136 | track->change_octave(1); 137 | track->add_note(0); 138 | CPPUNIT_ASSERT_EQUAL((int16_t)24, track->get_event(2).param); 139 | track->change_octave(-2); 140 | track->add_note(0); 141 | CPPUNIT_ASSERT_EQUAL((int16_t)0, track->get_event(3).param); 142 | } 143 | // Add a tie and verify that the duration of the previous note is changed. 144 | void test_tie_extend_duration() 145 | { 146 | track->set_quantize(8); 147 | track->add_note(0,24); 148 | track->add_tie(24); 149 | CPPUNIT_ASSERT_EQUAL((uint16_t)48, track->get_event(0).on_time); 150 | CPPUNIT_ASSERT_EQUAL((uint16_t)0, track->get_event(0).off_time); 151 | track->set_quantize(4); 152 | track->add_note(0,24); 153 | track->add_tie(24); 154 | CPPUNIT_ASSERT_EQUAL((uint16_t)24, track->get_event(1).on_time); 155 | CPPUNIT_ASSERT_EQUAL((uint16_t)24, track->get_event(1).off_time); 156 | } 157 | // Add note, then an unrelated event, then a tie and verify that a new tie event is added. 158 | void test_tie_extend_add_tie() 159 | { 160 | track->set_quantize(8); 161 | track->add_note(0,24); 162 | track->add_event(Event::VOL, 10); // type doesn't matter 163 | track->add_tie(24); 164 | CPPUNIT_ASSERT_EQUAL((Event::Type)Event::TIE, track->get_event(2).type); 165 | CPPUNIT_ASSERT_EQUAL((uint16_t)24, track->get_event(0).on_time); 166 | CPPUNIT_ASSERT_EQUAL((uint16_t)24, track->get_event(2).on_time); 167 | } 168 | // Add note, then an unrelated event, then a tie and verify that a new rest is added. 169 | void test_tie_extend_add_rest() 170 | { 171 | track->set_quantize(2); 172 | track->add_note(0,24); 173 | track->add_event(Event::VOL, 10); // type doesn't matter 174 | track->add_tie(24); 175 | CPPUNIT_ASSERT_EQUAL((Event::Type)Event::REST, track->get_event(2).type); 176 | CPPUNIT_ASSERT_EQUAL((uint16_t)12, track->get_event(0).on_time); 177 | CPPUNIT_ASSERT_EQUAL((uint16_t)12, track->get_event(0).off_time); 178 | CPPUNIT_ASSERT_EQUAL((uint16_t)24, track->get_event(2).off_time); 179 | } 180 | // Add an unrelated event, then a tie and verify that a new tie is added. 181 | void test_tie_only_add_tie2() 182 | { 183 | track->set_quantize(8); 184 | track->add_event(Event::VOL, 10); // type doesn't matter 185 | track->add_tie(24); 186 | CPPUNIT_ASSERT_EQUAL((Event::Type)Event::TIE, track->get_event(1).type); 187 | CPPUNIT_ASSERT_EQUAL((uint16_t)24, track->get_event(1).on_time); 188 | } 189 | // Add just a tie and verify that a new tie is added. 190 | void test_tie_only_add_tie() 191 | { 192 | track->set_quantize(8); 193 | track->add_tie(24); 194 | CPPUNIT_ASSERT_EQUAL((Event::Type)Event::TIE, track->get_event(0).type); 195 | CPPUNIT_ASSERT_EQUAL((uint16_t)24, track->get_event(0).on_time); 196 | } 197 | // Add slur and verify that the articulation of the previous note is legato. 198 | void test_slur() 199 | { 200 | track->set_quantize(4); 201 | track->add_note(0,24); 202 | track->add_slur(); 203 | track->add_note(1,24); 204 | CPPUNIT_ASSERT_EQUAL((uint16_t)24, track->get_event(0).on_time); 205 | CPPUNIT_ASSERT_EQUAL((uint16_t)0, track->get_event(0).off_time); 206 | } 207 | // Add slur and verify that the function returns error if it is impossible to set the previous note 208 | // to legato. 209 | void test_slur_impossible() 210 | { 211 | track->set_quantize(4); 212 | track->add_note(0,24); 213 | track->add_event(Event::REST); 214 | CPPUNIT_ASSERT(track->add_slur() == -1); 215 | } 216 | // Add a reverse rest and verify that the previous note was shortened. 217 | void test_reverse_rest_shorten_on_time() 218 | { 219 | track->set_quantize(8); 220 | track->add_note(0,24); 221 | track->reverse_rest(10); 222 | CPPUNIT_ASSERT_EQUAL((uint16_t)14, track->get_event(0).on_time); 223 | track->set_quantize(4); 224 | track->add_note(0,24); 225 | track->reverse_rest(20); 226 | CPPUNIT_ASSERT_EQUAL((uint16_t)4, track->get_event(1).on_time); 227 | CPPUNIT_ASSERT_EQUAL((uint16_t)0, track->get_event(1).off_time); 228 | } 229 | // Add a reverse rest and verify that the previous note was shortened. 230 | void test_reverse_rest_shorten_off_time() 231 | { 232 | track->set_quantize(2); 233 | track->add_note(0,24); 234 | track->reverse_rest(10); 235 | CPPUNIT_ASSERT_EQUAL((uint16_t)8, track->get_event(0).off_time); 236 | } 237 | // Attempt to add a reverse rest where it is impossible 238 | void test_reverse_rest_impossible() 239 | { 240 | track->add_event(Event::VOL,12); // just some dummy events 241 | track->add_event(Event::INS,34); 242 | track->add_event(Event::PAN,56); 243 | track->reverse_rest(48); // throw std::domain_error 244 | } 245 | void test_reverse_rest_overflow() 246 | { 247 | track->add_note(0,24); 248 | track->reverse_rest(48); // throw std::length_error 249 | } 250 | void test_get_event_count() 251 | { 252 | track->add_event(Event::VOL,12); // just some dummy events 253 | track->add_event(Event::INS,34); 254 | track->add_event(Event::PAN,56); 255 | unsigned long expected = 3; 256 | CPPUNIT_ASSERT_EQUAL(expected, track->get_event_count()); 257 | } 258 | void test_key_signature() 259 | { 260 | // set key to A major 261 | track->set_key_signature("A"); 262 | CPPUNIT_ASSERT_EQUAL((int8_t)1, track->get_key_signature('c')); 263 | CPPUNIT_ASSERT_EQUAL((int8_t)0, track->get_key_signature('d')); 264 | CPPUNIT_ASSERT_EQUAL((int8_t)0, track->get_key_signature('e')); 265 | CPPUNIT_ASSERT_EQUAL((int8_t)1, track->get_key_signature('f')); 266 | CPPUNIT_ASSERT_EQUAL((int8_t)1, track->get_key_signature('g')); 267 | CPPUNIT_ASSERT_EQUAL((int8_t)0, track->get_key_signature('a')); 268 | CPPUNIT_ASSERT_EQUAL((int8_t)0, track->get_key_signature('b')); 269 | //remove the c and f sharp 270 | track->set_key_signature("=cf"); 271 | CPPUNIT_ASSERT_EQUAL((int8_t)0, track->get_key_signature('c')); 272 | CPPUNIT_ASSERT_EQUAL((int8_t)0, track->get_key_signature('d')); 273 | CPPUNIT_ASSERT_EQUAL((int8_t)0, track->get_key_signature('e')); 274 | CPPUNIT_ASSERT_EQUAL((int8_t)0, track->get_key_signature('f')); 275 | CPPUNIT_ASSERT_EQUAL((int8_t)1, track->get_key_signature('g')); 276 | CPPUNIT_ASSERT_EQUAL((int8_t)0, track->get_key_signature('a')); 277 | CPPUNIT_ASSERT_EQUAL((int8_t)0, track->get_key_signature('b')); 278 | //add them back 279 | track->set_key_signature("+cf"); 280 | CPPUNIT_ASSERT_EQUAL((int8_t)1, track->get_key_signature('c')); 281 | CPPUNIT_ASSERT_EQUAL((int8_t)0, track->get_key_signature('d')); 282 | CPPUNIT_ASSERT_EQUAL((int8_t)0, track->get_key_signature('e')); 283 | CPPUNIT_ASSERT_EQUAL((int8_t)1, track->get_key_signature('f')); 284 | CPPUNIT_ASSERT_EQUAL((int8_t)1, track->get_key_signature('g')); 285 | CPPUNIT_ASSERT_EQUAL((int8_t)0, track->get_key_signature('a')); 286 | CPPUNIT_ASSERT_EQUAL((int8_t)0, track->get_key_signature('b')); 287 | // set key to Bb minor 288 | track->set_key_signature("b-"); 289 | CPPUNIT_ASSERT_EQUAL((int8_t)0, track->get_key_signature('c')); 290 | CPPUNIT_ASSERT_EQUAL((int8_t)-1, track->get_key_signature('d')); 291 | CPPUNIT_ASSERT_EQUAL((int8_t)-1, track->get_key_signature('e')); 292 | CPPUNIT_ASSERT_EQUAL((int8_t)0, track->get_key_signature('f')); 293 | CPPUNIT_ASSERT_EQUAL((int8_t)-1, track->get_key_signature('g')); 294 | CPPUNIT_ASSERT_EQUAL((int8_t)-1, track->get_key_signature('a')); 295 | CPPUNIT_ASSERT_EQUAL((int8_t)-1, track->get_key_signature('b')); 296 | // add a flat to C and F 297 | track->set_key_signature("-cf"); 298 | CPPUNIT_ASSERT_EQUAL((int8_t)-1, track->get_key_signature('c')); 299 | CPPUNIT_ASSERT_EQUAL((int8_t)-1, track->get_key_signature('d')); 300 | CPPUNIT_ASSERT_EQUAL((int8_t)-1, track->get_key_signature('e')); 301 | CPPUNIT_ASSERT_EQUAL((int8_t)-1, track->get_key_signature('f')); 302 | CPPUNIT_ASSERT_EQUAL((int8_t)-1, track->get_key_signature('g')); 303 | CPPUNIT_ASSERT_EQUAL((int8_t)-1, track->get_key_signature('a')); 304 | CPPUNIT_ASSERT_EQUAL((int8_t)-1, track->get_key_signature('b')); 305 | // try resetting and setting at the same time 306 | track->set_key_signature("=d+f"); 307 | CPPUNIT_ASSERT_EQUAL((int8_t)-1, track->get_key_signature('c')); 308 | CPPUNIT_ASSERT_EQUAL((int8_t)0, track->get_key_signature('d')); 309 | CPPUNIT_ASSERT_EQUAL((int8_t)-1, track->get_key_signature('e')); 310 | CPPUNIT_ASSERT_EQUAL((int8_t)1, track->get_key_signature('f')); 311 | CPPUNIT_ASSERT_EQUAL((int8_t)-1, track->get_key_signature('g')); 312 | CPPUNIT_ASSERT_EQUAL((int8_t)-1, track->get_key_signature('a')); 313 | CPPUNIT_ASSERT_EQUAL((int8_t)-1, track->get_key_signature('b')); 314 | } 315 | // Test a shuffle rhythm 316 | void test_shuffle() 317 | { 318 | track->set_shuffle(4); 319 | track->add_note(0,24); 320 | track->add_note(0,24); 321 | CPPUNIT_ASSERT_EQUAL((uint16_t)28, track->get_event(0).on_time); 322 | CPPUNIT_ASSERT_EQUAL((uint16_t)20, track->get_event(1).on_time); 323 | track->add_note(0,24); 324 | track->add_rest(24); 325 | CPPUNIT_ASSERT_EQUAL((uint16_t)28, track->get_event(2).on_time); 326 | CPPUNIT_ASSERT_EQUAL((uint16_t)20, track->get_event(3).off_time); 327 | track->add_note(0,24); 328 | track->add_tie(24); 329 | CPPUNIT_ASSERT_EQUAL((uint16_t)48, track->get_event(4).on_time); 330 | } 331 | }; 332 | 333 | CPPUNIT_TEST_SUITE_REGISTRATION(Track_Test); 334 | 335 | -------------------------------------------------------------------------------- /src/wave.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | This code is a bit ugly and should be refactored when I have the time... 3 | 2020-04-11: This code has now been slightly refactored. It is however still a little bit ugly. 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "wave.h" 12 | #include "input.h" 13 | #include "vgm.h" 14 | #include "stringf.h" 15 | #include "util.h" 16 | 17 | static bool operator==(const Wave_Bank::Sample& s1, const Wave_Bank::Sample& s2) 18 | { 19 | // Can't directly compare structs properly, so we convert to bytes instead 20 | // and then compare the arrays. 21 | return s1.to_bytes() == s2.to_bytes(); 22 | } 23 | 24 | //===================================================================== 25 | 26 | Wave_File::Wave_File(uint16_t channels, uint32_t rate, uint16_t bits) 27 | : channels(channels) 28 | , sbits(bits) 29 | , srate(rate) 30 | , use_smpl_chunk(0) 31 | , transpose(0) 32 | , lstart(0) 33 | , lend(0) 34 | { 35 | stype = 1; 36 | step = channels*(bits/8); 37 | } 38 | 39 | Wave_File::~Wave_File() 40 | { 41 | return; 42 | } 43 | 44 | int Wave_File::read(const std::string& filename) 45 | { 46 | uint8_t* filebuf; 47 | uint32_t filesize, pos=0, wavesize; 48 | channels = 0; 49 | 50 | if(load_file(filename,&filebuf,&filesize)) 51 | { 52 | return -1; 53 | } 54 | if(filesize < 13) 55 | { 56 | fprintf(stderr,"Malformed wav file '%s'\n", filename.c_str()); 57 | return -1; 58 | } 59 | if(memcmp(&filebuf[0],"RIFF",4)) 60 | { 61 | fprintf(stderr,"Riff header not found in '%s'\n", filename.c_str()); 62 | return -1; 63 | } 64 | wavesize = (*(uint32_t*)(filebuf+4)) + 8; 65 | pos += 8; 66 | if(filesize != wavesize) 67 | { 68 | fprintf(stderr,"Warning: reported file size and actual file size do not match.\n" 69 | "Reported %d, actual %d\n", wavesize, filesize); 70 | 71 | } 72 | if(memcmp(&filebuf[pos],"WAVE",4)) 73 | { 74 | fprintf(stderr,"'%s' is not a WAVE format file.\n", filename.c_str()); 75 | return -1; 76 | } 77 | pos += 4; 78 | while(pos < wavesize) 79 | { 80 | uint32_t chunksize, ret; 81 | 82 | chunksize = *(uint32_t*)(filebuf+pos+4) + 8; 83 | if(pos+chunksize > filesize) 84 | { 85 | printf("Illegal chunk size (%d, %d)\n", pos+chunksize+8, filesize); 86 | return -1; 87 | } 88 | ret = parse_chunk(filebuf+pos); 89 | if(!ret) 90 | { 91 | printf("Failed to parse chunk %c%c%c%c.\n",filebuf[pos],filebuf[pos+1],filebuf[pos+2],filebuf[pos+3]); 92 | return -1; 93 | } 94 | pos += chunksize; 95 | 96 | if(pos & 1) 97 | pos++; 98 | } 99 | free(filebuf); 100 | return 0; 101 | } 102 | 103 | int Wave_File::load_file(const std::string& filename, uint8_t** buffer, uint32_t* filesize) 104 | { 105 | if(std::ifstream is{filename, std::ios::binary|std::ios::ate}) 106 | { 107 | auto size = is.tellg(); 108 | *filesize = size; 109 | *buffer = (uint8_t*)calloc(1,size); 110 | is.seekg(0); 111 | if(is.read((char*)*buffer, size)) 112 | return 0; 113 | } 114 | return -1; 115 | } 116 | 117 | uint32_t Wave_File::parse_chunk(const uint8_t *fdata) 118 | { 119 | uint32_t chunkid = *(uint32_t*)(fdata); 120 | uint32_t chunksize = *(uint32_t*)(fdata+4); 121 | //printf("Parse the %c%c%c%c chunk with %d size\n", fdata[0],fdata[1],fdata[2],fdata[3], chunksize); 122 | uint32_t pos = 0; 123 | switch(chunkid) 124 | { 125 | default: 126 | break; 127 | case 0x20746d66: // 'fmt ' 128 | if(chunksize < 0x10) 129 | return 0; 130 | stype = *(uint16_t*)(fdata+0x08); 131 | channels = *(uint16_t*)(fdata+0x0a); 132 | sbits = *(uint16_t*)(fdata+0x16); 133 | step = (sbits * channels) / 8; 134 | srate = *(uint32_t*)(fdata+0x0c); 135 | slength = 0; 136 | if(stype != 1 || channels > 2 || step == 0) 137 | { 138 | fprintf(stderr,"unsupported format\n"); 139 | return 0; 140 | } 141 | if(data.size() != channels) 142 | data.resize(channels); 143 | break; 144 | case 0x61746164: // 'data' 145 | if(step == 0) 146 | return 0; 147 | for(const uint8_t* d = fdata+8; d < (fdata+8+chunksize);) 148 | { 149 | for(int ch=0; ch= 0x10) 171 | { 172 | transpose = *(uint32_t*)(fdata+0x14); 173 | if(!transpose) 174 | transpose -= 60; 175 | } 176 | if(chunksize >= 0x2c && *(uint32_t*)(fdata+0x24)) 177 | { 178 | lstart = *(uint32_t*)(fdata+0x2c+8); 179 | lend = *(uint32_t*)(fdata+0x2c+12) + 1; 180 | slength = lend; 181 | } 182 | break; 183 | } 184 | return chunksize; 185 | } 186 | 187 | 188 | //===================================================================== 189 | 190 | //! Creates a Wave_Bank 191 | Wave_Bank::Wave_Bank(unsigned long max_size, unsigned long bank_size) 192 | : max_size(max_size) 193 | , current_size(0) 194 | , bank_size(bank_size) 195 | , include_paths{""} 196 | , rom_data() 197 | , gaps() 198 | { 199 | rom_data.resize(max_size, 0); 200 | if(!bank_size) 201 | this->bank_size = max_size; 202 | } 203 | 204 | //! Wave_Bank destructor 205 | Wave_Bank::~Wave_Bank() 206 | { 207 | return; 208 | } 209 | 210 | //! Set a list of include paths to check when reading samples from a Tag. 211 | void Wave_Bank::set_include_paths(const Tag& tag) 212 | { 213 | include_paths = tag; 214 | } 215 | 216 | //! Convert and add sample to the waverom. 217 | unsigned int Wave_Bank::add_sample(const Tag& tag) 218 | { 219 | int status = -1; 220 | if(!tag.size()) 221 | { 222 | error_message = "Incomplete sample definition"; 223 | throw InputError(nullptr, error_message.c_str()); 224 | } 225 | std::string filename = tag[0]; 226 | Wave_File wf; 227 | for(auto&& i : include_paths) 228 | { 229 | std::string fn = i + filename; 230 | //std::cout << "attempt to load " << fn << "\n"; 231 | status = wf.read(fn); 232 | if(status == 0) 233 | break; 234 | } 235 | if(status) 236 | { 237 | error_message = filename + " not found"; 238 | throw InputError(nullptr, error_message.c_str()); 239 | } 240 | 241 | // convert sample 242 | std::vector sample = encode_sample("", wf.data[0]); 243 | Wave_Bank::Sample header = { 244 | 0, 245 | 0, 246 | wf.slength, 247 | wf.lstart, 248 | wf.lend, 249 | wf.srate, 250 | wf.transpose, 251 | 0}; 252 | 253 | // Allow overriding the sample rate and setting start offset 254 | int i = 1; 255 | uint32_t param; 256 | while(tag.size() > i) 257 | { 258 | if(std::sscanf(tag[i].c_str(), "rate = %u", ¶m) == 1) 259 | { 260 | header.rate = param; 261 | } 262 | else if(std::sscanf(tag[i].c_str(), "offset = %u", ¶m) == 1) 263 | { 264 | if(param > header.size) 265 | throw InputError(nullptr, "Sample offset cannot be greater than total length"); 266 | header.start += param; 267 | header.size -= param; 268 | } 269 | i++; 270 | } 271 | 272 | return add_sample(header, sample); 273 | }; 274 | 275 | //! Add sample to the waverom in raw format. 276 | unsigned int Wave_Bank::add_sample(Wave_Bank::Sample header, const std::vector& sample) 277 | { 278 | // Find duplicates of sample data and selected header parameters if needed 279 | int duplicate = find_duplicate(header, sample); 280 | 281 | if(duplicate != -1) 282 | { 283 | header.position = samples[duplicate].position; 284 | auto result = std::find(samples.begin(), samples.end(), header); 285 | if(result != samples.end()) 286 | { 287 | // Header is similar to an existing one, we reuse it 288 | return result - samples.begin(); 289 | } 290 | else 291 | { 292 | // Create a new header, while using the same sample data 293 | samples.push_back(header); 294 | return samples.size() - 1; 295 | } 296 | } 297 | else 298 | { 299 | // Create a new entry. 300 | uint32_t start_pos = -1; // Aligned start position 301 | uint32_t start = current_size; // Proposed start position 302 | // Check if the sample fits in a gap. 303 | unsigned int gap_id = find_gap(header, start_pos); 304 | if(gap_id != NO_FIT) 305 | { 306 | start = gaps[gap_id].start; 307 | gaps[gap_id].start = start_pos + header.size; 308 | } 309 | else 310 | { 311 | // Append sample to the end 312 | start_pos = fit_sample(header, start, max_size); 313 | } 314 | // Check if sample fits in ROM 315 | if(start_pos == NO_FIT) 316 | { 317 | error_message = stringf("Sample does not fit in remaining ROM space (%d bytes remaining, sample size is %d)", max_size - start_pos, sample.size()); 318 | throw InputError(nullptr, error_message.c_str()); 319 | } 320 | // Add a new gap if needed 321 | if(start_pos > start) 322 | { 323 | gaps.push_back({start, start_pos}); 324 | } 325 | // Move the end position if needed 326 | if(start_pos >= current_size) 327 | { 328 | current_size = start_pos + header.size; 329 | } 330 | 331 | printf("Append sample %d to ROM at %08x (size %08x)\n", samples.size(), start_pos, header.size); 332 | std::copy_n(sample.begin(), header.size, rom_data.begin() + start_pos); 333 | header.position = start_pos; 334 | samples.push_back(header); 335 | return samples.size() - 1; 336 | } 337 | } 338 | 339 | //! Get sample headers 340 | const std::vector& Wave_Bank::get_sample_headers() 341 | { 342 | return samples; 343 | } 344 | 345 | //! Get the sample ROM data 346 | const std::vector& Wave_Bank::get_rom_data() 347 | { 348 | return rom_data; 349 | } 350 | 351 | //! Get the number of unused allocated bytes in the Wave_Bank. 352 | unsigned int Wave_Bank::get_free_bytes() 353 | { 354 | return max_size - current_size; 355 | } 356 | 357 | //! Get the total size of alignment gaps. 358 | unsigned int Wave_Bank::get_total_gap() 359 | { 360 | unsigned int gap_size = 0; 361 | for(auto&& gap : gaps) 362 | { 363 | gap_size += gap.end - gap.start; 364 | } 365 | return gap_size; 366 | } 367 | 368 | //! Get the size of the largest gap. 369 | /*! 370 | * If there are no gaps, return 0. 371 | */ 372 | unsigned int Wave_Bank::get_largest_gap() 373 | { 374 | unsigned int largest_gap = 0; 375 | for(auto&& gap : gaps) 376 | { 377 | unsigned int gap_size = gap.end - gap.start; 378 | if(gap_size > largest_gap) 379 | largest_gap = gap_size; 380 | } 381 | return largest_gap; 382 | } 383 | 384 | //! Get error message 385 | const std::string& Wave_Bank::get_error() 386 | { 387 | return error_message; 388 | } 389 | 390 | //! Check if the sample fits in an existing alignment gap 391 | /*! 392 | * If the sample cannot fit in any gap, return NO_FIT. Otherwise, return 393 | * the index of the smallest gap that fits the sample. 394 | * 395 | * \p gap_start will be set with the aligned start position of the gap. 396 | */ 397 | unsigned int Wave_Bank::find_gap(const Wave_Bank::Sample& header, uint32_t& gap_start) const 398 | { 399 | unsigned int best_gap = NO_FIT; 400 | if(gaps.size()) 401 | { 402 | // Look for the smallest gap that fits our sample 403 | int best_gap_size = max_size; 404 | uint32_t start_pos; 405 | for(unsigned int i = 0; i < gaps.size(); i++) 406 | { 407 | int gap_size = gaps[i].end - gaps[i].start; 408 | start_pos = fit_sample(header, gaps[i].start, gaps[i].end); 409 | if(start_pos != NO_FIT && gap_size < best_gap_size) 410 | { 411 | best_gap = i; 412 | best_gap_size = gap_size; 413 | gap_start = start_pos; 414 | } 415 | } 416 | } 417 | return best_gap; 418 | } 419 | 420 | //! Encode the sample, convert it from 16-bit data to 8-bit. 421 | std::vector Wave_Bank::encode_sample(const std::string& encoding_type, const std::vector& input) 422 | { 423 | // default encoder, simply convert 16-bit to 8-bit unsigned 424 | std::vector output; 425 | for(auto&& i : input) 426 | { 427 | output.push_back((i >> 8) ^ 0x80); 428 | } 429 | return output; 430 | } 431 | 432 | //! Returns the next possible aligned start address for the rom. 433 | /*! 434 | * Given a sample header, a proposed start address and end address, 435 | * return the appropriate start address of the sample. If the sample 436 | * cannot fit within the boundaries. return NO_FIT. 437 | * 438 | * TODO: This code is optimized for MDSDRV with mid playback bank 439 | * switches only supported for 17.5khz sample rate for now. 440 | * When adding more sound drivers, this will have to be a generic 441 | * function that will reject samples crossing banks, with bank 442 | * crossing behavior determined on a per-driver basis. 443 | */ 444 | uint32_t Wave_Bank::fit_sample(const Wave_Bank::Sample& header, uint32_t start, uint32_t end) const 445 | { 446 | uint32_t sample_end = start + header.size; 447 | uint32_t start_bank = start / bank_size; 448 | uint32_t end_bank = sample_end / bank_size; 449 | // Adjust start address for bank crossing. 450 | // Smarter method if sample is only to be played at the same sample rate. 451 | // I will use this by default if the sample is too big to fit in a Z80 bank anyway. 452 | if (start_bank != end_bank && header.size > bank_size) 453 | start = (start + 0x1f) & 0xffffffe0; 454 | // Alternate behavior for MDSDRV. More ROM space but will handle sample rate switches. 455 | else if (start_bank != end_bank && (start % bank_size) != 0) 456 | start = (start_bank + 1) * bank_size; 457 | // Crossed end boundary? 458 | if ((start + header.size) > end) 459 | return NO_FIT; 460 | return start; 461 | } 462 | 463 | //! Look for duplicates in the sample ROM. 464 | /*! 465 | * If a duplicate is found, return the index to the duplicate wave entry. 466 | * Otherwise, return -1. 467 | */ 468 | int Wave_Bank::find_duplicate(const Wave_Bank::Sample& header, const std::vector& sample) const 469 | { 470 | int id = 0; 471 | for(auto&& i : samples) 472 | { 473 | // The reason for the loop start check is that some sound chips (like C352) 474 | // require that the looping part of the sample fit in the same bank. 475 | if( i.position + sample.size() <= rom_data.size() 476 | && i.loop_start <= header.loop_start 477 | && !memcmp(&sample[0], &rom_data[i.position], sample.size())) 478 | return id; 479 | id++; 480 | } 481 | return -1; 482 | } 483 | 484 | //===================================================================== 485 | //! Fill a sample header with values from a byte vector. 486 | /*! 487 | * The format matches the data created by to_bytes(). 488 | * 489 | * \exception std::out_of_range Input too small 490 | */ 491 | void Wave_Bank::Sample::from_bytes(std::vector input) 492 | { 493 | position = read_le32(input, 0); 494 | start = read_le32(input, 4); 495 | size = read_le32(input, 8); 496 | loop_start = read_le32(input, 12); 497 | loop_end = read_le32(input, 16); 498 | rate = read_le32(input, 20); 499 | transpose = read_le32(input, 24); 500 | flags = read_le32(input, 28); 501 | } 502 | 503 | //! Return a sample header as a byte vector. 504 | /*! 505 | * Output can be made as a header again using to_bytes(). 506 | */ 507 | std::vector Wave_Bank::Sample::to_bytes() const 508 | { 509 | auto output = std::vector(); 510 | write_le32(output, 0, position); 511 | write_le32(output, 4, start); 512 | write_le32(output, 8, size); 513 | write_le32(output, 12, loop_start); 514 | write_le32(output, 16, loop_end); 515 | write_le32(output, 20, rate); 516 | write_le32(output, 24, transpose); 517 | write_le32(output, 28, flags); // Reserved. 518 | return output; 519 | } 520 | --------------------------------------------------------------------------------