diff options
author | tpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2010-01-09 23:52:48 +0000 |
---|---|---|
committer | tpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2010-01-09 23:52:48 +0000 |
commit | 3ce9174229de91411a9abf5381a1f335fe0c6a98 (patch) | |
tree | 84b2736fa1b0d3fbf9c60fc04f510d2a13916b09 /amarok/src/engine/xine/xine-engine.cpp | |
download | amarok-3ce9174229de91411a9abf5381a1f335fe0c6a98.tar.gz amarok-3ce9174229de91411a9abf5381a1f335fe0c6a98.zip |
Added abandoned KDE3 version of Amarok
git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/applications/amarok@1072335 283d02a7-25f6-0310-bc7c-ecb5cbfe19da
Diffstat (limited to 'amarok/src/engine/xine/xine-engine.cpp')
-rw-r--r-- | amarok/src/engine/xine/xine-engine.cpp | 1342 |
1 files changed, 1342 insertions, 0 deletions
diff --git a/amarok/src/engine/xine/xine-engine.cpp b/amarok/src/engine/xine/xine-engine.cpp new file mode 100644 index 00000000..05412aca --- /dev/null +++ b/amarok/src/engine/xine/xine-engine.cpp @@ -0,0 +1,1342 @@ +/*************************************************************************** + * Copyright (C) 2005 Christophe Thommeret <hftom@free.fr> * + * (C) 2005 Ian Monroe <ian@monroe.nu> * + * (C) 2005,6 Mark Kretschmann <markey@web.de> * + * (C) 2004,5 Max Howell <max.howell@methylblue.com> * + * (C) 2003,4 J. Kofler <kaffeine@gmx.net> * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#define DEBUG_PREFIX "xine-engine" + +#include "xine-config.h" +#include "xinecfg.h" +#include "xine-engine.h" +#include "amarok.h" +#include "amarokconfig.h" +//these files are from libamarok +#include "playlist.h" +#include "enginecontroller.h" + +AMAROK_EXPORT_PLUGIN( XineEngine ) + +#include <climits> +#include <cstdlib> +#include <cmath> +#include "debug.h" + +#include <klocale.h> +#include <kmessagebox.h> +#include <kstandarddirs.h> + +#include <qapplication.h> +#include <qdir.h> + +extern "C" +{ + #include <unistd.h> + #include "xine-scope.h" +} + +#ifndef LLONG_MAX +#define LLONG_MAX 9223372036854775807LL +#endif + + +//define this to use xine in a more standard way +//#define XINE_SAFE_MODE + + +///some logging static globals +namespace Log +{ + static uint bufferCount = 0; + static uint scopeCallCount = 1; //prevent divideByZero + static uint noSuitableBuffer = 0; +} + +///returns the configuration we will use. there is no KInstance, so using this hacked up method. +//static inline QCString configPath() { return QFile::encodeName(KStandardDirs().localkdedir() + KStandardDirs::kde_default("data") + "amarok/xine-config"); } +static inline QCString configPath() { return QFile::encodeName(locate( "data", "amarok/") + "xine-config" ); } +static Fader *s_fader = 0; +static OutFader *s_outfader = 0; + + +XineEngine::XineEngine() + : EngineBase() + , m_xine( 0 ) + , m_stream( 0 ) + , m_audioPort( 0 ) + , m_eventQueue( 0 ) + , m_post( 0 ) + , m_preamp( 1.0 ) + , m_stopFader( false ) + , m_fadeOutRunning ( false ) + , m_equalizerEnabled( false ) +{ + addPluginProperty( "HasConfigure", "true" ); + addPluginProperty( "HasEqualizer", "true" ); + #ifndef __NetBSD__ // NetBSD does not offer audio mixing + addPluginProperty( "HasCrossfade", "true" ); + #endif + addPluginProperty("HasCDDA", "true"); // new property + debug() << "hello" << endl; + +} + +XineEngine::~XineEngine() +{ + // Wait until the fader thread is done + if( s_fader ) { + m_stopFader = true; + s_fader->resume(); // safety call if the engine is in the pause state + s_fader->wait(); + } + + delete s_fader; + delete s_outfader; + + if( AmarokConfig::fadeoutOnExit() ) { + bool terminateFader = false; + fadeOut( AmarokConfig::fadeoutLength(), &terminateFader, true ); // true == exiting + } + + if( m_xine ) xine_config_save( m_xine, configPath() ); + + if( m_stream ) xine_close( m_stream ); + if( m_eventQueue ) xine_event_dispose_queue( m_eventQueue ); + if( m_stream ) xine_dispose( m_stream ); + if( m_audioPort ) xine_close_audio_driver( m_xine, m_audioPort ); + if( m_post ) xine_post_dispose( m_xine, m_post ); + if( m_xine ) xine_exit( m_xine ); + + debug() << "xine closed\n"; + + debug() << "Scope statistics:\n" + << " Average list size: " << Log::bufferCount / Log::scopeCallCount << endl + << " Buffer failure: " << double(Log::noSuitableBuffer*100) / Log::scopeCallCount << "%\n"; +} + +bool +XineEngine::init() +{ + DEBUG_BLOCK + + debug() << "'Bringing joy to small mexican gerbils, a few weeks at a time.'\n"; + + m_xine = xine_new(); + + if( !m_xine ) { + KMessageBox::error( 0, i18n("Amarok could not initialize xine.") ); + return false; + } + + #ifdef XINE_SAFE_MODE + xine_engine_set_param( m_xine, XINE_ENGINE_PARAM_VERBOSITY, 99 ); + #endif + + xine_config_load( m_xine, configPath() ); + debug() << "w00t" << configPath() << endl; + + xine_init( m_xine ); + + makeNewStream(); + + #ifndef XINE_SAFE_MODE + startTimer( 200 ); //prunes the scope + #endif + + return true; +} + +bool +XineEngine::makeNewStream() +{ + m_currentAudioPlugin = XineCfg::outputPlugin(); + + m_audioPort = xine_open_audio_driver( m_xine, XineCfg::outputPlugin().local8Bit(), NULL ); + if( !m_audioPort ) { + //TODO make engine method that is the same but parents the dialog for us + KMessageBox::error( 0, i18n("xine was unable to initialize any audio drivers.") ); + return false; + } + + m_stream = xine_stream_new( m_xine, m_audioPort, NULL ); + if( !m_stream ) { + xine_close_audio_driver( m_xine, m_audioPort ); + m_audioPort = NULL; + KMessageBox::error( 0, i18n("Amarok could not create a new xine stream.") ); + return false; + } + + if( m_eventQueue ) + xine_event_dispose_queue( m_eventQueue ); + + xine_event_create_listener_thread( + m_eventQueue = xine_event_new_queue( m_stream ), + &XineEngine::XineEventListener, + (void*)this ); + + #ifndef XINE_SAFE_MODE + //implemented in xine-scope.h + m_post = scope_plugin_new( m_xine, m_audioPort ); + + xine_set_param( m_stream, XINE_PARAM_METRONOM_PREBUFFER, 6000 ); + xine_set_param( m_stream, XINE_PARAM_IGNORE_VIDEO, 1 ); + #endif +#ifdef XINE_PARAM_EARLY_FINISHED_EVENT + if ( xine_check_version(1,1,1) && !(m_xfadeLength > 0) ) { + // enable gapless playback + debug() << "gapless playback enabled." << endl; + //xine_set_param(m_stream, XINE_PARAM_EARLY_FINISHED_EVENT, 1 ); + } +#endif + return true; +} + +// Makes sure an audio port and a stream exist. +bool +XineEngine::ensureStream() +{ + if( !m_stream ) + return makeNewStream(); + + return true; +} + +bool +XineEngine::load( const KURL &url, bool isStream ) +{ + DEBUG_BLOCK + + if( !ensureStream() ) + return false; + + Engine::Base::load( url, isStream ); + + if( s_outfader ) { + s_outfader->finish(); + delete s_outfader; + } + + if( m_xfadeLength > 0 && xine_get_status( m_stream ) == XINE_STATUS_PLAY && + url.isLocalFile() && + xine_get_param( m_stream, XINE_PARAM_SPEED ) != XINE_SPEED_PAUSE && + ( m_xfadeNextTrack || //set by engine controller when switching tracks automatically + (uint) AmarokConfig::crossfadeType() == 0 || //crossfade always + (uint) AmarokConfig::crossfadeType() == 2 ) ) //crossfade when switching tracks manually + { + m_xfadeNextTrack = false; + // Stop a probably running fader + if( s_fader ) { + m_stopFader = true; + s_fader->finish(); // makes the fader stop abruptly + delete s_fader; + } + s_fader = new Fader( this, m_xfadeLength ); + setEqualizerParameters( m_intPreamp, m_equalizerGains ); + } + + // for users who stubbonly refuse to use DMIX or buy a good soundcard + // why doesn't xine do this? I cannot say. + xine_close( m_stream ); + + debug() << "Before xine_open() *****" << endl; + + if( xine_open( m_stream, QFile::encodeName( url.url() ) ) ) + { + debug() << "After xine_open() *****" << endl; + + #ifndef XINE_SAFE_MODE + //we must ensure the scope is pruned of old buffers + timerEvent( 0 ); + + xine_post_out_t *source = xine_get_audio_source( m_stream ); + xine_post_in_t *target = (xine_post_in_t*)xine_post_input( m_post, const_cast<char*>("audio in") ); + xine_post_wire( source, target ); + #endif + + playlistChanged(); + + return true; + } + else + { + #ifdef XINE_PARAM_GAPLESS_SWITCH + if ( xine_check_version(1,1,1) && !(m_xfadeLength > 0) ) + xine_set_param( m_stream, XINE_PARAM_GAPLESS_SWITCH, 0); + #endif + } + + // FAILURE to load! + //s_fader will delete itself + determineAndShowErrorMessage(); + + return false; +} + +bool +XineEngine::play( uint offset ) +{ + DEBUG_BLOCK + + if( !ensureStream() ) + return false; + + const bool has_audio = xine_get_stream_info( m_stream, XINE_STREAM_INFO_HAS_AUDIO ); + const bool audio_handled = xine_get_stream_info( m_stream, XINE_STREAM_INFO_AUDIO_HANDLED ); + + if (has_audio && audio_handled && xine_play( m_stream, 0, offset )) + { + if( s_fader ) + s_fader->start( QThread::LowestPriority ); + + emit stateChanged( Engine::Playing ); + + return true; + } + + //we need to stop the track that is prepped for crossfade + delete s_fader; + + emit stateChanged( Engine::Empty ); + + determineAndShowErrorMessage(); + + xine_close( m_stream ); + + return false; +} + +#include "statusbar/statusbar.h" + +void +XineEngine::determineAndShowErrorMessage() +{ + DEBUG_BLOCK + + QString body; + + debug() << "xine_get_error()\n"; + switch (xine_get_error( m_stream )) { + case XINE_ERROR_NO_INPUT_PLUGIN: + body = i18n("No suitable input plugin. This often means that the url's protocol is not supported. Network failures are other possible causes."); + break; + + case XINE_ERROR_NO_DEMUX_PLUGIN: + body = i18n("No suitable demux plugin. This often means that the file format is not supported."); + break; + + case XINE_ERROR_DEMUX_FAILED: + body = i18n("Demuxing failed."); + break; + + case XINE_ERROR_INPUT_FAILED: + body = i18n("Could not open file."); + break; + + case XINE_ERROR_MALFORMED_MRL: + body = i18n("The location is malformed."); + break; + + case XINE_ERROR_NONE: + // xine is thick. xine doesn't think there is an error + // but there may be! We check for other errors below. + + default: + if (!xine_get_stream_info( m_stream, XINE_STREAM_INFO_AUDIO_HANDLED )) + { + // xine can read the plugin but it didn't find any codec + // THUS xine=daft for telling us it could handle the format in canDecode! + body = i18n("There is no available decoder."); + QString const ext = Amarok::extension( m_url.url() ).lower(); + if (ext == "mp3" && EngineController::installDistroCodec( "xine-engine" )) + return; + } + else if (!xine_get_stream_info( m_stream, XINE_STREAM_INFO_HAS_AUDIO )) + body = i18n("There is no audio channel!"); + break; + } + + Amarok::StatusBar::instance()->longMessage( + "<b>" + i18n("Error Loading Media") + "</b><p>" + body + "<p>" + m_url.prettyURL(), + KDE::StatusBar::Error ); +} + +void +XineEngine::stop() +{ + if( s_fader && s_fader->running() ) + s_fader->resume(); // safety call if the engine is in the pause state + + if ( !m_stream ) + return; + + if( AmarokConfig::fadeout() && !m_fadeOutRunning || state() == Engine::Paused ) + { + s_outfader = new OutFader( this, AmarokConfig::fadeoutLength() ); + s_outfader->start(); + ::usleep( 100 ); //to be sure engine state won't be changed before it is checked in fadeOut() + m_url = KURL(); //to ensure we return Empty from state() + + std::fill( m_scope.begin(), m_scope.end(), 0 ); + } + else if( !m_fadeOutRunning ) + { + xine_stop( m_stream ); + xine_close( m_stream ); + xine_set_param( m_stream, XINE_PARAM_AUDIO_CLOSE_DEVICE, 1); + } + + emit stateChanged( Engine::Empty ); +} + +void +XineEngine::pause() +{ + if ( !m_stream ) + return; + + if( xine_get_param( m_stream, XINE_PARAM_SPEED ) != XINE_SPEED_PAUSE ) + { + if( s_fader && s_fader->running() ) + s_fader->pause(); + + xine_set_param( m_stream, XINE_PARAM_SPEED, XINE_SPEED_PAUSE ); + xine_set_param( m_stream, XINE_PARAM_AUDIO_CLOSE_DEVICE, 1); + emit stateChanged( Engine::Paused ); + + } +} + +void +XineEngine::unpause() +{ + if ( !m_stream ) + return; + + if( xine_get_param( m_stream, XINE_PARAM_SPEED ) == XINE_SPEED_PAUSE ) + { + if( s_fader && s_fader->running() ) + s_fader->resume(); + + xine_set_param( m_stream, XINE_PARAM_SPEED, XINE_SPEED_NORMAL ); + emit stateChanged( Engine::Playing ); + } +} + +Engine::State +XineEngine::state() const +{ + if ( !m_stream || m_fadeOutRunning ) + return Engine::Empty; + + switch( xine_get_status( m_stream ) ) + { + case XINE_STATUS_PLAY: return xine_get_param( m_stream, XINE_PARAM_SPEED ) != XINE_SPEED_PAUSE ? Engine::Playing : Engine::Paused; + case XINE_STATUS_IDLE: return Engine::Empty; + case XINE_STATUS_STOP: + default: return m_url.isEmpty() ? Engine::Empty : Engine::Idle; + } +} + +uint +XineEngine::position() const +{ + if ( state() == Engine::Empty ) + return 0; + + int pos; + int time = 0; + int length; + + // Workaround for problems when you seek too quickly, see BUG 99808 + int tmp = 0, i = 0; + while( ++i < 4 ) + { + xine_get_pos_length( m_stream, &pos, &time, &length ); + if( time > tmp ) break; + usleep( 100000 ); + } + + // Here we check for new metadata periodically, because xine does not emit an event + // in all cases (e.g. with ogg streams). See BUG 122505 + if ( state() != Engine::Idle && state() != Engine::Empty ) + { + const Engine::SimpleMetaBundle bundle = fetchMetaData(); + if( bundle.title != m_currentBundle.title || bundle.artist != m_currentBundle.artist ) { + debug() << "Metadata received." << endl; + m_currentBundle = bundle; + + XineEngine* p = const_cast<XineEngine*>( this ); + p->emit metaData( bundle ); + } + } + + return time; +} + +uint +XineEngine::length() const +{ + if ( !m_stream ) + return 0; + + // xine often delivers nonsense values for VBR files and such, so we only + // use the length for remote files + + if( m_url.isLocalFile() ) + return 0; + + else { + int pos; + int time; + int length = 0; + + xine_get_pos_length( m_stream, &pos, &time, &length ); + if( length < 0 ) + length=0; + + return length; + } +} + +void +XineEngine::seek( uint ms ) +{ + if( !ensureStream() ) + return; + + if( xine_get_param( m_stream, XINE_PARAM_SPEED ) == XINE_SPEED_PAUSE ) { + // FIXME this is a xine API issue really, they need to add a seek function + xine_play( m_stream, 0, (int)ms ); + xine_set_param( m_stream, XINE_PARAM_SPEED, XINE_SPEED_PAUSE ); + } + else + xine_play( m_stream, 0, (int)ms ); +} + +void +XineEngine::setVolumeSW( uint vol ) +{ + if ( !m_stream ) + return; + if( !s_fader ) + xine_set_param( m_stream, XINE_PARAM_AUDIO_AMP_LEVEL, static_cast<uint>( vol * m_preamp ) ); +} + +void +XineEngine::fadeOut( uint fadeLength, bool* terminate, bool exiting ) +{ + if( m_fadeOutRunning ) //Let us not start another fadeout... + return; + + m_fadeOutRunning = !m_fadeOutRunning; + const bool isPlaying = m_stream && ( xine_get_status( m_stream ) == XINE_STATUS_PLAY ); + const float originalVol = Engine::Base::makeVolumeLogarithmic( m_volume ) * m_preamp; + + // On shutdown, limit fadeout to 3 secs max, so that we don't risk getting killed + const int length = exiting ? QMIN( fadeLength, 3000 ) : fadeLength; + + if( length > 0 && isPlaying ) + { + // fader-class doesn't work in this spot as is, so some parts need to be copied here... (ugly) + uint stepsCount = length < 1000 ? length / 10 : 100; + uint stepSizeUs = (int)( 1000.0 * (float)length / (float)stepsCount ); + + ::usleep( stepSizeUs ); + QTime t; + t.start(); + float mix = 0.0; + while ( mix < 1.0 ) + { + if( *terminate ) break; + + ::usleep( stepSizeUs ); + float vol = Engine::Base::makeVolumeLogarithmic( m_volume ) * m_preamp; + float mix = (float)t.elapsed() / (float)length; + if ( mix > 1.0 ) + { + break; + } + if ( m_stream ) + { + float v = 4.0 * (1.0 - mix) / 3.0; + xine_set_param( m_stream, XINE_PARAM_AUDIO_AMP_LEVEL, (uint)( v < 1.0 ? vol * v : vol ) ); + } + } + } + if( m_fadeOutRunning && m_stream ) + xine_set_param( m_stream, XINE_PARAM_AUDIO_AMP_LEVEL, (uint) originalVol ); + m_fadeOutRunning = !m_fadeOutRunning; +} + +void +XineEngine::setEqualizerEnabled( bool enable ) +{ + if ( !m_stream ) + return; + + m_equalizerEnabled = enable; + + if( !enable ) { + QValueList<int> gains; + for( uint x = 0; x < 10; x++ ) + gains << -101; // sets eq gains to zero. + + setEqualizerParameters( 0, gains ); + } +} + +/* + sets the eq params for xine engine - have to rescale eq params to fitting range (adapted from kaffeine and xfmedia) + + preamp + pre: (-100..100) + post: (0.1..1.9) - this is not really a preamp but we use the xine preamp parameter for our normal volume. so we make a postamp. + + gains + pre: (-100..100) + post: (1..200) - (1 = down, 100 = middle, 200 = up, 0 = off) + */ +void +XineEngine::setEqualizerParameters( int preamp, const QValueList<int> &gains ) +{ + if ( !m_stream ) + return; + + m_equalizerGains = gains; + m_intPreamp = preamp; + QValueList<int>::ConstIterator it = gains.begin(); + + xine_set_param( m_stream, XINE_PARAM_EQ_30HZ, int( (*it )*0.995 + 100 ) ); + xine_set_param( m_stream, XINE_PARAM_EQ_60HZ, int( (*++it)*0.995 + 100 ) ); + xine_set_param( m_stream, XINE_PARAM_EQ_125HZ, int( (*++it)*0.995 + 100 ) ); + xine_set_param( m_stream, XINE_PARAM_EQ_250HZ, int( (*++it)*0.995 + 100 ) ); + xine_set_param( m_stream, XINE_PARAM_EQ_500HZ, int( (*++it)*0.995 + 100 ) ); + xine_set_param( m_stream, XINE_PARAM_EQ_1000HZ, int( (*++it)*0.995 + 100 ) ); + xine_set_param( m_stream, XINE_PARAM_EQ_2000HZ, int( (*++it)*0.995 + 100 ) ); + xine_set_param( m_stream, XINE_PARAM_EQ_4000HZ, int( (*++it)*0.995 + 100 ) ); + xine_set_param( m_stream, XINE_PARAM_EQ_8000HZ, int( (*++it)*0.995 + 100 ) ); + xine_set_param( m_stream, XINE_PARAM_EQ_16000HZ, int( (*++it)*0.995 + 100 ) ); + + m_preamp = ( preamp - 0.1 * preamp + 100 ) / 100.0; + setVolume( m_volume ); +} + +bool +XineEngine::canDecode( const KURL &url ) const +{ + static QStringList list; + if(list.isEmpty()) + { + char* exts = xine_get_file_extensions( m_xine ); + list = QStringList::split( ' ', exts ); + free( exts ); exts = NULL; + //images + list.remove("png"); + list.remove("jpg"); + list.remove("jpeg"); + list.remove("gif"); + list.remove("ilbm"); + list.remove("iff"); + //subtitles + list.remove("asc"); + list.remove("txt"); + list.remove("sub"); + list.remove("srt"); + list.remove("smi"); + list.remove("ssa"); +//HACK we also check for m4a because xine plays them but +//for some reason doesn't return the extension + if(!list.contains("m4a")) + list << "m4a"; + } + + if (url.protocol() == "cdda") + // play audio CDs pls + return true; + + QString path = url.path(); + + // partial downloads from Konqi and other browsers + // tend to have a .part extension + if (path.endsWith( ".part" )) + path = path.left( path.length() - 5 ); + + const QString ext = path.mid( path.findRev( '.' ) + 1 ).lower(); + + return list.contains( ext ); +} + +const Engine::Scope& +XineEngine::scope() +{ + if( !m_post || !m_stream || xine_get_status( m_stream ) != XINE_STATUS_PLAY ) + return m_scope; + + MyNode* const myList = scope_plugin_list( m_post ); + metronom_t* const myMetronom = scope_plugin_metronom( m_post ); + const int myChannels = scope_plugin_channels( m_post ); + int scopeidx = 0; + + if (myChannels > 2) + return m_scope; + + //prune the buffer list and update m_currentVpts + timerEvent( 0 ); + + for( int n, frame = 0; frame < 512; ) + { + MyNode *best_node = 0; + + for( MyNode *node = myList->next; node != myList; node = node->next, Log::bufferCount++ ) + if( node->vpts <= m_currentVpts && (!best_node || node->vpts > best_node->vpts) ) + best_node = node; + + if( !best_node || best_node->vpts_end < m_currentVpts ) { + Log::noSuitableBuffer++; break; } + + int64_t + diff = m_currentVpts; + diff -= best_node->vpts; + diff *= 1<<16; + diff /= myMetronom->pts_per_smpls; + + const int16_t* + data16 = best_node->mem; + data16 += diff; + + diff += diff % myChannels; //important correction to ensure we don't overflow the buffer + diff /= myChannels; //use units of frames, not samples + + //calculate the number of available samples in this buffer + n = best_node->num_frames; + n -= diff; + n += frame; //clipping for # of frames we need + + if( n > 512 ) + n = 512; //we don't want more than 512 frames + + for( int a, c; frame < n; ++frame, data16 += myChannels ) { + for( a = c = 0; c < myChannels; ++c ) + { + // we now give interleaved pcm to the scope + m_scope[scopeidx++] = data16[c]; + if (myChannels == 1) // duplicate mono samples + m_scope[scopeidx++] = data16[c]; + } + } + + m_currentVpts = best_node->vpts_end; + m_currentVpts++; //FIXME needs to be done for some reason, or you get situations where it uses same buffer again and again + } + + Log::scopeCallCount++; + + return m_scope; +} + +void +XineEngine::timerEvent( QTimerEvent* ) +{ + if ( !m_stream ) + return; + + //here we prune the buffer list regularly + + MyNode *myList = scope_plugin_list( m_post ); + + if ( ! myList ) return; + + //we operate on a subset of the list for thread-safety + MyNode * const first_node = myList->next; + MyNode const * const list_end = myList; + + m_currentVpts = (xine_get_status( m_stream ) == XINE_STATUS_PLAY) + ? xine_get_current_vpts( m_stream ) + : LLONG_MAX; //if state is not playing OR paused, empty the list + //: std::numeric_limits<int64_t>::max(); //TODO don't support crappy gcc 2.95 + + for( MyNode *prev = first_node, *node = first_node->next; node != list_end; node = node->next ) + { + //we never delete first_node + //this maintains thread-safety + if( node->vpts_end < m_currentVpts ) { + prev->next = node->next; + + free( node->mem ); + free( node ); + + node = prev; + } + + prev = node; + } +} + +Amarok::PluginConfig* +XineEngine::configure() const +{ + XineConfigDialog* xcf = new XineConfigDialog( m_xine ); + connect(xcf, SIGNAL( settingsSaved() ), this, SLOT( configChanged() )); + connect(this, SIGNAL( resetConfig(xine_t*) ), xcf, SLOT( reset(xine_t*) )); + return xcf; +} + +void +XineEngine::customEvent( QCustomEvent *e ) +{ + #define message static_cast<QString*>(e->data()) + + switch( e->type() ) + { + case 3000: //XINE_EVENT_UI_PLAYBACK_FINISHED + emit trackEnded(); + break; + + case 3001: + emit infoMessage( (*message).arg( m_url.prettyURL() ) ); + delete message; + break; + + case 3002: + emit statusText( *message ); + delete message; + break; + + case 3003: { //meta info has changed + debug() << "Metadata received." << endl; + const Engine::SimpleMetaBundle bundle = fetchMetaData(); + m_currentBundle = bundle; + emit metaData( bundle ); + } break; + + case 3004: + emit statusText( i18n("Redirecting to: ").arg( *message ) ); + load( KURL( *message ), false ); + play(); + delete message; + break; + case 3005: + emit lastFmTrackChange(); + break; + default: + ; + } + + #undef message +} +//SLOT +void XineEngine::configChanged() +{ + //reset xine to load new audio plugin + if( m_currentAudioPlugin != XineCfg::outputPlugin() ) + { + stop(); + xine_config_save( m_xine, configPath() ); + if( m_stream ) xine_close( m_stream ); + if( m_eventQueue ) xine_event_dispose_queue( m_eventQueue ); + m_eventQueue = NULL; + if( m_stream ) xine_dispose( m_stream ); + m_stream = NULL; + if( m_audioPort ) xine_close_audio_driver( m_xine, m_audioPort ); + m_audioPort = NULL; + if( m_post ) xine_post_dispose( m_xine, m_post ); + m_post = NULL; + if( m_xine ) xine_exit( m_xine ); + m_xine = NULL; + init(); + setEqualizerEnabled( m_equalizerEnabled ); + if( m_equalizerEnabled ) + setEqualizerParameters( m_intPreamp, m_equalizerGains ); + emit resetConfig(m_xine); + } +} + +//SLOT +void +XineEngine::playlistChanged() +{ + #ifdef XINE_PARAM_EARLY_FINISHED_EVENT + #ifdef XINE_PARAM_GAPLESS_SWITCH + if ( xine_check_version(1,1,1) && !(m_xfadeLength > 0) + && m_url.isLocalFile() && Playlist::instance()->isTrackAfter() ) + { + xine_set_param(m_stream, XINE_PARAM_EARLY_FINISHED_EVENT, 1 ); + debug() << "XINE_PARAM_EARLY_FINISHED_EVENT enabled" << endl; + } + else + { + //we don't want an early finish event if there is no track after the current one + xine_set_param(m_stream, XINE_PARAM_EARLY_FINISHED_EVENT, 0 ); + debug() << "XINE_PARAM_EARLY_FINISHED_EVENT disabled" << endl; + } + #endif + #endif +} + +static time_t last_error_time = 0; // hysteresis on xine errors +static int last_error = XINE_MSG_NO_ERROR; + +void +XineEngine::XineEventListener( void *p, const xine_event_t* xineEvent ) +{ + time_t current; + + if( !p ) return; + + #define xe static_cast<XineEngine*>(p) + + switch( xineEvent->type ) + { + case XINE_EVENT_UI_SET_TITLE: + + debug() << "XINE_EVENT_UI_SET_TITLE\n"; + + QApplication::postEvent( xe, new QCustomEvent( 3003 ) ); + + break; + + case XINE_EVENT_UI_PLAYBACK_FINISHED: + debug() << "XINE_EVENT_UI_PLAYBACK_FINISHED\n"; + + #ifdef XINE_PARAM_GAPLESS_SWITCH + if ( xine_check_version(1,1,1) && xe->m_url.isLocalFile() //Remote media break with gapless + //don't prepare for a track that isn't coming + && Playlist::instance() + && Playlist::instance()->isTrackAfter() + && !AmarokConfig::crossfade() ) + xine_set_param( xe->m_stream, XINE_PARAM_GAPLESS_SWITCH, 1); + #endif + //emit signal from GUI thread + QApplication::postEvent( xe, new QCustomEvent(3000) ); + break; + + case XINE_EVENT_PROGRESS: { + xine_progress_data_t* pd = (xine_progress_data_t*)xineEvent->data; + + QString + msg = "%1 %2%"; + msg = msg.arg( QString::fromUtf8( pd->description ) ) + .arg( KGlobal::locale()->formatNumber( pd->percent, 0 ) ); + + QCustomEvent *e = new QCustomEvent( 3002 ); + e->setData( new QString( msg ) ); + + QApplication::postEvent( xe, e ); + + } break; + + case XINE_EVENT_MRL_REFERENCE: { + /// xine has read the stream and found it actually links to something else + /// so we need to play that instead + + QString message = QString::fromUtf8( static_cast<xine_mrl_reference_data_t*>(xineEvent->data)->mrl ); + QCustomEvent *e = new QCustomEvent( 3004 ); + e->setData( new QString( message ) ); + + QApplication::postEvent( xe, e ); + + } break; + + case XINE_EVENT_UI_MESSAGE: + { + debug() << "message received from xine\n"; + + xine_ui_message_data_t *data = (xine_ui_message_data_t *)xineEvent->data; + QString message; + + switch( data->type ) + { + case XINE_MSG_NO_ERROR: + { + //series of \0 separated strings, terminated with a \0\0 + char str[2000]; + char *p = str; + for( char *msg = data->messages; !(*msg == '\0' && *(msg+1) == '\0'); ++msg, ++p ) + *p = *msg == '\0' ? '\n' : *msg; + *p = '\0'; + + debug() << str << endl; + + break; + } + + case XINE_MSG_ENCRYPTED_SOURCE: + break; + + case XINE_MSG_UNKNOWN_HOST: + message = i18n("The host is unknown for the URL: <i>%1</i>"); goto param; + case XINE_MSG_UNKNOWN_DEVICE: + message = i18n("The device name you specified seems invalid."); goto param; + case XINE_MSG_NETWORK_UNREACHABLE: + message = i18n("The network appears unreachable."); goto param; + case XINE_MSG_AUDIO_OUT_UNAVAILABLE: + message = i18n("Audio output unavailable; the device is busy."); goto param; + case XINE_MSG_CONNECTION_REFUSED: + message = i18n("The connection was refused for the URL: <i>%1</i>"); goto param; + case XINE_MSG_FILE_NOT_FOUND: + message = i18n("xine could not find the URL: <i>%1</i>"); goto param; + case XINE_MSG_PERMISSION_ERROR: + message = i18n("Access was denied for the URL: <i>%1</i>"); goto param; + case XINE_MSG_READ_ERROR: + message = i18n("The source cannot be read for the URL: <i>%1</i>"); goto param; + case XINE_MSG_LIBRARY_LOAD_ERROR: + message = i18n("A problem occurred while loading a library or decoder."); goto param; + + case XINE_MSG_GENERAL_WARNING: + message = i18n("General Warning"); goto explain; + case XINE_MSG_SECURITY: + message = i18n("Security Warning"); goto explain; + default: + message = i18n("Unknown Error"); goto explain; + + + explain: + + // Don't flood the user with error messages + if( (last_error_time + 10) > time( ¤t ) && + data->type == last_error ) + { + last_error_time = current; + return; + } + last_error_time = current; + last_error = data->type; + + if( data->explanation ) + { + message.prepend( "<b>" ); + message += "</b>:<p>"; + message += QString::fromUtf8( (char*)data + data->explanation ); + } + else break; //if no explanation then why bother! + + //FALL THROUGH + + param: + + // Don't flood the user with error messages + if((last_error_time + 10) > time(¤t) && + data->type == last_error) + { + last_error_time = current; + return; + } + last_error_time = current; + last_error = data->type; + + message.prepend( "<p>" ); + message += "<p>"; + + if(data->explanation) + { + message += "xine parameters: <i>"; + message += QString::fromUtf8( (char*)data + data->parameters ); + message += "</i>"; + } + else message += i18n("Sorry, no additional information is available."); + + QApplication::postEvent( xe, new QCustomEvent(QEvent::Type(3001), new QString(message)) ); + } + + } //case + case XINE_EVENT_UI_CHANNELS_CHANGED: //Flameeyes used this for last.fm track changes + QApplication::postEvent( xe, new QCustomEvent(QEvent::Type(3005) ) ); + break; + } //switch + + #undef xe +} + +Engine::SimpleMetaBundle +XineEngine::fetchMetaData() const +{ + Engine::SimpleMetaBundle bundle; + bundle.title = QString::fromUtf8( xine_get_meta_info( m_stream, XINE_META_INFO_TITLE ) ); + bundle.artist = QString::fromUtf8( xine_get_meta_info( m_stream, XINE_META_INFO_ARTIST ) ); + bundle.album = QString::fromUtf8( xine_get_meta_info( m_stream, XINE_META_INFO_ALBUM ) ); + bundle.comment = QString::fromUtf8( xine_get_meta_info( m_stream, XINE_META_INFO_COMMENT ) ); + bundle.genre = QString::fromUtf8( xine_get_meta_info( m_stream, XINE_META_INFO_GENRE ) ); + bundle.bitrate = QString::number( xine_get_stream_info( m_stream, XINE_STREAM_INFO_AUDIO_BITRATE ) / 1000 ); + bundle.samplerate = QString::number( xine_get_stream_info( m_stream, XINE_STREAM_INFO_AUDIO_SAMPLERATE ) ); + bundle.year = QString::fromUtf8( xine_get_meta_info( m_stream, XINE_META_INFO_YEAR ) ); + bundle.tracknr = QString::fromUtf8( xine_get_meta_info( m_stream, XINE_META_INFO_TRACK_NUMBER ) ); + + return bundle; +} + +bool XineEngine::metaDataForUrl(const KURL &url, Engine::SimpleMetaBundle &b) +{ + bool result = false; + xine_stream_t* tmpstream = xine_stream_new(m_xine, NULL, NULL); + if (xine_open(tmpstream, QFile::encodeName(url.url()))) { + QString audioCodec = QString::fromUtf8(xine_get_meta_info(tmpstream, XINE_META_INFO_SYSTEMLAYER)); + + if (audioCodec == "CDDA") { + QString title = QString::fromUtf8( + xine_get_meta_info(tmpstream, XINE_META_INFO_TITLE)); + if ((!title.isNull()) && (!title.isEmpty())) { //no meta info + b.title = title; + b.artist = + QString::fromUtf8( + xine_get_meta_info(tmpstream, XINE_META_INFO_ARTIST)); + b.album = + QString::fromUtf8( + xine_get_meta_info(tmpstream, XINE_META_INFO_ALBUM)); + b.genre = + QString::fromUtf8( + xine_get_meta_info(tmpstream, XINE_META_INFO_GENRE)); + b.year = + QString::fromUtf8( + xine_get_meta_info(tmpstream, XINE_META_INFO_YEAR)); + b.tracknr = + QString::fromUtf8( + xine_get_meta_info(tmpstream, XINE_META_INFO_TRACK_NUMBER)); + if( b.tracknr.isEmpty() ) + b.tracknr = url.filename(); + } else { + b.title = QString(i18n("Track %1")).arg(url.filename()); + b.album = i18n("AudioCD"); + } + } + + if (audioCodec == "CDDA" || audioCodec == "WAV") { + result = true; + int samplerate = xine_get_stream_info( tmpstream, XINE_STREAM_INFO_AUDIO_SAMPLERATE ); + + // xine would provide a XINE_STREAM_INFO_AUDIO_BITRATE, but unfortunately not for CDDA or WAV + // so we calculate the bitrate by our own + int bitsPerSample = xine_get_stream_info( tmpstream, XINE_STREAM_INFO_AUDIO_BITS ); + int nbrChannels = xine_get_stream_info( tmpstream, XINE_STREAM_INFO_AUDIO_CHANNELS ); + int bitrate = (samplerate * bitsPerSample * nbrChannels) / 1000; + + b.bitrate = QString::number(bitrate); + b.samplerate = QString::number(samplerate); + int pos, time, length = 0; + xine_get_pos_length(tmpstream, &pos, &time, &length); + b.length = QString::number(length / 1000); + } + xine_close(tmpstream); + } + xine_dispose(tmpstream); + return result; +} + +bool XineEngine::getAudioCDContents(const QString &device, KURL::List &urls) +{ + char **xine_urls = NULL; + int num; + int i = 0; + + if (!device.isNull()) { + debug() << "xine-engine setting CD Device to: " << device << endl; + xine_cfg_entry_t config; + if (!xine_config_lookup_entry(m_xine, "input.cdda_device", &config)) { + emit statusText(i18n("Failed CD device lookup in xine engine")); + return false; + } + config.str_value = (char *)device.latin1(); + xine_config_update_entry(m_xine, &config); + } + + emit statusText(i18n("Getting AudioCD contents...")); + + xine_urls = xine_get_autoplay_mrls(m_xine, "CD", &num); + + if (xine_urls) { + while (xine_urls[i]) { + urls << KURL(xine_urls[i]); + ++i; + } + } + else emit statusText(i18n("Could not read AudioCD")); + + return true; +} + +bool XineEngine::flushBuffer() +{ + return false; +} + +bool XineEngine::lastFmProxyRequired() +{ + return !( xine_check_version(1,1,9) ); +} + +////////////////////////////////////////////////////////////////////////////// +/// class Fader +////////////////////////////////////////////////////////////////////////////// + +Fader::Fader( XineEngine *engine, uint fadeMs ) + : QObject( engine ) + , QThread() + , m_engine( engine ) + , m_xine( engine->m_xine ) + , m_decrease( engine->m_stream ) + , m_increase( 0 ) + , m_port( engine->m_audioPort ) + , m_post( engine->m_post ) + , m_fadeLength( fadeMs ) + , m_paused( false ) + , m_terminated( false ) +{ + DEBUG_BLOCK + + if( engine->makeNewStream() ) + { + m_increase = engine->m_stream; + + xine_set_param( m_increase, XINE_PARAM_AUDIO_AMP_LEVEL, 0 ); + } + else { + s_fader = 0; + deleteLater(); + } +} + +Fader::~Fader() +{ + DEBUG_BLOCK + + wait(); + + xine_close( m_decrease ); + xine_dispose( m_decrease ); + xine_close_audio_driver( m_xine, m_port ); + if( m_post ) xine_post_dispose( m_xine, m_post ); + + if( !m_engine->m_stopFader ) + m_engine->setVolume( m_engine->volume() ); + + m_engine->m_stopFader = false; + s_fader = 0; +} + +void +Fader::run() +{ + DEBUG_BLOCK + + // do a volume change in 100 steps (or every 10ms) + uint stepsCount = m_fadeLength < 1000 ? m_fadeLength / 10 : 100; + uint stepSizeUs = (int)( 1000.0 * (float)m_fadeLength / (float)stepsCount ); + + float mix = 0.0; + float elapsedUs = 0.0; + while ( mix < 1.0 ) + { + if ( m_terminated ) + break; + // sleep a constant amount of time + QThread::usleep( stepSizeUs ); + + if ( m_paused ) + continue; + + elapsedUs += stepSizeUs; + + // get volume (amarok main * equalizer preamp) + float vol = Engine::Base::makeVolumeLogarithmic( m_engine->m_volume ) * m_engine->m_preamp; + + // compute the mix factor as the percentage of time spent since fade begun + float mix = (elapsedUs / 1000.0) / (float)m_fadeLength; + if ( mix > 1.0 ) + { + if ( m_increase ) + xine_set_param( m_increase, XINE_PARAM_AUDIO_AMP_LEVEL, (uint)vol ); + break; + } + + // change volume of streams (using dj-like cross-fade profile) + if ( m_decrease ) + { + //xine_set_param( m_decrease, XINE_PARAM_AUDIO_AMP_LEVEL, (uint)(vol * (1.0 - mix)) ); // linear + float v = 4.0 * (1.0 - mix) / 3.0; + xine_set_param( m_decrease, XINE_PARAM_AUDIO_AMP_LEVEL, (uint)( v < 1.0 ? vol * v : vol ) ); + } + if ( m_increase ) + { + //xine_set_param( m_increase, XINE_PARAM_AUDIO_AMP_LEVEL, (uint)(vol * mix) ); //linear + float v = 4.0 * mix / 3.0; + xine_set_param( m_increase, XINE_PARAM_AUDIO_AMP_LEVEL, (uint)( v < 1.0 ? vol * v : vol ) ); + } + } + + //stop using cpu! + xine_stop( m_decrease ); + + deleteLater(); +} + +void +Fader::pause() +{ + m_paused = true; +} + +void +Fader::resume() +{ + m_paused = false; +} + +void +Fader::finish() +{ + DEBUG_BLOCK + m_terminated = true; +} + +////////////////////////////////////////////////////////////////////////////// +/// class OutFader +////////////////////////////////////////////////////////////////////////////// + +OutFader::OutFader( XineEngine *engine, uint fadeLength ) + : QObject( engine ) + , QThread() + , m_engine( engine ) + , m_terminated( false ) + , m_fadeLength( fadeLength ) +{ + DEBUG_BLOCK +} + +OutFader::~OutFader() +{ + DEBUG_BLOCK + + wait(); + + s_outfader = 0; +} + +void +OutFader::run() +{ + DEBUG_BLOCK + + m_engine->fadeOut( m_fadeLength, &m_terminated ); + + xine_stop( m_engine->m_stream ); + xine_close( m_engine->m_stream ); + xine_set_param( m_engine->m_stream, XINE_PARAM_AUDIO_CLOSE_DEVICE, 1); + + deleteLater(); +} + +void +OutFader::finish() +{ + DEBUG_BLOCK + m_terminated = true; +} + +#include "xine-engine.moc" |