vgmstream/winamp/in_vgmstream.c

1155 lines
37 KiB
C
Raw Normal View History

2017-05-01 14:18:33 +02:00
/**
* vgmstream for Winamp
*/
2021-04-18 13:04:00 +02:00
#include "in_vgmstream.h"
2021-08-15 19:24:48 +02:00
#include "../version.h"
#ifndef VGMSTREAM_VERSION
2021-08-15 19:24:48 +02:00
#define VGMSTREAM_VERSION "unknown version " __DATE__
#endif
2021-08-15 19:24:48 +02:00
#define PLUGIN_NAME "vgmstream plugin " VGMSTREAM_VERSION
#define PLUGIN_INFO PLUGIN_NAME " (" __DATE__ ")"
2021-04-18 13:04:00 +02:00
/* ***************************************** */
/* IN_STATE */
/* ***************************************** */
2019-11-02 16:05:01 +01:00
#define EXT_BUFFER_SIZE 200
2018-11-03 19:28:05 +01:00
/* 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)
2018-11-03 19:28:05 +01:00
/* fixed list to simplify but could also malloc/free on init/close */
char working_extension_list[EXTENSION_LIST_SIZE] = {0};
2019-11-02 16:05:01 +01:00
/* current play state */
typedef struct {
int paused;
int decode_abort;
2020-07-29 19:35:04 +02:00
int seek_sample;
2019-11-02 16:05:01 +01:00
int decode_pos_ms;
int decode_pos_samples;
2020-07-29 19:35:04 +02:00
int length_samples;
2019-11-02 16:05:01 +01:00
int output_channels;
double volume;
} winamp_state_t;
2019-03-18 00:08:09 +01:00
/* Winamp needs at least 576 16-bit samples, stereo, doubled in case DSP effects are active */
#define SAMPLE_BUFFER_SIZE 576
2019-11-02 16:05:01 +01:00
const char* tagfile_name = "!tags.m3u";
2019-03-18 00:08:09 +01:00
/* plugin state */
HANDLE decode_thread_handle = INVALID_HANDLE_VALUE;
2020-07-21 19:47:02 +02:00
VGMSTREAM* vgmstream = NULL;
in_char lastfn[PATH_LIMIT] = {0}; /* name of the currently playing file */
2019-11-02 16:05:01 +01:00
winamp_settings_t defaults;
winamp_settings_t settings;
winamp_state_t state;
short sample_buffer[SAMPLE_BUFFER_SIZE*2 * VGMSTREAM_MAX_CHANNELS]; //todo maybe should be dynamic
2018-11-04 17:02:11 +01:00
2021-04-18 13:04:00 +02:00
/* ***************************************** */
/* IN_VGMSTREAM UTILS */
/* ***************************************** */
/* opens vgmstream for winamp */
2020-07-21 19:47:02 +02:00
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 */
2020-07-21 19:47:02 +02:00
STREAMFILE* sf = open_winamp_streamfile_by_ipath(fn); //open_stdio_streamfile(fn);
if (sf) {
sf->stream_index = stream_index;
vgmstream = init_vgmstream_from_STREAMFILE(sf);
close_streamfile(sf);
}
return vgmstream;
}
2017-08-19 10:26:21 +02:00
/* makes a modified filename, suitable to pass parameters around */
2021-04-18 13:04:00 +02:00
static void make_fn_subsong(in_char* dst, int dst_size, const in_char* filename, int stream_index) {
2017-08-19 10:26:21 +02:00
/* 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 */
2021-04-18 13:04:00 +02:00
static int split_subsongs(const in_char* filename, int stream_index, VGMSTREAM *vgmstream) {
2017-08-19 10:26:21 +02:00
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 */
2017-08-19 10:26:21 +02:00
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);
2018-11-03 19:28:05 +01:00
/* autoplay doesn't always advance to the first unpacked track, but manually fails somehow */
2017-08-19 10:26:21 +02:00
//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) */
2021-04-18 13:04:00 +02:00
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,'|');
2017-08-19 10:26:21 +02:00
if (tag==NULL) {
wa_strcpy(dst,fn);
if (end)
dst[end - fn] = '\0';
return 1;
}
dst[0] = '\0';
return 0;
}
2021-04-18 13:04:00 +02:00
static int parse_fn_int(const in_char* fn, const in_char* tag, int* num) {
const in_char* start = wa_strchr(fn,'|');
2017-08-19 10:26:21 +02:00
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") ))
2017-08-19 10:26:21 +02:00
return 1;
if (GetModuleHandle( TEXT("xmp-wadsp.dll") ))
2017-08-19 10:26:21 +02:00
return 1;
if (GetModuleHandle( TEXT("xmp-wma.dll") ))
2017-08-19 10:26:21 +02:00
return 1;
return 0;
}
/* Adds ext to Winamp's extension list */
2021-04-18 13:04:00 +02:00
static void add_extension(char* dst, int dst_len, const char* ext) {
char buf[EXT_BUFFER_SIZE];
char ext_upp[EXT_BUFFER_SIZE];
int ext_len, written;
int i,j;
2019-11-02 16:05:01 +01:00
if (dst_len <= 1)
return;
ext_len = strlen(ext);
/* find end of dst (double \0), saved in i */
2019-11-02 16:05:01 +01:00
for (i = 0; i < dst_len - 2 && (dst[i] || dst[i+1]); i++)
;
/* check if end reached or not enough room to add */
2019-11-02 16:05:01 +01:00
if (i == dst_len - 2 || i + EXT_BUFFER_SIZE+2 > dst_len - 2 || ext_len * 3 + 20+2 > EXT_BUFFER_SIZE) {
2018-11-03 19:28:05 +01:00
dst[i] = '\0';
dst[i+1] = '\0';
return;
}
if (i > 0)
i++;
/* uppercase ext */
2018-11-03 19:28:05 +01:00
for (j = 0; j < ext_len; j++)
ext_upp[j] = toupper(ext[j]);
ext_upp[j] = '\0';
/* copy new extension + double null terminate */
2019-04-07 01:48:18 +02:00
/* ex: "vgmstream\0vgmstream Audio File (*.VGMSTREAM)\0" */
2019-10-07 00:17:12 +02:00
written = snprintf(buf,sizeof(buf)-1, "%s%c%s Audio File (*.%s)%c", ext,'\0',ext_upp,ext_upp,'\0');
2018-11-03 19:28:05 +01:00
for (j = 0; j < written; i++,j++)
dst[i] = buf[j];
2018-11-03 19:28:05 +01:00
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. */
2021-04-18 13:04:00 +02:00
static void build_extension_list(char* winamp_list, int winamp_list_size) {
const char** ext_list;
size_t ext_list_len;
int i;
2021-04-18 13:04:00 +02:00
winamp_list[0] = '\0';
winamp_list[1] = '\0';
ext_list = vgmstream_get_formats(&ext_list_len);
2021-04-18 13:04:00 +02:00
for (i = 0; i < ext_list_len; i++) {
2019-11-02 16:05:01 +01:00
add_extension(winamp_list, winamp_list_size, ext_list[i]);
}
}
2017-04-30 02:09:35 +02:00
/* unicode utils */
2020-07-21 19:47:02 +02:00
static void get_title(in_char* dst, int dst_size, const in_char* fn, VGMSTREAM* infostream) {
2017-08-19 10:26:21 +02:00
in_char filename[PATH_LIMIT];
char buffer[PATH_LIMIT];
char filename_utf8[PATH_LIMIT];
2017-08-19 10:26:21 +02:00
parse_fn_string(fn, NULL, filename,PATH_LIMIT);
//parse_fn_int(fn, wa_L("$s"), &stream_index);
2017-08-19 10:26:21 +02:00
wa_ichar_to_char(filename_utf8, PATH_LIMIT, filename);
2017-08-19 10:26:21 +02:00
/* infostream gets added at first with index 0, then once played it re-adds proper numbers */
if (infostream) {
vgmstream_title_t tcfg = {0};
int is_first = infostream->stream_index == 0;
tcfg.force_title = settings.force_title;
tcfg.subsong_range = is_first;
tcfg.remove_extension = 1;
2017-08-19 10:26:21 +02:00
vgmstream_get_title(buffer, sizeof(buffer), filename_utf8, infostream, &tcfg);
wa_char_to_ichar(dst, dst_size, buffer);
2017-08-19 10:26:21 +02:00
}
}
2020-07-21 19:47:02 +02:00
static void apply_config(VGMSTREAM* vgmstream, winamp_settings_t* settings) {
vgmstream_cfg_t vcfg = {0};
2020-07-21 19:47:02 +02:00
vcfg.allow_play_forever = 1;
vcfg.play_forever = settings->loop_forever;
2020-07-30 23:50:56 +02:00
vcfg.loop_count = settings->loop_count;
vcfg.fade_time = settings->fade_time;
2020-07-21 19:47:02 +02:00
vcfg.fade_delay = settings->fade_delay;
vcfg.ignore_loop = settings->ignore_loop;
2020-07-21 19:47:02 +02:00
vgmstream_apply_config(vgmstream, &vcfg);
}
2021-04-18 13:04:00 +02:00
static int winampGetExtendedFileInfo_common(in_char* filename, char* metadata, char* ret, int retlen);
2021-04-18 13:04:00 +02:00
static double get_album_gain_volume(const in_char* fn) {
char replaygain[64];
double gain = 0.0;
int had_replaygain = 0;
if (settings.gain_type == REPLAYGAIN_NONE)
return 1.0;
replaygain[0] = '\0'; /* reset each time to make sure we read actual tags */
if (settings.gain_type == REPLAYGAIN_ALBUM
2021-04-18 13:04:00 +02:00
&& winampGetExtendedFileInfo_common((in_char*)fn, "replaygain_album_gain", replaygain, sizeof(replaygain))
&& replaygain[0] != '\0') {
gain = atof(replaygain);
had_replaygain = 1;
}
replaygain[0] = '\0';
if (!had_replaygain
2021-04-18 13:04:00 +02:00
&& winampGetExtendedFileInfo_common((in_char*)fn, "replaygain_track_gain", replaygain, sizeof(replaygain))
&& replaygain[0] != '\0') {
gain = atof(replaygain);
had_replaygain = 1;
}
if (had_replaygain) {
double vol = pow(10.0, gain / 20.0);
double peak = 1.0;
replaygain[0] = '\0';
if (settings.clip_type == REPLAYGAIN_ALBUM
2021-04-18 13:04:00 +02:00
&& winampGetExtendedFileInfo_common((in_char*)fn, "replaygain_album_peak", replaygain, sizeof(replaygain))
&& replaygain[0] != '\0') {
peak = atof(replaygain);
}
else if (settings.clip_type != REPLAYGAIN_NONE
2021-04-18 13:04:00 +02:00
&& winampGetExtendedFileInfo_common((in_char*)fn, "replaygain_track_peak", replaygain, sizeof(replaygain))
&& replaygain[0] != '\0') {
peak = atof(replaygain);
}
return peak != 1.0 ? min(vol, 1.0 / peak) : vol;
}
return 1.0;
}
/* ***************************************** */
/* IN_VGMSTREAM */
/* ***************************************** */
/* about dialog */
void winamp_About(HWND hwndParent) {
2021-04-18 13:04:00 +02:00
const char* ABOUT_TEXT =
2021-08-15 19:24:48 +02:00
PLUGIN_INFO "\n"
"by hcs, FastElbja, manakoAT, bxaimc, snakemeat, soneek, kode54, bnnm, Nicknine, Thealexbarney, CyberBotX, and many others\n"
2017-05-01 14:18:33 +02:00
"\n"
"Winamp plugin by hcs, others\n"
"\n"
2021-08-15 19:24:48 +02:00
"https://github.com/vgmstream/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() {
settings.is_xmplay = is_xmplay();
2021-08-26 18:43:36 +02:00
logger_init();
vgmstream_set_log_callback(VGM_LOG_LEVEL_ALL, logger_callback);
/* get ini config */
2019-11-02 16:05:01 +01:00
load_defaults(&defaults);
2021-04-18 13:04:00 +02:00
load_config(&input_module, &settings, &defaults);
2017-08-19 10:26:21 +02:00
/* XMPlay with in_vgmstream doesn't support most IPC_x messages so no playlist manipulation */
if (settings.is_xmplay) {
settings.disable_subsongs = 1;
2017-08-19 10:26:21 +02:00
}
/* dynamically make a list of supported extensions */
2019-11-02 16:05:01 +01:00
build_extension_list(working_extension_list, EXTENSION_LIST_SIZE);
}
/* called at program quit */
void winamp_Quit() {
2021-08-26 18:43:36 +02:00
logger_free();
}
/* 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;
/* Winamp seem to have bizarre handling of MP3 without standard names (ex song.mp3a),
* in that it'll try to open normally, rejected if unknown_exts_on is not set, and
* finally retry with "hi.mp3", accepted if exts_common_on is set. */
/* 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;
2017-08-19 10:26:21 +02:00
in_char filename[PATH_LIMIT];
int stream_index = 0;
2019-11-02 16:05:01 +01:00
/* shouldn't happen */
if (vgmstream)
2019-11-02 16:05:01 +01:00
return 1;
2017-08-19 10:26:21 +02:00
/* 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 */
2017-08-19 10:26:21 +02:00
vgmstream = init_vgmstream_winamp(filename,stream_index);
if (!vgmstream)
return 1;
2017-08-19 10:26:21 +02:00
/* add N subsongs to the playlist, if any */
if (split_subsongs(filename, stream_index, vgmstream)) {
close_vgmstream(vgmstream);
vgmstream = NULL;
return 1;
}
/* config */
2020-07-21 19:47:02 +02:00
apply_config(vgmstream, &settings);
2019-03-18 00:08:09 +01:00
/* 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);
2019-11-02 16:05:01 +01:00
vgmstream_mixing_enable(vgmstream, SAMPLE_BUFFER_SIZE, NULL /*&input_channels*/, &state.output_channels);
/* reset internals */
state.paused = 0;
state.decode_abort = 0;
2020-07-29 19:35:04 +02:00
state.seek_sample = -1;
2019-11-02 16:05:01 +01:00
state.decode_pos_ms = 0;
state.decode_pos_samples = 0;
2020-07-29 19:35:04 +02:00
state.length_samples = vgmstream_get_samples(vgmstream);
2019-11-02 16:05:01 +01:00
state.volume = get_album_gain_volume(fn);
/* save original name */
wa_strncpy(lastfn,fn,PATH_LIMIT);
/* open the output plugin */
2019-11-02 16:05:01 +01:00
max_latency = input_module.outMod->Open(vgmstream->sample_rate, state.output_channels, 16, 0, 0);
if (max_latency < 0) {
close_vgmstream(vgmstream);
vgmstream = NULL;
return 1;
}
/* set info display */
2020-07-21 19:47:02 +02:00
input_module.SetInfo(get_vgmstream_average_bitrate(vgmstream) / 1000, vgmstream->sample_rate / 1000, state.output_channels, 1);
/* setup visualization */
input_module.SAVSAInit(max_latency,vgmstream->sample_rate);
2019-11-02 16:05:01 +01:00
input_module.VSASetInfo(vgmstream->sample_rate, state.output_channels);
/* 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 */
2019-11-02 16:05:01 +01:00
SetThreadPriority(decode_thread_handle, priority_values[settings.thread_priority]);
return 0; /* success */
}
/* pause stream */
void winamp_Pause() {
2019-11-02 16:05:01 +01:00
state.paused = 1;
input_module.outMod->Pause(1);
}
/* unpause stream */
void winamp_UnPause() {
2019-11-02 16:05:01 +01:00
state.paused = 0;
input_module.outMod->Pause(0);
}
/* return 1 if paused, 0 if not */
int winamp_IsPaused() {
2019-11-02 16:05:01 +01:00
return state.paused;
}
/* stop (unload) stream */
void winamp_Stop() {
2020-07-29 19:35:04 +02:00
if (decode_thread_handle != INVALID_HANDLE_VALUE) {
2019-11-02 16:05:01 +01:00
state.decode_abort = 1;
/* arbitrary wait milliseconds (error can trigger if the system is *really* busy) */
if (WaitForSingleObject(decode_thread_handle, 5000) == WAIT_TIMEOUT) {
2019-11-02 16:05:01 +01:00
MessageBox(input_module.hMainWindow, TEXT("Error stopping decode thread\n"), ("Error"),MB_OK|MB_ICONERROR);
TerminateThread(decode_thread_handle, 0);
}
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() {
if (!vgmstream)
return 0;
2020-07-29 19:35:04 +02:00
return state.length_samples * 1000LL / vgmstream->sample_rate;
}
/* current output time in ms */
int winamp_GetOutputTime() {
2020-07-29 19:35:04 +02:00
int32_t pos_ms = state.decode_pos_ms;
/* for some reason this gets triggered hundred of times by non-classic skins when using subsongs */
if (!vgmstream)
return 0;
2020-07-29 19:35:04 +02:00
/* pretend we have reached destination if called while seeking is on */
if (state.seek_sample >= 0)
pos_ms = state.seek_sample * 1000LL / vgmstream->sample_rate;
return 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;
2020-07-29 19:35:04 +02:00
state.seek_sample = (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};
TCHAR tbuf[1024] = {0};
double tmpVolume = 1.0;
if (!fn || !*fn) {
/* no filename = current playing file */
if (!vgmstream)
return 0;
describe_vgmstream(vgmstream,description,sizeof(description));
}
else {
/* some other file in playlist given by filename */
2020-07-21 19:47:02 +02:00
VGMSTREAM* infostream = NULL;
2017-08-19 10:26:21 +02:00
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);
2017-08-19 10:26:21 +02:00
infostream = init_vgmstream_winamp(filename, stream_index);
if (!infostream) return 0;
2020-07-21 19:47:02 +02:00
apply_config(infostream, &settings);
2019-03-24 01:21:09 +01:00
2019-03-18 00:08:09 +01:00
vgmstream_mixing_autodownmix(infostream, settings.downmix_channels);
vgmstream_mixing_enable(infostream, 0, NULL, NULL);
describe_vgmstream(infostream,description,sizeof(description));
close_vgmstream(infostream);
infostream = NULL;
2019-11-02 16:05:01 +01:00
tmpVolume = get_album_gain_volume(fn);
}
snprintf(tmp, sizeof(tmp), "\nvolume: %.6f\n", tmpVolume);
concatn(sizeof(description), description, tmp);
concatn(sizeof(description), description, "\n" PLUGIN_INFO);
cfg_char_to_wchar(tbuf, sizeof(tbuf) / sizeof(TCHAR), description);
MessageBox(hwnd, tbuf, 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 */
2017-04-30 02:09:35 +02:00
if (!vgmstream)
return;
if (title) {
2017-08-19 10:26:21 +02:00
get_title(title,GETFILEINFO_TITLE_LENGTH, lastfn, vgmstream);
}
2017-04-30 02:09:35 +02:00
if (length_in_ms) {
*length_in_ms = winamp_GetLength();
}
}
else {
/* some other file in playlist given by filename */
2020-07-21 19:47:02 +02:00
VGMSTREAM* infostream = NULL;
2017-08-19 10:26:21 +02:00
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);
2017-04-30 02:09:35 +02:00
2017-08-19 10:26:21 +02:00
infostream = init_vgmstream_winamp(filename, stream_index);
if (!infostream) return;
2020-07-21 19:47:02 +02:00
apply_config(infostream, &settings);
2019-03-24 01:21:09 +01:00
2019-03-18 00:08:09 +01:00
vgmstream_mixing_autodownmix(infostream, settings.downmix_channels);
2019-03-24 01:21:09 +01:00
vgmstream_mixing_enable(infostream, 0, NULL, NULL);
if (title) {
2017-08-19 10:26:21 +02:00
get_title(title,GETFILEINFO_TITLE_LENGTH, fn, infostream);
}
2017-04-30 02:09:35 +02:00
if (length_in_ms) {
*length_in_ms = -1000;
if (infostream) {
2020-07-21 19:47:02 +02:00
const int num_samples = vgmstream_get_samples(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) {
}
2020-07-29 19:35:04 +02:00
/*****************************************************************************/
/* MAIN DECODE (some used in extended part too, so avoid globals) */
2020-08-12 17:40:47 +02:00
static void do_seek(winamp_state_t* state, VGMSTREAM* vgmstream) {
int play_forever = vgmstream_get_play_forever(vgmstream);
int seek_sample = state->seek_sample; /* local due to threads/race conditions changing state->seek_sample elsewhere */
2020-07-29 19:35:04 +02:00
2020-08-12 17:40:47 +02:00
/* ignore seeking past file, can happen using the right (->) key, ok if playing forever */
if (state->seek_sample > state->length_samples && !play_forever) {
2020-07-29 19:35:04 +02:00
state->seek_sample = -1;
2020-08-12 17:40:47 +02:00
//state->seek_sample = state->length_samples;
//seek_sample = state->length_samples;
state->decode_pos_samples = state->length_samples;
state->decode_pos_ms = state->decode_pos_samples * 1000LL / vgmstream->sample_rate;
return;
2020-07-29 19:35:04 +02:00
}
/* could divide in N seeks (from pos) for slower files so cursor moves, but doesn't seem too necessary */
2020-08-12 17:40:47 +02:00
seek_vgmstream(vgmstream, seek_sample);
2020-07-29 19:35:04 +02:00
2020-08-12 17:40:47 +02:00
state->decode_pos_samples = seek_sample;
2020-07-29 19:35:04 +02:00
state->decode_pos_ms = state->decode_pos_samples * 1000LL / vgmstream->sample_rate;
2020-08-12 17:40:47 +02:00
/* different sample: other seek may have been requested during seek_vgmstream */
if (state->seek_sample == seek_sample)
state->seek_sample = -1;
2020-07-29 19:35:04 +02:00
}
static void apply_gain(winamp_state_t* state, int samples_to_do) {
/* apply ReplayGain, if needed */
if (state->volume != 1.0) {
int j, k;
int channels = state->output_channels;
for (j = 0; j < samples_to_do; j++) {
for (k = 0; k < channels; k++) {
sample_buffer[j*channels + k] = (short)(sample_buffer[j*channels + k] * state->volume);
}
}
}
}
/* the decode thread */
DWORD WINAPI __stdcall decode(void *arg) {
2019-03-18 00:08:09 +01:00
const int max_buffer_samples = SAMPLE_BUFFER_SIZE;
2020-07-21 19:47:02 +02:00
int play_forever = vgmstream_get_play_forever(vgmstream);
2019-11-02 16:05:01 +01:00
while (!state.decode_abort) {
int samples_to_do;
int output_bytes;
2020-08-12 17:40:47 +02:00
if (state.decode_pos_samples + max_buffer_samples > state.length_samples && !play_forever) {
2020-07-29 19:35:04 +02:00
samples_to_do = state.length_samples - state.decode_pos_samples;
2020-08-12 17:40:47 +02:00
if (samples_to_do < 0) /* just in case */
samples_to_do = 0;
}
else {
samples_to_do = max_buffer_samples;
}
2019-11-02 16:05:01 +01:00
output_bytes = (samples_to_do * state.output_channels * sizeof(short));
if (input_module.dsp_isactive())
output_bytes = output_bytes * 2; /* Winamp's DSP may need double samples */
2020-07-29 19:35:04 +02:00
if (samples_to_do == 0 && state.seek_sample < 0) { /* track finished and not seeking */
input_module.outMod->CanWrite(); /* ? */
if (!input_module.outMod->IsPlaying()) {
PostMessage(input_module.hMainWindow, WM_WA_MPEG_EOF, 0,0); /* end */
return 0;
}
Sleep(10);
}
2020-07-29 19:35:04 +02:00
else if (state.seek_sample >= 0) { /* seek */
do_seek(&state, vgmstream);
2020-07-29 19:35:04 +02:00
/* flush Winamp buffers *after* fully seeking (allows to play buffered samples while we seek, feels a bit snappier) */
2020-08-12 17:40:47 +02:00
if (state.seek_sample < 0)
input_module.outMod->Flush(state.decode_pos_ms);
}
else if (input_module.outMod->CanWrite() >= output_bytes) { /* decode */
2020-07-21 19:47:02 +02:00
render_vgmstream(sample_buffer, samples_to_do, vgmstream);
2020-07-29 19:35:04 +02:00
apply_gain(&state, samples_to_do); /* apply ReplayGain, if needed */
/* output samples */
2020-07-29 19:35:04 +02:00
input_module.SAAddPCMData((char*)sample_buffer, state.output_channels, 16, state.decode_pos_ms);
input_module.VSAAddPCMData((char*)sample_buffer, state.output_channels, 16, state.decode_pos_ms);
if (input_module.dsp_isactive()) { /* find out DSP's needs */
2020-07-29 19:35:04 +02:00
int dsp_output_samples = input_module.dsp_dosamples(sample_buffer, samples_to_do, 16, state.output_channels, vgmstream->sample_rate);
2019-11-02 16:05:01 +01:00
output_bytes = dsp_output_samples * state.output_channels * sizeof(short);
}
input_module.outMod->Write((char*)sample_buffer, output_bytes);
2019-11-02 16:05:01 +01:00
state.decode_pos_samples += samples_to_do;
state.decode_pos_ms = state.decode_pos_samples * 1000LL / vgmstream->sample_rate;
}
else { /* can't write right now */
Sleep(20);
}
}
return 0;
}
/* configuration dialog */
void winamp_Config(HWND hwndParent) {
2019-11-02 16:05:01 +01:00
/* open dialog defined in resource.rc */
DialogBox(input_module.hDllInstance, (const TCHAR *)IDD_CONFIG, hwndParent, configDlgProc);
}
/* *********************************** */
/* main plugin def */
In_Module input_module = {
IN_VER,
2021-08-15 19:24:48 +02:00
PLUGIN_NAME,
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 */
};
2018-11-03 19:28:05 +01:00
__declspec(dllexport) In_Module * winampGetInModule2() {
return &input_module;
}
2018-11-03 19:28:05 +01:00
2018-11-04 17:02:11 +01:00
/* ************************************* */
/* 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;
2018-11-04 17:02:11 +01:00
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) {
2018-11-03 19:28:05 +01:00
STREAMFILE *tagFile = NULL;
2018-11-11 23:59:20 +01:00
in_char filename_clean[PATH_LIMIT];
2018-11-03 19:28:05 +01:00
char filename_utf8[PATH_LIMIT];
2018-11-04 17:02:11 +01:00
char tagfile_path_utf8[PATH_LIMIT];
in_char tagfile_path_i[PATH_LIMIT];
char *path;
2018-11-03 19:28:05 +01:00
if (settings.tagfile_disable) { /* reset values if setting changes during play */
last_tags.loaded = 0;
last_tags.tag_count = 0;
2018-11-04 17:02:11 +01:00
return;
}
2018-11-11 23:59:20 +01:00
/* clean extra part for subsong tags */
parse_fn_string(filename, NULL, filename_clean,PATH_LIMIT);
2018-11-04 17:02:11 +01:00
2018-11-11 23:59:20 +01:00
if (wa_strcmp(last_tags.filename, filename_clean) == 0) {
2018-11-04 17:02:11 +01:00
return; /* not changed, tags still apply */
}
last_tags.loaded = 0;
2018-11-04 17:02:11 +01:00
/* tags are now for this filename, find tagfile path */
2018-11-11 23:59:20 +01:00
wa_ichar_to_char(filename_utf8, PATH_LIMIT, filename_clean);
2018-11-04 17:02:11 +01:00
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);
2018-11-11 23:59:20 +01:00
wa_strcpy(last_tags.filename, filename_clean);
2018-11-04 17:02:11 +01:00
last_tags.tag_count = 0;
/* load all tags from tagfile */
tagFile = open_winamp_streamfile_by_ipath(tagfile_path_i);
if (tagFile != NULL) {
2020-07-21 19:47:02 +02:00
VGMSTREAM_TAGS* tags;
2019-03-04 22:50:05 +01:00
const char *tag_key, *tag_val;
2018-11-04 17:02:11 +01:00
int i;
2019-03-04 22:50:05 +01:00
tags = vgmstream_tags_init(&tag_key, &tag_val);
vgmstream_tags_reset(tags, filename_utf8);
while (vgmstream_tags_next_tag(tags, tagFile)) {
2018-11-04 17:02:11 +01:00
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++) {
2019-03-04 22:50:05 +01:00
if (strcmp(last_tags.keys[i], tag_key) == 0) {
2018-11-04 17:02:11 +01:00
current_tag = i;
repeated_tag = 1;
break;
}
}
last_tags.keys[current_tag][0] = '\0';
2019-03-04 22:50:05 +01:00
strncat(last_tags.keys[current_tag], tag_key, WINAMP_TAGS_ENTRY_SIZE);
2018-11-04 17:02:11 +01:00
last_tags.vals[current_tag][0] = '\0';
2019-03-04 22:50:05 +01:00
strncat(last_tags.vals[current_tag], tag_val, WINAMP_TAGS_ENTRY_SIZE);
2018-11-04 17:02:11 +01:00
if (!repeated_tag)
last_tags.tag_count++;
}
2019-03-04 22:50:05 +01:00
vgmstream_tags_close(tags);
2018-11-04 17:02:11 +01:00
close_streamfile(tagFile);
last_tags.loaded = 1;
2018-11-04 17:02:11 +01:00
}
}
/* 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;
2018-11-03 19:28:05 +01:00
/* 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;
2018-11-03 19:28:05 +01:00
/* 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
2018-11-04 17:02:11 +01:00
/* 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);
2018-11-03 19:28:05 +01:00
tag_found = 1;
2018-11-04 17:02:11 +01:00
break;
2018-11-03 19:28:05 +01:00
}
}
/* if tagfile exists but TITLE doesn't Winamp won't default to GetFileInfo, so call it
* manually as it's useful for files with stream names */
if (!tag_found && strcasecmp(metadata, "title") == 0) {
in_char ret_wchar[2048];
winamp_GetFileInfo(filename, ret_wchar, NULL);
wa_ichar_to_char(ret, retlen, ret_wchar);
return 1;
}
2018-11-03 19:28:05 +01:00
if (!tag_found)
goto fail;
return 1;
fail:
//TODO: is this always needed for Winamp to use replaygain?
//strcpy(ret, "1.0"); //should set some default value?
return strcasecmp(metadata, "replaygain_track_gain") == 0 ? 1 : 0;
2018-11-03 19:28:05 +01:00
}
/* for Winamp 5.24 */
__declspec (dllexport) int winampGetExtendedFileInfo(char *filename, char *metadata, char *ret, int retlen) {
in_char filename_wchar[PATH_LIMIT];
int ok;
2018-11-04 17:02:11 +01:00
if (settings.tagfile_disable)
return 0;
wa_char_to_ichar(filename_wchar,PATH_LIMIT, filename);
2018-11-03 19:28:05 +01:00
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) {
2018-11-04 17:02:11 +01:00
in_char filename_ichar[PATH_LIMIT];
2018-11-03 19:28:05 +01:00
char ret_utf8[2048];
int ok;
2018-11-04 17:02:11 +01:00
if (settings.tagfile_disable)
return 0;
wa_wchar_to_ichar(filename_ichar,PATH_LIMIT, filename);
2018-11-03 19:28:05 +01:00
2018-11-04 17:02:11 +01:00
ok = winampGetExtendedFileInfo_common(filename_ichar, metadata, ret_utf8,2048);
2018-11-03 19:28:05 +01:00
if (ok == 0)
return 0;
2018-11-04 17:02:11 +01:00
wa_char_to_wchar(ret,retlen, ret_utf8);
2018-11-03 19:28:05 +01:00
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) {
2019-04-07 01:48:18 +02:00
return 0;
2018-11-03 19:28:05 +01:00
}
2019-11-02 16:05:01 +01:00
__declspec(dllexport) int winampUninstallPlugin(HINSTANCE hDllInst, HWND hwndDlg, int param) {
/* may uninstall without restart as we aren't subclassing */
return IN_PLUGIN_UNINSTALL_NOW;
}
/* ************************************* */
/* EXTENDED DECODE */
/* ************************************* */
2019-11-02 16:05:01 +01:00
//TODO: call common functions to avoid so much repeated code
/* the following functions are used for ReplayGain and transcoding, in places like the media
* library or CD burner, if implemented. In usual Winamp fashion they are messy, barely
* documented, slightly different repeats of the above. */
2019-11-02 16:05:01 +01:00
winamp_state_t xstate;
short xsample_buffer[SAMPLE_BUFFER_SIZE*2 * VGMSTREAM_MAX_CHANNELS];
2019-11-02 16:05:01 +01:00
/* open the file and prepares to decode */
static void *winampGetExtendedRead_open_common(in_char *fn, int *size, int *bps, int *nch, int *srate) {
VGMSTREAM *xvgmstream = 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 */
2019-11-02 16:05:01 +01:00
xvgmstream = init_vgmstream_winamp(filename, stream_index);
if (!xvgmstream) {
return NULL;
}
/* config */
2020-07-21 19:47:02 +02:00
apply_config(xvgmstream, &settings);
/* enable after all config but before outbuf (though ATM outbuf is not dynamic so no need to read input_channels) */
2019-11-02 16:05:01 +01:00
vgmstream_mixing_autodownmix(xvgmstream, settings.downmix_channels);
vgmstream_mixing_enable(xvgmstream, SAMPLE_BUFFER_SIZE, NULL /*&input_channels*/, &xstate.output_channels);
/* reset internals */
2019-11-02 16:05:01 +01:00
xstate.paused = 0; /* unused */
xstate.decode_abort = 0; /* unused */
2020-07-29 19:35:04 +02:00
xstate.seek_sample = -1;
2019-11-02 16:05:01 +01:00
xstate.decode_pos_ms = 0; /* unused */
xstate.decode_pos_samples = 0;
2020-07-29 19:35:04 +02:00
xstate.length_samples = vgmstream_get_samples(xvgmstream);
2019-11-02 16:05:01 +01:00
xstate.volume = 1.0; /* unused */
if (size) /* bytes to decode (-1 if unknown) */
2020-07-29 19:35:04 +02:00
*size = xstate.length_samples * xstate.output_channels * sizeof(short);
2019-11-02 16:05:01 +01:00
if (bps)
*bps = 16;
2019-11-02 16:05:01 +01:00
if (nch)
*nch = xstate.output_channels;
if (srate)
*srate = xvgmstream->sample_rate;
return xvgmstream; /* handle passed to other extended functions */
}
2019-11-02 16:05:01 +01:00
__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);
}
2019-11-02 16:05:01 +01:00
__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);
}
2019-11-02 16:05:01 +01:00
/* decode len to dest buffer, called multiple times until file done or decoding is aborted */
__declspec(dllexport) size_t winampGetExtendedRead_getData(void *handle, char *dest, size_t len, int *killswitch) {
const int max_buffer_samples = SAMPLE_BUFFER_SIZE;
unsigned copied = 0;
int done = 0;
2020-07-21 19:47:02 +02:00
VGMSTREAM* xvgmstream = handle;
int play_forever;
if (!xvgmstream)
return 0;
2020-07-21 19:47:02 +02:00
play_forever = vgmstream_get_play_forever(xvgmstream);
2019-11-02 16:05:01 +01:00
while (copied + (max_buffer_samples * xvgmstream->channels * sizeof(short)) < len && !done) {
int samples_to_do;
2019-11-02 16:05:01 +01:00
2020-08-12 17:40:47 +02:00
if (xstate.decode_pos_samples + max_buffer_samples > xstate.length_samples && !play_forever) {
2020-07-29 19:35:04 +02:00
samples_to_do = xstate.length_samples - xstate.decode_pos_samples;
2020-08-12 17:40:47 +02:00
if (samples_to_do < 0) /* just in case */
samples_to_do = 0;
}
else {
samples_to_do = max_buffer_samples;
}
if (!samples_to_do) { /* track finished */
break;
}
2020-07-29 19:35:04 +02:00
else if (xstate.seek_sample != -1) { /* seek */
do_seek(&xstate, xvgmstream);
}
else { /* decode */
2019-11-02 16:05:01 +01:00
render_vgmstream(xsample_buffer, samples_to_do, xvgmstream);
/* output samples */
2019-11-02 16:05:01 +01:00
memcpy(&dest[copied], xsample_buffer, samples_to_do * xstate.output_channels * sizeof(short));
copied += samples_to_do * xstate.output_channels * sizeof(short);
2019-11-02 16:05:01 +01:00
xstate.decode_pos_samples += samples_to_do;
}
2019-11-02 16:05:01 +01:00
/* check decoding cancelled */
if (killswitch && *killswitch) {
break;
}
}
2019-11-02 16:05:01 +01:00
return copied; /* return 0 to signal file done */
}
2019-11-02 16:05:01 +01:00
/* seek in the file (possibly unused) */
__declspec(dllexport) int winampGetExtendedRead_setTime(void *handle, int time_in_ms) {
VGMSTREAM *xvgmstream = handle;
if (xvgmstream) {
2020-07-29 19:35:04 +02:00
xstate.seek_sample = (long long)time_in_ms * xvgmstream->sample_rate / 1000LL;
return 1;
}
return 0;
}
2019-11-02 16:05:01 +01:00
/* file done */
__declspec(dllexport) void winampGetExtendedRead_close(void *handle) {
VGMSTREAM *xvgmstream = handle;
if (xvgmstream) {
close_vgmstream(xvgmstream);
}
}
2019-11-02 16:05:01 +01:00
/* other winamp sekrit exports: */
#if 0
__declspec(dllexport) void winampAddUnifiedFileInfoPane(?) {
?
2018-11-03 19:28:05 +01:00
}
2019-11-02 16:05:01 +01:00
#endif