/** * vgmstream for Audacious */ #include #include #include #if DEBUG #include #include #endif #include extern "C" { #include "../src/vgmstream.h" #include "../src/api.h" } #include "plugin.h" #include "vfs.h" #include "../version.h" #ifndef VGMSTREAM_VERSION #define VGMSTREAM_VERSION "unknown version " __DATE__ #endif #define PLUGIN_NAME "vgmstream plugin " VGMSTREAM_VERSION #define PLUGIN_INFO PLUGIN_NAME " (" __DATE__ ")" #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; static const char* tagfile_name = "!tags.m3u"; /* 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", "tagfile_disable", "FALSE", NULL }; // N_(...) for i18n but not much point here const char VgmstreamPlugin::about[] = PLUGIN_INFO "\n" "by hcs, FastElbja, manakoAT, bxaimc, snakemeat, soneek, kode54, bnnm, Nicknine, Thealexbarney, CyberBotX, 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/vgmstream/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)), WidgetCheck(N_("Disable tagfile"), WidgetBool(settings.tagfile_disable)) }; 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; int ok = vgmstream_ctx_is_valid(filename, &cfg); if (!ok) { return false; } // just in case reject non-supported files, to avoid hijacking certain files like .vgm // (other plugins should have higher priority though) STREAMFILE* sf = open_vfs(filename); if (!sf) return false; VGMSTREAM* infostream = init_vgmstream_from_STREAMFILE(sf); close_streamfile(sf); if (!infostream) { return false; } close_vgmstream(infostream); return true; } /* default output in audacious is: "INFO/DEBUG plugin.cc:xxx [(fn name)]: (msg)" */ static void vgmstream_log(int level, const char* str) { if (level == VGM_LOG_LEVEL_DEBUG) AUDDBG("%s", str); else AUDINFO("%s", str); } // called on startup (main thread) bool VgmstreamPlugin::init() { AUDINFO("vgmstream plugin start\n"); vgmstream_settings_load(); vgmstream_set_log_callback(VGM_LOG_LEVEL_ALL, (void*)&vgmstream_log); return true; } // called on stop (main thread) void VgmstreamPlugin::cleanup() { AUDINFO("vgmstream plugin end\n"); vgmstream_settings_save(); } static int get_basename_subtune(const char* filename, char* buf, int buf_len, int* p_subtune) { int subtune; const char* pos = strrchr(filename, '?'); if (!pos) return 0; if (sscanf(pos, "?%i", &subtune) != 1) return 0; if (p_subtune) *p_subtune = subtune; strncpy(buf, filename, buf_len); char* pos2 = strrchr(buf, '?'); if (pos2) //removes '?' pos2[0] = '\0'; return 1; } 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); // Audacious first calls this as a regular file (use_subtune is 0). If file has subsongs, // you need to detect and call set_subtunes below and return. Then Audacious will call again // this and "play" with "filename?N" (where N=subtune, 1=first), that must be detected and handled // (see plugin.h) char basename[PATH_LIMIT]; //filename without '?' int subtune = 0; int use_subtune = get_basename_subtune(filename, basename, sizeof(basename), &subtune); STREAMFILE* sf = open_vfs(use_subtune ? basename : filename); if (!sf) return false; if (use_subtune) sf->stream_index = subtune; VGMSTREAM* infostream = init_vgmstream_from_STREAMFILE(sf); close_streamfile(sf); if (!infostream) { return false; } int total_subtunes = infostream->num_streams; // int was changed to short in some version, though vgmstream formats can exceed it if (total_subtunes > 32767) total_subtunes = 32767; // format has subsongs but Audacious didn't ask for subsong yet if (total_subtunes >= 1 && !use_subtune) { //set nullptr to leave subsong index linear (must add +1 to subtune) tuple.set_subtunes(total_subtunes, nullptr); close_vgmstream(infostream); return true; } 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"); if (use_subtune) { tuple.set_int(Tuple::Subtune, subtune); tuple.set_int(Tuple::NumSubtunes, infostream->num_streams); char title[1024]; vgmstream_get_title(title, sizeof(title), basename, infostream, NULL); tuple.set_str(Tuple::Title, title); //may be overwritten by tags } // this function is only called when files are added to playlist, // so to reload tags files need to readded if (!settings.tagfile_disable) { //todo improve string functions char tagfile_path[PATH_LIMIT]; strcpy(tagfile_path, filename); char *path = strrchr(tagfile_path,'/'); if (path != NULL) { path[1] = '\0'; /* includes "/", remove after that from tagfile_path */ strcat(tagfile_path,tagfile_name); } else { /* ??? */ strcpy(tagfile_path,tagfile_name); } STREAMFILE* sf_tags = open_vfs(tagfile_path); if (sf_tags != NULL) { VGMSTREAM_TAGS* tags; const char *tag_key, *tag_val; tags = vgmstream_tags_init(&tag_key, &tag_val); vgmstream_tags_reset(tags, filename); while (vgmstream_tags_next_tag(tags, sf_tags)) { // see tuple.h (ugly but other plugins do it like this) if (strcasecmp(tag_key, "ARTIST") == 0) tuple.set_str(Tuple::Artist, tag_val); else if (strcasecmp(tag_key, "ALBUMARTIST") == 0) tuple.set_str(Tuple::AlbumArtist, tag_val); else if (strcasecmp(tag_key, "TITLE") == 0) tuple.set_str(Tuple::Title, tag_val); else if (strcasecmp(tag_key, "ALBUM") == 0) tuple.set_str(Tuple::Album, tag_val); else if (strcasecmp(tag_key, "PERFORMER") == 0) tuple.set_str(Tuple::Performer, tag_val); else if (strcasecmp(tag_key, "COMPOSER") == 0) tuple.set_str(Tuple::Composer, tag_val); else if (strcasecmp(tag_key, "COMMENT") == 0) tuple.set_str(Tuple::Comment, tag_val); else if (strcasecmp(tag_key, "GENRE") == 0) tuple.set_str(Tuple::Genre, tag_val); else if (strcasecmp(tag_key, "TRACK") == 0) tuple.set_int(Tuple::Track, atoi(tag_val)); else if (strcasecmp(tag_key, "YEAR") == 0) tuple.set_int(Tuple::Year, atoi (tag_val)); #if defined(_AUD_PLUGIN_VERSION) && _AUD_PLUGIN_VERSION >= 48 // Audacious 3.8+ else if (strcasecmp(tag_key, "REPLAYGAIN_TRACK_GAIN") == 0) tuple.set_gain(Tuple::TrackGain, Tuple::GainDivisor, tag_val); else if (strcasecmp(tag_key, "REPLAYGAIN_TRACK_PEAK") == 0) tuple.set_gain(Tuple::TrackPeak, Tuple::PeakDivisor, tag_val); else if (strcasecmp(tag_key, "REPLAYGAIN_ALBUM_GAIN") == 0) tuple.set_gain(Tuple::AlbumGain, Tuple::GainDivisor, tag_val); else if (strcasecmp(tag_key, "REPLAYGAIN_ALBUM_PEAK") == 0) tuple.set_gain(Tuple::AlbumPeak, Tuple::PeakDivisor, tag_val); #endif } vgmstream_tags_close(tags); close_streamfile(sf_tags); } } 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); //handle subsongs (see read_info) char basename[PATH_LIMIT]; //filename without '?' int subtune = 0; int use_subtune = get_basename_subtune(filename, basename, sizeof(basename), &subtune); STREAMFILE* sf = open_vfs(use_subtune ? basename : filename); if (!sf) { AUDERR("failed opening file %s\n", filename); return false; } if (use_subtune) sf->stream_index = subtune; VGMSTREAM* vgmstream = init_vgmstream_from_STREAMFILE(sf); close_streamfile(sf); if (!vgmstream) { AUDINFO("filename %s is not a valid format\n", filename); 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); //TODO: detect how many written decode_pos_samples += to_do; } AUDINFO("play finished\n"); close_vgmstream(vgmstream); return true; }