2010-04-11 21:47:21 +02:00
|
|
|
/* This file is part of Clementine.
|
2010-11-20 14:27:10 +01:00
|
|
|
Copyright 2010, David Sansome <me@davidsansome.com>
|
2010-04-11 21:47:21 +02:00
|
|
|
|
|
|
|
Clementine is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
Clementine 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 General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2010-08-20 14:30:42 +02:00
|
|
|
#include <limits>
|
|
|
|
|
2012-10-23 17:34:25 +02:00
|
|
|
#include <QCoreApplication>
|
2014-10-01 15:06:22 +02:00
|
|
|
#include <QDir>
|
2014-03-29 13:47:48 +01:00
|
|
|
#include <QUuid>
|
2012-10-23 17:34:25 +02:00
|
|
|
|
2011-04-22 18:50:29 +02:00
|
|
|
#include "bufferconsumer.h"
|
2011-11-28 20:09:30 +01:00
|
|
|
#include "config.h"
|
2010-09-23 00:22:02 +02:00
|
|
|
#include "gstelementdeleter.h"
|
2010-04-11 21:47:21 +02:00
|
|
|
#include "gstengine.h"
|
2011-04-22 18:50:29 +02:00
|
|
|
#include "gstenginepipeline.h"
|
2012-04-25 00:29:19 +02:00
|
|
|
#include "core/concurrentrun.h"
|
2011-04-22 18:50:29 +02:00
|
|
|
#include "core/logging.h"
|
2014-10-01 15:06:22 +02:00
|
|
|
#include "core/mac_startup.h"
|
2012-06-08 15:34:00 +02:00
|
|
|
#include "core/signalchecker.h"
|
2011-11-28 19:11:09 +01:00
|
|
|
#include "core/utilities.h"
|
2014-12-18 23:35:21 +01:00
|
|
|
#include "internet/core/internetmodel.h"
|
|
|
|
#include "internet/spotify/spotifyserver.h"
|
|
|
|
#include "internet/spotify/spotifyservice.h"
|
2011-11-28 20:09:30 +01:00
|
|
|
|
2010-05-15 19:55:36 +02:00
|
|
|
const int GstEnginePipeline::kGstStateTimeoutNanosecs = 10000000;
|
|
|
|
const int GstEnginePipeline::kFaderFudgeMsec = 2000;
|
|
|
|
|
2010-07-12 22:55:09 +02:00
|
|
|
const int GstEnginePipeline::kEqBandCount = 10;
|
|
|
|
const int GstEnginePipeline::kEqBandFrequencies[] = {
|
2014-02-07 16:34:20 +01:00
|
|
|
60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000};
|
2010-07-12 22:55:09 +02:00
|
|
|
|
2011-03-20 22:40:53 +01:00
|
|
|
int GstEnginePipeline::sId = 1;
|
2014-02-06 14:48:00 +01:00
|
|
|
GstElementDeleter* GstEnginePipeline::sElementDeleter = nullptr;
|
2010-09-23 00:22:02 +02:00
|
|
|
|
2010-04-21 13:14:12 +02:00
|
|
|
GstEnginePipeline::GstEnginePipeline(GstEngine* engine)
|
2014-02-07 16:34:20 +01:00
|
|
|
: QObject(nullptr),
|
|
|
|
engine_(engine),
|
|
|
|
id_(sId++),
|
|
|
|
valid_(false),
|
|
|
|
sink_(GstEngine::kAutoSink),
|
|
|
|
segment_start_(0),
|
|
|
|
segment_start_received_(false),
|
2014-10-04 13:21:21 +02:00
|
|
|
emit_track_ended_on_stream_start_(false),
|
2014-02-07 16:34:20 +01:00
|
|
|
emit_track_ended_on_time_discontinuity_(false),
|
|
|
|
last_buffer_offset_(0),
|
|
|
|
eq_enabled_(false),
|
|
|
|
eq_preamp_(0),
|
|
|
|
stereo_balance_(0.0f),
|
|
|
|
rg_enabled_(false),
|
|
|
|
rg_mode_(0),
|
|
|
|
rg_preamp_(0.0),
|
|
|
|
rg_compression_(true),
|
|
|
|
buffer_duration_nanosec_(1 * kNsecPerSec),
|
2014-04-01 15:33:11 +02:00
|
|
|
buffer_min_fill_(33),
|
2014-02-07 16:34:20 +01:00
|
|
|
buffering_(false),
|
|
|
|
mono_playback_(false),
|
|
|
|
end_offset_nanosec_(-1),
|
|
|
|
next_beginning_offset_nanosec_(-1),
|
|
|
|
next_end_offset_nanosec_(-1),
|
|
|
|
ignore_next_seek_(false),
|
|
|
|
ignore_tags_(false),
|
|
|
|
pipeline_is_initialised_(false),
|
|
|
|
pipeline_is_connected_(false),
|
|
|
|
pending_seek_nanosec_(-1),
|
|
|
|
volume_percent_(100),
|
|
|
|
volume_modifier_(1.0),
|
|
|
|
pipeline_(nullptr),
|
|
|
|
uridecodebin_(nullptr),
|
|
|
|
audiobin_(nullptr),
|
|
|
|
queue_(nullptr),
|
|
|
|
audioconvert_(nullptr),
|
|
|
|
rgvolume_(nullptr),
|
|
|
|
rglimiter_(nullptr),
|
|
|
|
audioconvert2_(nullptr),
|
|
|
|
equalizer_(nullptr),
|
|
|
|
stereo_panorama_(nullptr),
|
|
|
|
volume_(nullptr),
|
|
|
|
audioscale_(nullptr),
|
|
|
|
audiosink_(nullptr) {
|
2010-09-23 00:22:02 +02:00
|
|
|
if (!sElementDeleter) {
|
|
|
|
sElementDeleter = new GstElementDeleter;
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
for (int i = 0; i < kEqBandCount; ++i) eq_band_gains_ << 0;
|
2010-04-11 21:47:21 +02:00
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void GstEnginePipeline::set_output_device(const QString& sink,
|
2014-03-29 10:06:33 +01:00
|
|
|
const QVariant& device) {
|
2010-04-11 21:47:21 +02:00
|
|
|
sink_ = sink;
|
|
|
|
device_ = device;
|
|
|
|
}
|
|
|
|
|
2010-05-23 15:07:15 +02:00
|
|
|
void GstEnginePipeline::set_replaygain(bool enabled, int mode, float preamp,
|
|
|
|
bool compression) {
|
|
|
|
rg_enabled_ = enabled;
|
|
|
|
rg_mode_ = mode;
|
|
|
|
rg_preamp_ = preamp;
|
|
|
|
rg_compression_ = compression;
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void GstEnginePipeline::set_buffer_duration_nanosec(
|
|
|
|
qint64 buffer_duration_nanosec) {
|
2011-02-13 19:29:27 +01:00
|
|
|
buffer_duration_nanosec_ = buffer_duration_nanosec;
|
2010-10-11 17:58:05 +02:00
|
|
|
}
|
|
|
|
|
2014-04-01 15:33:11 +02:00
|
|
|
void GstEnginePipeline::set_buffer_min_fill(int percent) {
|
|
|
|
buffer_min_fill_ = percent;
|
|
|
|
}
|
|
|
|
|
2012-05-20 20:50:25 +02:00
|
|
|
void GstEnginePipeline::set_mono_playback(bool enabled) {
|
|
|
|
mono_playback_ = enabled;
|
|
|
|
}
|
|
|
|
|
2011-09-25 20:24:44 +02:00
|
|
|
bool GstEnginePipeline::ReplaceDecodeBin(GstElement* new_bin) {
|
2010-05-08 19:39:12 +02:00
|
|
|
if (!new_bin) return false;
|
|
|
|
|
2011-04-26 20:39:38 +02:00
|
|
|
// Destroy the old elements if they are set
|
|
|
|
// Note that the caller to this function MUST schedule the old uridecodebin_
|
2011-09-25 20:24:44 +02:00
|
|
|
// for deletion in the main thread.
|
2010-05-08 19:39:12 +02:00
|
|
|
if (uridecodebin_) {
|
|
|
|
gst_bin_remove(GST_BIN(pipeline_), uridecodebin_);
|
2011-04-26 20:39:38 +02:00
|
|
|
}
|
2010-05-08 19:39:12 +02:00
|
|
|
|
|
|
|
uridecodebin_ = new_bin;
|
2010-07-12 21:09:59 +02:00
|
|
|
segment_start_ = 0;
|
|
|
|
segment_start_received_ = false;
|
2011-03-06 19:11:53 +01:00
|
|
|
pipeline_is_connected_ = false;
|
2010-05-08 19:39:12 +02:00
|
|
|
gst_bin_add(GST_BIN(pipeline_), uridecodebin_);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2010-07-14 13:16:56 +02:00
|
|
|
bool GstEnginePipeline::ReplaceDecodeBin(const QUrl& url) {
|
2014-02-06 16:49:49 +01:00
|
|
|
GstElement* new_bin = nullptr;
|
2011-11-28 19:11:09 +01:00
|
|
|
|
|
|
|
if (url.scheme() == "spotify") {
|
2013-09-17 16:49:29 +02:00
|
|
|
new_bin = gst_bin_new("spotify_bin");
|
|
|
|
|
|
|
|
// Create elements
|
|
|
|
GstElement* src = engine_->CreateElement("tcpserversrc", new_bin);
|
|
|
|
GstElement* gdp = engine_->CreateElement("gdpdepay", new_bin);
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!src || !gdp) return false;
|
2013-09-17 16:49:29 +02:00
|
|
|
|
|
|
|
// Pick a port number
|
|
|
|
const int port = Utilities::PickUnusedPort();
|
2014-02-06 16:49:49 +01:00
|
|
|
g_object_set(G_OBJECT(src), "host", "127.0.0.1", nullptr);
|
|
|
|
g_object_set(G_OBJECT(src), "port", port, nullptr);
|
2013-09-17 16:49:29 +02:00
|
|
|
|
|
|
|
// Link the elements
|
|
|
|
gst_element_link(src, gdp);
|
|
|
|
|
|
|
|
// Add a ghost pad
|
|
|
|
GstPad* pad = gst_element_get_static_pad(gdp, "src");
|
|
|
|
gst_element_add_pad(GST_ELEMENT(new_bin), gst_ghost_pad_new("src", pad));
|
|
|
|
gst_object_unref(GST_OBJECT(pad));
|
|
|
|
|
|
|
|
// Tell spotify to start sending data to us.
|
2014-02-07 16:34:20 +01:00
|
|
|
InternetModel::Service<SpotifyService>()->server()->StartPlaybackLater(
|
|
|
|
url.toString(), port);
|
2011-11-28 19:11:09 +01:00
|
|
|
} else {
|
|
|
|
new_bin = engine_->CreateElement("uridecodebin");
|
2014-02-07 16:34:20 +01:00
|
|
|
g_object_set(G_OBJECT(new_bin), "uri", url.toEncoded().constData(),
|
|
|
|
nullptr);
|
|
|
|
CHECKED_GCONNECT(G_OBJECT(new_bin), "drained", &SourceDrainedCallback,
|
|
|
|
this);
|
2012-06-08 15:34:00 +02:00
|
|
|
CHECKED_GCONNECT(G_OBJECT(new_bin), "pad-added", &NewPadCallback, this);
|
2014-02-07 16:34:20 +01:00
|
|
|
CHECKED_GCONNECT(G_OBJECT(new_bin), "notify::source", &SourceSetupCallback,
|
|
|
|
this);
|
2011-11-28 19:11:09 +01:00
|
|
|
}
|
|
|
|
|
2011-09-25 20:24:44 +02:00
|
|
|
return ReplaceDecodeBin(new_bin);
|
2010-07-14 13:16:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
GstElement* GstEnginePipeline::CreateDecodeBinFromString(const char* pipeline) {
|
2014-02-06 16:49:49 +01:00
|
|
|
GError* error = nullptr;
|
2010-07-14 13:16:56 +02:00
|
|
|
GstElement* bin = gst_parse_bin_from_description(pipeline, TRUE, &error);
|
2011-03-10 19:01:35 +01:00
|
|
|
|
2010-07-14 13:16:56 +02:00
|
|
|
if (error) {
|
|
|
|
QString message = QString::fromLocal8Bit(error->message);
|
2011-03-10 19:01:35 +01:00
|
|
|
int domain = error->domain;
|
|
|
|
int code = error->code;
|
2010-07-14 13:16:56 +02:00
|
|
|
g_error_free(error);
|
2010-04-11 21:47:21 +02:00
|
|
|
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Warning) << message;
|
2011-03-20 22:40:53 +01:00
|
|
|
emit Error(id(), message, domain, code);
|
2011-03-10 19:01:35 +01:00
|
|
|
|
2014-02-06 16:49:49 +01:00
|
|
|
return nullptr;
|
2011-03-10 19:01:35 +01:00
|
|
|
} else {
|
|
|
|
return bin;
|
2010-07-14 13:16:56 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool GstEnginePipeline::Init() {
|
2010-04-12 02:20:52 +02:00
|
|
|
// Here we create all the parts of the gstreamer pipeline - from the source
|
|
|
|
// to the sink. The parts of the pipeline are split up into bins:
|
2010-05-08 15:54:12 +02:00
|
|
|
// uri decode bin -> audio bin
|
|
|
|
// The uri decode bin is a gstreamer builtin that automatically picks the
|
|
|
|
// right type of source and decoder for the URI.
|
2011-08-19 22:56:53 +02:00
|
|
|
|
2010-04-12 02:20:52 +02:00
|
|
|
// The audio bin gets created here and contains:
|
2011-08-19 22:56:53 +02:00
|
|
|
// queue ! audioconvert ! <caps32>
|
|
|
|
// ! ( rgvolume ! rglimiter ! audioconvert2 ) ! tee
|
|
|
|
// rgvolume and rglimiter are only created when replaygain is enabled.
|
|
|
|
|
|
|
|
// After the tee the pipeline splits. One split is converted to 16-bit int
|
|
|
|
// samples for the scope, the other is kept as float32 and sent to the
|
|
|
|
// speaker.
|
|
|
|
// tee1 ! probe_queue ! probe_converter ! <caps16> ! probe_sink
|
|
|
|
// tee2 ! audio_queue ! equalizer_preamp ! equalizer ! volume ! audioscale
|
|
|
|
// ! convert ! audiosink
|
2010-04-12 02:20:52 +02:00
|
|
|
|
2014-10-14 10:18:48 +02:00
|
|
|
gst_segment_init(&last_decodebin_segment_, GST_FORMAT_TIME);
|
|
|
|
|
2010-04-11 21:47:21 +02:00
|
|
|
// Audio bin
|
|
|
|
audiobin_ = gst_bin_new("audiobin");
|
|
|
|
gst_bin_add(GST_BIN(pipeline_), audiobin_);
|
|
|
|
|
2011-08-19 22:56:53 +02:00
|
|
|
// Create the sink
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!(audiosink_ = engine_->CreateElement(sink_, audiobin_))) return false;
|
2010-04-11 21:47:21 +02:00
|
|
|
|
2014-03-29 10:06:33 +01:00
|
|
|
if (g_object_class_find_property(G_OBJECT_GET_CLASS(audiosink_), "device") &&
|
|
|
|
!device_.toString().isEmpty()) {
|
|
|
|
switch (device_.type()) {
|
|
|
|
case QVariant::Int:
|
2014-10-15 21:57:57 +02:00
|
|
|
g_object_set(G_OBJECT(audiosink_), "device", device_.toInt(), nullptr);
|
2014-03-29 10:06:33 +01:00
|
|
|
break;
|
|
|
|
case QVariant::String:
|
2014-10-15 21:57:57 +02:00
|
|
|
g_object_set(G_OBJECT(audiosink_), "device",
|
|
|
|
device_.toString().toUtf8().constData(), nullptr);
|
2014-03-29 10:06:33 +01:00
|
|
|
break;
|
2014-03-29 13:47:48 +01:00
|
|
|
|
2014-10-15 21:57:57 +02:00
|
|
|
#ifdef Q_OS_WIN32
|
2014-03-29 13:47:48 +01:00
|
|
|
case QVariant::ByteArray: {
|
|
|
|
GUID guid = QUuid(device_.toByteArray());
|
2014-10-15 21:57:57 +02:00
|
|
|
g_object_set(G_OBJECT(audiosink_), "device", &guid, nullptr);
|
2014-03-29 13:47:48 +01:00
|
|
|
break;
|
|
|
|
}
|
2014-10-15 21:57:57 +02:00
|
|
|
#endif // Q_OS_WIN32
|
2014-03-29 13:47:48 +01:00
|
|
|
|
2014-03-29 10:06:33 +01:00
|
|
|
default:
|
|
|
|
qLog(Warning) << "Unknown device type" << device_;
|
|
|
|
break;
|
|
|
|
}
|
2014-03-29 09:17:34 +01:00
|
|
|
}
|
2010-04-11 21:47:21 +02:00
|
|
|
|
2011-08-19 22:56:53 +02:00
|
|
|
// Create all the other elements
|
2014-02-07 16:34:20 +01:00
|
|
|
GstElement* tee, *probe_queue, *probe_converter, *probe_sink, *audio_queue,
|
|
|
|
*convert;
|
2011-08-19 22:56:53 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
queue_ = engine_->CreateElement("queue2", audiobin_);
|
|
|
|
audioconvert_ = engine_->CreateElement("audioconvert", audiobin_);
|
|
|
|
tee = engine_->CreateElement("tee", audiobin_);
|
2011-08-19 22:56:53 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
probe_queue = engine_->CreateElement("queue", audiobin_);
|
|
|
|
probe_converter = engine_->CreateElement("audioconvert", audiobin_);
|
|
|
|
probe_sink = engine_->CreateElement("fakesink", audiobin_);
|
2011-08-19 22:56:53 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
audio_queue = engine_->CreateElement("queue", audiobin_);
|
|
|
|
equalizer_preamp_ = engine_->CreateElement("volume", audiobin_);
|
|
|
|
equalizer_ = engine_->CreateElement("equalizer-nbands", audiobin_);
|
|
|
|
stereo_panorama_ = engine_->CreateElement("audiopanorama", audiobin_);
|
|
|
|
volume_ = engine_->CreateElement("volume", audiobin_);
|
|
|
|
audioscale_ = engine_->CreateElement("audioresample", audiobin_);
|
|
|
|
convert = engine_->CreateElement("audioconvert", audiobin_);
|
2011-08-19 22:56:53 +02:00
|
|
|
|
|
|
|
if (!queue_ || !audioconvert_ || !tee || !probe_queue || !probe_converter ||
|
|
|
|
!probe_sink || !audio_queue || !equalizer_preamp_ || !equalizer_ ||
|
2013-04-27 05:05:42 +02:00
|
|
|
!stereo_panorama_ || !volume_ || !audioscale_ || !convert) {
|
2011-08-19 22:56:53 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create the replaygain elements if it's enabled. event_probe is the
|
|
|
|
// audioconvert element we attach the probe to, which will change depending
|
|
|
|
// on whether replaygain is enabled. convert_sink is the element after the
|
|
|
|
// first audioconvert, which again will change.
|
|
|
|
GstElement* event_probe = audioconvert_;
|
|
|
|
GstElement* convert_sink = tee;
|
2010-05-23 15:07:15 +02:00
|
|
|
|
|
|
|
if (rg_enabled_) {
|
2014-02-07 16:34:20 +01:00
|
|
|
rgvolume_ = engine_->CreateElement("rgvolume", audiobin_);
|
|
|
|
rglimiter_ = engine_->CreateElement("rglimiter", audiobin_);
|
2011-08-19 22:56:53 +02:00
|
|
|
audioconvert2_ = engine_->CreateElement("audioconvert", audiobin_);
|
|
|
|
event_probe = audioconvert2_;
|
|
|
|
convert_sink = rgvolume_;
|
|
|
|
|
|
|
|
if (!rgvolume_ || !rglimiter_ || !audioconvert2_) {
|
|
|
|
return false;
|
|
|
|
}
|
2010-05-23 15:07:15 +02:00
|
|
|
|
|
|
|
// Set replaygain settings
|
2014-02-06 16:49:49 +01:00
|
|
|
g_object_set(G_OBJECT(rgvolume_), "album-mode", rg_mode_, nullptr);
|
|
|
|
g_object_set(G_OBJECT(rgvolume_), "pre-amp", double(rg_preamp_), nullptr);
|
2014-02-07 16:34:20 +01:00
|
|
|
g_object_set(G_OBJECT(rglimiter_), "enabled", int(rg_compression_),
|
|
|
|
nullptr);
|
2010-05-23 15:07:15 +02:00
|
|
|
}
|
2010-04-11 21:47:21 +02:00
|
|
|
|
2011-08-19 22:56:53 +02:00
|
|
|
// Create a pad on the outside of the audiobin and connect it to the pad of
|
|
|
|
// the first element.
|
2013-01-29 13:19:26 +01:00
|
|
|
GstPad* pad = gst_element_get_static_pad(queue_, "sink");
|
2010-04-11 21:47:21 +02:00
|
|
|
gst_element_add_pad(audiobin_, gst_ghost_pad_new("sink", pad));
|
|
|
|
gst_object_unref(pad);
|
|
|
|
|
2010-04-12 02:20:52 +02:00
|
|
|
// Add a data probe on the src pad of the audioconvert element for our scope.
|
|
|
|
// We do it here because we want pre-equalized and pre-volume samples
|
2011-08-19 22:56:53 +02:00
|
|
|
// so that our visualization are not be affected by them.
|
2013-01-29 13:19:26 +01:00
|
|
|
pad = gst_element_get_static_pad(event_probe, "src");
|
2014-10-15 21:57:57 +02:00
|
|
|
gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_EVENT_UPSTREAM,
|
|
|
|
&EventHandoffCallback, this, NULL);
|
2011-08-19 22:56:53 +02:00
|
|
|
gst_object_unref(pad);
|
2011-08-22 23:40:33 +02:00
|
|
|
|
|
|
|
// Configure the fakesink properly
|
2014-02-06 16:49:49 +01:00
|
|
|
g_object_set(G_OBJECT(probe_sink), "sync", TRUE, nullptr);
|
2013-01-29 13:19:26 +01:00
|
|
|
|
2010-07-12 22:55:09 +02:00
|
|
|
// Set the equalizer bands
|
2014-02-06 16:49:49 +01:00
|
|
|
g_object_set(G_OBJECT(equalizer_), "num-bands", 10, nullptr);
|
2010-07-12 22:55:09 +02:00
|
|
|
|
|
|
|
int last_band_frequency = 0;
|
2014-02-07 16:34:20 +01:00
|
|
|
for (int i = 0; i < kEqBandCount; ++i) {
|
2014-10-15 21:57:57 +02:00
|
|
|
GstObject* band = GST_OBJECT(
|
|
|
|
gst_child_proxy_get_child_by_index(GST_CHILD_PROXY(equalizer_), i));
|
2010-07-12 22:55:09 +02:00
|
|
|
|
|
|
|
const float frequency = kEqBandFrequencies[i];
|
|
|
|
const float bandwidth = frequency - last_band_frequency;
|
|
|
|
last_band_frequency = frequency;
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
g_object_set(G_OBJECT(band), "freq", frequency, "bandwidth", bandwidth,
|
|
|
|
"gain", 0.0f, nullptr);
|
2010-07-12 22:55:09 +02:00
|
|
|
g_object_unref(G_OBJECT(band));
|
|
|
|
}
|
|
|
|
|
2013-04-27 05:05:42 +02:00
|
|
|
// Set the stereo balance.
|
2014-02-07 16:34:20 +01:00
|
|
|
g_object_set(G_OBJECT(stereo_panorama_), "panorama", stereo_balance_,
|
|
|
|
nullptr);
|
2013-04-27 05:05:42 +02:00
|
|
|
|
2012-01-27 15:30:28 +01:00
|
|
|
// Set the buffer duration. We set this on this queue instead of the
|
2011-04-16 16:04:12 +02:00
|
|
|
// decode bin (in ReplaceDecodeBin()) because setting it on the decode bin
|
|
|
|
// only affects network sources.
|
2012-01-27 15:30:28 +01:00
|
|
|
// Disable the default buffer and byte limits, so we only buffer based on
|
|
|
|
// time.
|
2014-02-06 16:49:49 +01:00
|
|
|
g_object_set(G_OBJECT(queue_), "max-size-buffers", 0, nullptr);
|
|
|
|
g_object_set(G_OBJECT(queue_), "max-size-bytes", 0, nullptr);
|
2014-02-07 16:34:20 +01:00
|
|
|
g_object_set(G_OBJECT(queue_), "max-size-time", buffer_duration_nanosec_,
|
|
|
|
nullptr);
|
2014-04-01 15:33:11 +02:00
|
|
|
g_object_set(G_OBJECT(queue_), "low-percent", buffer_min_fill_, nullptr);
|
2012-01-27 12:07:12 +01:00
|
|
|
|
|
|
|
if (buffer_duration_nanosec_ > 0) {
|
2014-02-06 16:49:49 +01:00
|
|
|
g_object_set(G_OBJECT(queue_), "use-buffering", true, nullptr);
|
2012-01-27 12:07:12 +01:00
|
|
|
}
|
2011-04-16 16:04:12 +02:00
|
|
|
|
2014-09-21 14:39:30 +02:00
|
|
|
gst_element_link_many(queue_, audioconvert_, convert_sink, nullptr);
|
2010-04-11 21:47:21 +02:00
|
|
|
|
2011-08-19 22:56:53 +02:00
|
|
|
// Link the elements with special caps
|
2014-09-21 14:39:30 +02:00
|
|
|
// The scope path through the tee gets 16-bit ints.
|
2014-10-15 21:57:57 +02:00
|
|
|
GstCaps* caps16 = gst_caps_new_simple("audio/x-raw", "format", G_TYPE_STRING,
|
|
|
|
"S16LE", NULL);
|
2011-08-19 22:56:53 +02:00
|
|
|
gst_element_link_filtered(probe_converter, probe_sink, caps16);
|
|
|
|
gst_caps_unref(caps16);
|
2011-04-16 16:04:12 +02:00
|
|
|
|
2011-08-19 22:56:53 +02:00
|
|
|
// Link the outputs of tee to the queues on each path.
|
2014-09-21 11:35:43 +02:00
|
|
|
gst_pad_link(gst_element_get_request_pad(tee, "src_%u"),
|
2014-02-07 16:34:20 +01:00
|
|
|
gst_element_get_static_pad(probe_queue, "sink"));
|
2014-09-21 11:35:43 +02:00
|
|
|
gst_pad_link(gst_element_get_request_pad(tee, "src_%u"),
|
2014-02-07 16:34:20 +01:00
|
|
|
gst_element_get_static_pad(audio_queue, "sink"));
|
2011-08-19 22:56:53 +02:00
|
|
|
|
|
|
|
// Link replaygain elements if enabled.
|
|
|
|
if (rg_enabled_) {
|
2014-02-06 16:49:49 +01:00
|
|
|
gst_element_link_many(rgvolume_, rglimiter_, audioconvert2_, tee, nullptr);
|
2011-08-19 22:56:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Link everything else.
|
|
|
|
gst_element_link(probe_queue, probe_converter);
|
2014-02-07 16:34:20 +01:00
|
|
|
gst_element_link_many(audio_queue, equalizer_preamp_, equalizer_,
|
|
|
|
stereo_panorama_, volume_, audioscale_, convert,
|
|
|
|
audiosink_, nullptr);
|
2010-04-11 21:47:21 +02:00
|
|
|
|
2011-08-19 22:56:53 +02:00
|
|
|
// Add probes and handlers.
|
2014-09-21 11:35:43 +02:00
|
|
|
gst_pad_add_probe(gst_element_get_static_pad(probe_converter, "src"),
|
2014-10-15 21:57:57 +02:00
|
|
|
GST_PAD_PROBE_TYPE_BUFFER, HandoffCallback, this, nullptr);
|
2014-02-07 16:34:20 +01:00
|
|
|
gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)),
|
2014-06-09 08:20:24 +02:00
|
|
|
BusCallbackSync, this, nullptr);
|
2014-02-07 16:34:20 +01:00
|
|
|
bus_cb_id_ = gst_bus_add_watch(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)),
|
|
|
|
BusCallback, this);
|
2011-11-28 19:11:09 +01:00
|
|
|
|
|
|
|
MaybeLinkDecodeToAudio();
|
|
|
|
|
2010-04-11 21:47:21 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2011-11-28 19:11:09 +01:00
|
|
|
void GstEnginePipeline::MaybeLinkDecodeToAudio() {
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!uridecodebin_ || !audiobin_) return;
|
2011-11-28 19:11:09 +01:00
|
|
|
|
|
|
|
GstPad* pad = gst_element_get_static_pad(uridecodebin_, "src");
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!pad) return;
|
2011-11-28 19:11:09 +01:00
|
|
|
|
|
|
|
gst_object_unref(pad);
|
|
|
|
gst_element_link(uridecodebin_, audiobin_);
|
|
|
|
}
|
|
|
|
|
2010-07-14 13:16:56 +02:00
|
|
|
bool GstEnginePipeline::InitFromString(const QString& pipeline) {
|
|
|
|
pipeline_ = gst_pipeline_new("pipeline");
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
GstElement* new_bin =
|
|
|
|
CreateDecodeBinFromString(pipeline.toAscii().constData());
|
2010-07-14 13:16:56 +02:00
|
|
|
if (!new_bin) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!ReplaceDecodeBin(new_bin)) return false;
|
|
|
|
|
|
|
|
if (!Init()) return false;
|
|
|
|
return gst_element_link(new_bin, audiobin_);
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
bool GstEnginePipeline::InitFromUrl(const QUrl& url, qint64 end_nanosec) {
|
2010-07-14 13:16:56 +02:00
|
|
|
pipeline_ = gst_pipeline_new("pipeline");
|
2014-02-07 16:34:20 +01:00
|
|
|
|
2011-08-26 00:11:18 +02:00
|
|
|
if (url.scheme() == "cdda" && !url.path().isEmpty()) {
|
2011-08-10 00:49:36 +02:00
|
|
|
// Currently, Gstreamer can't handle input CD devices inside cdda URL. So
|
|
|
|
// we handle them ourselve: we extract the track number and re-create an
|
|
|
|
// URL with only cdda:// + the track number (which can be handled by
|
|
|
|
// Gstreamer). We keep the device in mind, and we will set it later using
|
|
|
|
// SourceSetupCallback
|
|
|
|
QStringList path = url.path().split('/');
|
|
|
|
url_ = QUrl(QString("cdda://%1").arg(path.takeLast()));
|
|
|
|
source_device_ = path.join("/");
|
|
|
|
} else {
|
|
|
|
url_ = url;
|
|
|
|
}
|
2011-03-06 17:35:47 +01:00
|
|
|
end_offset_nanosec_ = end_nanosec;
|
2010-07-14 13:16:56 +02:00
|
|
|
|
|
|
|
// Decode bin
|
2011-08-10 00:49:36 +02:00
|
|
|
if (!ReplaceDecodeBin(url_)) return false;
|
2010-07-14 13:16:56 +02:00
|
|
|
|
|
|
|
return Init();
|
|
|
|
}
|
|
|
|
|
2010-04-11 21:47:21 +02:00
|
|
|
GstEnginePipeline::~GstEnginePipeline() {
|
|
|
|
if (pipeline_) {
|
2014-10-15 21:57:57 +02:00
|
|
|
gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)),
|
|
|
|
nullptr, nullptr, nullptr);
|
2010-04-19 14:04:35 +02:00
|
|
|
g_source_remove(bus_cb_id_);
|
2010-04-11 21:47:21 +02:00
|
|
|
gst_element_set_state(pipeline_, GST_STATE_NULL);
|
|
|
|
gst_object_unref(GST_OBJECT(pipeline_));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
gboolean GstEnginePipeline::BusCallback(GstBus*, GstMessage* msg,
|
|
|
|
gpointer self) {
|
2010-04-12 02:20:52 +02:00
|
|
|
GstEnginePipeline* instance = reinterpret_cast<GstEnginePipeline*>(self);
|
2010-04-11 21:47:21 +02:00
|
|
|
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Debug) << instance->id() << "bus message" << GST_MESSAGE_TYPE_NAME(msg);
|
|
|
|
|
2011-04-17 16:11:37 +02:00
|
|
|
switch (GST_MESSAGE_TYPE(msg)) {
|
2010-04-19 14:04:35 +02:00
|
|
|
case GST_MESSAGE_ERROR:
|
|
|
|
instance->ErrorMessageReceived(msg);
|
2010-04-11 21:47:21 +02:00
|
|
|
break;
|
|
|
|
|
2010-04-15 01:59:11 +02:00
|
|
|
case GST_MESSAGE_TAG:
|
|
|
|
instance->TagMessageReceived(msg);
|
2010-04-11 21:47:21 +02:00
|
|
|
break;
|
|
|
|
|
2011-03-06 19:11:53 +01:00
|
|
|
case GST_MESSAGE_STATE_CHANGED:
|
|
|
|
instance->StateChangedMessageReceived(msg);
|
|
|
|
break;
|
|
|
|
|
2010-04-11 21:47:21 +02:00
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
2010-06-08 14:12:47 +02:00
|
|
|
|
2010-06-16 00:32:20 +02:00
|
|
|
return FALSE;
|
2010-04-11 21:47:21 +02:00
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
GstBusSyncReply GstEnginePipeline::BusCallbackSync(GstBus*, GstMessage* msg,
|
|
|
|
gpointer self) {
|
2010-04-12 02:20:52 +02:00
|
|
|
GstEnginePipeline* instance = reinterpret_cast<GstEnginePipeline*>(self);
|
2011-03-10 19:01:35 +01:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
qLog(Debug) << instance->id() << "sync bus message"
|
|
|
|
<< GST_MESSAGE_TYPE_NAME(msg);
|
2011-04-22 18:50:29 +02:00
|
|
|
|
2010-04-11 21:47:21 +02:00
|
|
|
switch (GST_MESSAGE_TYPE(msg)) {
|
|
|
|
case GST_MESSAGE_EOS:
|
2011-03-20 22:40:53 +01:00
|
|
|
emit instance->EndOfStreamReached(instance->id(), false);
|
2010-04-11 21:47:21 +02:00
|
|
|
break;
|
|
|
|
|
2010-04-15 01:59:11 +02:00
|
|
|
case GST_MESSAGE_TAG:
|
|
|
|
instance->TagMessageReceived(msg);
|
|
|
|
break;
|
|
|
|
|
2010-04-19 14:04:35 +02:00
|
|
|
case GST_MESSAGE_ERROR:
|
2011-03-20 20:18:54 +01:00
|
|
|
instance->ErrorMessageReceived(msg);
|
2010-04-19 14:04:35 +02:00
|
|
|
break;
|
|
|
|
|
2010-06-23 13:47:54 +02:00
|
|
|
case GST_MESSAGE_ELEMENT:
|
2010-08-28 20:48:16 +02:00
|
|
|
instance->ElementMessageReceived(msg);
|
2010-06-23 13:47:54 +02:00
|
|
|
break;
|
|
|
|
|
2011-03-06 19:11:53 +01:00
|
|
|
case GST_MESSAGE_STATE_CHANGED:
|
|
|
|
instance->StateChangedMessageReceived(msg);
|
|
|
|
break;
|
|
|
|
|
2012-01-23 15:58:10 +01:00
|
|
|
case GST_MESSAGE_BUFFERING:
|
|
|
|
instance->BufferingMessageReceived(msg);
|
|
|
|
break;
|
|
|
|
|
2012-11-18 01:06:46 +01:00
|
|
|
case GST_MESSAGE_STREAM_STATUS:
|
|
|
|
instance->StreamStatusMessageReceived(msg);
|
|
|
|
break;
|
|
|
|
|
2014-10-04 13:21:21 +02:00
|
|
|
case GST_MESSAGE_STREAM_START:
|
|
|
|
if (instance->emit_track_ended_on_stream_start_) {
|
|
|
|
qLog(Debug) << "New segment started, EOS will signal on next buffer "
|
|
|
|
"discontinuity";
|
|
|
|
instance->emit_track_ended_on_stream_start_ = false;
|
|
|
|
instance->emit_track_ended_on_time_discontinuity_ = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
2010-04-11 21:47:21 +02:00
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
2011-03-10 19:01:35 +01:00
|
|
|
|
2010-04-11 21:47:21 +02:00
|
|
|
return GST_BUS_PASS;
|
|
|
|
}
|
|
|
|
|
2012-11-18 01:06:46 +01:00
|
|
|
void GstEnginePipeline::StreamStatusMessageReceived(GstMessage* msg) {
|
|
|
|
GstStreamStatusType type;
|
|
|
|
GstElement* owner;
|
|
|
|
gst_message_parse_stream_status(msg, &type, &owner);
|
|
|
|
|
|
|
|
if (type == GST_STREAM_STATUS_TYPE_CREATE) {
|
|
|
|
const GValue* val = gst_message_get_stream_status_object(msg);
|
|
|
|
if (G_VALUE_TYPE(val) == GST_TYPE_TASK) {
|
|
|
|
GstTask* task = static_cast<GstTask*>(g_value_get_object(val));
|
2013-09-25 15:42:13 +02:00
|
|
|
gst_task_set_enter_callback(task, &TaskEnterCallback, this, NULL);
|
2012-11-18 01:06:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void GstEnginePipeline::TaskEnterCallback(GstTask*, GThread*, gpointer) {
|
2014-02-07 16:34:20 +01:00
|
|
|
// Bump the priority of the thread only on OS X
|
2012-11-18 01:06:46 +01:00
|
|
|
|
|
|
|
#ifdef Q_OS_DARWIN
|
|
|
|
sched_param param;
|
|
|
|
memset(¶m, 0, sizeof(param));
|
|
|
|
|
|
|
|
param.sched_priority = 99;
|
|
|
|
pthread_setschedparam(pthread_self(), SCHED_RR, ¶m);
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2010-08-28 20:48:16 +02:00
|
|
|
void GstEnginePipeline::ElementMessageReceived(GstMessage* msg) {
|
2010-06-23 13:47:54 +02:00
|
|
|
const GstStructure* structure = gst_message_get_structure(msg);
|
|
|
|
|
2010-08-28 20:48:16 +02:00
|
|
|
if (gst_structure_has_name(structure, "redirect")) {
|
2010-06-23 13:47:54 +02:00
|
|
|
const char* uri = gst_structure_get_string(structure, "new-location");
|
|
|
|
|
|
|
|
// Set the redirect URL. In mmssrc redirect messages come during the
|
|
|
|
// initial state change to PLAYING, so callers can pick up this URL after
|
|
|
|
// the state change has failed.
|
|
|
|
redirect_url_ = QUrl::fromEncoded(uri);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-04-19 14:04:35 +02:00
|
|
|
void GstEnginePipeline::ErrorMessageReceived(GstMessage* msg) {
|
|
|
|
GError* error;
|
|
|
|
gchar* debugs;
|
|
|
|
|
|
|
|
gst_message_parse_error(msg, &error, &debugs);
|
2010-04-30 17:37:57 +02:00
|
|
|
QString message = QString::fromLocal8Bit(error->message);
|
2010-06-23 13:47:54 +02:00
|
|
|
QString debugstr = QString::fromLocal8Bit(debugs);
|
2011-03-10 19:01:35 +01:00
|
|
|
int domain = error->domain;
|
|
|
|
int code = error->code;
|
2010-04-19 14:30:19 +02:00
|
|
|
g_error_free(error);
|
|
|
|
free(debugs);
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!redirect_url_.isEmpty() &&
|
|
|
|
debugstr.contains(
|
|
|
|
"A redirect message was posted on the bus and should have been "
|
|
|
|
"handled by the application.")) {
|
2010-06-23 13:47:54 +02:00
|
|
|
// mmssrc posts a message on the bus *and* makes an error message when it
|
|
|
|
// wants to do a redirect. We handle the message, but now we have to
|
|
|
|
// ignore the error too.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Error) << id() << debugstr;
|
|
|
|
|
2011-03-20 22:40:53 +01:00
|
|
|
emit Error(id(), message, domain, code);
|
2010-04-19 14:04:35 +02:00
|
|
|
}
|
|
|
|
|
2010-04-15 01:59:11 +02:00
|
|
|
void GstEnginePipeline::TagMessageReceived(GstMessage* msg) {
|
2014-02-06 16:49:49 +01:00
|
|
|
GstTagList* taglist = nullptr;
|
2010-04-15 01:59:11 +02:00
|
|
|
gst_message_parse_tag(msg, &taglist);
|
|
|
|
|
|
|
|
Engine::SimpleMetaBundle bundle;
|
|
|
|
bundle.title = ParseTag(taglist, GST_TAG_TITLE);
|
|
|
|
bundle.artist = ParseTag(taglist, GST_TAG_ARTIST);
|
|
|
|
bundle.comment = ParseTag(taglist, GST_TAG_COMMENT);
|
|
|
|
bundle.album = ParseTag(taglist, GST_TAG_ALBUM);
|
|
|
|
|
|
|
|
gst_tag_list_free(taglist);
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
if (ignore_tags_) return;
|
2010-09-04 15:50:53 +02:00
|
|
|
|
2010-04-15 01:59:11 +02:00
|
|
|
if (!bundle.title.isEmpty() || !bundle.artist.isEmpty() ||
|
|
|
|
!bundle.comment.isEmpty() || !bundle.album.isEmpty())
|
2011-03-20 22:40:53 +01:00
|
|
|
emit MetadataFound(id(), bundle);
|
2010-04-15 01:59:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
QString GstEnginePipeline::ParseTag(GstTagList* list, const char* tag) const {
|
2014-02-06 16:49:49 +01:00
|
|
|
gchar* data = nullptr;
|
2010-04-15 01:59:11 +02:00
|
|
|
bool success = gst_tag_list_get_string(list, tag, &data);
|
|
|
|
|
|
|
|
QString ret;
|
|
|
|
if (success && data) {
|
2010-04-22 22:48:35 +02:00
|
|
|
ret = QString::fromUtf8(data);
|
2010-04-15 01:59:11 +02:00
|
|
|
g_free(data);
|
|
|
|
}
|
2010-04-21 00:20:20 +02:00
|
|
|
return ret.trimmed();
|
2010-04-15 01:59:11 +02:00
|
|
|
}
|
|
|
|
|
2011-03-06 19:11:53 +01:00
|
|
|
void GstEnginePipeline::StateChangedMessageReceived(GstMessage* msg) {
|
2011-03-07 21:00:03 +01:00
|
|
|
if (msg->src != GST_OBJECT(pipeline_)) {
|
|
|
|
// We only care about state changes of the whole pipeline.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2011-03-06 19:11:53 +01:00
|
|
|
GstState old_state, new_state, pending;
|
|
|
|
gst_message_parse_state_changed(msg, &old_state, &new_state, &pending);
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!pipeline_is_initialised_ &&
|
|
|
|
(new_state == GST_STATE_PAUSED || new_state == GST_STATE_PLAYING)) {
|
2011-03-06 19:11:53 +01:00
|
|
|
pipeline_is_initialised_ = true;
|
|
|
|
if (pending_seek_nanosec_ != -1 && pipeline_is_connected_) {
|
|
|
|
QMetaObject::invokeMethod(this, "Seek", Qt::QueuedConnection,
|
|
|
|
Q_ARG(qint64, pending_seek_nanosec_));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
if (pipeline_is_initialised_ && new_state != GST_STATE_PAUSED &&
|
|
|
|
new_state != GST_STATE_PLAYING) {
|
2011-03-06 19:11:53 +01:00
|
|
|
pipeline_is_initialised_ = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-01-23 15:58:10 +01:00
|
|
|
void GstEnginePipeline::BufferingMessageReceived(GstMessage* msg) {
|
2012-01-27 12:07:12 +01:00
|
|
|
// Only handle buffering messages from the queue2 element in audiobin - not
|
2012-06-09 18:52:39 +02:00
|
|
|
// the one that's created automatically by uridecodebin.
|
2012-01-27 12:07:12 +01:00
|
|
|
if (GST_ELEMENT(GST_MESSAGE_SRC(msg)) != queue_) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-10-05 11:57:57 +02:00
|
|
|
// If we are loading new next track, we don't have to pause the playback.
|
|
|
|
// The buffering is for the next track and not the current one.
|
|
|
|
if (emit_track_ended_on_stream_start_) {
|
|
|
|
qLog(Debug) << "Buffering next track";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2012-01-23 15:58:10 +01:00
|
|
|
int percent = 0;
|
|
|
|
gst_message_parse_buffering(msg, &percent);
|
|
|
|
|
|
|
|
const GstState current_state = state();
|
|
|
|
|
|
|
|
if (percent == 0 && current_state == GST_STATE_PLAYING && !buffering_) {
|
|
|
|
buffering_ = true;
|
|
|
|
emit BufferingStarted();
|
|
|
|
|
2012-02-13 21:49:25 +01:00
|
|
|
SetState(GST_STATE_PAUSED);
|
2012-01-23 15:58:10 +01:00
|
|
|
} else if (percent == 100 && buffering_) {
|
|
|
|
buffering_ = false;
|
|
|
|
emit BufferingFinished();
|
|
|
|
|
2012-02-13 21:49:25 +01:00
|
|
|
SetState(GST_STATE_PLAYING);
|
2012-01-23 15:58:10 +01:00
|
|
|
} else if (buffering_) {
|
|
|
|
emit BufferingProgress(percent);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void GstEnginePipeline::NewPadCallback(GstElement*, GstPad* pad,
|
|
|
|
gpointer self) {
|
2010-04-12 02:20:52 +02:00
|
|
|
GstEnginePipeline* instance = reinterpret_cast<GstEnginePipeline*>(self);
|
2014-02-07 16:34:20 +01:00
|
|
|
GstPad* const audiopad =
|
|
|
|
gst_element_get_static_pad(instance->audiobin_, "sink");
|
2010-04-11 21:47:21 +02:00
|
|
|
|
2014-10-14 10:18:48 +02:00
|
|
|
// Link decodebin's sink pad to audiobin's src pad.
|
2010-04-11 21:47:21 +02:00
|
|
|
if (GST_PAD_IS_LINKED(audiopad)) {
|
2014-02-07 16:34:20 +01:00
|
|
|
qLog(Warning) << instance->id()
|
|
|
|
<< "audiopad is already linked, unlinking old pad";
|
2010-04-11 21:47:21 +02:00
|
|
|
gst_pad_unlink(audiopad, GST_PAD_PEER(audiopad));
|
|
|
|
}
|
|
|
|
|
|
|
|
gst_pad_link(pad, audiopad);
|
|
|
|
gst_object_unref(audiopad);
|
2011-03-06 19:11:53 +01:00
|
|
|
|
2014-10-14 10:18:48 +02:00
|
|
|
// Offset the timestamps on all the buffers coming out of the decodebin so
|
|
|
|
// they line up exactly with the end of the last buffer from the old
|
|
|
|
// decodebin.
|
|
|
|
// "Running time" is the time since the last flushing seek.
|
|
|
|
GstClockTime running_time = gst_segment_to_running_time(
|
2014-10-15 21:57:57 +02:00
|
|
|
&instance->last_decodebin_segment_, GST_FORMAT_TIME,
|
2014-10-14 10:18:48 +02:00
|
|
|
instance->last_decodebin_segment_.position);
|
|
|
|
gst_pad_set_offset(pad, running_time);
|
|
|
|
|
|
|
|
// Add a probe to the pad so we can update last_decodebin_segment_.
|
2014-10-15 21:57:57 +02:00
|
|
|
gst_pad_add_probe(
|
|
|
|
pad, static_cast<GstPadProbeType>(GST_PAD_PROBE_TYPE_BUFFER |
|
|
|
|
GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM |
|
|
|
|
GST_PAD_PROBE_TYPE_EVENT_FLUSH),
|
|
|
|
DecodebinProbe, instance, nullptr);
|
2014-10-14 10:18:48 +02:00
|
|
|
|
2011-03-06 19:11:53 +01:00
|
|
|
instance->pipeline_is_connected_ = true;
|
2014-02-07 16:34:20 +01:00
|
|
|
if (instance->pending_seek_nanosec_ != -1 &&
|
|
|
|
instance->pipeline_is_initialised_) {
|
2011-03-06 19:11:53 +01:00
|
|
|
QMetaObject::invokeMethod(instance, "Seek", Qt::QueuedConnection,
|
|
|
|
Q_ARG(qint64, instance->pending_seek_nanosec_));
|
|
|
|
}
|
2010-04-11 21:47:21 +02:00
|
|
|
}
|
|
|
|
|
2014-10-14 10:18:48 +02:00
|
|
|
GstPadProbeReturn GstEnginePipeline::DecodebinProbe(GstPad* pad,
|
|
|
|
GstPadProbeInfo* info,
|
|
|
|
gpointer data) {
|
|
|
|
GstEnginePipeline* instance = reinterpret_cast<GstEnginePipeline*>(data);
|
|
|
|
const GstPadProbeType info_type = GST_PAD_PROBE_INFO_TYPE(info);
|
|
|
|
|
|
|
|
if (info_type & GST_PAD_PROBE_TYPE_BUFFER) {
|
|
|
|
// The decodebin produced a buffer. Record its end time, so we can offset
|
|
|
|
// the buffers produced by the next decodebin when transitioning to the next
|
|
|
|
// song.
|
|
|
|
GstBuffer* buffer = GST_PAD_PROBE_INFO_BUFFER(info);
|
|
|
|
|
|
|
|
GstClockTime timestamp = GST_BUFFER_TIMESTAMP(buffer);
|
|
|
|
GstClockTime duration = GST_BUFFER_DURATION(buffer);
|
|
|
|
if (timestamp == GST_CLOCK_TIME_NONE) {
|
|
|
|
timestamp = instance->last_decodebin_segment_.position;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (duration != GST_CLOCK_TIME_NONE) {
|
|
|
|
timestamp += duration;
|
|
|
|
}
|
|
|
|
|
|
|
|
instance->last_decodebin_segment_.position = timestamp;
|
|
|
|
} else if (info_type & GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM) {
|
|
|
|
GstEvent* event = GST_PAD_PROBE_INFO_EVENT(info);
|
|
|
|
GstEventType event_type = GST_EVENT_TYPE(event);
|
|
|
|
|
|
|
|
if (event_type == GST_EVENT_SEGMENT) {
|
|
|
|
// A new segment started, we need to save this to calculate running time
|
|
|
|
// offsets later.
|
|
|
|
gst_event_copy_segment(event, &instance->last_decodebin_segment_);
|
|
|
|
} else if (event_type == GST_EVENT_FLUSH_START) {
|
|
|
|
// A flushing seek resets the running time to 0, so remove any offset
|
|
|
|
// we set on this pad before.
|
|
|
|
gst_pad_set_offset(pad, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return GST_PAD_PROBE_OK;
|
|
|
|
}
|
|
|
|
|
2014-06-09 08:20:24 +02:00
|
|
|
GstPadProbeReturn GstEnginePipeline::HandoffCallback(GstPad*,
|
|
|
|
GstPadProbeInfo* info,
|
|
|
|
gpointer self) {
|
2010-04-12 02:20:52 +02:00
|
|
|
GstEnginePipeline* instance = reinterpret_cast<GstEnginePipeline*>(self);
|
2013-09-25 15:42:13 +02:00
|
|
|
GstBuffer* buf = gst_pad_probe_info_get_buffer(info);
|
2010-04-11 21:47:21 +02:00
|
|
|
|
2010-06-06 16:06:23 +02:00
|
|
|
QList<BufferConsumer*> consumers;
|
|
|
|
{
|
|
|
|
QMutexLocker l(&instance->buffer_consumers_mutex_);
|
|
|
|
consumers = instance->buffer_consumers_;
|
|
|
|
}
|
|
|
|
|
2014-02-10 14:29:07 +01:00
|
|
|
for (BufferConsumer* consumer : consumers) {
|
2010-04-11 21:47:21 +02:00
|
|
|
gst_buffer_ref(buf);
|
2011-03-20 22:40:53 +01:00
|
|
|
consumer->ConsumeBuffer(buf, instance->id());
|
2010-04-11 21:47:21 +02:00
|
|
|
}
|
2010-04-12 03:59:21 +02:00
|
|
|
|
2011-03-06 17:35:47 +01:00
|
|
|
// Calculate the end time of this buffer so we can stop playback if it's
|
|
|
|
// after the end time of this song.
|
|
|
|
if (instance->end_offset_nanosec_ > 0) {
|
|
|
|
quint64 start_time = GST_BUFFER_TIMESTAMP(buf) - instance->segment_start_;
|
|
|
|
quint64 duration = GST_BUFFER_DURATION(buf);
|
|
|
|
quint64 end_time = start_time + duration;
|
|
|
|
|
|
|
|
if (end_time > instance->end_offset_nanosec_) {
|
|
|
|
if (instance->has_next_valid_url()) {
|
|
|
|
if (instance->next_url_ == instance->url_ &&
|
2014-02-07 16:34:20 +01:00
|
|
|
instance->next_beginning_offset_nanosec_ ==
|
|
|
|
instance->end_offset_nanosec_) {
|
2011-03-06 17:35:47 +01:00
|
|
|
// The "next" song is actually the next segment of this file - so
|
|
|
|
// cheat and keep on playing, but just tell the Engine we've moved on.
|
|
|
|
instance->end_offset_nanosec_ = instance->next_end_offset_nanosec_;
|
|
|
|
instance->next_url_ = QUrl();
|
|
|
|
instance->next_beginning_offset_nanosec_ = 0;
|
|
|
|
instance->next_end_offset_nanosec_ = 0;
|
|
|
|
|
|
|
|
// GstEngine will try to seek to the start of the new section, but
|
|
|
|
// we're already there so ignore it.
|
|
|
|
instance->ignore_next_seek_ = true;
|
2011-03-20 22:40:53 +01:00
|
|
|
emit instance->EndOfStreamReached(instance->id(), true);
|
2011-03-06 17:35:47 +01:00
|
|
|
} else {
|
|
|
|
// We have a next song but we can't cheat, so move to it normally.
|
|
|
|
instance->TransitionToNext();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// There's no next song
|
2011-03-20 22:40:53 +01:00
|
|
|
emit instance->EndOfStreamReached(instance->id(), false);
|
2011-03-06 17:35:47 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-06-04 14:17:44 +02:00
|
|
|
if (instance->emit_track_ended_on_time_discontinuity_) {
|
2013-06-08 06:31:29 +02:00
|
|
|
if (GST_BUFFER_FLAG_IS_SET(buf, GST_BUFFER_FLAG_DISCONT) ||
|
|
|
|
GST_BUFFER_OFFSET(buf) < instance->last_buffer_offset_) {
|
2013-06-04 14:17:44 +02:00
|
|
|
qLog(Debug) << "Buffer discontinuity - emitting EOS";
|
|
|
|
instance->emit_track_ended_on_time_discontinuity_ = false;
|
|
|
|
emit instance->EndOfStreamReached(instance->id(), true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-06-08 06:31:29 +02:00
|
|
|
instance->last_buffer_offset_ = GST_BUFFER_OFFSET(buf);
|
|
|
|
|
2013-09-25 15:42:13 +02:00
|
|
|
return GST_PAD_PROBE_OK;
|
2010-04-11 21:47:21 +02:00
|
|
|
}
|
|
|
|
|
2014-06-09 08:20:24 +02:00
|
|
|
GstPadProbeReturn GstEnginePipeline::EventHandoffCallback(GstPad*,
|
|
|
|
GstPadProbeInfo* info,
|
|
|
|
gpointer self) {
|
2010-07-11 15:31:03 +02:00
|
|
|
GstEnginePipeline* instance = reinterpret_cast<GstEnginePipeline*>(self);
|
2013-09-25 15:42:13 +02:00
|
|
|
GstEvent* e = gst_pad_probe_info_get_event(info);
|
2010-07-11 15:31:03 +02:00
|
|
|
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Debug) << instance->id() << "event" << GST_EVENT_TYPE_NAME(e);
|
|
|
|
|
2014-10-04 13:21:21 +02:00
|
|
|
switch (GST_EVENT_TYPE(e)) {
|
|
|
|
case GST_EVENT_SEGMENT:
|
|
|
|
if (!instance->segment_start_received_) {
|
|
|
|
// The segment start time is used to calculate the proper offset of data
|
|
|
|
// buffers from the start of the stream
|
|
|
|
const GstSegment* segment = nullptr;
|
|
|
|
gst_event_parse_segment(e, &segment);
|
|
|
|
instance->segment_start_ = segment->start;
|
|
|
|
instance->segment_start_received_ = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
break;
|
2010-07-11 15:31:03 +02:00
|
|
|
}
|
|
|
|
|
2013-09-25 15:42:13 +02:00
|
|
|
return GST_PAD_PROBE_OK;
|
2010-07-11 15:31:03 +02:00
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void GstEnginePipeline::SourceDrainedCallback(GstURIDecodeBin* bin,
|
|
|
|
gpointer self) {
|
2010-05-08 19:39:12 +02:00
|
|
|
GstEnginePipeline* instance = reinterpret_cast<GstEnginePipeline*>(self);
|
|
|
|
|
2011-01-03 19:03:15 +01:00
|
|
|
if (instance->has_next_valid_url()) {
|
2011-03-06 17:35:47 +01:00
|
|
|
instance->TransitionToNext();
|
|
|
|
}
|
|
|
|
}
|
2010-07-12 21:10:32 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void GstEnginePipeline::SourceSetupCallback(GstURIDecodeBin* bin,
|
|
|
|
GParamSpec* pspec, gpointer self) {
|
2011-08-10 00:49:36 +02:00
|
|
|
GstEnginePipeline* instance = reinterpret_cast<GstEnginePipeline*>(self);
|
|
|
|
GstElement* element;
|
2014-02-06 16:49:49 +01:00
|
|
|
g_object_get(bin, "source", &element, nullptr);
|
2012-10-23 17:34:25 +02:00
|
|
|
if (!element) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (g_object_class_find_property(G_OBJECT_GET_CLASS(element), "device") &&
|
2011-08-26 00:11:18 +02:00
|
|
|
!instance->source_device().isEmpty()) {
|
2011-08-10 00:49:36 +02:00
|
|
|
// Gstreamer is not able to handle device in URL (refering to Gstreamer
|
|
|
|
// documentation, this might be added in the future). Despite that, for now
|
|
|
|
// we include device inside URL: we decompose it during Init and set device
|
|
|
|
// here, when this callback is called.
|
2014-02-07 16:34:20 +01:00
|
|
|
g_object_set(element, "device",
|
|
|
|
instance->source_device().toLocal8Bit().constData(), nullptr);
|
2011-08-10 00:49:36 +02:00
|
|
|
}
|
2015-03-27 14:56:08 +01:00
|
|
|
if (g_object_class_find_property(G_OBJECT_GET_CLASS(element),
|
|
|
|
"extra-headers") &&
|
|
|
|
instance->url().host().contains("amazonaws.com")) {
|
|
|
|
GstStructure* headers = gst_structure_new(
|
|
|
|
"extra-headers", "Authorization", G_TYPE_STRING,
|
|
|
|
instance->url().fragment().toAscii().data(), nullptr);
|
|
|
|
g_object_set(element, "extra-headers", headers, nullptr);
|
2011-09-20 19:48:07 +02:00
|
|
|
gst_structure_free(headers);
|
2011-09-19 23:52:21 +02:00
|
|
|
}
|
2012-07-12 14:09:20 +02:00
|
|
|
|
2012-10-23 17:34:25 +02:00
|
|
|
if (g_object_class_find_property(G_OBJECT_GET_CLASS(element), "user-agent")) {
|
2014-02-07 16:34:20 +01:00
|
|
|
QString user_agent =
|
|
|
|
QString("%1 %2").arg(QCoreApplication::applicationName(),
|
|
|
|
QCoreApplication::applicationVersion());
|
|
|
|
g_object_set(element, "user-agent", user_agent.toUtf8().constData(),
|
|
|
|
nullptr);
|
2014-10-01 15:06:22 +02:00
|
|
|
|
|
|
|
#ifdef Q_OS_DARWIN
|
2015-04-27 12:19:58 +02:00
|
|
|
g_object_set(element, "tls-database", instance->engine_->tls_database(), nullptr);
|
2014-10-01 15:06:22 +02:00
|
|
|
g_object_set(element, "ssl-use-system-ca-file", false, nullptr);
|
2015-04-27 12:19:58 +02:00
|
|
|
g_object_set(element, "ssl-strict", TRUE, nullptr);
|
2014-10-01 15:06:22 +02:00
|
|
|
#endif
|
2012-10-23 17:34:25 +02:00
|
|
|
}
|
2011-08-10 00:49:36 +02:00
|
|
|
}
|
|
|
|
|
2011-03-06 17:35:47 +01:00
|
|
|
void GstEnginePipeline::TransitionToNext() {
|
|
|
|
GstElement* old_decode_bin = uridecodebin_;
|
2010-09-04 15:50:53 +02:00
|
|
|
|
2011-03-06 17:35:47 +01:00
|
|
|
ignore_tags_ = true;
|
2010-05-08 19:39:12 +02:00
|
|
|
|
2011-03-06 17:35:47 +01:00
|
|
|
ReplaceDecodeBin(next_url_);
|
|
|
|
gst_element_set_state(uridecodebin_, GST_STATE_PLAYING);
|
2011-11-28 19:11:09 +01:00
|
|
|
MaybeLinkDecodeToAudio();
|
2010-05-08 19:39:12 +02:00
|
|
|
|
2011-03-06 17:35:47 +01:00
|
|
|
url_ = next_url_;
|
|
|
|
end_offset_nanosec_ = next_end_offset_nanosec_;
|
|
|
|
next_url_ = QUrl();
|
|
|
|
next_beginning_offset_nanosec_ = 0;
|
|
|
|
next_end_offset_nanosec_ = 0;
|
2010-07-12 21:10:32 +02:00
|
|
|
|
2011-04-17 16:11:37 +02:00
|
|
|
// This function gets called when the source has been drained, even if the
|
2014-10-04 13:21:21 +02:00
|
|
|
// song hasn't finished playing yet. We'll get a new stream when it really
|
2011-04-17 16:11:37 +02:00
|
|
|
// does finish, so emit TrackEnded then.
|
2014-10-04 13:21:21 +02:00
|
|
|
emit_track_ended_on_stream_start_ = true;
|
2010-09-04 15:50:53 +02:00
|
|
|
|
2011-03-06 17:35:47 +01:00
|
|
|
// This has to happen *after* the gst_element_set_state on the new bin to
|
|
|
|
// fix an occasional race condition deadlock.
|
|
|
|
sElementDeleter->DeleteElementLater(old_decode_bin);
|
|
|
|
|
|
|
|
ignore_tags_ = false;
|
2010-05-08 19:39:12 +02:00
|
|
|
}
|
|
|
|
|
2010-04-11 21:47:21 +02:00
|
|
|
qint64 GstEnginePipeline::position() const {
|
|
|
|
gint64 value = 0;
|
2013-09-25 15:42:13 +02:00
|
|
|
gst_element_query_position(pipeline_, GST_FORMAT_TIME, &value);
|
2010-04-11 21:47:21 +02:00
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
|
|
qint64 GstEnginePipeline::length() const {
|
|
|
|
gint64 value = 0;
|
2013-09-25 15:42:13 +02:00
|
|
|
gst_element_query_duration(pipeline_, GST_FORMAT_TIME, &value);
|
2010-04-11 21:47:21 +02:00
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
|
|
GstState GstEnginePipeline::state() const {
|
|
|
|
GstState s, sp;
|
2011-03-13 23:57:49 +01:00
|
|
|
if (gst_element_get_state(pipeline_, &s, &sp, kGstStateTimeoutNanosecs) ==
|
2010-04-11 21:47:21 +02:00
|
|
|
GST_STATE_CHANGE_FAILURE)
|
|
|
|
return GST_STATE_NULL;
|
|
|
|
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
2011-05-04 00:38:24 +02:00
|
|
|
QFuture<GstStateChangeReturn> GstEnginePipeline::SetState(GstState state) {
|
2014-09-06 19:21:23 +02:00
|
|
|
if (url_.scheme() == "spotify" && !buffering_) {
|
2014-10-15 21:57:57 +02:00
|
|
|
const GstState current_state = this->state();
|
2014-09-06 19:21:23 +02:00
|
|
|
|
2014-10-15 21:57:57 +02:00
|
|
|
if (state == GST_STATE_PAUSED && current_state == GST_STATE_PLAYING) {
|
|
|
|
SpotifyService* spotify = InternetModel::Service<SpotifyService>();
|
2014-09-06 19:21:23 +02:00
|
|
|
|
2014-10-15 21:57:57 +02:00
|
|
|
// Need to schedule this in the spotify service's thread
|
|
|
|
QMetaObject::invokeMethod(spotify, "SetPaused", Qt::QueuedConnection,
|
|
|
|
Q_ARG(bool, true));
|
|
|
|
} else if (state == GST_STATE_PLAYING &&
|
|
|
|
current_state == GST_STATE_PAUSED) {
|
|
|
|
SpotifyService* spotify = InternetModel::Service<SpotifyService>();
|
2014-09-06 19:21:23 +02:00
|
|
|
|
2014-10-15 21:57:57 +02:00
|
|
|
// Need to schedule this in the spotify service's thread
|
|
|
|
QMetaObject::invokeMethod(spotify, "SetPaused", Qt::QueuedConnection,
|
|
|
|
Q_ARG(bool, false));
|
|
|
|
}
|
2014-09-06 19:21:23 +02:00
|
|
|
}
|
2012-04-25 00:29:19 +02:00
|
|
|
return ConcurrentRun::Run<GstStateChangeReturn, GstElement*, GstState>(
|
|
|
|
&set_state_threadpool_, &gst_element_set_state, pipeline_, state);
|
2010-04-11 21:47:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
bool GstEnginePipeline::Seek(qint64 nanosec) {
|
2011-03-06 17:35:47 +01:00
|
|
|
if (ignore_next_seek_) {
|
|
|
|
ignore_next_seek_ = false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2011-03-06 19:11:53 +01:00
|
|
|
if (!pipeline_is_connected_ || !pipeline_is_initialised_) {
|
|
|
|
pending_seek_nanosec_ = nanosec;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
pending_seek_nanosec_ = -1;
|
2010-04-22 18:54:09 +02:00
|
|
|
return gst_element_seek_simple(pipeline_, GST_FORMAT_TIME,
|
|
|
|
GST_SEEK_FLAG_FLUSH, nanosec);
|
2010-04-11 21:47:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void GstEnginePipeline::SetEqualizerEnabled(bool enabled) {
|
2010-07-12 22:55:09 +02:00
|
|
|
eq_enabled_ = enabled;
|
|
|
|
UpdateEqualizer();
|
2010-04-11 21:47:21 +02:00
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void GstEnginePipeline::SetEqualizerParams(int preamp,
|
|
|
|
const QList<int>& band_gains) {
|
2010-07-12 22:55:09 +02:00
|
|
|
eq_preamp_ = preamp;
|
|
|
|
eq_band_gains_ = band_gains;
|
|
|
|
UpdateEqualizer();
|
|
|
|
}
|
|
|
|
|
2013-04-27 05:05:42 +02:00
|
|
|
void GstEnginePipeline::SetStereoBalance(float value) {
|
|
|
|
stereo_balance_ = value;
|
|
|
|
UpdateStereoBalance();
|
|
|
|
}
|
|
|
|
|
2010-07-12 22:55:09 +02:00
|
|
|
void GstEnginePipeline::UpdateEqualizer() {
|
|
|
|
// Update band gains
|
2014-02-07 16:34:20 +01:00
|
|
|
for (int i = 0; i < kEqBandCount; ++i) {
|
2010-07-12 22:55:09 +02:00
|
|
|
float gain = eq_enabled_ ? eq_band_gains_[i] : 0.0;
|
|
|
|
if (gain < 0)
|
|
|
|
gain *= 0.24;
|
|
|
|
else
|
|
|
|
gain *= 0.12;
|
|
|
|
|
2013-09-25 15:42:13 +02:00
|
|
|
GstObject* band = GST_OBJECT(
|
|
|
|
gst_child_proxy_get_child_by_index(GST_CHILD_PROXY(equalizer_), i));
|
2014-02-06 16:49:49 +01:00
|
|
|
g_object_set(G_OBJECT(band), "gain", gain, nullptr);
|
2010-07-12 22:55:09 +02:00
|
|
|
g_object_unref(G_OBJECT(band));
|
|
|
|
}
|
2010-04-11 21:47:21 +02:00
|
|
|
|
2010-07-12 22:55:09 +02:00
|
|
|
// Update preamp
|
|
|
|
float preamp = 1.0;
|
|
|
|
if (eq_enabled_)
|
2010-07-12 23:00:15 +02:00
|
|
|
preamp = float(eq_preamp_ + 100) * 0.01; // To scale from 0.0 to 2.0
|
2010-04-11 21:47:21 +02:00
|
|
|
|
2014-02-06 16:49:49 +01:00
|
|
|
g_object_set(G_OBJECT(equalizer_preamp_), "volume", preamp, nullptr);
|
2010-04-11 21:47:21 +02:00
|
|
|
}
|
|
|
|
|
2013-04-27 05:05:42 +02:00
|
|
|
void GstEnginePipeline::UpdateStereoBalance() {
|
|
|
|
if (stereo_panorama_) {
|
2014-02-07 16:34:20 +01:00
|
|
|
g_object_set(G_OBJECT(stereo_panorama_), "panorama", stereo_balance_,
|
|
|
|
nullptr);
|
2013-04-27 05:05:42 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-04-11 21:47:21 +02:00
|
|
|
void GstEnginePipeline::SetVolume(int percent) {
|
2010-04-11 23:40:26 +02:00
|
|
|
volume_percent_ = percent;
|
|
|
|
UpdateVolume();
|
|
|
|
}
|
|
|
|
|
|
|
|
void GstEnginePipeline::SetVolumeModifier(qreal mod) {
|
|
|
|
volume_modifier_ = mod;
|
|
|
|
UpdateVolume();
|
|
|
|
}
|
|
|
|
|
|
|
|
void GstEnginePipeline::UpdateVolume() {
|
|
|
|
float vol = double(volume_percent_) * 0.01 * volume_modifier_;
|
2014-02-06 16:49:49 +01:00
|
|
|
g_object_set(G_OBJECT(volume_), "volume", vol, nullptr);
|
2010-04-11 21:47:21 +02:00
|
|
|
}
|
2010-04-12 01:03:39 +02:00
|
|
|
|
2011-02-13 19:29:27 +01:00
|
|
|
void GstEnginePipeline::StartFader(qint64 duration_nanosec,
|
2010-04-12 01:03:39 +02:00
|
|
|
QTimeLine::Direction direction,
|
2013-04-22 21:42:04 +02:00
|
|
|
QTimeLine::CurveShape shape,
|
|
|
|
bool use_fudge_timer) {
|
2011-02-14 20:34:37 +01:00
|
|
|
const int duration_msec = duration_nanosec / kNsecPerMsec;
|
2011-02-13 19:29:27 +01:00
|
|
|
|
2010-05-19 15:26:23 +02:00
|
|
|
// If there's already another fader running then start from the same time
|
|
|
|
// that one was already at.
|
|
|
|
int start_time = direction == QTimeLine::Forward ? 0 : duration_msec;
|
2013-04-22 21:42:04 +02:00
|
|
|
if (fader_ && fader_->state() == QTimeLine::Running) {
|
|
|
|
if (duration_msec == fader_->duration()) {
|
|
|
|
start_time = fader_->currentTime();
|
|
|
|
} else {
|
|
|
|
// Calculate the position in the new fader with the same value from
|
|
|
|
// the old fader, so no volume jumps appear
|
2014-02-07 16:34:20 +01:00
|
|
|
qreal time = qreal(duration_msec) *
|
|
|
|
(qreal(fader_->currentTime()) / qreal(fader_->duration()));
|
2013-04-22 21:42:04 +02:00
|
|
|
start_time = qRound(time);
|
|
|
|
}
|
|
|
|
}
|
2010-05-19 15:26:23 +02:00
|
|
|
|
|
|
|
fader_.reset(new QTimeLine(duration_msec, this));
|
2014-02-07 16:34:20 +01:00
|
|
|
connect(fader_.get(), SIGNAL(valueChanged(qreal)),
|
|
|
|
SLOT(SetVolumeModifier(qreal)));
|
2010-05-19 15:26:23 +02:00
|
|
|
connect(fader_.get(), SIGNAL(finished()), SLOT(FaderTimelineFinished()));
|
2010-04-12 01:03:39 +02:00
|
|
|
fader_->setDirection(direction);
|
|
|
|
fader_->setCurveShape(shape);
|
2010-05-19 15:26:23 +02:00
|
|
|
fader_->setCurrentTime(start_time);
|
|
|
|
fader_->resume();
|
|
|
|
|
|
|
|
fader_fudge_timer_.stop();
|
2013-04-22 21:42:04 +02:00
|
|
|
use_fudge_timer_ = use_fudge_timer;
|
2010-04-12 01:03:39 +02:00
|
|
|
|
|
|
|
SetVolumeModifier(fader_->currentValue());
|
|
|
|
}
|
2010-05-15 19:55:36 +02:00
|
|
|
|
|
|
|
void GstEnginePipeline::FaderTimelineFinished() {
|
2010-05-19 15:26:23 +02:00
|
|
|
fader_.reset();
|
|
|
|
|
2010-05-15 19:55:36 +02:00
|
|
|
// Wait a little while longer before emitting the finished signal (and
|
|
|
|
// probably distroying the pipeline) to account for delays in the audio
|
|
|
|
// server/driver.
|
2013-04-22 21:42:04 +02:00
|
|
|
if (use_fudge_timer_) {
|
|
|
|
fader_fudge_timer_.start(kFaderFudgeMsec, this);
|
|
|
|
} else {
|
|
|
|
// Even here we cannot emit the signal directly, as it result in a
|
|
|
|
// stutter when resuming playback. So use a quest small time, so you
|
|
|
|
// won't notice the difference when resuming playback
|
|
|
|
// (You get here when the pause fading is active)
|
|
|
|
fader_fudge_timer_.start(250, this);
|
|
|
|
}
|
2010-05-15 19:55:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void GstEnginePipeline::timerEvent(QTimerEvent* e) {
|
|
|
|
if (e->timerId() == fader_fudge_timer_.timerId()) {
|
2010-05-19 15:26:23 +02:00
|
|
|
fader_fudge_timer_.stop();
|
2010-05-15 19:55:36 +02:00
|
|
|
emit FaderFinished();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
QObject::timerEvent(e);
|
|
|
|
}
|
2010-06-06 16:06:23 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void GstEnginePipeline::AddBufferConsumer(BufferConsumer* consumer) {
|
2010-06-06 16:06:23 +02:00
|
|
|
QMutexLocker l(&buffer_consumers_mutex_);
|
|
|
|
buffer_consumers_ << consumer;
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void GstEnginePipeline::RemoveBufferConsumer(BufferConsumer* consumer) {
|
2010-06-06 16:06:23 +02:00
|
|
|
QMutexLocker l(&buffer_consumers_mutex_);
|
|
|
|
buffer_consumers_.removeAll(consumer);
|
|
|
|
}
|
|
|
|
|
|
|
|
void GstEnginePipeline::RemoveAllBufferConsumers() {
|
|
|
|
QMutexLocker l(&buffer_consumers_mutex_);
|
|
|
|
buffer_consumers_.clear();
|
|
|
|
}
|
2011-03-06 17:35:47 +01:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void GstEnginePipeline::SetNextUrl(const QUrl& url, qint64 beginning_nanosec,
|
2011-03-06 17:35:47 +01:00
|
|
|
qint64 end_nanosec) {
|
|
|
|
next_url_ = url;
|
|
|
|
next_beginning_offset_nanosec_ = beginning_nanosec;
|
|
|
|
next_end_offset_nanosec_ = end_nanosec;
|
|
|
|
}
|