2021-04-11 17:22:13 +02:00
/**
* SPDX - FileCopyrightText : 2017 ( c ) Matthieu Gallien < matthieu_gallien @ yahoo . fr >
* SPDX - FileCopyrightText : 2021 Bart De Vries < bart @ mogwai . be >
*
* SPDX - License - Identifier : LGPL - 3.0 - or - later
2021-04-11 15:26:28 +02:00
*/
# include "audiomanager.h"
2021-04-12 22:36:17 +02:00
# include <algorithm>
2021-04-11 15:26:28 +02:00
# include <QTimer>
# include <QAudio>
2021-04-11 18:55:09 +02:00
# include <QEventLoop>
2021-04-11 15:26:28 +02:00
2021-04-11 21:08:25 +02:00
# include "powermanagementinterface.h"
# include "datamanager.h"
# include "settingsmanager.h"
2021-04-12 22:18:04 +02:00
static const double MAX_RATE = 1.0 ;
static const double MIN_RATE = 2.5 ;
static const qint64 SKIP_STEP = 10000 ;
2021-04-11 15:26:28 +02:00
class AudioManagerPrivate
{
private :
PowerManagementInterface mPowerInterface ;
2021-04-11 18:11:36 +02:00
QMediaPlayer m_player ;
2021-04-11 15:26:28 +02:00
2021-04-11 18:11:36 +02:00
Entry * m_entry = nullptr ;
2021-04-12 22:18:04 +02:00
bool m_readyToPlay = false ;
2021-04-13 14:31:52 +02:00
bool m_isSeekable = false ;
2021-04-12 22:18:04 +02:00
bool m_lockPositionSaving = false ; // sort of lock mutex to prevent updating the player position while changing sources (which will emit lots of playerPositionChanged signals)
2021-04-11 15:26:28 +02:00
friend class AudioManager ;
} ;
AudioManager : : AudioManager ( QObject * parent ) : QObject ( parent ) , d ( std : : make_unique < AudioManagerPrivate > ( ) )
{
2021-04-11 18:11:36 +02:00
connect ( & d - > m_player , & QMediaPlayer : : mutedChanged , this , & AudioManager : : playerMutedChanged ) ;
connect ( & d - > m_player , & QMediaPlayer : : volumeChanged , this , & AudioManager : : playerVolumeChanged ) ;
2021-04-11 23:07:21 +02:00
connect ( & d - > m_player , & QMediaPlayer : : mediaChanged , this , & AudioManager : : sourceChanged ) ;
2021-04-11 18:11:36 +02:00
connect ( & d - > m_player , & QMediaPlayer : : mediaStatusChanged , this , & AudioManager : : statusChanged ) ;
connect ( & d - > m_player , & QMediaPlayer : : mediaStatusChanged , this , & AudioManager : : mediaStatusChanged ) ;
connect ( & d - > m_player , & QMediaPlayer : : stateChanged , this , & AudioManager : : playbackStateChanged ) ;
connect ( & d - > m_player , & QMediaPlayer : : stateChanged , this , & AudioManager : : playerStateChanged ) ;
connect ( & d - > m_player , & QMediaPlayer : : playbackRateChanged , this , & AudioManager : : playbackRateChanged ) ;
connect ( & d - > m_player , QOverload < QMediaPlayer : : Error > : : of ( & QMediaPlayer : : error ) , this , & AudioManager : : errorChanged ) ;
connect ( & d - > m_player , & QMediaPlayer : : durationChanged , this , & AudioManager : : durationChanged ) ;
connect ( & d - > m_player , & QMediaPlayer : : positionChanged , this , & AudioManager : : positionChanged ) ;
2021-04-11 20:30:12 +02:00
connect ( & d - > m_player , & QMediaPlayer : : positionChanged , this , & AudioManager : : savePlayPosition ) ;
2021-04-16 21:42:27 +02:00
2021-04-15 22:51:40 +02:00
connect ( & DataManager : : instance ( ) , & DataManager : : queueEntryMoved , this , & AudioManager : : canGoNextChanged ) ;
2021-04-16 21:42:27 +02:00
connect ( & DataManager : : instance ( ) , & DataManager : : queueEntryAdded , this , & AudioManager : : canGoNextChanged ) ;
connect ( & DataManager : : instance ( ) , & DataManager : : queueEntryRemoved , this , & AudioManager : : canGoNextChanged ) ;
2021-04-13 14:31:52 +02:00
// we'll send custom seekableChanged signal to work around QMediaPlayer glitches
2021-04-11 21:08:25 +02:00
// Check if an entry was playing when the program was shut down and restore it
2021-04-17 20:55:01 +02:00
if ( DataManager : : instance ( ) . lastPlayingEntry ( ) ! = QStringLiteral ( " none " ) )
setEntry ( DataManager : : instance ( ) . getEntry ( DataManager : : instance ( ) . lastPlayingEntry ( ) ) ) ;
2021-04-11 15:26:28 +02:00
}
AudioManager : : ~ AudioManager ( )
{
d - > mPowerInterface . setPreventSleep ( false ) ;
}
Entry * AudioManager : : entry ( ) const
{
2021-04-11 18:11:36 +02:00
return d - > m_entry ;
2021-04-11 15:26:28 +02:00
}
bool AudioManager : : muted ( ) const
{
2021-04-11 18:11:36 +02:00
return d - > m_player . isMuted ( ) ;
2021-04-11 15:26:28 +02:00
}
qreal AudioManager : : volume ( ) const
{
2021-04-11 18:11:36 +02:00
auto realVolume = static_cast < qreal > ( d - > m_player . volume ( ) / 100.0 ) ;
2021-04-11 15:26:28 +02:00
auto userVolume = static_cast < qreal > ( QAudio : : convertVolume ( realVolume , QAudio : : LinearVolumeScale , QAudio : : LogarithmicVolumeScale ) ) ;
return userVolume * 100.0 ;
}
QUrl AudioManager : : source ( ) const
{
2021-04-11 18:11:36 +02:00
return d - > m_player . media ( ) . request ( ) . url ( ) ;
2021-04-11 15:26:28 +02:00
}
QMediaPlayer : : Error AudioManager : : error ( ) const
{
2021-04-11 18:11:36 +02:00
if ( d - > m_player . error ( ) ! = QMediaPlayer : : NoError ) {
qDebug ( ) < < " AudioManager::error " < < d - > m_player . errorString ( ) ;
2021-04-11 15:26:28 +02:00
}
2021-04-11 18:11:36 +02:00
return d - > m_player . error ( ) ;
2021-04-11 15:26:28 +02:00
}
qint64 AudioManager : : duration ( ) const
{
2021-04-11 18:11:36 +02:00
return d - > m_player . duration ( ) ;
2021-04-11 15:26:28 +02:00
}
qint64 AudioManager : : position ( ) const
{
2021-04-11 18:11:36 +02:00
return d - > m_player . position ( ) ;
2021-04-11 15:26:28 +02:00
}
bool AudioManager : : seekable ( ) const
{
2021-04-13 14:31:52 +02:00
return d - > m_isSeekable ;
2021-04-11 15:26:28 +02:00
}
2021-04-11 23:07:21 +02:00
bool AudioManager : : canPlay ( ) const
{
2021-04-12 22:18:04 +02:00
return ( d - > m_readyToPlay ) ;
}
bool AudioManager : : canPause ( ) const
{
return ( d - > m_readyToPlay ) ;
}
2021-04-12 22:44:05 +02:00
bool AudioManager : : canSkipForward ( ) const
2021-04-12 22:18:04 +02:00
{
return ( d - > m_readyToPlay ) ;
}
2021-04-12 22:44:05 +02:00
bool AudioManager : : canSkipBackward ( ) const
2021-04-12 22:18:04 +02:00
{
return ( d - > m_readyToPlay ) ;
2021-04-11 23:07:21 +02:00
}
2021-04-11 15:26:28 +02:00
QMediaPlayer : : State AudioManager : : playbackState ( ) const
{
2021-04-11 18:11:36 +02:00
return d - > m_player . state ( ) ;
2021-04-11 15:26:28 +02:00
}
2021-04-11 17:13:44 +02:00
qreal AudioManager : : playbackRate ( ) const
{
2021-04-11 18:11:36 +02:00
return d - > m_player . playbackRate ( ) ;
2021-04-11 17:13:44 +02:00
}
2021-04-12 22:18:04 +02:00
qreal AudioManager : : minimumPlaybackRate ( ) const
{
return MIN_RATE ;
}
qreal AudioManager : : maximumPlaybackRate ( ) const
{
return MAX_RATE ;
}
2021-04-11 15:26:28 +02:00
QMediaPlayer : : MediaStatus AudioManager : : status ( ) const
{
2021-04-11 18:11:36 +02:00
return d - > m_player . mediaStatus ( ) ;
2021-04-11 15:26:28 +02:00
}
void AudioManager : : setEntry ( Entry * entry )
{
2021-04-14 11:32:57 +02:00
d - > m_lockPositionSaving = true ;
// First check if the previous track needs to be marked as read
// TODO: make grace time a setting in SettingsManager
if ( d - > m_entry ) {
qDebug ( ) < < " Checking previous track " ;
qDebug ( ) < < " Left time " < < ( duration ( ) - position ( ) ) ;
qDebug ( ) < < " MediaStatus " < < d - > m_player . mediaStatus ( ) ;
if ( ( ( duration ( ) - position ( ) ) < 15000 )
| | ( d - > m_player . mediaStatus ( ) = = QMediaPlayer : : EndOfMedia ) ) {
qDebug ( ) < < " Mark as read: " < < d - > m_entry - > title ( ) ;
d - > m_entry - > setRead ( true ) ;
d - > m_entry - > enclosure ( ) - > setPlayPosition ( 0 ) ;
2021-04-16 20:38:13 +02:00
d - > m_entry - > setQueueStatus ( false ) ; // i.e. remove from queue TODO: make this a choice in settings
2021-04-13 22:11:12 +02:00
}
2021-04-14 11:32:57 +02:00
}
if ( entry ! = nullptr ) {
2021-04-13 22:11:12 +02:00
qDebug ( ) < < " Going to change source " ;
2021-04-11 18:11:36 +02:00
d - > m_entry = entry ;
2021-04-14 11:32:57 +02:00
Q_EMIT entryChanged ( entry ) ;
2021-04-11 18:55:09 +02:00
d - > m_player . setMedia ( QUrl ( QStringLiteral ( " file:// " ) + d - > m_entry - > enclosure ( ) - > path ( ) ) ) ;
2021-04-12 09:40:18 +02:00
// save the current playing track in the settingsfile for restoring on startup
2021-04-17 20:55:01 +02:00
DataManager : : instance ( ) . setLastPlayingEntry ( d - > m_entry - > id ( ) ) ;
2021-04-11 20:30:12 +02:00
qDebug ( ) < < " Changed source to " < < d - > m_entry - > title ( ) ;
2021-04-11 18:55:09 +02:00
2021-04-11 20:30:12 +02:00
qint64 startingPosition = d - > m_entry - > enclosure ( ) - > playPosition ( ) ;
2021-04-11 18:55:09 +02:00
// What follows is a dirty hack to get the player positioned at the
// correct spot. The audio only becomes seekable when the player is
// actually playing. So we start the playback and then set a timer to
// wait until the stream becomes seekable; then switch position and
// immediately pause the playback.
// Unfortunately, this will produce an audible glitch with the current
// QMediaPlayer backend.
d - > m_player . play ( ) ;
2021-04-12 11:40:57 +02:00
if ( ! d - > m_player . isSeekable ( ) ) {
2021-04-11 18:55:09 +02:00
QEventLoop loop ;
QTimer timer ;
timer . setSingleShot ( true ) ;
timer . setInterval ( 2000 ) ;
loop . connect ( & timer , SIGNAL ( timeout ( ) ) , & loop , SLOT ( quit ( ) ) ) ;
loop . connect ( & d - > m_player , SIGNAL ( seekableChanged ( bool ) ) , & loop , SLOT ( quit ( ) ) ) ;
qDebug ( ) < < " Starting waiting loop " ;
loop . exec ( ) ;
}
2021-04-12 11:40:57 +02:00
if ( d - > m_player . mediaStatus ( ) ! = QMediaPlayer : : BufferedMedia ) {
QEventLoop loop ;
QTimer timer ;
timer . setSingleShot ( true ) ;
timer . setInterval ( 2000 ) ;
loop . connect ( & timer , SIGNAL ( timeout ( ) ) , & loop , SLOT ( quit ( ) ) ) ;
loop . connect ( & d - > m_player , SIGNAL ( mediaStatusChanged ( QMediaPlayer : : MediaStatus ) ) , & loop , SLOT ( quit ( ) ) ) ;
qDebug ( ) < < " Starting waiting loop on media status " < < d - > m_player . mediaStatus ( ) ;
loop . exec ( ) ;
} qDebug ( ) < < " Changing position " ;
2021-04-11 18:55:09 +02:00
if ( startingPosition > 1000 ) d - > m_player . setPosition ( startingPosition ) ;
d - > m_player . pause ( ) ;
2021-04-12 22:18:04 +02:00
d - > m_readyToPlay = true ;
Q_EMIT canPlayChanged ( ) ;
Q_EMIT canPauseChanged ( ) ;
2021-04-12 22:44:05 +02:00
Q_EMIT canSkipForwardChanged ( ) ;
Q_EMIT canSkipBackwardChanged ( ) ;
2021-04-13 20:51:00 +02:00
Q_EMIT canGoNextChanged ( ) ;
2021-04-13 14:31:52 +02:00
d - > m_isSeekable = true ;
Q_EMIT seekableChanged ( true ) ;
2021-04-18 22:21:59 +02:00
qDebug ( ) < < " Duration " < < d - > m_player . duration ( ) / 1000 < < d - > m_entry - > enclosure ( ) - > duration ( ) ;
// Finally, check if duration mentioned in enclosure corresponds to real duration
if ( ( d - > m_player . duration ( ) / 1000 ) ! = d - > m_entry - > enclosure ( ) - > duration ( ) ) {
d - > m_entry - > enclosure ( ) - > setDuration ( d - > m_player . duration ( ) / 1000 ) ;
qDebug ( ) < < " Correcting duration of " < < d - > m_entry - > id ( ) < < " to " < < d - > m_player . duration ( ) / 1000 ;
}
2021-04-12 22:18:04 +02:00
} else {
2021-04-17 20:55:01 +02:00
DataManager : : instance ( ) . setLastPlayingEntry ( QStringLiteral ( " none " ) ) ;
2021-04-14 11:32:57 +02:00
d - > m_entry = nullptr ;
Q_EMIT entryChanged ( nullptr ) ;
2021-04-12 22:18:04 +02:00
d - > m_readyToPlay = false ;
2021-04-14 11:32:57 +02:00
Q_EMIT durationChanged ( 0 ) ;
Q_EMIT positionChanged ( 0 ) ;
2021-04-12 22:18:04 +02:00
Q_EMIT canPlayChanged ( ) ;
Q_EMIT canPauseChanged ( ) ;
2021-04-12 22:44:05 +02:00
Q_EMIT canSkipForwardChanged ( ) ;
Q_EMIT canSkipBackwardChanged ( ) ;
2021-04-13 20:51:00 +02:00
Q_EMIT canGoNextChanged ( ) ;
2021-04-13 14:31:52 +02:00
d - > m_isSeekable = false ;
Q_EMIT seekableChanged ( false ) ;
2021-04-11 18:11:36 +02:00
}
2021-04-14 11:32:57 +02:00
// Unlock the position saving lock
d - > m_lockPositionSaving = false ;
2021-04-11 15:26:28 +02:00
}
void AudioManager : : setMuted ( bool muted )
{
2021-04-11 18:11:36 +02:00
d - > m_player . setMuted ( muted ) ;
2021-04-11 15:26:28 +02:00
}
void AudioManager : : setVolume ( qreal volume )
{
qDebug ( ) < < " AudioManager::setVolume " < < volume ;
auto realVolume = static_cast < qreal > ( QAudio : : convertVolume ( volume / 100.0 , QAudio : : LogarithmicVolumeScale , QAudio : : LinearVolumeScale ) ) ;
2021-04-11 18:11:36 +02:00
d - > m_player . setVolume ( qRound ( realVolume * 100 ) ) ;
2021-04-11 15:26:28 +02:00
}
2021-04-11 18:11:36 +02:00
/*
2021-04-11 15:26:28 +02:00
void AudioManager : : setSource ( const QUrl & source )
{
qDebug ( ) < < " AudioManager::setSource " < < source ;
2021-04-11 18:11:36 +02:00
d - > m_player . setMedia ( { source } ) ;
2021-04-11 15:26:28 +02:00
}
2021-04-11 18:11:36 +02:00
*/
2021-04-11 15:26:28 +02:00
2021-04-12 22:18:04 +02:00
void AudioManager : : setPosition ( qint64 position )
2021-04-11 17:13:44 +02:00
{
2021-04-12 22:18:04 +02:00
qDebug ( ) < < " AudioManager::setPosition " < < position ;
2021-04-11 17:13:44 +02:00
2021-04-12 22:18:04 +02:00
d - > m_player . setPosition ( position ) ;
2021-04-11 17:13:44 +02:00
}
2021-04-12 22:18:04 +02:00
void AudioManager : : setPlaybackRate ( const qreal rate )
2021-04-11 15:26:28 +02:00
{
2021-04-12 22:18:04 +02:00
qDebug ( ) < < " AudioManager::setPlaybackRate " < < rate ;
2021-04-11 15:26:28 +02:00
2021-04-12 22:18:04 +02:00
d - > m_player . setPlaybackRate ( rate ) ;
2021-04-11 15:26:28 +02:00
}
void AudioManager : : play ( )
{
qDebug ( ) < < " AudioManager::play " ;
2021-04-11 18:11:36 +02:00
d - > m_player . play ( ) ;
2021-04-13 14:31:52 +02:00
d - > m_isSeekable = true ;
Q_EMIT seekableChanged ( d - > m_isSeekable ) ;
2021-04-11 15:26:28 +02:00
}
void AudioManager : : pause ( )
{
qDebug ( ) < < " AudioManager::pause " ;
2021-04-13 14:31:52 +02:00
d - > m_player . play ( ) ;
d - > m_isSeekable = true ;
2021-04-11 18:11:36 +02:00
d - > m_player . pause ( ) ;
2021-04-11 15:26:28 +02:00
}
2021-04-12 22:18:04 +02:00
void AudioManager : : playPause ( )
{
if ( playbackState ( ) = = QMediaPlayer : : State : : PausedState )
play ( ) ;
else if ( playbackState ( ) = = QMediaPlayer : : State : : PlayingState )
pause ( ) ;
}
2021-04-11 15:26:28 +02:00
void AudioManager : : stop ( )
{
qDebug ( ) < < " AudioManager::stop " ;
2021-04-11 18:11:36 +02:00
d - > m_player . stop ( ) ;
2021-04-13 14:31:52 +02:00
d - > m_isSeekable = false ;
Q_EMIT seekableChanged ( d - > m_isSeekable ) ;
2021-04-11 15:26:28 +02:00
}
void AudioManager : : seek ( qint64 position )
{
qDebug ( ) < < " AudioManager::seek " < < position ;
2021-04-11 18:11:36 +02:00
d - > m_player . setPosition ( position ) ;
2021-04-11 15:26:28 +02:00
}
2021-04-12 22:44:05 +02:00
void AudioManager : : skipForward ( )
2021-04-11 23:07:21 +02:00
{
2021-04-12 22:44:05 +02:00
qDebug ( ) < < " AudioManager::skipForward " ;
2021-04-12 22:36:17 +02:00
seek ( std : : min ( ( position ( ) + SKIP_STEP ) , duration ( ) ) ) ;
2021-04-11 23:07:21 +02:00
}
2021-04-12 22:44:05 +02:00
void AudioManager : : skipBackward ( )
2021-04-11 23:07:21 +02:00
{
2021-04-12 22:44:05 +02:00
qDebug ( ) < < " AudioManager::skipBackward " ;
2021-04-12 22:36:17 +02:00
seek ( std : : max ( ( qint64 ) 0 , ( position ( ) - SKIP_STEP ) ) ) ;
2021-04-11 23:07:21 +02:00
}
2021-04-12 22:18:04 +02:00
2021-04-13 20:51:00 +02:00
bool AudioManager : : canGoNext ( ) const
{
2021-04-13 22:11:12 +02:00
// TODO: extend with streaming capability
2021-04-14 11:32:57 +02:00
if ( d - > m_entry ) {
int index = DataManager : : instance ( ) . getQueue ( ) . indexOf ( d - > m_entry - > id ( ) ) ;
if ( index > = 0 ) {
// check if there is a next track
if ( index < DataManager : : instance ( ) . getQueue ( ) . count ( ) - 1 ) {
Entry * next_entry = DataManager : : instance ( ) . getEntry ( DataManager : : instance ( ) . getQueue ( ) [ index + 1 ] ) ;
if ( next_entry - > enclosure ( ) ) {
qDebug ( ) < < " Enclosure status " < < next_entry - > enclosure ( ) - > path ( ) < < next_entry - > enclosure ( ) - > status ( ) ;
if ( next_entry - > enclosure ( ) - > status ( ) = = Enclosure : : Downloaded ) {
return true ;
}
2021-04-13 20:51:00 +02:00
}
}
}
}
return false ;
}
void AudioManager : : next ( )
{
if ( canGoNext ( ) ) {
2021-04-14 11:32:57 +02:00
QMediaPlayer : : State previousTrackState = playbackState ( ) ;
int index = DataManager : : instance ( ) . getQueue ( ) . indexOf ( d - > m_entry - > id ( ) ) ;
qDebug ( ) < < " Skipping to " < < DataManager : : instance ( ) . getQueue ( ) [ index + 1 ] ;
2021-04-13 20:51:00 +02:00
setEntry ( DataManager : : instance ( ) . getEntry ( DataManager : : instance ( ) . getQueue ( ) [ index + 1 ] ) ) ;
2021-04-14 11:32:57 +02:00
if ( previousTrackState = = QMediaPlayer : : PlayingState ) play ( ) ;
2021-04-13 20:51:00 +02:00
} else {
2021-04-14 11:32:57 +02:00
qDebug ( ) < < " Next track cannot be played, changing entry to nullptr " ;
2021-04-13 20:51:00 +02:00
setEntry ( nullptr ) ;
}
}
2021-04-11 15:26:28 +02:00
void AudioManager : : mediaStatusChanged ( )
{
2021-04-11 18:11:36 +02:00
qDebug ( ) < < " AudioManager::mediaStatusChanged " < < d - > m_player . mediaStatus ( ) ;
2021-04-13 20:51:00 +02:00
// File has reached the end and has stopped
if ( d - > m_player . mediaStatus ( ) = = QMediaPlayer : : EndOfMedia ) {
2021-04-13 22:11:12 +02:00
next ( ) ;
2021-04-13 20:51:00 +02:00
}
2021-04-11 15:26:28 +02:00
}
void AudioManager : : playerStateChanged ( )
{
2021-04-11 18:11:36 +02:00
qDebug ( ) < < " AudioManager::playerStateChanged " < < d - > m_player . state ( ) ;
2021-04-11 15:26:28 +02:00
2021-04-11 18:11:36 +02:00
switch ( d - > m_player . state ( ) )
2021-04-11 15:26:28 +02:00
{
case QMediaPlayer : : State : : StoppedState :
Q_EMIT stopped ( ) ;
d - > mPowerInterface . setPreventSleep ( false ) ;
break ;
case QMediaPlayer : : State : : PlayingState :
Q_EMIT playing ( ) ;
d - > mPowerInterface . setPreventSleep ( true ) ;
break ;
case QMediaPlayer : : State : : PausedState :
Q_EMIT paused ( ) ;
d - > mPowerInterface . setPreventSleep ( false ) ;
break ;
}
}
void AudioManager : : playerVolumeChanged ( )
{
2021-04-11 18:11:36 +02:00
qDebug ( ) < < " AudioManager::playerVolumeChanged " < < d - > m_player . volume ( ) ;
2021-04-11 15:26:28 +02:00
QTimer : : singleShot ( 0 , [ this ] ( ) { Q_EMIT volumeChanged ( ) ; } ) ;
}
void AudioManager : : playerMutedChanged ( )
{
qDebug ( ) < < " AudioManager::playerMutedChanged " ;
QTimer : : singleShot ( 0 , [ this ] ( ) { Q_EMIT mutedChanged ( muted ( ) ) ; } ) ;
}
2021-04-11 20:30:12 +02:00
void AudioManager : : savePlayPosition ( qint64 position )
{
2021-04-14 11:32:57 +02:00
if ( ! d - > m_lockPositionSaving ) {
if ( d - > m_entry ) {
if ( d - > m_entry - > enclosure ( ) ) {
d - > m_entry - > enclosure ( ) - > setPlayPosition ( position ) ;
}
}
}
2021-04-11 20:30:12 +02:00
qDebug ( ) < < d - > m_player . mediaStatus ( ) ;
}