/* * xmp-openmpt.cpp * --------------- * Purpose: libopenmpt xmplay input plugin implementation * Notes : (currently none) * Authors: OpenMPT Devs * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. */ #ifndef NO_XMPLAY #ifndef _CRT_SECURE_NO_WARNINGS #define _CRT_SECURE_NO_WARNINGS #endif #if !defined(WINVER) && !defined(_WIN32_WINDOWS) #ifndef _WIN32_WINNT #define _WIN32_WINNT 0x0501 // _WIN32_WINNT_WINXP #endif #endif #if !defined(MPT_BUILD_RETRO) #if defined(_MSC_VER) #define MPT_WITH_MFC #endif #else #if defined(_WIN32_WINNT) #if (_WIN32_WINNT >= 0x0501) #if defined(_MSC_VER) #define MPT_WITH_MFC #endif #endif #endif #endif #if defined(MPT_WITH_MFC) #define _AFX_NO_MFC_CONTROLS_IN_DIALOGS // Avoid binary bloat from linking unused MFC controls #endif // MPT_WITH_MFC #ifndef NOMINMAX #define NOMINMAX #endif #if defined(MPT_WITH_MFC) #include #include #endif // MPT_WITH_MFC #include #include #ifdef LIBOPENMPT_BUILD_DLL #undef LIBOPENMPT_BUILD_DLL #endif #ifdef _MSC_VER #ifndef _CRT_SECURE_NO_WARNINGS #define _CRT_SECURE_NO_WARNINGS #endif #ifndef _SCL_SECURE_NO_WARNINGS #define _SCL_SECURE_NO_WARNINGS #endif #endif // _MSC_VER #include #include #include #include "libopenmpt.hpp" #include "libopenmpt_ext.hpp" #include "libopenmpt_plugin_settings.hpp" #include "libopenmpt_plugin_gui.hpp" #include "svn_version.h" #if defined(OPENMPT_VERSION_REVISION) static const char * xmp_openmpt_string = "OpenMPT (" OPENMPT_API_VERSION_STRING "." OPENMPT_API_VERSION_STRINGIZE(OPENMPT_VERSION_REVISION) ")"; #else static const char * xmp_openmpt_string = "OpenMPT (" OPENMPT_API_VERSION_STRING ")"; #endif #define USE_XMPLAY_FILE_IO #define USE_XMPLAY_ISTREAM #include "xmplay/xmpin.h" // Shortcut block assigned to the OpenMPT plugin by un4seen. enum { openmpt_shortcut_first = 0x21000, openmpt_shortcut_tempo_decrease = openmpt_shortcut_first, openmpt_shortcut_tempo_increase, openmpt_shortcut_pitch_decrease, openmpt_shortcut_pitch_increase, openmpt_shortcut_switch_interpolation, openmpt_shortcut_last = 0x21fff, openmpt_shortcut_ex = 0x80000000, // Use extended version of the shortcut callback }; #include #include #include #include #include #include #include #include #include #include #include #define SHORT_TITLE "xmp-openmpt" #define SHORTER_TITLE "openmpt" static CRITICAL_SECTION xmpopenmpt_mutex; class xmpopenmpt_lock { public: xmpopenmpt_lock() { EnterCriticalSection( &xmpopenmpt_mutex ); } ~xmpopenmpt_lock() { LeaveCriticalSection( &xmpopenmpt_mutex ); } }; static XMPFUNC_IN * xmpfin = nullptr; static XMPFUNC_MISC * xmpfmisc = nullptr; static XMPFUNC_REGISTRY * xmpfregistry = nullptr; static XMPFUNC_FILE * xmpffile = nullptr; static XMPFUNC_TEXT * xmpftext = nullptr; static XMPFUNC_STATUS * xmpfstatus = nullptr; struct self_xmplay_t; static self_xmplay_t * self = 0; static void save_options(); static void apply_and_save_options(); static std::string convert_to_native( const std::string & str ); static std::string StringEncode( const std::wstring &src, UINT codepage ); static std::wstring StringDecode( const std::string & src, UINT codepage ); #if defined(UNICODE) static std::wstring StringToWINAPI( const std::wstring & src ); #else static std::string StringToWINAPI( const std::wstring & src ); #endif class xmp_openmpt_settings : public libopenmpt::plugin::settings { protected: void read_setting( const std::string & key, const std::basic_string & keyW, int & val ) override { libopenmpt::plugin::settings::read_setting( key, keyW, val ); int storedVal = 0; if ( xmpfregistry->GetInt( "OpenMPT", key.c_str(), &storedVal ) ) { val = storedVal; } } void write_setting( const std::string & key, const std::basic_string & /* keyW */ , int val ) override { if ( !xmpfregistry->SetInt( "OpenMPT", key.c_str(), &val ) ) { // error } // ok } public: xmp_openmpt_settings() : libopenmpt::plugin::settings(TEXT(SHORT_TITLE), false) { return; } virtual ~xmp_openmpt_settings() { return; } }; struct self_xmplay_t { std::vector subsong_lengths; std::vector subsong_names; std::size_t samplerate = 48000; std::size_t num_channels = 2; xmp_openmpt_settings settings; openmpt::module_ext * mod = nullptr; bool set_format_called = false; openmpt::ext::pattern_vis * pattern_vis = nullptr; std::int32_t tempo_factor = 0, pitch_factor = 0; bool single_subsong_mode = false; self_xmplay_t() { settings.changed = apply_and_save_options; } void on_new_mod() { set_format_called = false; self->pattern_vis = static_cast( self->mod->get_interface( openmpt::ext::pattern_vis_id ) ); } void delete_mod() { if ( mod ) { pattern_vis = 0; set_format_called = false; delete mod; mod = 0; } } ~self_xmplay_t() { return; } }; static std::string convert_to_native( const std::string & str ) { char * native_string = xmpftext->Utf8( str.c_str(), -1 ); std::string result = native_string ? native_string : ""; if ( native_string ) { xmpfmisc->Free( native_string ); native_string = 0; } return result; } static std::string StringEncode( const std::wstring &src, UINT codepage ) { int required_size = WideCharToMultiByte( codepage, 0, src.c_str(), -1, nullptr, 0, nullptr, nullptr); if(required_size <= 0) { return std::string(); } std::vector encoded_string( required_size ); WideCharToMultiByte( codepage, 0, src.c_str(), -1, &encoded_string[0], encoded_string.size(), nullptr, nullptr); return &encoded_string[0]; } static std::wstring StringDecode( const std::string & src, UINT codepage ) { int required_size = MultiByteToWideChar( codepage, 0, src.c_str(), -1, nullptr, 0 ); if(required_size <= 0) { return std::wstring(); } std::vector decoded_string( required_size ); MultiByteToWideChar( codepage, 0, src.c_str(), -1, &decoded_string[0], decoded_string.size() ); return &decoded_string[0]; } #if defined(UNICODE) static std::wstring StringToWINAPI( const std::wstring & src ) { return src; } #else static std::string StringToWINAPI( const std::wstring & src ) { return StringEncode( src, CP_ACP ); } #endif template static inline Tstring StringReplace( Tstring str, const Tstring2 & oldStr_, const Tstring3 & newStr_ ) { std::size_t pos = 0; const Tstring oldStr = oldStr_; const Tstring newStr = newStr_; while ( ( pos = str.find( oldStr, pos ) ) != Tstring::npos ) { str.replace( pos, oldStr.length(), newStr ); pos += newStr.length(); } return str; } static std::string StringUpperCase( std::string str ) { std::transform( str.begin(), str.end(), str.begin(), []( char c ) { return static_cast( std::toupper( c ) ); } ); return str; } static std::string seconds_to_string( float time ) { std::int64_t time_ms = static_cast( time * 1000 ); std::int64_t seconds = ( time_ms / 1000 ) % 60; std::int64_t minutes = ( time_ms / ( 1000 * 60 ) ) % 60; std::int64_t hours = ( time_ms / ( 1000 * 60 * 60 ) ); std::ostringstream str; if ( hours > 0 ) { str << hours << ":"; } str << std::setfill('0') << std::setw(2) << minutes; str << ":"; str << std::setfill('0') << std::setw(2) << seconds; return str.str(); } static void save_settings_to_map( std::map & result, const libopenmpt::plugin::settings & s ) { result.clear(); result[ "Samplerate_Hz" ] = s.samplerate; result[ "Channels" ] = s.channels; result[ "MasterGain_milliBel" ] = s.mastergain_millibel; result[ "StereoSeparation_Percent" ] = s.stereoseparation; result[ "RepeatCount" ] = s.repeatcount; result[ "InterpolationFilterLength" ] = s.interpolationfilterlength; result[ "UseAmigaResampler" ] = s.use_amiga_resampler; result[ "AmigaFilterType" ] = s.amiga_filter_type; result[ "VolumeRampingStrength" ] = s.ramping; } static inline void load_map_setting( const std::map & map, const std::string & key, int & val ) { auto it = map.find( key ); if ( it != map.end() ) { val = it->second; } } static void load_settings_from_map( libopenmpt::plugin::settings & s, const std::map & map ) { load_map_setting( map, "Samplerate_Hz", s.samplerate ); load_map_setting( map, "Channels", s.channels ); load_map_setting( map, "MasterGain_milliBel", s.mastergain_millibel ); load_map_setting( map, "StereoSeparation_Percent", s.stereoseparation ); load_map_setting( map, "RepeatCount", s.repeatcount ); load_map_setting( map, "InterpolationFilterLength", s.interpolationfilterlength ); load_map_setting( map, "UseAmigaResampler", s.use_amiga_resampler ); load_map_setting( map, "AmigaFilterType", s.amiga_filter_type ); load_map_setting( map, "VolumeRampingStrength", s.ramping ); } static void load_settings_from_xml( libopenmpt::plugin::settings & s, const std::string & xml ) { pugi::xml_document doc; doc.load_string( xml.c_str() ); pugi::xml_node settings_node = doc.child( "settings" ); std::map map; for ( const auto & attr : settings_node.attributes() ) { map[ attr.name() ] = attr.as_int(); } load_settings_from_map( s, map ); } static void save_settings_to_xml( std::string & xml, const libopenmpt::plugin::settings & s ) { std::map map; save_settings_to_map( map, s ); pugi::xml_document doc; pugi::xml_node settings_node = doc.append_child( "settings" ); for ( const auto & setting : map ) { settings_node.append_attribute( setting.first.c_str() ).set_value( setting.second ); } std::ostringstream buf; doc.save( buf ); xml = buf.str(); } static void apply_options() { if ( self->mod ) { if ( !self->set_format_called ) { // SetFormat will only be called once after loading a file. // We cannot apply samplerate or numchannels changes afterwards during playback. self->samplerate = self->settings.samplerate; self->num_channels = self->settings.channels; } self->mod->set_repeat_count( self->settings.repeatcount ); self->mod->set_render_param( openmpt::module::RENDER_MASTERGAIN_MILLIBEL, self->settings.mastergain_millibel ); self->mod->set_render_param( openmpt::module::RENDER_STEREOSEPARATION_PERCENT, self->settings.stereoseparation ); self->mod->set_render_param( openmpt::module::RENDER_INTERPOLATIONFILTER_LENGTH, self->settings.interpolationfilterlength ); self->mod->set_render_param( openmpt::module::RENDER_VOLUMERAMPING_STRENGTH, self->settings.ramping ); self->mod->ctl_set_boolean( "render.resampler.emulate_amiga", self->settings.use_amiga_resampler ? true : false ); switch ( self->settings.amiga_filter_type ) { case 0: self->mod->ctl_set_text( "render.resampler.emulate_amiga_type", "auto" ); break; case 1: self->mod->ctl_set_text( "render.resampler.emulate_amiga_type", "unfiltered" ); break; case 0xA500: self->mod->ctl_set_text( "render.resampler.emulate_amiga_type", "a500" ); break; case 0xA1200: self->mod->ctl_set_text( "render.resampler.emulate_amiga_type", "a1200" ); break; } } } static void save_options() { self->settings.save(); } static void apply_and_save_options() { apply_options(); save_options(); } static void reset_options() { self->settings = xmp_openmpt_settings(); self->settings.changed = apply_and_save_options; self->settings.load(); } // get config (return size of config data) (OPTIONAL) static DWORD WINAPI openmpt_GetConfig( void * config ) { std::string xml; save_settings_to_xml( xml, self->settings ); if ( config ) { std::memcpy( config, xml.c_str(), xml.length() + 1 ); } return xml.length() + 1; } // apply config (OPTIONAL) static void WINAPI openmpt_SetConfig( void * config, DWORD size ) { reset_options(); if ( config ) { load_settings_from_xml( self->settings, std::string( (char*)config, (char*)config + size ) ); apply_options(); } } static void WINAPI ShortcutHandler( DWORD id ) { if ( !self->mod ) { return; } bool tempo_changed = false, pitch_changed = false; switch ( id ) { case openmpt_shortcut_tempo_decrease: self->tempo_factor--; tempo_changed = true; break; case openmpt_shortcut_tempo_increase: self->tempo_factor++; tempo_changed = true; break; case openmpt_shortcut_pitch_decrease: self->pitch_factor--; pitch_changed = true; break; case openmpt_shortcut_pitch_increase: self->pitch_factor++; pitch_changed = true; break; case openmpt_shortcut_switch_interpolation: self->settings.interpolationfilterlength *= 2; if ( self->settings.interpolationfilterlength > 8 ) { self->settings.interpolationfilterlength = 1; } apply_and_save_options(); const char *s = nullptr; switch ( self->settings.interpolationfilterlength ) { case 1: s = "Interpolation: Off"; break; case 2: s = "Interpolation: Linear"; break; case 4: s = "Interpolation: Cubic"; break; case 8: s = "Interpolation: Polyphase"; break; } if ( s ) { xmpfmisc->ShowBubble( s, 0 ); } break; } self->tempo_factor = std::min( 48, std::max( -48, self->tempo_factor ) ); self->pitch_factor = std::min( 48, std::max( -48, self->pitch_factor ) ); const double tempo_factor = std::pow( 2.0, self->tempo_factor / 24.0 ); const double pitch_factor = std::pow( 2.0, self->pitch_factor / 24.0 ); if ( tempo_changed ) { std::ostringstream s; s << "Tempo: " << static_cast( 100.0 * tempo_factor ) << "%"; xmpfmisc->ShowBubble( s.str().c_str(), 0 ); } else if ( pitch_changed) { std::ostringstream s; s << "Pitch: "; if ( self->pitch_factor > 0 ) s << "+"; else if ( self->pitch_factor == 0 ) s << "+/-"; s << (self->pitch_factor * 0.5) << " semitones"; xmpfmisc->ShowBubble( s.str().c_str(), 0 ); } openmpt::ext::interactive *interactive = static_cast( self->mod->get_interface( openmpt::ext::interactive_id ) ); interactive->set_tempo_factor( tempo_factor ); interactive->set_pitch_factor( pitch_factor ); xmpfin->SetLength( static_cast( self->mod->get_duration_seconds() / tempo_factor ), TRUE ); } static double timeinfo_position = 0.0; struct timeinfo { bool valid; double seconds; std::int32_t pattern; std::int32_t row; }; static std::queue timeinfos; static void reset_timeinfos( double position = 0.0 ) { while ( !timeinfos.empty() ) { timeinfos.pop(); } timeinfo_position = position; } static void update_timeinfos( std::int32_t samplerate, std::int32_t count ) { timeinfo_position += (double)count / (double)samplerate; timeinfo info; info.valid = true; info.seconds = timeinfo_position; info.pattern = self->mod->get_current_pattern(); info.row = self->mod->get_current_row(); timeinfos.push( info ); } static timeinfo current_timeinfo; static timeinfo lookup_timeinfo( double seconds ) { timeinfo info = current_timeinfo; #if 0 info.seconds = timeinfo_position; info.pattern = self->mod->get_current_pattern(); info.row = self->mod->get_current_row(); #endif while ( timeinfos.size() > 0 && timeinfos.front().seconds <= seconds ) { info = timeinfos.front(); timeinfos.pop(); } current_timeinfo = info; return current_timeinfo; } static void clear_current_timeinfo() { current_timeinfo = timeinfo(); } static void WINAPI openmpt_About( HWND win ) { std::ostringstream about; about << SHORT_TITLE << " version " << openmpt::string::get( "library_version" ) << " " << "(built " << openmpt::string::get( "build" ) << ")" << std::endl; about << " Copyright (c) 2013-2022 OpenMPT Project Developers and Contributors (https://lib.openmpt.org/)" << std::endl; about << " OpenMPT version " << openmpt::string::get( "core_version" ) << std::endl; about << std::endl; about << openmpt::string::get( "contact" ) << std::endl; about << std::endl; about << "Show full credits?" << std::endl; if ( MessageBox( win, StringToWINAPI( StringDecode( about.str(), CP_UTF8 ) ).c_str(), TEXT(SHORT_TITLE), MB_ICONINFORMATION | MB_YESNOCANCEL | MB_DEFBUTTON1 ) != IDYES ) { return; } std::ostringstream credits; credits << openmpt::string::get( "credits" ); credits << "Additional thanks to:" << std::endl; credits << std::endl; credits << "Arseny Kapoulkine for pugixml" << std::endl; credits << "https://pugixml.org/" << std::endl; #if 1 libopenmpt::plugin::gui_show_file_info( win, TEXT(SHORT_TITLE), StringToWINAPI( StringReplace( StringDecode( credits.str(), CP_UTF8 ), L"\n", L"\r\n" ) ) ); #else MessageBox( win, StringToWINAPI( StringReplace( StringDecode( credits.str(), CP_UTF8 ), L"\n", L"\r\n" ) ).c_str(), TEXT(SHORT_TITLE), MB_OK ); #endif } static void WINAPI openmpt_Config( HWND win ) { #if 1 libopenmpt::plugin::gui_edit_settings( &self->settings, win, TEXT(SHORT_TITLE) ); #else static_cast(win); #endif apply_and_save_options(); } #ifdef USE_XMPLAY_FILE_IO #ifdef USE_XMPLAY_ISTREAM class xmplay_streambuf : public std::streambuf { public: explicit xmplay_streambuf( XMPFILE & file ); private: int_type underflow() override; xmplay_streambuf( const xmplay_streambuf & ); xmplay_streambuf & operator = ( const xmplay_streambuf & ); private: XMPFILE & file; static const std::size_t put_back = 4096; static const std::size_t buf_size = 65536; std::vector buffer; }; // class xmplay_streambuf xmplay_streambuf::xmplay_streambuf( XMPFILE & file_ ) : file(file_), buffer(buf_size) { char * end = &buffer.front() + buffer.size(); setg( end, end, end ); } std::streambuf::int_type xmplay_streambuf::underflow() { if ( gptr() < egptr() ) { return traits_type::to_int_type( *gptr() ); } char * base = &buffer.front(); char * start = base; if ( eback() == base ) { std::size_t put_back_count = std::min( put_back, static_cast( egptr() - base ) ); std::memmove( base, egptr() - put_back_count, put_back_count ); start += put_back_count; } std::size_t n = xmpffile->Read( file, start, buffer.size() - ( start - base ) ); if ( n == 0 ) { return traits_type::eof(); } setg( base, start, start + n ); return traits_type::to_int_type( *gptr() ); } class xmplay_istream : public std::istream { private: xmplay_streambuf buf; private: xmplay_istream( const xmplay_istream & ); xmplay_istream & operator = ( const xmplay_istream & ); public: xmplay_istream( XMPFILE & file ) : std::istream(&buf), buf(file) { return; } ~xmplay_istream() { return; } }; // class xmplay_istream // Stream for memory-based files (required for could_open_probability) struct xmplay_membuf : std::streambuf { xmplay_membuf( const char * base, size_t size ) { char* p( const_cast( base ) ); setg(p, p, p + size); } }; struct xmplay_imemstream : virtual xmplay_membuf, std::istream { xmplay_imemstream( const char * base, size_t size ) : xmplay_membuf( base, size ) , std::istream( static_cast(this)) { return; } }; #else // !USE_XMPLAY_ISTREAM static std::vector read_XMPFILE_vector( XMPFILE & file ) { std::vector data( xmpffile->GetSize( file ) ); if ( data.size() != xmpffile->Read( file, data.data(), data.size() ) ) { return std::vector(); } return data; } static std::string read_XMPFILE_string( XMPFILE & file ) { std::vector data = read_XMPFILE_vector( file ); return std::string( data.begin(), data.end() ); } #endif // USE_XMPLAY_ISTREAM #endif // USE_XMPLAY_FILE_IO static std::string string_replace( std::string str, const std::string & oldStr, const std::string & newStr ) { std::size_t pos = 0; while((pos = str.find(oldStr, pos)) != std::string::npos) { str.replace(pos, oldStr.length(), newStr); pos += newStr.length(); } return str; } static void write_xmplay_string( char * dst, std::string src ) { // xmplay buffers are ~40kB, be conservative and truncate at 32kB-2 if ( !dst ) { return; } src = src.substr( 0, (1<<15) - 2 ); std::strcpy( dst, src.c_str() ); } static std::string extract_date( const openmpt::module & mod ) { std::string result = mod.get_metadata("date"); if ( result.empty() ) { // Search the sample, instrument and message texts for possible release years. // We'll look for things that may vaguely resemble a release year, such as 4-digit numbers // or 2-digit numbers with a leading apostrophe. Generally, only years between // 1988 (release of Ultimate SoundTracker) and the current year + 1 (safety margin) will // be considered. std::string s = " " + mod.get_metadata("message"); auto names = mod.get_sample_names(); for ( const auto & name : names ) { s += " " + name; } names = mod.get_instrument_names(); for ( const auto & name : names ) { s += " " + name; } s += " "; int32_t best_year = 0; SYSTEMTIME time; GetSystemTime( &time ); const int32_t current_year = time.wYear + 1; #define MPT_NUMERIC( x ) ( ( x >= '0' ) && ( x <= '9' ) ) for ( auto i = s.cbegin(); i != s.cend(); ++i ) { std::size_t len = s.length(); std::size_t idx = i - s.begin(); std::size_t remaining = len - idx; if ( ( remaining >= 6 ) && !MPT_NUMERIC( i[0] ) && MPT_NUMERIC( i[1] ) && MPT_NUMERIC( i[2] ) && MPT_NUMERIC( i[3] ) && MPT_NUMERIC( i[4] ) && !MPT_NUMERIC( i[5] ) ) { // Four-digit year const int32_t year = ( i[1] - '0' ) * 1000 + ( i[2] - '0' ) * 100 + ( i[3] - '0' ) * 10 + ( i[4] - '0' ); if ( year >= 1988 && year <= current_year ) { best_year = std::max( year, best_year ); } } else if ( ( remaining >= 4 ) && ( i[0] == '\'' ) && MPT_NUMERIC( i[1] ) && MPT_NUMERIC( i[2] ) && !MPT_NUMERIC( i[3] ) ) { // Apostrophe + two-digit year const int32_t year = ( i[1] - '0' ) * 10 + ( i[2] - '0' ); if ( year >= 88 && year <= 99 ) { best_year = std::max( 1900 + year, best_year ); } else if ( year >= 00 && ( 2000 + year ) <= current_year ) { best_year = std::max( 2000 + year, best_year ); } } } #undef MPT_NUMERIC if ( best_year != 0 ) { std::ostringstream os; os << best_year; result = os.str(); } } return result; } static void append_xmplay_tag( std::string & tags, const std::string & tag, const std::string & val ) { if ( tag.empty() ) { return; } if ( val.empty() ) { return; } tags.append( tag ); tags.append( 1, '\0' ); tags.append( val ); tags.append( 1, '\0' ); } static char * build_xmplay_tags( const openmpt::module & mod, int32_t subsong = -1 ) { std::string tags; const std::string title = mod.get_metadata("title"); const auto subsong_names = mod.get_subsong_names(); auto first_subsong = subsong_names.cbegin(), last_subsong = subsong_names.cend(); if ( subsong >= 0 && static_cast( subsong ) < subsong_names.size() ) { first_subsong += subsong; last_subsong = first_subsong + 1; } else { last_subsong = first_subsong + 1; } for ( auto subsong_name = first_subsong; subsong_name != last_subsong; subsong_name++ ) { append_xmplay_tag( tags, "filetype", convert_to_native( StringUpperCase( mod.get_metadata( "type" ) ) ) ); append_xmplay_tag( tags, "title", convert_to_native( ( subsong_name->empty() || subsong == -1 ) ? title : *subsong_name ) ); append_xmplay_tag( tags, "artist", convert_to_native( mod.get_metadata( "artist" ) ) ); append_xmplay_tag( tags, "album", convert_to_native( mod.get_metadata( "xmplay-album" ) ) ); // todo, libopenmpt does not support that append_xmplay_tag( tags, "date", convert_to_native( extract_date( mod ) ) ); append_xmplay_tag( tags, "track", convert_to_native( mod.get_metadata( "xmplay-tracknumber" ) ) ); // todo, libopenmpt does not support that append_xmplay_tag( tags, "genre", convert_to_native( mod.get_metadata( "xmplay-genre" ) ) ); // todo, libopenmpt does not support that append_xmplay_tag( tags, "comment", convert_to_native( mod.get_metadata( "message" ) ) ); tags.append( 1, '\0' ); } char * result = static_cast( xmpfmisc->Alloc( tags.size() ) ); if ( !result ) { return nullptr; } std::copy( tags.data(), tags.data() + tags.size(), result ); return result; } static float * build_xmplay_length( const openmpt::module & /* mod */ ) { float * result = static_cast( xmpfmisc->Alloc( sizeof( float ) * self->subsong_lengths.size() ) ); if ( !result ) { return nullptr; } for ( std::size_t i = 0; i < self->subsong_lengths.size(); ++i ) { result[i] = self->subsong_lengths[i]; } return result; } static void clear_xmplay_string( char * str ) { if ( !str ) { return; } str[0] = '\0'; } static std::string sanitize_xmplay_info_string( const std::string & str ) { std::string result; result.reserve(str.size()); for ( auto c : str ) { switch ( c ) { case '\0': case '\t': case '\r': case '\n': break; default: result.push_back( c ); break; } } return result; } static std::string sanitize_xmplay_multiline_info_string( const std::string & str ) { std::string result; result.reserve(str.size()); for ( auto c : str ) { switch ( c ) { case '\0': case '\t': case '\r': break; case '\n': result.push_back( '\r' ); result.push_back( '\t' ); break; default: result.push_back( c ); break; } } return result; } static std::string sanitize_xmplay_multiline_string( const std::string & str ) { std::string result; result.reserve(str.size()); for ( auto c : str ) { switch ( c ) { case '\0': case '\t': break; default: result.push_back( c ); break; } } return result; } // check if a file is playable by this plugin // more thorough checks can be saved for the GetFileInfo and Open functions static BOOL WINAPI openmpt_CheckFile( const char * filename, XMPFILE file ) { static_cast( filename ); try { #ifdef USE_XMPLAY_FILE_IO #ifdef USE_XMPLAY_ISTREAM switch ( xmpffile->GetType( file ) ) { case XMPFILE_TYPE_MEMORY: { xmplay_imemstream s( reinterpret_cast( xmpffile->GetMemory( file ) ), xmpffile->GetSize( file ) ); return ( openmpt::probe_file_header( openmpt::probe_file_header_flags_default2, s ) == openmpt::probe_file_header_result_success ) ? TRUE : FALSE; } break; case XMPFILE_TYPE_FILE: case XMPFILE_TYPE_NETFILE: case XMPFILE_TYPE_NETSTREAM: default: { xmplay_istream s( file ); return ( openmpt::probe_file_header( openmpt::probe_file_header_flags_default2, s ) == openmpt::probe_file_header_result_success ) ? TRUE : FALSE; } break; } #else if ( xmpffile->GetType( file ) == XMPFILE_TYPE_MEMORY ) { std::string data( reinterpret_cast( xmpffile->GetMemory( file ) ), xmpffile->GetSize( file ) ); std::istringstream s( data ); return ( openmpt::probe_file_header( openmpt::probe_file_header_flags_default2, s ) == openmpt::probe_file_header_result_success ) ? TRUE : FALSE; } else { std::string data = read_XMPFILE_string( file ); std::istringstream s(data); return ( openmpt::probe_file_header( openmpt::probe_file_header_flags_default2, s ) == openmpt::probe_file_header_result_success ) ? TRUE : FALSE; } #endif #else std::ifstream s( filename, std::ios_base::binary ); return ( openmpt::probe_file_header( openmpt::probe_file_header_flags_default2, s ) == openmpt::probe_file_header_result_success ) ? TRUE : FALSE; #endif } catch ( ... ) { return FALSE; } return FALSE; } static DWORD WINAPI openmpt_GetFileInfo( const char * filename, XMPFILE file, float * * length, char * * tags ) { static_cast( filename ); try { std::map< std::string, std::string > ctls { { "load.skip_plugins", "1" }, { "load.skip_samples", "1" }, }; #ifdef USE_XMPLAY_FILE_IO #ifdef USE_XMPLAY_ISTREAM switch ( xmpffile->GetType( file ) ) { case XMPFILE_TYPE_MEMORY: { openmpt::module mod( xmpffile->GetMemory( file ), xmpffile->GetSize( file ), std::clog, ctls ); if ( length ) { *length = build_xmplay_length( mod ); } if ( tags ) { *tags = build_xmplay_tags( mod ); } } break; case XMPFILE_TYPE_FILE: case XMPFILE_TYPE_NETFILE: case XMPFILE_TYPE_NETSTREAM: default: { xmplay_istream s( file ); openmpt::module mod( s, std::clog, ctls ); if ( length ) { *length = build_xmplay_length( mod ); } if ( tags ) { *tags = build_xmplay_tags( mod ); } } break; } #else if ( xmpffile->GetType( file ) == XMPFILE_TYPE_MEMORY ) { openmpt::module mod( xmpffile->GetMemory( file ), xmpffile->GetSize( file ), std::clog, ctls ); if ( length ) { *length = build_xmplay_length( mod ); } if ( tags ) { *tags = build_xmplay_tags( mod ); } } else { openmpt::module mod( read_XMPFILE_vector( file ), std::clog, ctls ); if ( length ) { *length = build_xmplay_length( mod ); } if ( tags ) { *tags = build_xmplay_tags( mod ); } } #endif #else std::ifstream s( filename, std::ios_base::binary ); openmpt::module mod( s, std::clog, ctls ); if ( length ) { *length = build_xmplay_length( mod ); } if ( tags ) { *tags = build_xmplay_tags( mod ); } #endif } catch ( ... ) { if ( length ) *length = nullptr; if ( tags ) *tags = nullptr; return 0; } return self->subsong_lengths.size() + XMPIN_INFO_NOSUBTAGS; } // open a file for playback // return: 0=failed, 1=success, 2=success and XMPlay can close the file static DWORD WINAPI openmpt_Open( const char * filename, XMPFILE file ) { static_cast( filename ); xmpopenmpt_lock guard; reset_options(); try { std::map< std::string, std::string > ctls { { "seek.sync_samples", "1" }, { "play.at_end", "continue" }, }; self->delete_mod(); #ifdef USE_XMPLAY_FILE_IO #ifdef USE_XMPLAY_ISTREAM switch ( xmpffile->GetType( file ) ) { case XMPFILE_TYPE_MEMORY: self->mod = new openmpt::module_ext( xmpffile->GetMemory( file ), xmpffile->GetSize( file ), std::clog, ctls ); break; case XMPFILE_TYPE_FILE: case XMPFILE_TYPE_NETFILE: case XMPFILE_TYPE_NETSTREAM: default: { xmplay_istream s( file ); self->mod = new openmpt::module_ext( s, std::clog, ctls ); } break; } #else if ( xmpffile->GetType( file ) == XMPFILE_TYPE_MEMORY ) { self->mod = new openmpt::module_ext( xmpffile->GetMemory( file ), xmpffile->GetSize( file ), std::clog, ctls ); } else { self->mod = new openmpt::module_ext( read_XMPFILE_vector( file ), std::clog, ctls ); } #endif #else self->mod = new openmpt::module_ext( std::ifstream( filename, std::ios_base::binary ), std::clog, ctls ); #endif self->on_new_mod(); clear_current_timeinfo(); reset_timeinfos(); apply_options(); std::int32_t num_subsongs = self->mod->get_num_subsongs(); self->subsong_lengths.resize( num_subsongs ); for ( std::int32_t i = 0; i < num_subsongs; ++i ) { self->mod->select_subsong( i ); self->subsong_lengths[i] = static_cast( self->mod->get_duration_seconds() ); } self->subsong_names = self->mod->get_subsong_names(); self->mod->select_subsong( 0 ); self->tempo_factor = 0; self->pitch_factor = 0; xmpfin->SetLength( self->subsong_lengths[0], TRUE ); return 2; } catch ( ... ) { self->delete_mod(); return 0; } return 0; } // close the file static void WINAPI openmpt_Close() { xmpopenmpt_lock guard; self->delete_mod(); } // set the sample format (in=user chosen format, out=file format if different) static void WINAPI openmpt_SetFormat( XMPFORMAT * form ) { if ( !form ) { return; } // SetFormat will only be called once after loading a file. // We cannot apply samplerate or numchannels changes afterwards during playback. self->set_format_called = true; if ( !self->mod ) { form->rate = 0; form->chan = 0; form->res = 0; return; } if ( self->settings.samplerate != 0 ) { form->rate = self->samplerate; } else { if ( form->rate > 0 ) { self->samplerate = form->rate; } else { form->rate = 48000; self->samplerate = 48000; } } if ( self->settings.channels != 0 ) { form->chan = self->num_channels; } else { if ( form->chan > 0 ) { if ( form->chan > 2 ) { form->chan = 4; self->num_channels = 4; } else { self->num_channels = form->chan; } } else { form->chan = 2; self->num_channels = 2; } } form->res = 4; // float } // get the tags static char * WINAPI openmpt_GetTags() { if ( !self->mod ) { char * tags = static_cast( xmpfmisc->Alloc( 1 ) ); tags[0] = '\0'; return tags; } return build_xmplay_tags( *self->mod, std::max( 0, self->mod->get_selected_subsong() ) ); } // get the main panel info text static void WINAPI openmpt_GetInfoText( char * format, char * length ) { if ( !self->mod ) { clear_xmplay_string( format ); clear_xmplay_string( length ); return; } if ( format ) { std::ostringstream str; str << StringUpperCase( self->mod->get_metadata("type") ) << " - " << self->mod->get_num_channels() << " ch" << " - " << "(via " << SHORTER_TITLE << ")" ; write_xmplay_string( format, sanitize_xmplay_info_string( str.str() ) ); } if ( length ) { std::ostringstream str; str << length << " - " << self->mod->get_num_orders() << " orders" ; write_xmplay_string( length, sanitize_xmplay_info_string( str.str() ) ); } } // get text for "General" info window // separate headings and values with a tab (\t), end each line with a carriage-return (\r) static void WINAPI openmpt_GetGeneralInfo( char * buf ) { if ( !self->mod ) { clear_xmplay_string( buf ); return; } std::ostringstream str; str << "\r"; bool metadatainfo = false; if ( !self->mod->get_metadata("artist").empty() ) { metadatainfo = true; str << "Artist" << "\t" << sanitize_xmplay_info_string( self->mod->get_metadata("artist") ) << "\r"; } const std::string date = extract_date( *self->mod ); if ( !date.empty() ) { metadatainfo = true; str << "Date" << "\t" << sanitize_xmplay_info_string( date ) << "\r"; } if ( metadatainfo ) { str << "\r"; } str << "Format" << "\t" << sanitize_xmplay_info_string( self->mod->get_metadata("type") ) << " (" << sanitize_xmplay_info_string( self->mod->get_metadata("type_long") ) << ")" << "\r"; if ( !self->mod->get_metadata("originaltype").empty() ) { str << "Original Type" << "\t" << sanitize_xmplay_info_string( self->mod->get_metadata("originaltype") ) << " (" << sanitize_xmplay_info_string( self->mod->get_metadata("originaltype_long") ) << ")" << "\r"; } if ( !self->mod->get_metadata("container").empty() ) { str << "Container" << "\t" << sanitize_xmplay_info_string( self->mod->get_metadata("container") ) << " (" << sanitize_xmplay_info_string( self->mod->get_metadata("container_long") ) << ")" << "\r"; } str << "Channels" << "\t" << self->mod->get_num_channels() << "\r" << "Orders" << "\t" << self->mod->get_num_orders() << "\r" << "Patterns" << "\t" << self->mod->get_num_patterns() << "\r"; if ( self->mod->get_num_instruments() != 0 ) { str << "Instruments" << "\t" << self->mod->get_num_instruments() << "\r"; } str << "Samples" << "\t" << self->mod->get_num_samples() << "\r"; if( !self->single_subsong_mode && self->subsong_lengths.size() > 1 ) { for ( std::size_t i = 0; i < self->subsong_lengths.size(); ++i ) { str << ( i == 0 ? "Subsongs\t" : "\t" ) << (i + 1) << ". " << seconds_to_string( self->subsong_lengths[i]) << " " << self->subsong_names[i] << "\r"; } } str << "\r" << "Tracker" << "\t" << sanitize_xmplay_info_string( self->mod->get_metadata("tracker") ) << "\r" << "Player" << "\t" << "xmp-openmpt" << " version " << openmpt::string::get( "library_version" ) << "\r" ; std::string warnings = self->mod->get_metadata("warnings"); if ( !warnings.empty() ) { str << "Warnings" << "\t" << sanitize_xmplay_multiline_info_string( warnings ) << "\r"; } str << "\r"; write_xmplay_string( buf, str.str() ); } // get text for "Message" info window // separate tag names and values with a tab (\t), and end each line with a carriage-return (\r) static void WINAPI openmpt_GetMessage( char * buf ) { if ( !self->mod ) { clear_xmplay_string( buf ); return; } write_xmplay_string( buf, convert_to_native( sanitize_xmplay_multiline_string( string_replace( self->mod->get_metadata("message"), "\n", "\r" ) ) ) ); } // Seek to a position (in granularity units) // return the new position in seconds (-1 = failed) static double WINAPI openmpt_SetPosition( DWORD pos ) { if ( !self->mod ) { return -1.0; } if ( pos == static_cast(static_cast(XMPIN_POS_LOOP)) ) { // If the time of the loop start position is known, that should be returned, otherwise -2 can be returned to let the time run on. // There is currently no way to easily figure out at which time the loop restarts. return -2; } else if ( pos == static_cast(static_cast(XMPIN_POS_AUTOLOOP)) ) { // In the auto-looping case, the function should only loop when a loop has been detected, and otherwise return -1 // If the time of the loop start position is known, that should be returned, otherwise -2 can be returned to let the time run on. // There is currently no way to easily figure out at which time the loop restarts. return -2; } if ( pos & XMPIN_POS_SUBSONG ) { self->single_subsong_mode = ( pos & XMPIN_POS_SUBSONG1 ) != 0; const int32_t subsong = pos & 0xffff; try { self->mod->select_subsong( subsong ); } catch ( ... ) { return 0.0; } openmpt::ext::interactive *interactive = static_cast( self->mod->get_interface( openmpt::ext::interactive_id ) ); xmpfin->SetLength( static_cast( self->subsong_lengths[ subsong ] / interactive->get_tempo_factor() ), TRUE ); xmpfin->UpdateTitle( nullptr ); reset_timeinfos( 0 ); return 0.0; } double new_position = self->mod->set_position_seconds( static_cast( pos ) * 0.001 ); reset_timeinfos( new_position ); return new_position; } // Get the seeking granularity in seconds static double WINAPI openmpt_GetGranularity() { return 0.001; } // get some sample data, always floating-point // count=number of floats to write (not bytes or samples) // return number of floats written. if it's less than requested, playback is ended... // so wait for more if there is more to come (use CheckCancel function to check if user wants to cancel) static DWORD WINAPI openmpt_Process( float * dstbuf, DWORD count ) { xmpopenmpt_lock guard; if ( !self->mod || self->num_channels == 0 ) { return 0; } update_timeinfos( self->samplerate, 0 ); std::size_t frames = count / self->num_channels; std::size_t frames_to_render = frames; std::size_t frames_rendered = 0; while ( frames_to_render > 0 ) { std::size_t frames_chunk = std::min( frames_to_render, static_cast( ( self->samplerate + 99 ) / 100 ) ); // 100 Hz timing info update interval switch ( self->num_channels ) { case 1: { frames_chunk = self->mod->read( self->samplerate, frames_chunk, dstbuf ); } break; case 2: { frames_chunk = self->mod->read_interleaved_stereo( self->samplerate, frames_chunk, dstbuf ); } break; case 4: { frames_chunk = self->mod->read_interleaved_quad( self->samplerate, frames_chunk, dstbuf ); } break; } dstbuf += frames_chunk * self->num_channels; if ( frames_chunk == 0 ) { break; } update_timeinfos( self->samplerate, frames_chunk ); frames_to_render -= frames_chunk; frames_rendered += frames_chunk; } if ( frames_rendered == 0 ) { return 0; } return frames_rendered * self->num_channels; } static void add_names( std::ostream & str, const std::string & title, const std::vector & names, int display_offset ) { if ( names.size() > 0 ) { bool valid = false; for ( std::size_t i = 0; i < names.size(); i++ ) { if ( names[i] != "" ) { valid = true; } } if ( !valid ) { return; } str << title << " Names:" << "\r"; for ( std::size_t i = 0; i < names.size(); i++ ) { str << std::setfill('0') << std::setw(2) << (display_offset + i) << std::setw(0) << "\t" << convert_to_native( names[i] ) << "\r"; } str << "\r"; } } static void WINAPI openmpt_GetSamples( char * buf ) { if ( !self->mod ) { clear_xmplay_string( buf ); return; } std::ostringstream str; add_names( str, "Instrument", self->mod->get_instrument_names(), 1 ); add_names( str, "Sample", self->mod->get_sample_names(), 1 ); add_names( str, "Channel", self->mod->get_channel_names(), 1 ); add_names( str, "Order", self->mod->get_order_names(), 0 ); add_names( str, "Pattern", self->mod->get_pattern_names(), 0 ); write_xmplay_string( buf, str.str() ); } static DWORD WINAPI openmpt_GetSubSongs( float * length ) { *length = 0.0f; for ( auto sub_length : self->subsong_lengths ) { *length += sub_length; } return static_cast( self->subsong_lengths.size() ); } enum ColorIndex { col_background = 0, col_unknown, col_text, col_empty, col_instr, col_vol, col_pitch, col_global, col_max }; static ColorIndex effect_type_to_color_index( openmpt::ext::pattern_vis::effect_type effect_type ) { switch ( effect_type ) { case openmpt::ext::pattern_vis::effect_unknown: return col_unknown; break; case openmpt::ext::pattern_vis::effect_general: return col_text ; break; case openmpt::ext::pattern_vis::effect_global : return col_global ; break; case openmpt::ext::pattern_vis::effect_volume : return col_vol ; break; case openmpt::ext::pattern_vis::effect_panning: return col_instr ; break; case openmpt::ext::pattern_vis::effect_pitch : return col_pitch ; break; default: return col_unknown; break; } } const struct Columns { int num_chars; int color; } pattern_columns[] = { { 3, col_text }, // C-5 { 2, col_instr }, // 01 { 3, col_vol }, // v64 { 3, col_pitch }, // EFF }; static const int max_cols = 4; static void assure_width( std::string & str, std::size_t width ) { if ( str.length() == width ) { return; } else if ( str.length() < width ) { str += std::string( width - str.length(), ' ' ); } else if ( str.length() > width ) { str = str.substr( 0, width ); } } struct ColorRGBA { uint8_t r, g, b, a; }; union Color { ColorRGBA rgba; COLORREF dw; }; static_assert(sizeof(Color) == 4); HDC visDC; HGDIOBJ visbitmap; Color viscolors[col_max]; HPEN vispens[col_max]; HBRUSH visbrushs[col_max]; HFONT visfont; static int last_pattern = -1; static Color invert_color( Color c ) { Color res; res.rgba.a = c.rgba.a; res.rgba.r = 255 - c.rgba.r; res.rgba.g = 255 - c.rgba.g; res.rgba.b = 255 - c.rgba.b; return res; } static BOOL WINAPI VisOpen(DWORD colors[3]) { xmpopenmpt_lock guard; visDC = 0; visbitmap = 0; visfont = 0; viscolors[col_background].dw = colors[0]; viscolors[col_unknown].dw = colors[1]; viscolors[col_text].dw = colors[2]; viscolors[col_global] = invert_color( viscolors[col_background] ); const int r = viscolors[col_text].rgba.r, g = viscolors[col_text].rgba.g, b = viscolors[col_text].rgba.b; viscolors[col_empty].rgba.r = static_cast( (r + viscolors[col_background].rgba.r) / 2 ); viscolors[col_empty].rgba.g = static_cast( (g + viscolors[col_background].rgba.g) / 2 ); viscolors[col_empty].rgba.b = static_cast( (b + viscolors[col_background].rgba.b) / 2 ); viscolors[col_empty].rgba.a = 0; #define MIXCOLOR(col, c1, c2, c3) { \ viscolors[col] = viscolors[col_text]; \ int mix = viscolors[col].rgba.c1 + 0xA0; \ viscolors[col].rgba.c1 = static_cast( mix ); \ if ( mix > 0xFF ) { \ viscolors[col].rgba.c2 = std::max( static_cast( c2 - viscolors[col].rgba.c1 / 2 ), std::uint8_t(0) ); \ viscolors[col].rgba.c3 = std::max( static_cast( c3 - viscolors[col].rgba.c1 / 2 ), std::uint8_t(0) ); \ viscolors[col].rgba.c1 = 0xFF; \ } } MIXCOLOR(col_instr, g, r, b); MIXCOLOR(col_vol, b, r, g); MIXCOLOR(col_pitch, r, g, b); #undef MIXCOLOR for( int i = 0; i < col_max; ++i ) { vispens[i] = CreatePen( PS_SOLID, 1, viscolors[i].dw ); visbrushs[i] = CreateSolidBrush( viscolors[i].dw ); } clear_current_timeinfo(); if ( !self->mod ) { return FALSE; } return TRUE; } static void WINAPI VisClose() { xmpopenmpt_lock guard; for( int i = 0; i < col_max; ++i ) { DeletePen( vispens[i] ); DeleteBrush( visbrushs[i] ); } DeleteFont( visfont ); DeleteBitmap( visbitmap ); if ( visDC ) { DeleteDC( visDC ); } } static void WINAPI VisSize( HDC /* dc */ , SIZE * /* size */ ) { xmpopenmpt_lock guard; last_pattern = -1; // Force redraw } #if 0 static BOOL WINAPI VisRender( DWORD * /* buf */ , SIZE /* size */ , DWORD /* flags */ ) { xmpopenmpt_lock guard; return FALSE; } #endif static int get_pattern_width( int chars_per_channel, int spaces_per_channel, int num_cols, int text_size, int channels ) { int pattern_width = ((chars_per_channel * channels + 4) * text_size) + (spaces_per_channel * channels + channels - (num_cols == 1 ? 1 : 2)) * (text_size / 2); return pattern_width; } static BOOL WINAPI VisRenderDC( HDC dc, SIZE size, DWORD flags ) { xmpopenmpt_lock guard; RECT rect; if ( !visfont ) { // Force usage of a nice monospace font LOGFONT logfont; GetObject ( GetCurrentObject( dc, OBJ_FONT ), sizeof(logfont), &logfont ); _tcscpy( logfont.lfFaceName, TEXT("Lucida Console") ); visfont = CreateFontIndirect( &logfont ); } SIZE text_size; SelectFont( dc, visfont ); if ( GetTextExtentPoint32( dc, TEXT("W"), 1, &text_size ) == FALSE ) { return FALSE; } if ( flags & XMPIN_VIS_INIT ) { last_pattern = -1; } timeinfo info = lookup_timeinfo( xmpfstatus->GetTime() ); if ( !info.valid ) { RECT bgrect; bgrect.top = 0; bgrect.left = 0; bgrect.right = size.cx; bgrect.bottom = size.cy; FillRect(dc, &bgrect, visbrushs[col_background]); return TRUE; } int pattern = info.pattern; int current_row = info.row; const std::size_t channels = self->mod->get_num_channels(); const std::size_t rows = self->mod->get_pattern_num_rows( pattern ); const std::size_t num_half_chars = std::max( static_cast( 2 * size.cx / text_size.cx ), std::size_t(8) ) - 8; //const std::size_t num_rows = size.cy / text_size.cy; // Spaces between pattern components are half width, full space at channel end const std::size_t half_chars_per_channel = num_half_chars / channels; std::size_t chars_per_channel, spaces_per_channel; std::size_t num_cols; std::size_t col0_width = pattern_columns[0].num_chars; for ( num_cols = sizeof ( pattern_columns ) / sizeof ( pattern_columns[0] ); num_cols >= 1; num_cols-- ) { chars_per_channel = 0; spaces_per_channel = num_cols > 1 ? num_cols : 0; // No extra space if we only display notes for ( std::size_t i = 0; i < num_cols; i++ ) { chars_per_channel += pattern_columns[i].num_chars; } if ( half_chars_per_channel >= chars_per_channel * 2 + spaces_per_channel + 1 || num_cols == 1 ) { break; } } if ( !self->settings.vis_allow_scroll ) { if ( num_cols == 1 ) { spaces_per_channel = 0; while ( get_pattern_width( chars_per_channel, spaces_per_channel, num_cols, text_size.cx, channels ) > size.cx && chars_per_channel > 1 ) { chars_per_channel -= 1; } col0_width = chars_per_channel; chars_per_channel = col0_width; } } int pattern_width = get_pattern_width( chars_per_channel, spaces_per_channel, num_cols, text_size.cx, channels ); int pattern_height = rows * text_size.cy; if ( !visDC || last_pattern != pattern ) { DeleteBitmap( visbitmap ); if ( visDC ) { DeleteDC( visDC ); } visDC = CreateCompatibleDC( dc ); visbitmap = CreateCompatibleBitmap( dc, pattern_width, pattern_height ); SelectBitmap( visDC, visbitmap ); SelectBrush( visDC, vispens[col_unknown] ); SelectBrush( visDC, visbrushs[col_background] ); SelectFont( visDC, visfont ); rect.top = 0; rect.left = 0; rect.right = pattern_width; rect.bottom = pattern_height; FillRect( visDC, &rect, visbrushs[col_background] ); SetBkColor( visDC, viscolors[col_background].dw ); POINT pos; pos.y = 0; for ( std::size_t row = 0; row < rows; row++ ) { pos.x = 0; std::ostringstream s; s.imbue(std::locale::classic()); s << std::setfill('0') << std::setw(3) << row; const std::string rowstr = s.str(); SetTextColor( visDC, viscolors[1].dw ); TextOutA( visDC, pos.x, pos.y, rowstr.c_str(), rowstr.length() ); pos.x += 4 * text_size.cx; for ( std::size_t channel = 0; channel < channels; ++channel ) { struct coldata { std::string text; bool is_empty; ColorIndex color; coldata() : is_empty(false) , color(col_unknown) { return; } }; coldata cols[max_cols]; for ( std::size_t col = 0; col < max_cols; ++col ) { switch ( col ) { case 0: cols[col].text = self->mod->format_pattern_row_channel_command( pattern, row, channel, openmpt::module::command_note ); break; case 1: cols[col].text = self->mod->format_pattern_row_channel_command( pattern, row, channel, openmpt::module::command_instrument ); break; case 2: cols[col].text = self->mod->format_pattern_row_channel_command( pattern, row, channel, openmpt::module::command_volumeffect ) + self->mod->format_pattern_row_channel_command( pattern, row, channel, openmpt::module::command_volume ); break; case 3: cols[col].text = self->mod->format_pattern_row_channel_command( pattern, row, channel, openmpt::module::command_effect ) + self->mod->format_pattern_row_channel_command( pattern, row, channel, openmpt::module::command_parameter ); break; } int color = pattern_columns[col].color; if ( self->pattern_vis && ( col == 2 || col == 3 ) ) { if ( col == 2 ) { color = effect_type_to_color_index( self->pattern_vis->get_pattern_row_channel_volume_effect_type( pattern, row, channel ) ); } if ( col == 3 ) { color = effect_type_to_color_index( self->pattern_vis->get_pattern_row_channel_effect_type( pattern, row, channel ) ); } } switch ( cols[col].text[0] ) { case ' ': [[fallthrough]]; case '.': cols[col].is_empty = true; [[fallthrough]]; case '^': [[fallthrough]]; case '=': [[fallthrough]]; case '~': color = col_empty; break; } cols[col].color = (ColorIndex)color; } if ( num_cols <= 3 && !cols[3].is_empty ) { if ( cols[2].is_empty ) { cols[2] = cols[3]; } else if ( cols[0].is_empty ) { cols[0] = cols[3]; } } if ( num_cols <= 2 && !cols[2].is_empty ) { if ( cols[0].is_empty ) { cols[0] = cols[2]; } } for ( std::size_t col = 0; col < num_cols; ++col ) { std::size_t col_width = ( num_cols > 1 ) ? pattern_columns[col].num_chars : col0_width; assure_width( cols[col].text, col_width ); SetTextColor( visDC, viscolors[cols[col].color].dw ); TextOutA( visDC, pos.x, pos.y, cols[col].text.c_str(), cols[col].text.length() ); pos.x += col_width * text_size.cx + text_size.cx / 2; } // Extra padding if ( num_cols > 1 ) { pos.x += text_size.cx / 2; } } pos.y += text_size.cy; } } rect.top = 0; rect.left = 0; rect.right = size.cx; rect.bottom = size.cy; FillRect( dc, &rect, visbrushs[col_background] ); int offset_x = (size.cx - pattern_width) / 2; int offset_y = (size.cy - text_size.cy) / 2 - current_row * text_size.cy; int src_offset_x = 0; int src_offset_y = 0; if ( offset_x < 0 ) { src_offset_x -= offset_x; pattern_width = std::min( static_cast( pattern_width + offset_x ), static_cast( size.cx ) ); offset_x = 0; } if ( offset_y < 0 ) { src_offset_y -= offset_y; pattern_height = std::min( static_cast( pattern_height + offset_y ), static_cast( size.cy ) ); offset_y = 0; } BitBlt( dc, offset_x, offset_y, pattern_width, pattern_height, visDC, src_offset_x, src_offset_y , SRCCOPY ); // Highlight current row rect.left = offset_x; rect.top = (size.cy - text_size.cy) / 2; rect.right = rect.left + pattern_width; rect.bottom = rect.top + text_size.cy; InvertRect( dc, &rect ); last_pattern = pattern; return TRUE; } #if 0 static void WINAPI VisButton( DWORD /* x */ , DWORD /* y */ ) { //xmpopenmpt_lock guard; } #endif static XMPIN xmpin = { #ifdef USE_XMPLAY_FILE_IO 0 | #else XMPIN_FLAG_NOXMPFILE | #endif XMPIN_FLAG_CONFIG | XMPIN_FLAG_LOOP, xmp_openmpt_string, nullptr, // "libopenmpt\0mptm/mptmz", openmpt_About, openmpt_Config, openmpt_CheckFile, openmpt_GetFileInfo, openmpt_Open, openmpt_Close, nullptr, // reserved openmpt_SetFormat, openmpt_GetTags, openmpt_GetInfoText, openmpt_GetGeneralInfo, openmpt_GetMessage, openmpt_SetPosition, openmpt_GetGranularity, nullptr, // GetBuffering openmpt_Process, nullptr, // WriteFile openmpt_GetSamples, openmpt_GetSubSongs, // GetSubSongs nullptr, // GetCues nullptr, // GetDownloaded "OpenMPT Pattern Display", VisOpen, VisClose, VisSize, /*VisRender,*/nullptr, VisRenderDC, /*VisButton,*/nullptr, nullptr, // reserved2 openmpt_GetConfig, openmpt_SetConfig }; static const char * xmp_openmpt_default_exts = "OpenMPT\0mptm/mptmz"; static char * file_formats; static void xmp_openmpt_on_dll_load() { ZeroMemory( &xmpopenmpt_mutex, sizeof( xmpopenmpt_mutex ) ); #if defined(_MSC_VER) #pragma warning(push) #pragma warning(disable:28125) // The function 'InitializeCriticalSection' must be called from within a try/except block: The requirement might be conditional. #endif // _MSC_VER InitializeCriticalSection( &xmpopenmpt_mutex ); #if defined(_MSC_VER) #pragma warning(pop) #endif // _MSC_VER std::vector extensions = openmpt::get_supported_extensions(); std::string filetypes_string = "OpenMPT"; filetypes_string.push_back('\0'); bool first = true; for ( const auto & ext : extensions ) { if ( first ) { first = false; } else { filetypes_string.push_back('/'); } filetypes_string += ext; } filetypes_string.push_back('\0'); file_formats = (char*)HeapAlloc( GetProcessHeap(), 0, filetypes_string.size() ); if ( file_formats ) { std::copy( filetypes_string.begin(), filetypes_string.end(), file_formats ); xmpin.exts = file_formats; } else { xmpin.exts = xmp_openmpt_default_exts; } self = new self_xmplay_t(); } static void xmp_openmpt_on_dll_unload() { delete self; self = nullptr; if ( xmpin.exts != xmp_openmpt_default_exts ) { HeapFree(GetProcessHeap(), 0, (LPVOID)const_cast(xmpin.exts)); } xmpin.exts = nullptr; DeleteCriticalSection( &xmpopenmpt_mutex ); } static XMPIN * XMPIN_GetInterface_cxx( DWORD face, InterfaceProc faceproc ) { if ( face != XMPIN_FACE ) return nullptr; xmpfin=(XMPFUNC_IN*)faceproc(XMPFUNC_IN_FACE); xmpfmisc=(XMPFUNC_MISC*)faceproc(XMPFUNC_MISC_FACE); xmpfregistry=(XMPFUNC_REGISTRY*)faceproc(XMPFUNC_REGISTRY_FACE); xmpffile=(XMPFUNC_FILE*)faceproc(XMPFUNC_FILE_FACE); xmpftext=(XMPFUNC_TEXT*)faceproc(XMPFUNC_TEXT_FACE); xmpfstatus=(XMPFUNC_STATUS*)faceproc(XMPFUNC_STATUS_FACE); // Register keyboard shortcuts static constexpr std::pair shortcuts[] = { { openmpt_shortcut_ex | openmpt_shortcut_tempo_decrease, "OpenMPT - Decrease Tempo" }, { openmpt_shortcut_ex | openmpt_shortcut_tempo_increase, "OpenMPT - Increase Tempo" }, { openmpt_shortcut_ex | openmpt_shortcut_pitch_decrease, "OpenMPT - Decrease Pitch" }, { openmpt_shortcut_ex | openmpt_shortcut_pitch_increase, "OpenMPT - Increase Pitch" }, { openmpt_shortcut_ex | openmpt_shortcut_switch_interpolation, "OpenMPT - Switch Interpolation" }, }; XMPSHORTCUT cut; cut.procex = &ShortcutHandler; for ( const auto & shortcut : shortcuts ) { cut.id = shortcut.first; cut.text = shortcut.second; xmpfmisc->RegisterShortcut( &cut ); } self->settings.load(); return &xmpin; } extern "C" { // XMPLAY expects a WINAPI (which is __stdcall) function using an undecorated symbol name. #if defined(__GNUC__) XMPIN * WINAPI XMPIN_GetInterface_( DWORD face, InterfaceProc faceproc ); XMPIN * WINAPI XMPIN_GetInterface_( DWORD face, InterfaceProc faceproc ) { return XMPIN_GetInterface_cxx( face, faceproc ); } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wattribute-alias" // clang-format off __declspec(dllexport) void XMPIN_GetInterface() __attribute__((alias("XMPIN_GetInterface_@8"))); // clang-format on #pragma GCC diagnostic pop #else XMPIN * WINAPI XMPIN_GetInterface( DWORD face, InterfaceProc faceproc ) { return XMPIN_GetInterface_cxx( face, faceproc ); } #pragma comment(linker, "/EXPORT:XMPIN_GetInterface=_XMPIN_GetInterface@8") #endif } // extern "C" #if defined(MPT_WITH_MFC) && defined(_MFC_VER) namespace libopenmpt { namespace plugin { void DllMainAttach() { xmp_openmpt_on_dll_load(); } void DllMainDetach() { xmp_openmpt_on_dll_unload(); } } // namespace plugin } // namespace libopenmpt #else BOOL WINAPI DllMain( HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved ); BOOL WINAPI DllMain( HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved ) { static_cast(hinstDLL); static_cast(lpvReserved); switch ( fdwReason ) { case DLL_PROCESS_ATTACH: xmp_openmpt_on_dll_load(); break; case DLL_PROCESS_DETACH: xmp_openmpt_on_dll_unload(); break; } return TRUE; } #endif #endif // NO_XMPLAY