/* GStreamer ReplayGain analysis * * Copyright (C) 2006 Rene Stadler * * gstrganalysis.c: Element that performs the ReplayGain analysis * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA */ /** * SECTION:element-rganalysis * * * * GstRgAnalysis analyzes raw audio sample data in accordance with the * proposed ReplayGain * standard for calculating the ideal replay gain for music * tracks and albums. The element is designed as a pass-through * filter that never modifies any data. As it receives an EOS event, * it finalizes the ongoing analysis and generates a tag list * containing the results. It is sent downstream with a TAG event and * posted on the message bus with a TAG message. The EOS event is * forwarded as normal afterwards. Result tag lists at least contain * the tags #GST_TAG_TRACK_GAIN and #GST_TAG_TRACK_PEAK. * * Album processing * * Analyzing several streams sequentially and assigning them a common * result gain is known as "album processing". If this gain is used * during playback (by switching to "album mode"), all tracks receive * the same amplification. This keeps the relative volume levels * between the tracks intact. To enable this, set the num-tracks property to * the number of streams that will be processed as album tracks. * Every time an EOS event is received, the value of this property * will be decremented by one. As it reaches zero, it is assumed that * the last track of the album finished. The tag list for the final * stream will contain the additional tags #GST_TAG_ALBUM_GAIN and * #GST_TAG_ALBUM_PEAK. All other streams just get the two track tags * posted because the values for the album tags are not known before * all tracks are analyzed. Applications need to make sure that the * album gain and peak values are also associated with the other * tracks when storing the results. It is thus a bit more complex to * implement, but should not be avoided since the album gain is * generally more valuable for use during playback than the track * gain. * * Skipping processing * * For assisting transcoder/converter applications, the element can * silently skip the processing of streams that already contain the * necessary meta data tags. Data will flow as usual but the element * will not consume CPU time and will not generate result tags. To * enable possible skipping, set the forced property to #FALSE. * If used in conjunction with album processing, the element will skip * the number of remaining album tracks if a full set of tags is found * for the first track. If a subsequent track of the album is missing * tags, processing cannot start again. If this is undesired, your * application has to scan all files beforehand and enable forcing of * processing if needed. * * Tips * * * Because the generated metadata tags become available at the end of * streams, downstream muxer and encoder elements are normally unable * to save them in their output since they generally save metadata in * the file header. Therefore, it is often necessary that * applications read the results in a bus event handler for the tag * message. Obtaining the values this way is always needed for album * processing since the album gain and peak values need to be * associated with all tracks of an album, not just the last one. * * * To perform album processing, the element has to preserve data * between streams. This cannot survive a state change to the NULL or * READY state. If you change your pipeline's state to NULL or READY * between tracks, lock the rganalysis element's state using * gst_element_set_locked_state() when it is in PAUSED or PLAYING. As * with any other element, don't forget to unlock it again and set it * to the NULL state before dropping the last reference. * * * If the total number of album tracks is unknown beforehand, set the * num-tracks property to some large value like #G_MAXINT (or set it * to >= 2 before each track starts). Before the last track ends, set * the property value to 1. * * * Compliance * * Analyzing the ReplayGain pink noise reference waveform will compute * a result of +6.00dB instead of the expected 0.00dB because the * default reference level is 89dB. To obtain values as lined out in * the original proposal of ReplayGain, set the reference-level * property to 83. Almost all software uses 89dB as a reference * however, which works against the tendency of the algorithm to * advise to drastically lower the volume of music with a highly * compressed dynamic range and high average output levels. This * tendency is normally to be fought during playback (if wanted), by * using a default pre-amp value of at least +6.00dB. At one point, * the majority of analyzer implementations switched to 89dB which * moved this adjustment to the analyzing/metadata writing process. * This change has been acknowledged by the author of the ReplayGain * proposal, however at the time of this writing, the webpage is still * not updated. * * Example launch lines * Analyze a simple test waveform: * * gst-launch -t audiotestsrc wave=sine num-buffers=512 ! rganalysis ! fakesink * * Analyze a given file: * * gst-launch -t filesrc location="Some file.ogg" ! decodebin ! audioconvert ! audioresample ! rganalysis ! fakesink * * Analyze the pink noise reference file: * * gst-launch -t gnomevfssrc location=http://replaygain.hydrogenaudio.org/ref_pink.wav ! wavparse ! rganalysis ! fakesink * * Acknowledgements * * This element is based on code used in the vorbisgain program * and many others. The relevant parts are copyrighted by David * Robinson, Glen Sawyer and Frank Klemm. * * */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include "gstrganalysis.h" GST_DEBUG_CATEGORY_STATIC (gst_rg_analysis_debug); #define GST_CAT_DEFAULT gst_rg_analysis_debug static const GstElementDetails rganalysis_details = { "ReplayGain analysis", "Filter/Analyzer/Audio", "Perform the ReplayGain analysis", "Ren\xc3\xa9 Stadler " }; /* Default property value. */ #define FORCED_DEFAULT TRUE enum { PROP_0, PROP_NUM_TRACKS, PROP_FORCED, PROP_REFERENCE_LEVEL }; /* The ReplayGain algorithm is intended for use with mono and stereo * audio. The used implementation has filter coefficients for the * "usual" sample rates in the 8000 to 48000 Hz range. */ #define REPLAY_GAIN_CAPS \ "channels = (int) { 1, 2 }, " \ "rate = (int) { 8000, 11025, 12000, 16000, 22050, 24000, 32000, " \ "44100, 48000 }" static GstStaticPadTemplate sink_factory = GST_STATIC_PAD_TEMPLATE ("sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS ("audio/x-raw-float, " "width = (int) 32, " "endianness = (int) BYTE_ORDER, " REPLAY_GAIN_CAPS "; " "audio/x-raw-int, " "width = (int) 16, " "depth = (int) [ 1, 16 ], " "signed = (boolean) true, " "endianness = (int) BYTE_ORDER, " REPLAY_GAIN_CAPS)); static GstStaticPadTemplate src_factory = GST_STATIC_PAD_TEMPLATE ("src", GST_PAD_SRC, GST_PAD_ALWAYS, GST_STATIC_CAPS ("audio/x-raw-float, " "width = (int) 32, " "endianness = (int) BYTE_ORDER, " REPLAY_GAIN_CAPS "; " "audio/x-raw-int, " "width = (int) 16, " "depth = (int) [ 1, 16 ], " "signed = (boolean) true, " "endianness = (int) BYTE_ORDER, " REPLAY_GAIN_CAPS)); GST_BOILERPLATE (GstRgAnalysis, gst_rg_analysis, GstBaseTransform, GST_TYPE_BASE_TRANSFORM); static void gst_rg_analysis_class_init (GstRgAnalysisClass * klass); static void gst_rg_analysis_init (GstRgAnalysis * filter, GstRgAnalysisClass * gclass); static void gst_rg_analysis_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec); static void gst_rg_analysis_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec); static gboolean gst_rg_analysis_start (GstBaseTransform * base); static gboolean gst_rg_analysis_set_caps (GstBaseTransform * base, GstCaps * incaps, GstCaps * outcaps); static GstFlowReturn gst_rg_analysis_transform_ip (GstBaseTransform * base, GstBuffer * buf); static gboolean gst_rg_analysis_event (GstBaseTransform * base, GstEvent * event); static gboolean gst_rg_analysis_stop (GstBaseTransform * base); static void gst_rg_analysis_handle_tags (GstRgAnalysis * filter, const GstTagList * tag_list); static void gst_rg_analysis_handle_eos (GstRgAnalysis * filter); static gboolean gst_rg_analysis_track_result (GstRgAnalysis * filter, GstTagList ** tag_list); static gboolean gst_rg_analysis_album_result (GstRgAnalysis * filter, GstTagList ** tag_list); static void gst_rg_analysis_base_init (gpointer g_class) { GstElementClass *element_class = GST_ELEMENT_CLASS (g_class); gst_element_class_add_pad_template (element_class, gst_static_pad_template_get (&src_factory)); gst_element_class_add_pad_template (element_class, gst_static_pad_template_get (&sink_factory)); gst_element_class_set_details (element_class, &rganalysis_details); GST_DEBUG_CATEGORY_INIT (gst_rg_analysis_debug, "rganalysis", 0, "ReplayGain analysis element"); } static void gst_rg_analysis_class_init (GstRgAnalysisClass * klass) { GObjectClass *gobject_class; GstBaseTransformClass *trans_class; gobject_class = (GObjectClass *) klass; gobject_class->set_property = gst_rg_analysis_set_property; gobject_class->get_property = gst_rg_analysis_get_property; g_object_class_install_property (gobject_class, PROP_NUM_TRACKS, g_param_spec_int ("num-tracks", "Number of album tracks", "Number of remaining tracks in the album", 0, G_MAXINT, 0, G_PARAM_READWRITE)); g_object_class_install_property (gobject_class, PROP_FORCED, g_param_spec_boolean ("forced", "Force processing", "Analyze streams even when ReplayGain tags exist", FORCED_DEFAULT, G_PARAM_READWRITE)); g_object_class_install_property (gobject_class, PROP_REFERENCE_LEVEL, g_param_spec_double ("reference-level", "Reference level", "Reference level in dB (83.0 for original proposal)", 0.0, G_MAXDOUBLE, RG_REFERENCE_LEVEL, G_PARAM_READWRITE)); trans_class = (GstBaseTransformClass *) klass; trans_class->start = GST_DEBUG_FUNCPTR (gst_rg_analysis_start); trans_class->set_caps = GST_DEBUG_FUNCPTR (gst_rg_analysis_set_caps); trans_class->transform_ip = GST_DEBUG_FUNCPTR (gst_rg_analysis_transform_ip); trans_class->event = GST_DEBUG_FUNCPTR (gst_rg_analysis_event); trans_class->stop = GST_DEBUG_FUNCPTR (gst_rg_analysis_stop); trans_class->passthrough_on_same_caps = TRUE; } static void gst_rg_analysis_init (GstRgAnalysis * filter, GstRgAnalysisClass * gclass) { filter->num_tracks = 0; filter->forced = FORCED_DEFAULT; filter->reference_level = RG_REFERENCE_LEVEL; filter->ctx = NULL; filter->analyze = NULL; } static void gst_rg_analysis_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec) { GstRgAnalysis *filter = GST_RG_ANALYSIS (object); switch (prop_id) { case PROP_NUM_TRACKS: filter->num_tracks = g_value_get_int (value); break; case PROP_FORCED: filter->forced = g_value_get_boolean (value); break; case PROP_REFERENCE_LEVEL: filter->reference_level = g_value_get_double (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gst_rg_analysis_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec) { GstRgAnalysis *filter = GST_RG_ANALYSIS (object); switch (prop_id) { case PROP_NUM_TRACKS: g_value_set_int (value, filter->num_tracks); break; case PROP_FORCED: g_value_set_boolean (value, filter->forced); break; case PROP_REFERENCE_LEVEL: g_value_set_double (value, filter->reference_level); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static gboolean gst_rg_analysis_start (GstBaseTransform * base) { GstRgAnalysis *filter = GST_RG_ANALYSIS (base); filter->ignore_tags = FALSE; filter->skip = FALSE; filter->has_track_gain = FALSE; filter->has_track_peak = FALSE; filter->has_album_gain = FALSE; filter->has_album_peak = FALSE; filter->ctx = rg_analysis_new (); filter->analyze = NULL; GST_DEBUG_OBJECT (filter, "Started"); return TRUE; } static gboolean gst_rg_analysis_set_caps (GstBaseTransform * base, GstCaps * in_caps, GstCaps * out_caps) { GstRgAnalysis *filter = GST_RG_ANALYSIS (base); GstStructure *structure; const gchar *mime_type; gint n_channels, sample_rate, sample_bit_size, sample_size; g_return_val_if_fail (filter->ctx != NULL, FALSE); GST_DEBUG_OBJECT (filter, "set_caps in %" GST_PTR_FORMAT " out %" GST_PTR_FORMAT, in_caps, out_caps); structure = gst_caps_get_structure (in_caps, 0); mime_type = gst_structure_get_name (structure); if (!gst_structure_get_int (structure, "width", &sample_bit_size) || !gst_structure_get_int (structure, "channels", &n_channels) || !gst_structure_get_int (structure, "rate", &sample_rate)) goto invalid_format; if (!rg_analysis_set_sample_rate (filter->ctx, sample_rate)) goto invalid_format; if (sample_bit_size % 8 != 0) goto invalid_format; sample_size = sample_bit_size / 8; if (strcmp (mime_type, "audio/x-raw-float") == 0) { if (sample_size != sizeof (gfloat)) goto invalid_format; /* The depth is not variable for float formats of course. It just * makes the transform function nice and simple if the * rg_analysis_analyze_* functions have a common signature. */ filter->depth = sizeof (gfloat) * 8; if (n_channels == 1) filter->analyze = rg_analysis_analyze_mono_float; else if (n_channels == 2) filter->analyze = rg_analysis_analyze_stereo_float; else goto invalid_format; } else if (strcmp (mime_type, "audio/x-raw-int") == 0) { if (sample_size != sizeof (gint16)) goto invalid_format; if (!gst_structure_get_int (structure, "depth", &filter->depth)) goto invalid_format; if (filter->depth < 1 || filter->depth > 16) goto invalid_format; if (n_channels == 1) filter->analyze = rg_analysis_analyze_mono_int16; else if (n_channels == 2) filter->analyze = rg_analysis_analyze_stereo_int16; else goto invalid_format; } else { goto invalid_format; } return TRUE; /* Errors. */ invalid_format: { filter->analyze = NULL; GST_ELEMENT_ERROR (filter, CORE, NEGOTIATION, ("Invalid incoming caps: %" GST_PTR_FORMAT, in_caps), (NULL)); return FALSE; } } static GstFlowReturn gst_rg_analysis_transform_ip (GstBaseTransform * base, GstBuffer * buf) { GstRgAnalysis *filter = GST_RG_ANALYSIS (base); g_return_val_if_fail (filter->ctx != NULL, GST_FLOW_ERROR); g_return_val_if_fail (filter->analyze != NULL, GST_FLOW_ERROR); if (filter->skip) return GST_FLOW_OK; GST_DEBUG_OBJECT (filter, "Processing buffer of size %u", GST_BUFFER_SIZE (buf)); filter->analyze (filter->ctx, GST_BUFFER_DATA (buf), GST_BUFFER_SIZE (buf), filter->depth); return GST_FLOW_OK; } static gboolean gst_rg_analysis_event (GstBaseTransform * base, GstEvent * event) { GstRgAnalysis *filter = GST_RG_ANALYSIS (base); g_return_val_if_fail (filter->ctx != NULL, TRUE); switch (GST_EVENT_TYPE (event)) { case GST_EVENT_EOS: { GST_DEBUG_OBJECT (filter, "Received EOS event"); gst_rg_analysis_handle_eos (filter); GST_DEBUG_OBJECT (filter, "Passing on EOS event"); break; } case GST_EVENT_TAG: { GstTagList *tag_list; /* The reference to the tag list is borrowed. */ gst_event_parse_tag (event, &tag_list); gst_rg_analysis_handle_tags (filter, tag_list); break; } default: break; } return GST_BASE_TRANSFORM_CLASS (parent_class)->event (base, event); } static gboolean gst_rg_analysis_stop (GstBaseTransform * base) { GstRgAnalysis *filter = GST_RG_ANALYSIS (base); g_return_val_if_fail (filter->ctx != NULL, FALSE); rg_analysis_destroy (filter->ctx); filter->ctx = NULL; GST_DEBUG_OBJECT (filter, "Stopped"); return TRUE; } static void gst_rg_analysis_handle_tags (GstRgAnalysis * filter, const GstTagList * tag_list) { gboolean album_processing = (filter->num_tracks > 0); gdouble dummy; if (!album_processing) filter->ignore_tags = FALSE; if (filter->skip && album_processing) { GST_INFO_OBJECT (filter, "Ignoring TAG event: Skipping album"); return; } else if (filter->skip) { GST_INFO_OBJECT (filter, "Ignoring TAG event: Skipping track"); return; } else if (filter->ignore_tags) { GST_INFO_OBJECT (filter, "Ignoring TAG event: Cannot skip anyways"); return; } filter->has_track_gain |= gst_tag_list_get_double (tag_list, GST_TAG_TRACK_GAIN, &dummy); filter->has_track_peak |= gst_tag_list_get_double (tag_list, GST_TAG_TRACK_PEAK, &dummy); filter->has_album_gain |= gst_tag_list_get_double (tag_list, GST_TAG_ALBUM_GAIN, &dummy); filter->has_album_peak |= gst_tag_list_get_double (tag_list, GST_TAG_ALBUM_PEAK, &dummy); if (!(filter->has_track_gain && filter->has_track_peak)) { GST_INFO_OBJECT (filter, "Track tags not complete yet"); return; } if (album_processing && !(filter->has_album_gain && filter->has_album_peak)) { GST_INFO_OBJECT (filter, "Album tags not complete yet"); return; } if (filter->forced) { GST_INFO_OBJECT (filter, "Existing tags are sufficient, but processing anyway (forced)"); return; } filter->skip = TRUE; rg_analysis_reset (filter->ctx); if (!album_processing) GST_INFO_OBJECT (filter, "Existing tags are sufficient, will not process this track"); else GST_INFO_OBJECT (filter, "Existing tags are sufficient, will not process this album"); } static void gst_rg_analysis_handle_eos (GstRgAnalysis * filter) { gboolean album_processing = (filter->num_tracks > 0); gboolean album_finished = (filter->num_tracks == 1); gboolean album_skipping = album_processing && filter->skip; filter->has_track_gain = FALSE; filter->has_track_peak = FALSE; if (album_finished) { filter->ignore_tags = FALSE; filter->skip = FALSE; filter->has_album_gain = FALSE; filter->has_album_peak = FALSE; } else if (!album_skipping) { filter->skip = FALSE; } /* We might have just fully processed a track because it has * incomplete tags. If we do album processing and allow skipping * (not forced), prevent switching to skipping if a later track with * full tags comes along: */ if (!filter->forced && album_processing && !album_finished) filter->ignore_tags = TRUE; if (!filter->skip) { GstTagList *tag_list = NULL; gboolean track_success; gboolean album_success = FALSE; track_success = gst_rg_analysis_track_result (filter, &tag_list); if (album_finished) album_success = gst_rg_analysis_album_result (filter, &tag_list); else if (!album_processing) rg_analysis_reset_album (filter->ctx); if (track_success || album_success) { GST_DEBUG_OBJECT (filter, "Posting tag list with results"); /* This steals our reference to the list: */ gst_element_found_tags_for_pad (GST_ELEMENT (filter), GST_BASE_TRANSFORM_SRC_PAD (GST_BASE_TRANSFORM (filter)), tag_list); } } if (album_processing) { filter->num_tracks--; if (!album_finished) GST_INFO_OBJECT (filter, "Album not finished yet (num-tracks is now %u)", filter->num_tracks); else GST_INFO_OBJECT (filter, "Album finished (num-tracks is now 0)"); } if (album_processing) g_object_notify (G_OBJECT (filter), "num-tracks"); } static gboolean gst_rg_analysis_track_result (GstRgAnalysis * filter, GstTagList ** tag_list) { gboolean track_success; gdouble track_gain, track_peak; track_success = rg_analysis_track_result (filter->ctx, &track_gain, &track_peak); if (track_success) { track_gain += filter->reference_level - RG_REFERENCE_LEVEL; GST_INFO_OBJECT (filter, "Track gain is %+.2f dB, peak %.6f", track_gain, track_peak); } else { GST_INFO_OBJECT (filter, "Track was too short to analyze"); } if (track_success) { if (*tag_list == NULL) *tag_list = gst_tag_list_new (); gst_tag_list_add (*tag_list, GST_TAG_MERGE_APPEND, GST_TAG_TRACK_PEAK, track_peak, GST_TAG_TRACK_GAIN, track_gain, NULL); } return track_success; } static gboolean gst_rg_analysis_album_result (GstRgAnalysis * filter, GstTagList ** tag_list) { gboolean album_success; gdouble album_gain, album_peak; album_success = rg_analysis_album_result (filter->ctx, &album_gain, &album_peak); if (album_success) { album_gain += filter->reference_level - RG_REFERENCE_LEVEL; GST_INFO_OBJECT (filter, "Album gain is %+.2f dB, peak %.6f", album_gain, album_peak); } else { GST_INFO_OBJECT (filter, "Album was too short to analyze"); } if (album_success) { if (*tag_list == NULL) *tag_list = gst_tag_list_new (); gst_tag_list_add (*tag_list, GST_TAG_MERGE_APPEND, GST_TAG_ALBUM_PEAK, album_peak, GST_TAG_ALBUM_GAIN, album_gain, NULL); } return album_success; } static gboolean plugin_init (GstPlugin * plugin) { return gst_element_register (plugin, "rganalysis", GST_RANK_NONE, GST_TYPE_RG_ANALYSIS); } GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, GST_VERSION_MINOR, "replaygain", "ReplayGain analysis", plugin_init, VERSION, GST_LICENSE, GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN);