/** * vgmstream for Audacious */ #include #include #include #if DEBUG #include #include #endif #include extern "C" { #include "../src/vgmstream.h" #include "../src/plugins.h" } #include "plugin.h" #include "vfs.h" #ifndef VERSION #include "version.h" #endif #ifndef VERSION #define VERSION "(unknown version)" #endif #define CFG_ID "vgmstream" // ID for storing in audacious #define MIN_BUFFER_SIZE 576 /* global state */ /*EXPORT*/ VgmstreamPlugin aud_plugin_instance; audacious_settings_t settings; /* Audacious will first send the file to a plugin based on this static extension list. If none * accepts it'll try again all plugins, ordered by priority, until one accepts the file. Problem is, * mpg123 plugin has higher priority and tendency to accept files that aren't even MP3. To fix this * we declare a few conflicting formats so we have a better chance. * The extension affects only this priority and in all cases file must accepted during "is_our_file". */ const char *const VgmstreamPlugin::exts[] = { "ahx","asf","awc","ckd","fsb","genh","msf","p3d","rak","scd","str","txth","xvag", nullptr }; const char *const VgmstreamPlugin::defaults[] = { "loop_forever", "FALSE", "ignore_loop", "FALSE", "loop_count", "2.0", "fade_length", "10.0", "fade_delay", "0.0", "downmix_channels", "2", "exts_unknown_on", "FALSE", "exts_common_on", "FALSE", NULL }; // N_(...) for i18n but not much point here const char VgmstreamPlugin::about[] = "vgmstream plugin " VERSION " " __DATE__ "\n" "by hcs, FastElbja, manakoAT, bxaimc, snakemeat, soneek, kode54, bnnm and many others\n" "\n" "Audacious plugin:\n" "ported to Audacious 3.6 by Brandon Whitehead\n" "adopted from Audacious 3 port by Thomas Eppers\n" "originally written by Todd Jeffreys (http://voidpointer.org/)\n" "\n" "https://github.com/kode54/vgmstream/\n" "https://sourceforge.net/projects/vgmstream/ (original)"; /* widget config: {min, max, step} */ const PreferencesWidget VgmstreamPlugin::widgets[] = { WidgetLabel(N_("vgmstream config")), WidgetCheck(N_("Loop forever:"), WidgetBool(settings.loop_forever)), WidgetCheck(N_("Ignore loop:"), WidgetBool(settings.ignore_loop)), WidgetSpin(N_("Loop count:"), WidgetFloat(settings.loop_count), {1, 100, 1.0}), WidgetSpin(N_("Fade length:"), WidgetFloat(settings.fade_time), {0, 60, 0.1}), WidgetSpin(N_("Fade delay:"), WidgetFloat(settings.fade_delay), {0, 60, 0.1}), WidgetSpin(N_("Downmix:"), WidgetInt(settings.downmix_channels), {0, 8, 1}), WidgetCheck(N_("Enable unknown exts"), WidgetBool(settings.exts_unknown_on)), // Audacious 3.6 will only match one plugin so this option has no actual use // (ex. a fake .flac only gets to the FLAC plugin and never to vgmstream, even on error) //WidgetCheck(N_("Enable common exts"), WidgetBool(settings.exts_common_on)), }; void vgmstream_settings_load() { AUDINFO("load settings\n"); aud_config_set_defaults(CFG_ID, VgmstreamPlugin::defaults); settings.loop_forever = aud_get_bool(CFG_ID, "loop_forever"); settings.ignore_loop = aud_get_bool(CFG_ID, "ignore_loop"); settings.loop_count = aud_get_double(CFG_ID, "loop_count"); settings.fade_time = aud_get_double(CFG_ID, "fade_length"); settings.fade_delay = aud_get_double(CFG_ID, "fade_delay"); settings.downmix_channels = aud_get_int(CFG_ID, "downmix_channels"); settings.exts_unknown_on = aud_get_bool(CFG_ID, "exts_unknown_on"); settings.exts_common_on = aud_get_bool(CFG_ID, "exts_common_on"); } void vgmstream_settings_save() { AUDINFO("save settings\n"); aud_set_bool(CFG_ID, "loop_forever", settings.loop_forever); aud_set_bool(CFG_ID, "ignore_loop", settings.ignore_loop); aud_set_double(CFG_ID, "loop_count", settings.loop_count); aud_set_double(CFG_ID, "fade_length", settings.fade_time); aud_set_double(CFG_ID, "fade_delay", settings.fade_delay); aud_set_int(CFG_ID, "downmix_channels", settings.downmix_channels); aud_set_bool(CFG_ID, "exts_unknown_on", settings.exts_unknown_on); aud_set_bool(CFG_ID, "exts_common_on", settings.exts_common_on); } const PluginPreferences VgmstreamPlugin::prefs = { {widgets}, vgmstream_settings_load, vgmstream_settings_save }; // validate extension (thread safe) bool VgmstreamPlugin::is_our_file(const char * filename, VFSFile & file) { AUDDBG("test file=%s\n", filename); vgmstream_ctx_valid_cfg cfg = {0}; cfg.accept_unknown = settings.exts_unknown_on; cfg.accept_common = settings.exts_common_on; return vgmstream_ctx_is_valid(filename, &cfg) > 0 ? true : false; } // called on startup (main thread) bool VgmstreamPlugin::init() { AUDINFO("plugin start\n"); vgmstream_settings_load(); return true; } // called on stop (main thread) void VgmstreamPlugin::cleanup() { AUDINFO("plugin end\n"); vgmstream_settings_save(); } #if 0 static int get_filename_subtune(const char* filename) { int subtune; int pos = strstr("?")) if (pos <= 0) return -1; if (sscanf(filename + pos, "%i", &subtune) != 1) return -1; return subtune; } #endif static void apply_config(VGMSTREAM* vgmstream, audacious_settings_t* settings) { vgmstream_cfg_t vcfg = {0}; vcfg.allow_play_forever = 1; vcfg.play_forever = settings->loop_forever; vcfg.loop_count = settings->loop_count; vcfg.fade_time = settings->fade_time; vcfg.fade_delay = settings->fade_delay; vcfg.ignore_loop = settings->ignore_loop; vgmstream_apply_config(vgmstream, &vcfg); } // internal helper, called every time user adds a new file to playlist static bool read_info(const char* filename, Tuple & tuple) { AUDINFO("read file=%s\n", filename); #if 0 //todo subsongs: // in theory just set FlagSubtunes in plugin.h, and set_subtunes below // Audacious will call "play" and this(?) with filename?N where N=subtune // and you just load subtune N, but I can't get it working int subtune; string basename; int subtune = get_filename_subtune(basename, &subtune); //must use basename to open streamfile #endif STREAMFILE* sf = open_vfs(filename); if (!sf) return false; VGMSTREAM* infostream = init_vgmstream_from_STREAMFILE(sf); if (!infostream) { close_streamfile(sf); return false; } #if 0 int total_subtunes = infostream->num_streams; //somehow max was changed to short in recent versions, though // some formats can exceed this if (total_subtunes > 32767) total_subtunes = 32767; if (infostream->num_streams > 1 && subtune <= 0) { //set nullptr to leave subsong index linear (must add +1 to subtune) tuple.set_subtunes(total_subtunes, nullptr); return true; //must return? } sf->stream_index = (subtune + 1); #endif apply_config(infostream, &settings); int output_channels = infostream->channels; vgmstream_mixing_autodownmix(infostream, settings.downmix_channels); vgmstream_mixing_enable(infostream, 0, NULL, &output_channels); int bitrate = get_vgmstream_average_bitrate(infostream); int length_samples = vgmstream_get_samples(infostream); int length_ms = length_samples * 1000LL / infostream->sample_rate; //todo: set_format may throw std::bad_alloc if output_channels isn't supported (only 2?) // short form, not sure if better way tuple.set_format("vgmstream codec", output_channels, infostream->sample_rate, bitrate); tuple.set_filename(filename); //used? tuple.set_int(Tuple::Bitrate, bitrate); //in kb/s tuple.set_int(Tuple::Length, length_ms); //todo here we could call describe_vgmstream() and get substring to add tags and stuff tuple.set_str(Tuple::Codec, "vgmstream codec"); //tuple.set_int(Tuple::Subtune, subtune); //tuple.set_int(Tuple::NumSubtunes, subtune); //done un set_subtunes //todo tags (see tuple.h) //tuple.set_int (Tuple::Track, ...); //tuple.set_str (Tuple::Artist, ...); //tuple.set_str (Tuple::Album, ...); close_streamfile(sf); close_vgmstream(infostream); return true; } // thread safe (for Audacious <= 3.7, unused otherwise) Tuple VgmstreamPlugin::read_tuple(const char * filename, VFSFile & file) { Tuple tuple; read_info(filename, tuple); return tuple; } // thread safe (for Audacious >= 3.8, unused otherwise) bool VgmstreamPlugin::read_tag(const char * filename, VFSFile & file, Tuple & tuple, Index * image) { return read_info(filename, tuple); } // internal util to seek during play static void do_seek(VGMSTREAM* vgmstream, int seek_ms, int& current_sample_pos) { AUDINFO("seeking\n"); // compute from ms to samples int seek_sample = (long long)seek_ms * vgmstream->sample_rate / 1000L; seek_vgmstream(vgmstream, seek_sample); current_sample_pos = seek_sample; } // called on play (play thread) bool VgmstreamPlugin::play(const char * filename, VFSFile & file) { AUDINFO("play file=%s\n", filename); STREAMFILE* sf = open_vfs(filename); if (!sf) { AUDERR("failed opening file %s\n", filename); return false; } VGMSTREAM* vgmstream = init_vgmstream_from_STREAMFILE(sf); close_streamfile(sf); if (!vgmstream) { AUDINFO("filename %s is not a valid format\n", filename); close_vgmstream(vgmstream); return false; } int bitrate = get_vgmstream_average_bitrate(vgmstream); set_stream_bitrate(bitrate); //todo apply config apply_config(vgmstream, &settings); int input_channels = vgmstream->channels; int output_channels = vgmstream->channels; /* enable after all config but before outbuf */ vgmstream_mixing_autodownmix(vgmstream, settings.downmix_channels); vgmstream_mixing_enable(vgmstream, MIN_BUFFER_SIZE, &input_channels, &output_channels); //FMT_S8 / FMT_S16_NE / FMT_S24_NE / FMT_S32_NE / FMT_FLOAT open_audio(FMT_S16_LE, vgmstream->sample_rate, output_channels); // play short buffer[MIN_BUFFER_SIZE * input_channels]; int max_buffer_samples = MIN_BUFFER_SIZE; int play_forever = vgmstream_get_play_forever(vgmstream); int length_samples = vgmstream_get_samples(vgmstream); int decode_pos_samples = 0; while (!check_stop()) { int to_do = max_buffer_samples; // handle seek request int seek_value = check_seek(); if (seek_value >= 0) { do_seek(vgmstream, seek_value, decode_pos_samples); continue; } // check stream finished if (!play_forever) { if (decode_pos_samples >= length_samples) break; if (decode_pos_samples + to_do > length_samples) to_do = length_samples - decode_pos_samples; } render_vgmstream(buffer, to_do, vgmstream); write_audio(buffer, to_do * sizeof(short) * output_channels); decode_pos_samples += to_do; } AUDINFO("play finished\n"); close_vgmstream(vgmstream); return true; }