diff options
author | Jef <jef@targetspot.com> | 2024-09-24 08:54:57 -0400 |
---|---|---|
committer | Jef <jef@targetspot.com> | 2024-09-24 08:54:57 -0400 |
commit | 20d28e80a5c861a9d5f449ea911ab75b4f37ad0d (patch) | |
tree | 12f17f78986871dd2cfb0a56e5e93b545c1ae0d0 /Src/external_dependencies/openmpt-trunk/libopenmpt/xmp-openmpt.cpp | |
parent | 537bcbc86291b32fc04ae4133ce4d7cac8ebe9a7 (diff) | |
download | winamp-20d28e80a5c861a9d5f449ea911ab75b4f37ad0d.tar.gz |
Initial community commit
Diffstat (limited to 'Src/external_dependencies/openmpt-trunk/libopenmpt/xmp-openmpt.cpp')
-rw-r--r-- | Src/external_dependencies/openmpt-trunk/libopenmpt/xmp-openmpt.cpp | 1893 |
1 files changed, 1893 insertions, 0 deletions
diff --git a/Src/external_dependencies/openmpt-trunk/libopenmpt/xmp-openmpt.cpp b/Src/external_dependencies/openmpt-trunk/libopenmpt/xmp-openmpt.cpp new file mode 100644 index 00000000..4221fe44 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/libopenmpt/xmp-openmpt.cpp @@ -0,0 +1,1893 @@ +/* + * 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 <afxwin.h> +#include <afxcmn.h> +#endif // MPT_WITH_MFC +#include <windows.h> +#include <WindowsX.h> + +#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 <cctype> +#include <cstring> + +#include <tchar.h> + +#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 <algorithm> +#include <fstream> +#include <iomanip> +#include <iostream> +#include <iterator> +#include <map> +#include <queue> +#include <sstream> +#include <string> + +#include <cmath> + +#include <pugixml.hpp> + +#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<TCHAR> & 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<TCHAR> & /* 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<float> subsong_lengths; + std::vector<std::string> 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<openmpt::ext::pattern_vis *>( 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<CHAR> 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<WCHAR> 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 <typename Tstring, typename Tstring2, typename Tstring3> +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<char>( std::toupper( c ) ); } ); + return str; +} + +static std::string seconds_to_string( float time ) { + std::int64_t time_ms = static_cast<std::int64_t>( 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<std::string,int> & 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<std::string,int> & 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<std::string,int> & 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<std::string,int> 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<std::string,int> 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<std::int32_t>( 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<openmpt::ext::interactive *>( self->mod->get_interface( openmpt::ext::interactive_id ) ); + interactive->set_tempo_factor( tempo_factor ); + interactive->set_pitch_factor( pitch_factor ); + xmpfin->SetLength( static_cast<float>( 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<timeinfo> 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<void>(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<char> 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<std::size_t>( 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<char *>( 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<std::streambuf *>(this)) { + return; + } +}; + +#else // !USE_XMPLAY_ISTREAM + +static std::vector<char> read_XMPFILE_vector( XMPFILE & file ) { + std::vector<char> data( xmpffile->GetSize( file ) ); + if ( data.size() != xmpffile->Read( file, data.data(), data.size() ) ) { + return std::vector<char>(); + } + return data; +} + +static std::string read_XMPFILE_string( XMPFILE & file ) { + std::vector<char> 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<size_t>( 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<char*>( 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<float*>( 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<void>( filename ); + try { + #ifdef USE_XMPLAY_FILE_IO + #ifdef USE_XMPLAY_ISTREAM + switch ( xmpffile->GetType( file ) ) { + case XMPFILE_TYPE_MEMORY: + { + xmplay_imemstream s( reinterpret_cast<const char *>( 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<const char*>( 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<void>( 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<void>( 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<float>( 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<char*>( 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<DWORD>(static_cast<LONG>(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<DWORD>(static_cast<LONG>(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<openmpt::ext::interactive *>( self->mod->get_interface( openmpt::ext::interactive_id ) ); + xmpfin->SetLength( static_cast<float>( 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<double>( 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<std::size_t>( ( 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<std::string> & 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<DWORD>( 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<std::uint8_t>( (r + viscolors[col_background].rgba.r) / 2 ); + viscolors[col_empty].rgba.g = static_cast<std::uint8_t>( (g + viscolors[col_background].rgba.g) / 2 ); + viscolors[col_empty].rgba.b = static_cast<std::uint8_t>( (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<std::uint8_t>( mix ); \ + if ( mix > 0xFF ) { \ + viscolors[col].rgba.c2 = std::max( static_cast<std::uint8_t>( c2 - viscolors[col].rgba.c1 / 2 ), std::uint8_t(0) ); \ + viscolors[col].rgba.c3 = std::max( static_cast<std::uint8_t>( 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<std::size_t>( 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<int>( pattern_width + offset_x ), static_cast<int>( size.cx ) ); + offset_x = 0; + } + + if ( offset_y < 0 ) { + src_offset_y -= offset_y; + pattern_height = std::min( static_cast<int>( pattern_height + offset_y ), static_cast<int>( 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<std::string> 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<char*>(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<DWORD, const char *> 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<void>(hinstDLL); + static_cast<void>(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 |