/** * vgmstream for Winamp */ /* Normally Winamp opens unicode files by their DOS 8.3 name. #define this to use wchar_t filenames, * which must be opened with _wfopen in a WINAMP_STREAMFILE (needed for dual files like .pos). * Only for Winamp paths, other parts would need #define UNICODE for Windows. */ #ifdef VGM_WINAMP_UNICODE #define UNICODE_INPUT_PLUGIN #endif #ifdef _MSC_VER #define _CRT_SECURE_NO_DEPRECATE #endif #include #include #include #include #include #include #include #include "../src/vgmstream.h" #include "../src/plugins.h" #include "sdk/in2.h" #include "sdk/wa_ipc.h" #include "sdk/ipc_pe.h" #include "resource.h" #ifndef VERSION #include "version.h" #endif #ifndef VERSION #define VERSION "(unknown version)" #endif #define PLUGIN_DESCRIPTION "vgmstream plugin " VERSION " " __DATE__ /* ************************************* */ /* plugin module (declared at the bottom of this file) */ In_Module input_module; DWORD WINAPI __stdcall decode(void *arg); /* Winamp Play extension list, to accept and associate extensions in Windows */ #define EXTENSION_LIST_SIZE (0x2000 * 6) #define EXT_BUFFER_SIZE 200 /* fixed list to simplify but could also malloc/free on init/close */ char working_extension_list[EXTENSION_LIST_SIZE] = {0}; typedef enum { REPLAYGAIN_NONE, REPLAYGAIN_ALBUM, REPLAYGAIN_TRACK } ReplayGainType; /* defaults */ typedef struct { double fade_seconds; double fade_delay_seconds; double loop_count; int ignore_loop; int loop_forever; int thread_priority; int disable_subsongs; int downmix_channels; int tagfile_disable; int exts_unknown_on; int exts_common_on; ReplayGainType gain_type; ReplayGainType clip_type; } winamp_settings; /* current song settings */ typedef struct { int song_play_forever; double song_loop_count; double song_fade_time; double song_fade_delay; int song_ignore_loop; int song_really_force_loop; int song_ignore_fade; } winamp_song_config; winamp_settings settings; winamp_song_config config; /* Winamp needs at least 576 16-bit samples, stereo, doubled in case DSP effects are active */ #define SAMPLE_BUFFER_SIZE 576 short sample_buffer[SAMPLE_BUFFER_SIZE*2 * VGMSTREAM_MAX_CHANNELS]; //todo maybe should be dynamic /* plugin state */ VGMSTREAM * vgmstream = NULL; HANDLE decode_thread_handle = INVALID_HANDLE_VALUE; int paused = 0; int decode_abort = 0; int seek_needed_samples = -1; int decode_pos_ms = 0; int decode_pos_samples = 0; int stream_length_samples = 0; int fade_samples = 0; int output_channels = 0; double volume = 1.0; const char* tagfile_name = "!tags.m3u"; in_char lastfn[PATH_LIMIT] = {0}; /* name of the currently playing file */ /* ************************************* */ /* IN_UNICODE */ /* ************************************* */ //todo safe ops //todo there must be a better way to handle unicode... #ifdef UNICODE_INPUT_PLUGIN #define wa_strcmp wcscmp #define wa_strcpy wcscpy #define wa_strncpy wcsncpy #define wa_strcat wcscat #define wa_strlen wcslen #define wa_strchr wcschr #define wa_sscanf swscanf #define wa_snprintf _snwprintf #define wa_strrchr wcsrchr #define wa_fileinfo fileinfoW #define wa_IPC_PE_INSERTFILENAME IPC_PE_INSERTFILENAMEW #define wa_L(x) L ##x #else #define wa_strcmp strcmp #define wa_strcpy strcpy #define wa_strncpy strncpy #define wa_strcat strcat #define wa_strlen strlen #define wa_strchr strchr #define wa_sscanf sscanf #define wa_snprintf snprintf #define wa_strrchr strrchr #define wa_fileinfo fileinfo #define wa_IPC_PE_INSERTFILENAME IPC_PE_INSERTFILENAME #define wa_L(x) x #endif /* converts from utf16 to utf8 (if unicode is on) */ static void wa_ichar_to_char(char *dst, size_t dstsize, const in_char *wsrc) { #ifdef UNICODE_INPUT_PLUGIN /* converto to UTF8 codepage, default separate bytes, source wstr, wstr lenght, */ //int size_needed = WideCharToMultiByte(CP_UTF8,0, src,-1, NULL,0, NULL, NULL); WideCharToMultiByte(CP_UTF8,0, wsrc,-1, dst,dstsize, NULL, NULL); #else strcpy(dst,wsrc); #endif } /* converts from utf8 to utf16 (if unicode is on) */ static void wa_char_to_ichar(in_char *wdst, size_t wdstsize, const char *src) { #ifdef UNICODE_INPUT_PLUGIN //int size_needed = MultiByteToWideChar(CP_UTF8,0, src,-1, NULL,0); MultiByteToWideChar(CP_UTF8,0, src,-1, wdst,wdstsize); #else strcpy(wdst,src); #endif } /* copies from utf16 to utf16 (if unicode is active) */ static void wa_wchar_to_ichar(in_char *wdst, size_t wdstsize, const wchar_t *src) { #ifdef UNICODE_INPUT_PLUGIN wcscpy(wdst,src); #else strcpy(wdst,src); //todo ??? #endif } /* copies from utf16 to utf16 */ static void wa_char_to_wchar(wchar_t *wdst, size_t wdstsize, const char *src) { #ifdef UNICODE_INPUT_PLUGIN MultiByteToWideChar(CP_UTF8,0, src,-1, wdst,wdstsize); #else strcpy(wdst,src); //todo ??? #endif } /* opens a utf16 (unicode) path */ static FILE* wa_fopen(const in_char *wpath) { #ifdef UNICODE_INPUT_PLUGIN return _wfopen(wpath,L"rb"); #else return fopen(wpath,"rb"); #endif } /* dupes a utf16 (unicode) file */ static FILE* wa_fdopen(int fd) { #ifdef UNICODE_INPUT_PLUGIN return _wfdopen(fd,L"rb"); #else return fdopen(fd,"rb"); #endif } /* ************************************* */ /* IN_STREAMFILE */ /* ************************************* */ /* a STREAMFILE that operates via STDIOSTREAMFILE but handles Winamp's unicode (in_char) paths */ typedef struct { STREAMFILE sf; STREAMFILE *stdiosf; FILE *infile_ref; /* pointer to the infile in stdiosf (partially handled by stdiosf) */ } WINAMP_STREAMFILE; static STREAMFILE *open_winamp_streamfile_by_file(FILE *infile, const char * path); static STREAMFILE *open_winamp_streamfile_by_ipath(const in_char *wpath); static size_t wasf_read(WINAMP_STREAMFILE *streamfile, uint8_t *dest, off_t offset, size_t length) { return streamfile->stdiosf->read(streamfile->stdiosf,dest,offset,length); } static off_t wasf_get_size(WINAMP_STREAMFILE *streamfile) { return streamfile->stdiosf->get_size(streamfile->stdiosf); } static off_t wasf_get_offset(WINAMP_STREAMFILE *streamfile) { return streamfile->stdiosf->get_offset(streamfile->stdiosf); } static void wasf_get_name(WINAMP_STREAMFILE *streamfile, char *buffer, size_t length) { streamfile->stdiosf->get_name(streamfile->stdiosf, buffer, length); } static STREAMFILE *wasf_open(WINAMP_STREAMFILE *streamFile, const char *const filename, size_t buffersize) { int newfd; FILE *newfile; STREAMFILE *newstreamFile; in_char wpath[PATH_LIMIT]; char name[PATH_LIMIT]; if (!filename) return NULL; /* if same name, duplicate the file pointer we already have open */ //unsure if all this is needed streamFile->stdiosf->get_name(streamFile->stdiosf, name, PATH_LIMIT); if (streamFile->infile_ref && !strcmp(name,filename)) { if (((newfd = dup(fileno(streamFile->infile_ref))) >= 0) && (newfile = wa_fdopen(newfd))) { newstreamFile = open_winamp_streamfile_by_file(newfile,filename); if (newstreamFile) { return newstreamFile; } // failure, close it and try the default path (which will probably fail a second time) fclose(newfile); } } /* STREAMFILEs carry char/UTF8 names, convert to wchar for Winamp */ wa_char_to_ichar(wpath,PATH_LIMIT, filename); return open_winamp_streamfile_by_ipath(wpath); } static void wasf_close(WINAMP_STREAMFILE *streamfile) { /* closes infile_ref + frees in the internal STDIOSTREAMFILE (fclose for wchar is not needed) */ streamfile->stdiosf->close(streamfile->stdiosf); free(streamfile); /* and the current struct */ } static STREAMFILE *open_winamp_streamfile_by_file(FILE *infile, const char * path) { WINAMP_STREAMFILE *this_sf = NULL; STREAMFILE *stdiosf = NULL; this_sf = calloc(1,sizeof(WINAMP_STREAMFILE)); if (!this_sf) goto fail; stdiosf = open_stdio_streamfile_by_file(infile, path); if (!stdiosf) goto fail; this_sf->sf.read = (void*)wasf_read; this_sf->sf.get_size = (void*)wasf_get_size; this_sf->sf.get_offset = (void*)wasf_get_offset; this_sf->sf.get_name = (void*)wasf_get_name; this_sf->sf.open = (void*)wasf_open; this_sf->sf.close = (void*)wasf_close; this_sf->stdiosf = stdiosf; this_sf->infile_ref = infile; return &this_sf->sf; /* pointer to STREAMFILE start = rest of the custom data follows */ fail: close_streamfile(stdiosf); free(this_sf); return NULL; } static STREAMFILE *open_winamp_streamfile_by_ipath(const in_char *wpath) { FILE *infile = NULL; STREAMFILE *streamFile; char path[PATH_LIMIT]; /* convert to UTF-8 if needed for internal use */ wa_ichar_to_char(path,PATH_LIMIT, wpath); /* open a FILE from a Winamp (possibly UTF-16) path */ infile = wa_fopen(wpath); if (!infile) { /* allow non-existing files in some cases */ if (!vgmstream_is_virtual_filename(path)) return NULL; } streamFile = open_winamp_streamfile_by_file(infile,path); if (!streamFile) { if (infile) fclose(infile); } return streamFile; } /* opens vgmstream for winamp */ static VGMSTREAM* init_vgmstream_winamp(const in_char *fn, int stream_index) { VGMSTREAM * vgmstream = NULL; //return init_vgmstream(fn); /* manually init streamfile to pass the stream index */ STREAMFILE *streamFile = open_winamp_streamfile_by_ipath(fn); //open_stdio_streamfile(fn); if (streamFile) { streamFile->stream_index = stream_index; vgmstream = init_vgmstream_from_STREAMFILE(streamFile); close_streamfile(streamFile); } return vgmstream; } /* ************************************* */ /* IN_CONFIG */ /* ************************************* */ /* Windows unicode, separate from Winamp's unicode flag */ #ifdef UNICODE #define cfg_strncpy wcsncpy #define cfg_strncat wcsncat #define cfg_sprintf _swprintf #define cfg_sscanf swscanf #define cfg_strlen wcslen #define cfg_strrchr wcsrchr #else #define cfg_strncpy strncpy #define cfg_strncat strncat #define cfg_sprintf sprintf #define cfg_sscanf sscanf #define cfg_strlen strlen #define cfg_strrchr strrchr #endif /* converts from utf8 to utf16 (if unicode is active) */ static void cfg_char_to_wchar(TCHAR *wdst, size_t wdstsize, const char *src) { #ifdef UNICODE //int size_needed = MultiByteToWideChar(CP_UTF8,0, src,-1, NULL,0); MultiByteToWideChar(CP_UTF8,0, src,-1, wdst,wdstsize); #else strcpy(wdst,src); #endif } /* config */ #define CONFIG_APP_NAME TEXT("vgmstream plugin") #define CONFIG_INI_NAME TEXT("plugin.ini") #define DEFAULT_FADE_SECONDS TEXT("10.00") #define DEFAULT_FADE_DELAY_SECONDS TEXT("0.00") #define DEFAULT_LOOP_COUNT TEXT("2.00") #define DEFAULT_THREAD_PRIORITY 3 #define DEFAULT_LOOP_FOREVER 0 #define DEFAULT_IGNORE_LOOP 0 #define DEFAULT_DISABLE_SUBSONGS 0 #define DEFAULT_DOWNMIX_CHANNELS 0 #define DEFAULT_TAGFILE_DISABLE 0 #define DEFAULT_EXTS_UNKNOWN_ON 0 #define DEFAULT_EXTS_COMMON_ON 0 #define DEFAULT_GAIN_TYPE 1 #define DEFAULT_CLIP_TYPE 2 #define INI_ENTRY_FADE_SECONDS TEXT("fade_seconds") #define INI_ENTRY_FADE_DELAY_SECONDS TEXT("fade_delay") #define INI_ENTRY_LOOP_COUNT TEXT("loop_count") #define INI_ENTRY_THREAD_PRIORITY TEXT("thread_priority") #define INI_ENTRY_LOOP_FOREVER TEXT("loop_forever") #define INI_ENTRY_IGNORE_LOOP TEXT("ignore_loop") #define INI_ENTRY_DISABLE_SUBSONGS TEXT("disable_subsongs") #define INI_ENTRY_DOWNMIX_CHANNELS TEXT("downmix_channels") #define INI_ENTRY_TAGFILE_DISABLE TEXT("tagfile_disable") #define INI_ENTRY_EXTS_UNKNOWN_ON TEXT("exts_unknown_on") #define INI_ENTRY_EXTS_COMMON_ON TEXT("exts_common_on") #define INI_ENTRY_GAIN_TYPE TEXT("gain_type") #define INI_ENTRY_CLIP_TYPE TEXT("clip_type") TCHAR *priority_strings[] = { TEXT("Idle"), TEXT("Lowest"), TEXT("Below Normal"), TEXT("Normal"), TEXT("Above Normal"), TEXT("Highest (not recommended)"), TEXT("Time Critical (not recommended)") }; int priority_values[] = { THREAD_PRIORITY_IDLE, THREAD_PRIORITY_LOWEST, THREAD_PRIORITY_BELOW_NORMAL, THREAD_PRIORITY_NORMAL, THREAD_PRIORITY_ABOVE_NORMAL, THREAD_PRIORITY_HIGHEST, THREAD_PRIORITY_TIME_CRITICAL }; TCHAR *replaygain_strings[] = { TEXT("None"), TEXT("Album"), TEXT("Peak") }; // todo finish UNICODE (requires IPC_GETINIDIRECTORYW from later SDKs to read the ini path properly) /* Winamp INI reader */ static void GetINIFileName(TCHAR *iniFile) { /* if we're running on a newer winamp version that better supports * saving of settings to a per-user directory, use that directory - if not * then just revert to the old behaviour */ if(IsWindow(input_module.hMainWindow) && SendMessage(input_module.hMainWindow, WM_WA_IPC,0,IPC_GETVERSION) >= 0x5000) { TCHAR * iniDir = (TCHAR *)SendMessage(input_module.hMainWindow, WM_WA_IPC, 0, IPC_GETINIDIRECTORY); cfg_strncpy(iniFile, iniDir, PATH_LIMIT); cfg_strncat(iniFile, TEXT("\\Plugins\\"), PATH_LIMIT); /* can't be certain that \Plugins already exists in the user dir */ CreateDirectory(iniFile,NULL); cfg_strncat(iniFile, CONFIG_INI_NAME, PATH_LIMIT); } else { TCHAR * lastSlash; GetModuleFileName(NULL, iniFile, PATH_LIMIT); lastSlash = cfg_strrchr(iniFile, TEXT('\\')); *(lastSlash + 1) = 0; cfg_strncat(iniFile, TEXT("Plugins\\") CONFIG_INI_NAME,PATH_LIMIT); } } static void load_config() { TCHAR iniFile[PATH_LIMIT]; TCHAR buf[256]; size_t buf_size = 256; int consumed, res; GetINIFileName(iniFile); settings.thread_priority = GetPrivateProfileInt(CONFIG_APP_NAME,INI_ENTRY_THREAD_PRIORITY,DEFAULT_THREAD_PRIORITY,iniFile); if (settings.thread_priority < 0 || settings.thread_priority > 6) { cfg_sprintf(buf, TEXT("%d"),DEFAULT_THREAD_PRIORITY); WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_THREAD_PRIORITY,buf,iniFile); settings.thread_priority = DEFAULT_THREAD_PRIORITY; } GetPrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_FADE_SECONDS,DEFAULT_FADE_SECONDS,buf,buf_size,iniFile); res = cfg_sscanf(buf, TEXT("%lf%n"),&settings.fade_seconds,&consumed); if (res < 1 || consumed != cfg_strlen(buf) || settings.fade_seconds < 0) { WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_FADE_SECONDS,DEFAULT_FADE_SECONDS,iniFile); cfg_sscanf(DEFAULT_FADE_SECONDS, TEXT("%lf"),&settings.fade_seconds); } GetPrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_FADE_DELAY_SECONDS,DEFAULT_FADE_DELAY_SECONDS,buf,buf_size,iniFile); res = cfg_sscanf(buf, TEXT("%lf%n"),&settings.fade_delay_seconds,&consumed); if (res < 1 || consumed != cfg_strlen(buf)) { WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_FADE_DELAY_SECONDS,DEFAULT_FADE_DELAY_SECONDS,iniFile); cfg_sscanf(DEFAULT_FADE_DELAY_SECONDS, TEXT("%lf"),&settings.fade_delay_seconds); } GetPrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_LOOP_COUNT,DEFAULT_LOOP_COUNT,buf,buf_size,iniFile); res = cfg_sscanf(buf, TEXT("%lf%n"),&settings.loop_count,&consumed); if (res < 1 || consumed != cfg_strlen(buf) || settings.loop_count < 0) { WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_LOOP_COUNT,DEFAULT_LOOP_COUNT,iniFile); cfg_sscanf(DEFAULT_LOOP_COUNT, TEXT("%lf"),&settings.loop_count); } settings.loop_forever = GetPrivateProfileInt(CONFIG_APP_NAME,INI_ENTRY_LOOP_FOREVER,DEFAULT_LOOP_FOREVER,iniFile); settings.ignore_loop = GetPrivateProfileInt(CONFIG_APP_NAME,INI_ENTRY_IGNORE_LOOP,DEFAULT_IGNORE_LOOP,iniFile); if (settings.loop_forever && settings.ignore_loop) { cfg_sprintf(buf, TEXT("%d"),DEFAULT_LOOP_FOREVER); WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_LOOP_FOREVER,buf,iniFile); settings.loop_forever = DEFAULT_LOOP_FOREVER; cfg_sprintf(buf, TEXT("%d"),DEFAULT_IGNORE_LOOP); WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_IGNORE_LOOP,buf,iniFile); settings.ignore_loop = DEFAULT_IGNORE_LOOP; } settings.disable_subsongs = GetPrivateProfileInt(CONFIG_APP_NAME,INI_ENTRY_DISABLE_SUBSONGS,DEFAULT_DISABLE_SUBSONGS,iniFile); //if (settings.disable_subsongs < 0) { //unneeded? // sprintf(buf, TEXT("%d"),DEFAULT_DISABLE_SUBSONGS); // WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_DISABLE_SUBSONGS,buf,iniFile); // settings.disable_subsongs = DEFAULT_DISABLE_SUBSONGS; //} settings.downmix_channels = GetPrivateProfileInt(CONFIG_APP_NAME,INI_ENTRY_DOWNMIX_CHANNELS,DEFAULT_DOWNMIX_CHANNELS,iniFile); if (settings.downmix_channels < 0) { cfg_sprintf(buf, TEXT("%d"),DEFAULT_DOWNMIX_CHANNELS); WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_DOWNMIX_CHANNELS,buf,iniFile); settings.downmix_channels = DEFAULT_DOWNMIX_CHANNELS; } settings.tagfile_disable = GetPrivateProfileInt(CONFIG_APP_NAME,INI_ENTRY_TAGFILE_DISABLE,DEFAULT_TAGFILE_DISABLE,iniFile); settings.exts_unknown_on = GetPrivateProfileInt(CONFIG_APP_NAME,INI_ENTRY_EXTS_UNKNOWN_ON,DEFAULT_EXTS_UNKNOWN_ON,iniFile); settings.exts_common_on = GetPrivateProfileInt(CONFIG_APP_NAME,INI_ENTRY_EXTS_COMMON_ON,DEFAULT_EXTS_COMMON_ON,iniFile); settings.gain_type = GetPrivateProfileInt(CONFIG_APP_NAME, INI_ENTRY_GAIN_TYPE, DEFAULT_GAIN_TYPE, iniFile); settings.clip_type = GetPrivateProfileInt(CONFIG_APP_NAME, INI_ENTRY_CLIP_TYPE, DEFAULT_CLIP_TYPE, iniFile); } /* config dialog handler */ INT_PTR CALLBACK configDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { TCHAR buf[256]; TCHAR iniFile[PATH_LIMIT]; static int mypri; HANDLE hSlider; HANDLE hReplayGain; HANDLE hClipProtect; size_t buf_size = 256; int i; switch (uMsg) { case WM_CLOSE: /* hide dialog */ EndDialog(hDlg,TRUE); return TRUE; case WM_INITDIALOG: /* open dialog */ GetINIFileName(iniFile); //todo unneeded? hSlider = GetDlgItem(hDlg,IDC_THREAD_PRIORITY_SLIDER); SendMessage(hSlider, TBM_SETRANGE, (WPARAM) TRUE, /* redraw flag */ (LPARAM) MAKELONG(1, 7)); /* min. & max. positions */ SendMessage(hSlider, TBM_SETPOS, (WPARAM) TRUE, /* redraw flag */ (LPARAM) settings.thread_priority+1); mypri = settings.thread_priority; SetDlgItemText(hDlg,IDC_THREAD_PRIORITY_TEXT,priority_strings[settings.thread_priority]); hReplayGain = GetDlgItem(hDlg, IDC_REPLAYGAIN); hClipProtect = GetDlgItem(hDlg, IDC_CLIPPROTECT); for (i = 0; i < 3; i++) { SendMessage(hReplayGain, CB_ADDSTRING, 0, (LPARAM)replaygain_strings[i]); SendMessage(hClipProtect, CB_ADDSTRING, 0, (LPARAM)replaygain_strings[i]); } SendMessage(hReplayGain, CB_SETCURSEL, settings.gain_type, 0); SendMessage(hClipProtect, CB_SETCURSEL, settings.clip_type, 0); cfg_sprintf(buf, TEXT("%.2lf"),settings.fade_seconds); SetDlgItemText(hDlg,IDC_FADE_SECONDS,buf); cfg_sprintf(buf, TEXT("%.2lf"),settings.fade_delay_seconds); SetDlgItemText(hDlg,IDC_FADE_DELAY_SECONDS,buf); cfg_sprintf(buf, TEXT("%.2lf"),settings.loop_count); SetDlgItemText(hDlg,IDC_LOOP_COUNT,buf); if (settings.loop_forever) CheckDlgButton(hDlg,IDC_LOOP_FOREVER,BST_CHECKED); else if (settings.ignore_loop) CheckDlgButton(hDlg,IDC_IGNORE_LOOP,BST_CHECKED); else CheckDlgButton(hDlg,IDC_LOOP_NORMALLY,BST_CHECKED); if (settings.disable_subsongs) CheckDlgButton(hDlg,IDC_DISABLE_SUBSONGS,BST_CHECKED); cfg_sprintf(buf, TEXT("%d"),settings.downmix_channels); SetDlgItemText(hDlg,IDC_DOWNMIX_CHANNELS,buf); if (settings.tagfile_disable) CheckDlgButton(hDlg,IDC_TAGFILE_DISABLE,BST_CHECKED); if (settings.exts_unknown_on) CheckDlgButton(hDlg,IDC_EXTS_UNKNOWN_ON,BST_CHECKED); if (settings.exts_common_on) CheckDlgButton(hDlg,IDC_EXTS_COMMON_ON,BST_CHECKED); break; case WM_COMMAND: /* button presses */ switch (GET_WM_COMMAND_ID(wParam, lParam)) { case IDOK: /* read and verify new values */ { double temp_fade_seconds; double temp_fade_delay_seconds; double temp_loop_count; int temp_downmix_channels; int consumed, res; GetDlgItemText(hDlg,IDC_FADE_SECONDS,buf,buf_size); res = cfg_sscanf(buf, TEXT("%lf%n"),&temp_fade_seconds,&consumed); if (res < 1 || consumed != cfg_strlen(buf) || temp_fade_seconds < 0) { MessageBox(hDlg, TEXT("Invalid value for Fade Length\n") TEXT("Must be a number greater than or equal to zero"), TEXT("Error"),MB_OK|MB_ICONERROR); break; } GetDlgItemText(hDlg,IDC_FADE_DELAY_SECONDS,buf,buf_size); res = cfg_sscanf(buf, TEXT("%lf%n"),&temp_fade_delay_seconds,&consumed); if (res < 1 || consumed != cfg_strlen(buf)) { MessageBox(hDlg, TEXT("Invalid value for Fade Delay\n") TEXT("Must be a number"), TEXT("Error"),MB_OK|MB_ICONERROR); break; } GetDlgItemText(hDlg,IDC_LOOP_COUNT,buf,buf_size); res = cfg_sscanf(buf, TEXT("%lf%n"),&temp_loop_count,&consumed); if (res < 1 || consumed != cfg_strlen(buf) || temp_loop_count < 0) { MessageBox(hDlg, TEXT("Invalid value for Loop Count\n") TEXT("Must be a number greater than or equal to zero"), TEXT("Error"),MB_OK|MB_ICONERROR); break; } GetDlgItemText(hDlg,IDC_DOWNMIX_CHANNELS,buf,buf_size); res = cfg_sscanf(buf, TEXT("%d%n"),&temp_downmix_channels,&consumed); if (res < 1 || consumed != cfg_strlen(buf) || temp_downmix_channels < 0) { MessageBox(hDlg, TEXT("Invalid value for Downmix Channels\n") TEXT("Must be a number greater than or equal to zero"), TEXT("Error"),MB_OK|MB_ICONERROR); break; } GetINIFileName(iniFile); settings.thread_priority = mypri; cfg_sprintf(buf, TEXT("%d"),settings.thread_priority); WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_THREAD_PRIORITY,buf,iniFile); settings.fade_seconds = temp_fade_seconds; cfg_sprintf(buf, TEXT("%.2lf"),settings.fade_seconds); WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_FADE_SECONDS,buf,iniFile); settings.fade_delay_seconds = temp_fade_delay_seconds; cfg_sprintf(buf, TEXT("%.2lf"),settings.fade_delay_seconds); WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_FADE_DELAY_SECONDS,buf,iniFile); settings.loop_count = temp_loop_count; cfg_sprintf(buf, TEXT("%.2lf"),settings.loop_count); WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_LOOP_COUNT,buf,iniFile); settings.loop_forever = (IsDlgButtonChecked(hDlg,IDC_LOOP_FOREVER) == BST_CHECKED); cfg_sprintf(buf, TEXT("%d"),settings.loop_forever); WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_LOOP_FOREVER,buf,iniFile); settings.ignore_loop = (IsDlgButtonChecked(hDlg,IDC_IGNORE_LOOP) == BST_CHECKED); cfg_sprintf(buf, TEXT("%d"),settings.ignore_loop); WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_IGNORE_LOOP,buf,iniFile); settings.disable_subsongs = (IsDlgButtonChecked(hDlg,IDC_DISABLE_SUBSONGS) == BST_CHECKED); cfg_sprintf(buf, TEXT("%d"),settings.disable_subsongs); WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_DISABLE_SUBSONGS,buf,iniFile); settings.loop_count = temp_loop_count; cfg_sprintf(buf, TEXT("%.2lf"),settings.loop_count); WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_LOOP_COUNT,buf,iniFile); settings.downmix_channels = temp_downmix_channels; cfg_sprintf(buf, TEXT("%d"),settings.downmix_channels); WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_DOWNMIX_CHANNELS,buf,iniFile); settings.tagfile_disable = (IsDlgButtonChecked(hDlg,IDC_TAGFILE_DISABLE) == BST_CHECKED); cfg_sprintf(buf, TEXT("%d"),settings.tagfile_disable); WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_TAGFILE_DISABLE,buf,iniFile); settings.exts_unknown_on = (IsDlgButtonChecked(hDlg,IDC_EXTS_UNKNOWN_ON) == BST_CHECKED); cfg_sprintf(buf, TEXT("%d"),settings.exts_unknown_on); WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_EXTS_UNKNOWN_ON,buf,iniFile); settings.exts_common_on = (IsDlgButtonChecked(hDlg,IDC_EXTS_COMMON_ON) == BST_CHECKED); cfg_sprintf(buf, TEXT("%d"),settings.exts_common_on); WritePrivateProfileString(CONFIG_APP_NAME,INI_ENTRY_EXTS_COMMON_ON,buf,iniFile); hReplayGain = GetDlgItem(hDlg, IDC_REPLAYGAIN); settings.gain_type = SendMessage(hReplayGain, CB_GETCURSEL, 0, 0); cfg_sprintf(buf, TEXT("%d"), settings.gain_type); WritePrivateProfileString(CONFIG_APP_NAME, INI_ENTRY_GAIN_TYPE, buf, iniFile); hClipProtect = GetDlgItem(hDlg, IDC_CLIPPROTECT); settings.clip_type = SendMessage(hClipProtect, CB_GETCURSEL, 0, 0); cfg_sprintf(buf, TEXT("%d"), settings.clip_type); WritePrivateProfileString(CONFIG_APP_NAME, INI_ENTRY_CLIP_TYPE, buf, iniFile); } EndDialog(hDlg,TRUE); break; case IDCANCEL: EndDialog(hDlg,TRUE); break; case IDC_DEFAULT_BUTTON: hSlider = GetDlgItem(hDlg,IDC_THREAD_PRIORITY_SLIDER); SendMessage(hSlider, TBM_SETRANGE, (WPARAM) TRUE, /* redraw flag */ (LPARAM) MAKELONG(1, 7)); /* min. & max. positions */ SendMessage(hSlider, TBM_SETPOS, (WPARAM) TRUE, /* redraw flag */ (LPARAM) DEFAULT_THREAD_PRIORITY+1); mypri = DEFAULT_THREAD_PRIORITY; SetDlgItemText(hDlg,IDC_THREAD_PRIORITY_TEXT,priority_strings[mypri]); hReplayGain = GetDlgItem(hDlg, IDC_REPLAYGAIN); SendMessage(hReplayGain, CB_SETCURSEL, DEFAULT_GAIN_TYPE, 0); hClipProtect = GetDlgItem(hDlg, IDC_CLIPPROTECT); SendMessage(hClipProtect, CB_SETCURSEL, DEFAULT_CLIP_TYPE, 0); SetDlgItemText(hDlg,IDC_FADE_SECONDS,DEFAULT_FADE_SECONDS); SetDlgItemText(hDlg,IDC_FADE_DELAY_SECONDS,DEFAULT_FADE_DELAY_SECONDS); SetDlgItemText(hDlg,IDC_LOOP_COUNT,DEFAULT_LOOP_COUNT); CheckDlgButton(hDlg,IDC_LOOP_FOREVER,BST_UNCHECKED); CheckDlgButton(hDlg,IDC_IGNORE_LOOP,BST_UNCHECKED); CheckDlgButton(hDlg,IDC_LOOP_NORMALLY,BST_CHECKED); CheckDlgButton(hDlg,IDC_DISABLE_SUBSONGS,BST_UNCHECKED); SetDlgItemText(hDlg,IDC_DOWNMIX_CHANNELS,DEFAULT_DOWNMIX_CHANNELS); CheckDlgButton(hDlg,IDC_TAGFILE_DISABLE,BST_UNCHECKED); CheckDlgButton(hDlg,IDC_EXTS_UNKNOWN_ON,BST_UNCHECKED); CheckDlgButton(hDlg,IDC_EXTS_COMMON_ON,BST_UNCHECKED); break; default: return FALSE; } return FALSE; case WM_HSCROLL: /* priority scroll */ if ((struct HWND__ *)lParam==GetDlgItem(hDlg,IDC_THREAD_PRIORITY_SLIDER)) { if (LOWORD(wParam)==TB_THUMBPOSITION || LOWORD(wParam)==TB_THUMBTRACK) { mypri = HIWORD(wParam)-1; } else { mypri = SendMessage(GetDlgItem(hDlg,IDC_THREAD_PRIORITY_SLIDER),TBM_GETPOS,0,0)-1; } SetDlgItemText(hDlg,IDC_THREAD_PRIORITY_TEXT,priority_strings[mypri]); } break; default: return FALSE; } return TRUE; } /* ***************************************** */ /* IN_VGMSTREAM UTILS */ /* ***************************************** */ /* makes a modified filename, suitable to pass parameters around */ static void make_fn_subsong(in_char * dst, int dst_size, const in_char * filename, int stream_index) { /* Follows "(file)(config)(ext)". Winamp needs to "see" (ext) to validate, and file goes first so relative * files work in M3Us (path is added). Protocols a la "vgmstream://(config)(file)" work but don't get full paths. */ wa_snprintf(dst,dst_size, wa_L("%s|$s=%i|.vgmstream"), filename, stream_index); } /* unpacks the subsongs by adding entries to the playlist */ static int split_subsongs(const in_char * filename, int stream_index, VGMSTREAM *vgmstream) { int i, playlist_index; HWND hPlaylistWindow; if (settings.disable_subsongs || vgmstream->num_streams <= 1) return 0; /* don't split if no subsongs */ if (stream_index > 0 || vgmstream->stream_index > 0) return 0; /* no split if already playing subsong */ hPlaylistWindow = (HWND)SendMessage(input_module.hMainWindow, WM_WA_IPC, IPC_GETWND_PE, IPC_GETWND); playlist_index = SendMessage(input_module.hMainWindow,WM_WA_IPC,0,IPC_GETLISTPOS); /* The only way to pass info around in Winamp is encoding it into the filename, so a fake name * is created with the index. Then, winamp_Play (and related) intercepts and reads the index. */ for (i = 0; i < vgmstream->num_streams; i++) { in_char stream_fn[PATH_LIMIT]; make_fn_subsong(stream_fn,PATH_LIMIT, filename, (i+1)); /* encode index in filename */ /* insert at index */ { COPYDATASTRUCT cds = {0}; wa_fileinfo f; wa_strncpy(f.file, stream_fn,MAX_PATH-1); f.file[MAX_PATH-1] = '\0'; f.index = playlist_index + (i+1); cds.dwData = wa_IPC_PE_INSERTFILENAME; cds.lpData = (void*)&f; cds.cbData = sizeof(wa_fileinfo); SendMessage(hPlaylistWindow,WM_COPYDATA,0,(LPARAM)&cds); } /* IPC_ENQUEUEFILE can pre-set the title without needing the Playlist handle, but can't insert at index */ } /* remove current file from the playlist */ SendMessage(hPlaylistWindow, WM_WA_IPC, IPC_PE_DELETEINDEX, playlist_index); /* autoplay doesn't always advance to the first unpacked track, but manually fails somehow */ //SendMessage(input_module.hMainWindow,WM_WA_IPC,playlist_index,IPC_SETPLAYLISTPOS); //SendMessage(input_module.hMainWindow,WM_WA_IPC,0,IPC_STARTPLAY); return 1; } /* parses a modified filename ('fakename') extracting tags parameters (NULL tag for first = filename) */ static int parse_fn_string(const in_char * fn, const in_char * tag, in_char * dst, int dst_size) { const in_char *end = wa_strchr(fn,'|'); if (tag==NULL) { wa_strcpy(dst,fn); if (end) dst[end - fn] = '\0'; return 1; } dst[0] = '\0'; return 0; } static int parse_fn_int(const in_char * fn, const in_char * tag, int * num) { const in_char * start = wa_strchr(fn,'|'); if (start > 0) { wa_sscanf(start+1, wa_L("$s=%i "), num); return 1; } else { *num = 0; return 0; } } /* try to detect XMPlay, which can't interact with the playlist = no splitting */ static int is_xmplay() { if (GetModuleHandle( TEXT("xmplay.exe") )) return 1; if (GetModuleHandle( TEXT("xmp-wadsp.dll") )) return 1; if (GetModuleHandle( TEXT("xmp-wma.dll") )) return 1; return 0; } /* Adds ext to Winamp's extension list */ static void add_extension(int length, char * dst, const char * ext) { char buf[EXT_BUFFER_SIZE]; char ext_upp[EXT_BUFFER_SIZE]; int ext_len, written; int i,j; if (length <= 1) return; ext_len = strlen(ext); /* find end of dst (double \0), saved in i */ for (i = 0; i < length-2 && (dst[i] || dst[i+1]); i++) ; /* check if end reached or not enough room to add */ if (i == length-2 || i + EXT_BUFFER_SIZE+2 > length-2 || ext_len * 3 + 20+2 > EXT_BUFFER_SIZE) { dst[i] = '\0'; dst[i+1] = '\0'; return; } if (i > 0) i++; /* uppercase ext */ for (j = 0; j < ext_len; j++) ext_upp[j] = toupper(ext[j]); ext_upp[j] = '\0'; /* copy new extension + double null terminate */ /* ex: "vgmstream\0vgmstream Audio File (*.VGMSTREAM)\0" */ written = sprintf(buf, "%s%c%s Audio File (*.%s)%c", ext,'\0',ext_upp,ext_upp,'\0'); for (j = 0; j < written; i++,j++) dst[i] = buf[j]; dst[i] = '\0'; dst[i+1] = '\0'; } /* Creates Winamp's extension list, a single string that ends with \0\0. * Each extension must be in this format: "extension\0Description\0" * The list is used to accept extensions by default when IsOurFile returns 0, and to register file types. * It could be ignored/empty and just detect in IsOurFile instead. */ static void build_extension_list() { const char ** ext_list; size_t ext_list_len; int i; working_extension_list[0]='\0'; working_extension_list[1]='\0'; ext_list = vgmstream_get_formats(&ext_list_len); for (i=0; i < ext_list_len; i++) { add_extension(EXTENSION_LIST_SIZE, working_extension_list, ext_list[i]); } } /* unicode utils */ static void get_title(in_char * dst, int dst_size, const in_char * fn, VGMSTREAM * infostream) { in_char *basename; in_char buffer[PATH_LIMIT]; in_char filename[PATH_LIMIT]; //int stream_index = 0; parse_fn_string(fn, NULL, filename,PATH_LIMIT); //parse_fn_int(fn, wa_L("$s"), &stream_index); basename = (in_char*)filename + wa_strlen(filename); /* find end */ while (*basename != '\\' && basename >= filename) /* and find last "\" */ basename--; basename++; wa_strcpy(dst,basename); /* infostream gets added at first with index 0, then once played it re-adds proper numbers */ if (infostream) { const char* info_name = infostream->stream_name; int info_streams = infostream->num_streams; int info_subsong = infostream->stream_index; int is_first = infostream->stream_index == 0; /* show number if file has more than 1 subsong */ if (info_streams > 1) { if (is_first) wa_snprintf(buffer,PATH_LIMIT, wa_L("#1~%i"), info_streams); else wa_snprintf(buffer,PATH_LIMIT, wa_L("#%i"), info_subsong); wa_strcat(dst,buffer); } /* show name if file has subsongs (implicitly shows also for TXTP) */ if (info_name[0] != '\0' && ((info_streams > 0 && !is_first) || info_streams == 1)) { in_char stream_name[PATH_LIMIT]; wa_char_to_ichar(stream_name, PATH_LIMIT, info_name); wa_snprintf(buffer,PATH_LIMIT, wa_L(" (%s)"), stream_name); wa_strcat(dst,buffer); } } } static void set_config_defaults(winamp_song_config *current) { current->song_play_forever = settings.loop_forever; current->song_loop_count = settings.loop_count; current->song_fade_time = settings.fade_seconds; current->song_fade_delay = settings.fade_delay_seconds; current->song_ignore_loop = settings.ignore_loop; current->song_really_force_loop = 0; current->song_ignore_fade = 0; } static void apply_config(VGMSTREAM * vgmstream, winamp_song_config *current) { /* honor suggested config, if any (defined order matters) * note that ignore_fade and play_forever should take priority */ if (vgmstream->config_loop_count) { current->song_loop_count = vgmstream->config_loop_count; } if (vgmstream->config_fade_delay) { current->song_fade_delay = vgmstream->config_fade_delay; } if (vgmstream->config_fade_time) { current->song_fade_time = vgmstream->config_fade_time; } if (vgmstream->config_force_loop) { current->song_really_force_loop = 1; } if (vgmstream->config_ignore_loop) { current->song_ignore_loop = 1; } if (vgmstream->config_ignore_fade) { current->song_ignore_fade = 1; } /* remove non-compatible options */ if (current->song_play_forever) { current->song_ignore_fade = 0; current->song_ignore_loop = 0; } /* change loop stuff, in no particular order */ if (current->song_really_force_loop) { vgmstream_force_loop(vgmstream, 1, 0,vgmstream->num_samples); } if (current->song_ignore_loop) { vgmstream_force_loop(vgmstream, 0, 0,0); } /* loop N times, but also play stream end instead of fading out */ if (current->song_loop_count > 0 && current->song_ignore_fade) { vgmstream_set_loop_target(vgmstream, (int)current->song_loop_count); current->song_fade_time = 0; /* force no fade */ } } static int winampGetExtendedFileInfo_common(in_char* filename, char *metadata, char* ret, int retlen); static double getAlbumGainVolume(const in_char *fn) { char replaygain_gain[64], replaygain_peak[64]; double gain = 0.0; int had_replaygain = 0; if (settings.gain_type != REPLAYGAIN_NONE) { if (settings.gain_type == REPLAYGAIN_ALBUM && winampGetExtendedFileInfo_common((in_char *)fn, "replaygain_album_gain", replaygain_gain, sizeof(replaygain_gain))) { gain = atof(replaygain_gain); had_replaygain = 1; } if (!had_replaygain && winampGetExtendedFileInfo_common((in_char *)fn, "replaygain_track_gain", replaygain_gain, sizeof(replaygain_gain))) { gain = atof(replaygain_gain); had_replaygain = 1; } if (had_replaygain) { double vol = pow(10.0, gain / 20.0), peak = 1.0; if (settings.clip_type == REPLAYGAIN_ALBUM && winampGetExtendedFileInfo_common((in_char *)fn, "replaygain_album_peak", replaygain_peak, sizeof(replaygain_peak))) { peak = atof(replaygain_peak); } else if (settings.clip_type != REPLAYGAIN_NONE && winampGetExtendedFileInfo_common((in_char *)fn, "replaygain_track_peak", replaygain_peak, sizeof(replaygain_peak))) { peak = atof(replaygain_peak); } return peak != 1.0 ? min(vol, 1.0 / peak) : vol; } } return 1.0; } /* ***************************************** */ /* IN_VGMSTREAM */ /* ***************************************** */ /* about dialog */ void winamp_About(HWND hwndParent) { const char *ABOUT_TEXT = PLUGIN_DESCRIPTION "\n" "by hcs, FastElbja, manakoAT, bxaimc, snakemeat, soneek, kode54, bnnm and many others\n" "\n" "Winamp plugin by hcs, others\n" "\n" "https://github.com/kode54/vgmstream/\n" "https://sourceforge.net/projects/vgmstream/ (original)"; { TCHAR buf[1024]; size_t buf_size = 1024; cfg_char_to_wchar(buf, buf_size, ABOUT_TEXT); MessageBox(hwndParent, buf,TEXT("about in_vgmstream"),MB_OK); } } /* called at program init */ void winamp_Init() { /* get ini config */ load_config(); /* XMPlay with in_vgmstream doesn't support most IPC_x messages so no playlist manipulation */ if (is_xmplay()) { settings.disable_subsongs = 1; } /* dynamically make a list of supported extensions */ build_extension_list(); } /* called at program quit */ void winamp_Quit() { } /* called before extension checks, to allow detection of mms://, etc */ int winamp_IsOurFile(const in_char *fn) { vgmstream_ctx_valid_cfg cfg = {0}; char filename_utf8[PATH_LIMIT]; wa_ichar_to_char(filename_utf8, PATH_LIMIT, fn); cfg.skip_standard = 1; /* validated by Winamp */ cfg.accept_unknown = settings.exts_unknown_on; cfg.accept_common = settings.exts_common_on; /* returning 0 here means it only accepts the extensions in working_extension_list */ return vgmstream_ctx_is_valid(filename_utf8, &cfg); } /* request to start playing a file */ int winamp_Play(const in_char *fn) { int max_latency; in_char filename[PATH_LIMIT]; int stream_index = 0; if (vgmstream) return 1; // TODO: this should either pop up an error box or close the file /* check for info encoded in the filename */ parse_fn_string(fn, NULL, filename,PATH_LIMIT); parse_fn_int(fn, wa_L("$s"), &stream_index); /* open the stream */ vgmstream = init_vgmstream_winamp(filename,stream_index); if (!vgmstream) return 1; /* add N subsongs to the playlist, if any */ if (split_subsongs(filename, stream_index, vgmstream)) { close_vgmstream(vgmstream); vgmstream = NULL; return 1; } /* config */ set_config_defaults(&config); apply_config(vgmstream, &config); /* enable after all config but before outbuf (though ATM outbuf is not dynamic so no need to read input_channels) */ vgmstream_mixing_autodownmix(vgmstream, settings.downmix_channels); vgmstream_mixing_enable(vgmstream, SAMPLE_BUFFER_SIZE, NULL /*&input_channels*/, &output_channels); /* save original name */ wa_strncpy(lastfn,fn,PATH_LIMIT); /* open the output plugin */ max_latency = input_module.outMod->Open(vgmstream->sample_rate,output_channels, 16, 0, 0); if (max_latency < 0) { close_vgmstream(vgmstream); vgmstream = NULL; return 1; } /* set info display */ input_module.SetInfo(get_vgmstream_average_bitrate(vgmstream)/1000, vgmstream->sample_rate/1000, output_channels, 1); /* setup visualization */ input_module.SAVSAInit(max_latency,vgmstream->sample_rate); input_module.VSASetInfo(vgmstream->sample_rate,output_channels); /* reset internals */ decode_abort = 0; seek_needed_samples = -1; decode_pos_ms = 0; decode_pos_samples = 0; paused = 0; stream_length_samples = get_vgmstream_play_samples(config.song_loop_count,config.song_fade_time,config.song_fade_delay,vgmstream); fade_samples = (int)(config.song_fade_time * vgmstream->sample_rate); volume = getAlbumGainVolume(fn); /* start */ decode_thread_handle = CreateThread( NULL, /* handle cannot be inherited */ 0, /* stack size, 0=default */ decode, /* thread start routine */ NULL, /* no parameter to start routine */ 0, /* run thread immediately */ NULL); /* don't keep track of the thread id */ SetThreadPriority(decode_thread_handle,priority_values[settings.thread_priority]); //todo don't use priority values directly? return 0; /* success */ } /* pause stream */ void winamp_Pause() { paused = 1; input_module.outMod->Pause(1); } /* unpause stream */ void winamp_UnPause() { paused = 0; input_module.outMod->Pause(0); } /* return 1 if paused, 0 if not */ int winamp_IsPaused() { return paused; } /* stop (unload) stream */ void winamp_Stop() { if (decode_thread_handle != INVALID_HANDLE_VALUE) { decode_abort = 1; /* arbitrary wait length */ if (WaitForSingleObject(decode_thread_handle,1000) == WAIT_TIMEOUT) { TerminateThread(decode_thread_handle,0); // TODO: error? } CloseHandle(decode_thread_handle); decode_thread_handle = INVALID_HANDLE_VALUE; } close_vgmstream(vgmstream); vgmstream = NULL; input_module.outMod->Close(); input_module.SAVSADeInit(); } /* get length in ms */ int winamp_GetLength() { return stream_length_samples * 1000LL / vgmstream->sample_rate; } /* current output time in ms */ int winamp_GetOutputTime() { return decode_pos_ms + (input_module.outMod->GetOutputTime()-input_module.outMod->GetWrittenTime()); } /* seeks to point in stream (in ms) */ void winamp_SetOutputTime(int time_in_ms) { if (!vgmstream) return; seek_needed_samples = (long long)time_in_ms * vgmstream->sample_rate / 1000LL; } /* pass these commands through */ void winamp_SetVolume(int volume) { input_module.outMod->SetVolume(volume); } void winamp_SetPan(int pan) { input_module.outMod->SetPan(pan); } /* display info box (ALT+3) */ int winamp_InfoBox(const in_char *fn, HWND hwnd) { char description[1024] = {0}, tmp[1024] = {0}; size_t description_size = 1024; double tmpVolume = 1.0; concatn(description_size,description,PLUGIN_DESCRIPTION "\n\n"); if (!fn || !*fn) { /* no filename = current playing file */ if (!vgmstream) return 0; describe_vgmstream(vgmstream,description,description_size); } else { /* some other file in playlist given by filename */ VGMSTREAM * infostream = NULL; winamp_song_config infoconfig = {0}; in_char filename[PATH_LIMIT]; int stream_index = 0; /* check for info encoded in the filename */ parse_fn_string(fn, NULL, filename,PATH_LIMIT); parse_fn_int(fn, wa_L("$s"), &stream_index); infostream = init_vgmstream_winamp(filename, stream_index); if (!infostream) return 0; set_config_defaults(&infoconfig); apply_config(infostream, &infoconfig); vgmstream_mixing_autodownmix(infostream, settings.downmix_channels); vgmstream_mixing_enable(infostream, 0, NULL, NULL); describe_vgmstream(infostream,description,description_size); close_vgmstream(infostream); infostream = NULL; tmpVolume = getAlbumGainVolume(fn); } { TCHAR buf[1024] = {0}; size_t buf_size = 1024; snprintf(tmp, sizeof(tmp), "\nvolume: %.6f", tmpVolume); concatn(description_size, description, tmp); cfg_char_to_wchar(buf, buf_size, description); MessageBox(hwnd,buf,TEXT("Stream info"),MB_OK); } return 0; } /* retrieve title (playlist name) and time on the current or other file in the playlist */ void winamp_GetFileInfo(const in_char *fn, in_char *title, int *length_in_ms) { if (!fn || !*fn) { /* no filename = current playing file */ if (!vgmstream) return; if (title) { get_title(title,GETFILEINFO_TITLE_LENGTH, lastfn, vgmstream); } if (length_in_ms) { *length_in_ms = winamp_GetLength(); } } else { /* some other file in playlist given by filename */ VGMSTREAM * infostream = NULL; winamp_song_config infoconfig = {0}; in_char filename[PATH_LIMIT]; int stream_index = 0; /* check for info encoded in the filename */ parse_fn_string(fn, NULL, filename,PATH_LIMIT); parse_fn_int(fn, wa_L("$s"), &stream_index); infostream = init_vgmstream_winamp(filename, stream_index); if (!infostream) return; set_config_defaults(&infoconfig); apply_config(infostream, &infoconfig); vgmstream_mixing_autodownmix(infostream, settings.downmix_channels); vgmstream_mixing_enable(infostream, 0, NULL, NULL); if (title) { get_title(title,GETFILEINFO_TITLE_LENGTH, fn, infostream); } if (length_in_ms) { *length_in_ms = -1000; if (infostream) { const int num_samples = get_vgmstream_play_samples( infoconfig.song_loop_count,infoconfig.song_fade_time,infoconfig.song_fade_delay,infostream); *length_in_ms = num_samples * 1000LL /infostream->sample_rate; } } close_vgmstream(infostream); infostream = NULL; } } /* eq stuff */ void winamp_EQSet(int on, char data[10], int preamp) { } /* the decode thread */ DWORD WINAPI __stdcall decode(void *arg) { const int max_buffer_samples = SAMPLE_BUFFER_SIZE; const int max_samples = stream_length_samples; while (!decode_abort) { int samples_to_do; int output_bytes; if (decode_pos_samples + max_buffer_samples > stream_length_samples && (!settings.loop_forever || !vgmstream->loop_flag)) samples_to_do = stream_length_samples - decode_pos_samples; else samples_to_do = max_buffer_samples; /* seek setup (max samples to skip if still seeking, mark done) */ if (seek_needed_samples != -1) { /* reset if we need to seek backwards */ if (seek_needed_samples < decode_pos_samples) { reset_vgmstream(vgmstream); apply_config(vgmstream, &config); /* config is undone by reset */ decode_pos_samples = 0; decode_pos_ms = 0; } /* adjust seeking past file, can happen using the right (->) key * (should be done here and not in SetOutputTime due to threads/race conditions) */ if (seek_needed_samples > max_samples && !settings.loop_forever) { seek_needed_samples = max_samples; } /* adjust max samples to seek */ if (decode_pos_samples < seek_needed_samples) { samples_to_do = seek_needed_samples - decode_pos_samples; if (samples_to_do > max_buffer_samples) samples_to_do = max_buffer_samples; } else { seek_needed_samples = -1; } /* flush Winamp buffers */ input_module.outMod->Flush((int)decode_pos_ms); } output_bytes = (samples_to_do * output_channels * sizeof(short)); if (input_module.dsp_isactive()) output_bytes = output_bytes * 2; /* Winamp's DSP may need double samples */ if (samples_to_do == 0) { /* track finished */ input_module.outMod->CanWrite(); /* ? */ if (!input_module.outMod->IsPlaying()) { PostMessage(input_module.hMainWindow, WM_WA_MPEG_EOF, 0,0); /* end */ return 0; } Sleep(10); } else if (seek_needed_samples != -1) { /* seek */ render_vgmstream(sample_buffer,samples_to_do,vgmstream); /* discard decoded samples and keep seeking */ decode_pos_samples += samples_to_do; decode_pos_ms = decode_pos_samples * 1000LL / vgmstream->sample_rate; } else if (input_module.outMod->CanWrite() >= output_bytes) { /* decode */ render_vgmstream(sample_buffer,samples_to_do,vgmstream); /* apply ReplayGain, if needed */ if (volume != 1.0) { int j, k; for (j = 0; j < samples_to_do; j++) { for (k = 0; k < vgmstream->channels; k++) { sample_buffer[j*vgmstream->channels + k] = (short)(sample_buffer[j*vgmstream->channels + k] * volume); } } } /* fade near the end */ if (vgmstream->loop_flag && fade_samples > 0 && !settings.loop_forever) { int fade_channels = output_channels; int samples_into_fade = decode_pos_samples - (stream_length_samples - fade_samples); if (samples_into_fade + samples_to_do > 0) { int j, k; for (j = 0; j < samples_to_do; j++, samples_into_fade++) { if (samples_into_fade > 0) { const double fadedness = (double)(fade_samples-samples_into_fade)/fade_samples; for (k = 0; k < fade_channels; k++) { sample_buffer[j*fade_channels+k] = (short)(sample_buffer[j*fade_channels+k]*fadedness); } } } } } /* output samples */ input_module.SAAddPCMData((char*)sample_buffer,output_channels,16,decode_pos_ms); input_module.VSAAddPCMData((char*)sample_buffer,output_channels,16,decode_pos_ms); if (input_module.dsp_isactive()) { /* find out DSP's needs */ int dsp_output_samples = input_module.dsp_dosamples(sample_buffer,samples_to_do,16,output_channels,vgmstream->sample_rate); output_bytes = dsp_output_samples * output_channels * sizeof(short); } input_module.outMod->Write((char*)sample_buffer, output_bytes); decode_pos_samples += samples_to_do; decode_pos_ms = decode_pos_samples*1000LL/vgmstream->sample_rate; } else { /* can't write right now */ Sleep(20); } } return 0; } /* configuration dialog */ void winamp_Config(HWND hwndParent) { /* defined in resource.rc */ DialogBox(input_module.hDllInstance, (const TCHAR *)IDD_CONFIG, hwndParent, configDlgProc); } /* *********************************** */ /* main plugin def */ In_Module input_module = { IN_VER, PLUGIN_DESCRIPTION, 0, /* hMainWindow (filled in by Winamp) */ 0, /* hDllInstance (filled in by Winamp) */ working_extension_list, 1, /* is_seekable flag */ 9, /* UsesOutputPlug flag */ winamp_Config, winamp_About, winamp_Init, winamp_Quit, winamp_GetFileInfo, winamp_InfoBox, winamp_IsOurFile, winamp_Play, winamp_Pause, winamp_UnPause, winamp_IsPaused, winamp_Stop, winamp_GetLength, winamp_GetOutputTime, winamp_SetOutputTime, winamp_SetVolume, winamp_SetPan, 0,0,0,0,0,0,0,0,0, /* vis stuff */ 0,0, /* dsp stuff */ winamp_EQSet, NULL, /* SetInfo */ 0 /* outMod */ }; __declspec(dllexport) In_Module * winampGetInModule2() { return &input_module; } /* ************************************* */ /* IN_TAGS */ /* ************************************* */ /* could malloc and stuff but totals aren't much bigger than PATH_LIMITs anyway */ #define WINAMP_TAGS_ENTRY_MAX 30 #define WINAMP_TAGS_ENTRY_SIZE 2048 typedef struct { int loaded; in_char filename[PATH_LIMIT]; /* tags are loaded for this file */ int tag_count; char keys[WINAMP_TAGS_ENTRY_MAX][WINAMP_TAGS_ENTRY_SIZE+1]; char vals[WINAMP_TAGS_ENTRY_MAX][WINAMP_TAGS_ENTRY_SIZE+1]; } winamp_tags; winamp_tags last_tags; /* Loads all tags for a filename in a temp struct to improve performance, as * Winamp requests one tag at a time and may reask for the same tag several times */ static void load_tagfile_info(in_char* filename) { STREAMFILE *tagFile = NULL; in_char filename_clean[PATH_LIMIT]; char filename_utf8[PATH_LIMIT]; char tagfile_path_utf8[PATH_LIMIT]; in_char tagfile_path_i[PATH_LIMIT]; char *path; if (settings.tagfile_disable) { /* reset values if setting changes during play */ last_tags.loaded = 0; last_tags.tag_count = 0; return; } /* clean extra part for subsong tags */ parse_fn_string(filename, NULL, filename_clean,PATH_LIMIT); if (wa_strcmp(last_tags.filename, filename_clean) == 0) { return; /* not changed, tags still apply */ } last_tags.loaded = 0; /* tags are now for this filename, find tagfile path */ wa_ichar_to_char(filename_utf8, PATH_LIMIT, filename_clean); strcpy(tagfile_path_utf8,filename_utf8); path = strrchr(tagfile_path_utf8,'\\'); if (path != NULL) { path[1] = '\0'; /* includes "\", remove after that from tagfile_path */ strcat(tagfile_path_utf8,tagfile_name); } else { /* ??? */ strcpy(tagfile_path_utf8,tagfile_name); } wa_char_to_ichar(tagfile_path_i, PATH_LIMIT, tagfile_path_utf8); wa_strcpy(last_tags.filename, filename_clean); last_tags.tag_count = 0; /* load all tags from tagfile */ tagFile = open_winamp_streamfile_by_ipath(tagfile_path_i); if (tagFile != NULL) { VGMSTREAM_TAGS *tags; const char *tag_key, *tag_val; int i; tags = vgmstream_tags_init(&tag_key, &tag_val); vgmstream_tags_reset(tags, filename_utf8); while (vgmstream_tags_next_tag(tags, tagFile)) { int repeated_tag = 0; int current_tag = last_tags.tag_count; if (current_tag >= WINAMP_TAGS_ENTRY_MAX) continue; /* should overwrite repeated tags as global tags may appear multiple times */ for (i = 0; i < current_tag; i++) { if (strcmp(last_tags.keys[i], tag_key) == 0) { current_tag = i; repeated_tag = 1; break; } } last_tags.keys[current_tag][0] = '\0'; strncat(last_tags.keys[current_tag], tag_key, WINAMP_TAGS_ENTRY_SIZE); last_tags.vals[current_tag][0] = '\0'; strncat(last_tags.vals[current_tag], tag_val, WINAMP_TAGS_ENTRY_SIZE); if (!repeated_tag) last_tags.tag_count++; } vgmstream_tags_close(tags); close_streamfile(tagFile); last_tags.loaded = 1; } } /* Winamp repeatedly calls this for every known tag currently used in the Advanced Title Formatting (ATF) * config, 'metadata' being the requested tag. Returns 0 on failure/tag not found. * May be called again after certain actions (adding file to playlist, Play, GetFileInfo, etc), and * doesn't seem the plugin can tell Winamp all tags it supports at once or use custom tags. */ //todo unicode stuff could be improved... probably static int winampGetExtendedFileInfo_common(in_char* filename, char *metadata, char* ret, int retlen) { int i, tag_found; int max_len; /* load list current tags, if necessary */ load_tagfile_info(filename); if (!last_tags.loaded) /* tagfile not found, fail so default get_title takes over */ goto fail; /* always called (value in ms), must return ok so other tags get called */ if (strcasecmp(metadata, "length") == 0) { strcpy(ret, "0");//todo should export but shows GetFileInfo's ms if not provided return 1; } #if 0 /* special case to fill WA5's unified dialog */ if (strcasecmp(metadata, "formatinformation") == 0) { generate_format_string(...); } #endif /* find requested tag */ tag_found = 0; max_len = (retlen > 0) ? retlen-1 : retlen; for (i = 0; i < last_tags.tag_count; i++) { if (strcasecmp(metadata,last_tags.keys[i]) == 0) { ret[0] = '\0'; strncat(ret, last_tags.vals[i], max_len); tag_found = 1; break; } } if (!tag_found) goto fail; return 1; fail: return strcasecmp(metadata, "replaygain_track_gain") == 0 ? 1 : 0; } /* for Winamp 5.24 */ __declspec (dllexport) int winampGetExtendedFileInfo(char *filename, char *metadata, char *ret, int retlen) { in_char filename_wchar[PATH_LIMIT]; int ok; if (settings.tagfile_disable) return 0; wa_char_to_ichar(filename_wchar,PATH_LIMIT, filename); ok = winampGetExtendedFileInfo_common(filename_wchar, metadata, ret, retlen); if (ok == 0) return 0; return 1; } /* for Winamp 5.3+ */ __declspec (dllexport) int winampGetExtendedFileInfoW(wchar_t *filename, char *metadata, wchar_t *ret, int retlen) { in_char filename_ichar[PATH_LIMIT]; char ret_utf8[2048]; int ok; if (settings.tagfile_disable) return 0; wa_wchar_to_ichar(filename_ichar,PATH_LIMIT, filename); ok = winampGetExtendedFileInfo_common(filename_ichar, metadata, ret_utf8,2048); if (ok == 0) return 0; wa_char_to_wchar(ret,retlen, ret_utf8); return 1; } /* return 1 if you want winamp to show it's own file info dialogue, 0 if you want to show your own (via In_Module.InfoBox) * if returning 1, remember to implement winampGetExtendedFileInfo("formatinformation")! */ __declspec(dllexport) int winampUseUnifiedFileInfoDlg(const wchar_t * fn) { return 0; } winamp_song_config ext_config; short ext_sample_buffer[SAMPLE_BUFFER_SIZE*2 * VGMSTREAM_MAX_CHANNELS]; //todo maybe should be dynamic int ext_seek_needed_samples = -1; int ext_decode_pos_samples = 0; int ext_stream_length_samples = -1; int ext_fade_samples = 0; int ext_output_channels = 0; static void *winampGetExtendedRead_open_common(in_char *fn, int *size, int *bps, int *nch, int *srate) { VGMSTREAM *ext_vgmstream = NULL; in_char filename[PATH_LIMIT]; int stream_index = 0; /* check for info encoded in the filename */ parse_fn_string(fn, NULL, filename, PATH_LIMIT); parse_fn_int(fn, wa_L("$s"), &stream_index); /* open the stream */ ext_vgmstream = init_vgmstream_winamp(filename, stream_index); if (!ext_vgmstream) { return NULL; } /* config */ set_config_defaults(&ext_config); apply_config(ext_vgmstream, &ext_config); /* enable after all config but before outbuf (though ATM outbuf is not dynamic so no need to read input_channels) */ vgmstream_mixing_autodownmix(ext_vgmstream, settings.downmix_channels); vgmstream_mixing_enable(ext_vgmstream, SAMPLE_BUFFER_SIZE, NULL /*&input_channels*/, &ext_output_channels); /* reset internals */ ext_seek_needed_samples = -1; ext_decode_pos_samples = 0; ext_stream_length_samples = get_vgmstream_play_samples(ext_config.song_loop_count, ext_config.song_fade_time, ext_config.song_fade_delay, ext_vgmstream); ext_fade_samples = (int)(ext_config.song_fade_time * ext_vgmstream->sample_rate); if (size) { *size = ext_stream_length_samples * ext_output_channels * 2; } if (bps) { *bps = 16; } if (nch) { *nch = ext_output_channels; } if (srate) { *srate = ext_vgmstream->sample_rate; } return ext_vgmstream; } __declspec(dllexport) void *winampGetExtendedRead_open(const char *fn, int *size, int *bps, int *nch, int *srate) { in_char filename_wchar[PATH_LIMIT]; wa_char_to_ichar(filename_wchar, PATH_LIMIT, fn); return winampGetExtendedRead_open_common(filename_wchar, size, bps, nch, srate); } __declspec(dllexport) void *winampGetExtendedRead_openW(const wchar_t *fn, int *size, int *bps, int *nch, int *srate) { in_char filename_ichar[PATH_LIMIT]; wa_wchar_to_ichar(filename_ichar, PATH_LIMIT, fn); return winampGetExtendedRead_open_common(filename_ichar, size, bps, nch, srate); } __declspec(dllexport) size_t winampGetExtendedRead_getData(void *handle, char *dest, size_t len, int *killswitch) { const int max_buffer_samples = SAMPLE_BUFFER_SIZE; const int max_samples = ext_stream_length_samples; unsigned copied = 0; int done = 0; VGMSTREAM *ext_vgmstream = handle; if (!ext_vgmstream) { return 0; } while (copied + max_buffer_samples * ext_vgmstream->channels * 2 < len && !done) { int samples_to_do; if (ext_decode_pos_samples + max_buffer_samples > ext_stream_length_samples && (!settings.loop_forever || !ext_vgmstream->loop_flag)) samples_to_do = ext_stream_length_samples - ext_decode_pos_samples; else samples_to_do = max_buffer_samples; /* seek setup (max samples to skip if still seeking, mark done) */ if (ext_seek_needed_samples != -1) { /* reset if we need to seek backwards */ if (ext_seek_needed_samples < ext_decode_pos_samples) { reset_vgmstream(ext_vgmstream); apply_config(ext_vgmstream, &ext_config); /* config is undone by reset */ ext_decode_pos_samples = 0; } /* adjust seeking past file, can happen using the right (->) key * (should be done here and not in SetOutputTime due to threads/race conditions) */ if (ext_seek_needed_samples > max_samples && !settings.loop_forever) { ext_seek_needed_samples = max_samples; } /* adjust max samples to seek */ if (ext_decode_pos_samples < ext_seek_needed_samples) { samples_to_do = ext_seek_needed_samples - ext_decode_pos_samples; if (samples_to_do > max_buffer_samples) samples_to_do = max_buffer_samples; } else { ext_seek_needed_samples = -1; } } if (!samples_to_do) { /* track finished */ break; } else if (ext_seek_needed_samples != -1) { /* seek */ render_vgmstream(ext_sample_buffer, samples_to_do, ext_vgmstream); /* discard decoded samples and keep seeking */ ext_decode_pos_samples += samples_to_do; } else { /* decode */ render_vgmstream(ext_sample_buffer, samples_to_do, ext_vgmstream); /* fade near the end */ if (ext_vgmstream->loop_flag && ext_fade_samples > 0 && !settings.loop_forever) { int fade_channels = ext_output_channels; int samples_into_fade = ext_decode_pos_samples - (ext_stream_length_samples - ext_fade_samples); if (samples_into_fade + ext_decode_pos_samples > 0) { int j, k; for (j = 0; j < samples_to_do; j++, samples_into_fade++) { if (samples_into_fade > 0) { const double fadedness = (double)(ext_fade_samples-samples_into_fade)/ext_fade_samples; for (k = 0; k < fade_channels; k++) { ext_sample_buffer[j*fade_channels+k] = (short)(ext_sample_buffer[j*fade_channels+k]*fadedness); } } } } } /* output samples */ memcpy(&dest[copied], ext_sample_buffer, samples_to_do * ext_output_channels * 2); copied += samples_to_do * ext_output_channels * 2; ext_decode_pos_samples += samples_to_do; } if (killswitch && *killswitch) { break; } } return copied; } __declspec(dllexport) int winampGetExtendedRead_setTime(void *handle, int time_in_ms) { VGMSTREAM *ext_vgmstream = handle; if (ext_vgmstream) { ext_seek_needed_samples = (long long)time_in_ms * ext_vgmstream->sample_rate / 1000LL; return 1; } return 0; } __declspec(dllexport) void winampGetExtendedRead_close(void *handle) { VGMSTREAM *ext_vgmstream = handle; if (ext_vgmstream) { close_vgmstream(ext_vgmstream); } } __declspec(dllexport) int winampUninstallPlugin(HINSTANCE hDllInst, HWND hwndDlg, int param) { /* may uninstall without restart as we aren't subclassing */ return IN_PLUGIN_UNINSTALL_NOW; } /* winamp sekrit exports: */ /* EXPORTS winampGetInModule2 @1 winampGetExtendedFileInfo @2 winampGetExtendedFileInfoW @3 winampAddUnifiedFileInfoPane @4 winampUseUnifiedFileInfoDlg @5 winampGetExtendedRead_close @6 winampGetExtendedRead_getData @7 winampGetExtendedRead_open @8 winampGetExtendedRead_openW @9 winampGetExtendedRead_setTime @10 winampUninstallPlugin @11 */