1 // Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
2 // Copyright (C) 2010 Winch Gate Property Limited
4 // This source file has been modified by the following contributors:
5 // Copyright (C) 2013 Laszlo KIS-ADAM (dfighter) <dfighter1985@gmail.com>
6 // Copyright (C) 2020 Jan BOON (Kaetemi) <jan.boon@kaetemi.be>
8 // This program is free software: you can redistribute it and/or modify
9 // it under the terms of the GNU Affero General Public License as
10 // published by the Free Software Foundation, either version 3 of the
11 // License, or (at your option) any later version.
13 // This program is distributed in the hope that it will be useful,
14 // but WITHOUT ANY WARRANTY; without even the implied warranty of
15 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 // GNU Affero General Public License for more details.
18 // You should have received a copy of the GNU Affero General Public License
19 // along with this program. If not, see <http://www.gnu.org/licenses/>.
25 #include "music_player.h"
26 #include "nel/gui/action_handler.h"
28 #include "../sound_manager.h"
29 #include "interface_manager.h"
30 #include "../client_cfg.h"
32 #include "nel/misc/thread.h"
33 #include "nel/misc/mutex.h"
36 using namespace NLMISC
;
40 extern HINSTANCE HInstance
;
43 extern UDriver
*Driver
;
46 #define MP3_PLAYER_PLAYLIST_LIST "ui:interface:playlist:content:songs:list"
47 #define TEMPLATE_PLAYLIST_SONG "playlist_song"
48 #define TEMPLATE_PLAYLIST_SONG_TITLE "title"
49 #define TEMPLATE_PLAYLIST_SONG_DURATION "duration"
51 #define MP3_SAVE_SHUFFLE "UI:SAVE:MP3_SHUFFLE"
52 #define MP3_SAVE_REPEAT "UI:SAVE:MP3_REPEAT"
54 CMusicPlayer MusicPlayer
;
55 static NLMISC::CUnfairMutex MusicPlayerMutex
;
57 // ***************************************************************************
58 class CMusicPlayerWorker
: public NLMISC::IRunnable
64 std::vector
<std::string
> _Files
;
67 CMusicPlayerWorker(): _Running(false), _Thread(NULL
)
82 bool isRunning() const { return _Running
; }
89 while(_Running
&& SoundMngr
&& i
< _Files
.size())
91 // get copy incase _Files changes
92 std::string
filename(_Files
[i
]);
97 if (SoundMngr
->getMixer()->getSongTitle(filename
, title
, length
))
99 MusicPlayer
.updateSong(filename
, title
, length
);
110 void getSongsInfo(const std::vector
<std::string
> &filenames
)
119 _Thread
= IThread::create(this);
120 nlassert(_Thread
!= NULL
);
136 static CMusicPlayerWorker MusicPlayerWorker
;
138 // ***************************************************************************
140 CMusicPlayer::CMusicPlayer ()
142 _CurrentSongIndex
= 0;
148 bool CMusicPlayer::isRepeatEnabled() const
150 return (NLGUI::CDBManager::getInstance()->getDbProp(MP3_SAVE_REPEAT
)->getValue32() == 1);
153 bool CMusicPlayer::isShuffleEnabled() const
155 return (NLGUI::CDBManager::getInstance()->getDbProp(MP3_SAVE_SHUFFLE
)->getValue32() == 1);
158 // ***************************************************************************
159 void CMusicPlayer::playSongs (const std::vector
<std::string
> &filenames
)
162 for (uint i
=0; i
<filenames
.size(); i
++)
164 _Songs
.push_back(CSongs(filenames
[i
], CFile::getFilename(filenames
[i
]), 0.f
));
167 // reset song index if out of bounds
168 if (_CurrentSongIndex
> _Songs
.size())
169 _CurrentSongIndex
= 0;
171 if (isShuffleEnabled())
172 shuffleAndRebuildPlaylist();
176 // If pause, stop, else play will resume
177 if (_State
== Paused
|| _Songs
.empty())
180 // get song title/duration using worker thread
181 MusicPlayerWorker
.getSongsInfo(filenames
);
184 // ***************************************************************************
185 void CMusicPlayer::updatePlaylist(uint index
, bool state
)
187 if (index
>= _Songs
.size()) return;
189 std::string rowId
= toString("%s:s%d:bg", MP3_PLAYER_PLAYLIST_LIST
, index
);
190 CInterfaceElement
*pIE
= dynamic_cast<CInterfaceElement
*>(CWidgetManager::getInstance()->getElementFromId(rowId
));
191 if (pIE
) pIE
->setActive(state
);
194 void CMusicPlayer::updatePlaylist(sint prevIndex
)
196 if (prevIndex
>= 0 && prevIndex
< _Songs
.size())
198 updatePlaylist(prevIndex
, false);
201 updatePlaylist(_CurrentSongIndex
, true);
204 // ***************************************************************************
205 // called from worker thread
206 void CMusicPlayer::updateSong(const std::string filename
, const std::string title
, float length
)
208 CAutoMutex
<CUnfairMutex
> mutex(MusicPlayerMutex
);
210 _SongUpdateQueue
.push_back(CSongs(filename
, title
, length
));
213 // ***************************************************************************
215 void CMusicPlayer::updateSongs()
217 CAutoMutex
<CUnfairMutex
> mutex(MusicPlayerMutex
);
218 if (!_SongUpdateQueue
.empty())
220 for(uint i
= 0; i
< _SongUpdateQueue
.size(); ++i
)
222 updateSong(_SongUpdateQueue
[i
]);
224 _SongUpdateQueue
.clear();
228 // ***************************************************************************
229 void CMusicPlayer::updateSong(const CSongs
&song
)
232 while(index
< _Songs
.size())
234 if (_Songs
[index
].Filename
== song
.Filename
)
236 _Songs
[index
].Title
= song
.Title
;
237 _Songs
[index
].Length
= song
.Length
;
243 if (index
== _Songs
.size())
245 nlwarning("Unknown song file '%s'", song
.Filename
.c_str());
249 std::string
rowId(toString("%s:s%d", MP3_PLAYER_PLAYLIST_LIST
, index
));
250 CInterfaceGroup
*pIG
= dynamic_cast<CInterfaceGroup
*>(CWidgetManager::getInstance()->getElementFromId(rowId
));
253 nlwarning("Playlist row '%s' not found", rowId
.c_str());
258 pVT
= dynamic_cast<CViewText
*>(pIG
->getView(TEMPLATE_PLAYLIST_SONG_TITLE
));
261 pVT
->setHardText(song
.Title
);
265 nlwarning("title element '%s' not found", TEMPLATE_PLAYLIST_SONG_TITLE
);
268 pVT
= dynamic_cast<CViewText
*>(pIG
->getView(TEMPLATE_PLAYLIST_SONG_DURATION
));
271 uint min
= (sint32
)(song
.Length
/ 60) % 60;
272 uint sec
= (sint32
)(song
.Length
) % 60;
273 uint hour
= song
.Length
/ 3600;
274 std::string
duration(toString("%02d:%02d", min
, sec
));
276 duration
= toString("%02d:", hour
) + duration
;
278 pVT
->setHardText(duration
);
282 nlwarning("duration element '%s' not found", TEMPLATE_PLAYLIST_SONG_DURATION
);
286 // ***************************************************************************
287 void CMusicPlayer::shuffleAndRebuildPlaylist()
289 std::random_shuffle(_Songs
.begin(), _Songs
.end());
293 // ***************************************************************************
294 void CMusicPlayer::rebuildPlaylist()
296 CGroupList
*pList
= dynamic_cast<CGroupList
*>(CWidgetManager::getInstance()->getElementFromId(MP3_PLAYER_PLAYLIST_LIST
));
299 pList
->clearGroups();
300 pList
->setDynamicDisplaySize(true);
301 bool found
= _CurrentSong
.Filename
.empty();
302 for (uint i
=0; i
< _Songs
.size(); ++i
)
304 if (!found
&& _CurrentSong
.Filename
== _Songs
[i
].Filename
)
307 _CurrentSongIndex
= i
;
310 std::string
duration("--:--");
311 if (_Songs
[i
].Length
> 0)
313 uint min
= (sint32
)(_Songs
[i
].Length
/ 60) % 60;
314 uint sec
= (sint32
)(_Songs
[i
].Length
) % 60;
315 uint hour
= _Songs
[i
].Length
/ 3600;
316 duration
= toString("%02d:%02d", min
, sec
);
318 duration
= toString("%02d:", hour
) + duration
;
321 vector
< pair
<string
, string
> > vParams
;
322 vParams
.push_back(pair
<string
, string
>("id", "s" + toString(i
)));
323 vParams
.push_back(pair
<string
, string
>("index", toString(i
)));
324 CInterfaceGroup
*pNew
= CWidgetManager::getInstance()->getParser()->createGroupInstance(TEMPLATE_PLAYLIST_SONG
, pList
->getId(), vParams
);
327 CViewText
*pVT
= dynamic_cast<CViewText
*>(pNew
->getView(TEMPLATE_PLAYLIST_SONG_TITLE
));
330 pVT
->setText(_Songs
[i
].Title
);
333 pVT
= dynamic_cast<CViewText
*>(pNew
->getView(TEMPLATE_PLAYLIST_SONG_DURATION
));
336 pVT
->setText(duration
);
339 pNew
->setParent(pList
);
340 pList
->addChild(pNew
);
343 pList
->invalidateCoords();
350 // ***************************************************************************
352 void CMusicPlayer::play (sint index
)
360 createPlaylistFromMusic();
369 sint prevSongIndex
= _CurrentSongIndex
;
371 if (index
>= 0 && index
< (sint
)_Songs
.size())
373 if (_State
== Paused
)
378 _CurrentSongIndex
= index
;
384 nlassert (_CurrentSongIndex
<_Songs
.size());
386 /* If the player is paused, resume, else, play the current song */
387 if (_State
== Paused
)
389 SoundMngr
->resumeMusic();
393 SoundMngr
->playMusic(_Songs
[_CurrentSongIndex
].Filename
, 0, true, false, false);
398 _PlayStart
= CTime::getLocalTime() - _PauseTime
;
400 _CurrentSong
= _Songs
[_CurrentSongIndex
];
402 updatePlaylist(prevSongIndex
);
404 NLGUI::CDBManager::getInstance()->getDbProp("UI:TEMP:MP3_PLAYING")->setValueBool(true);
408 // ***************************************************************************
410 void CMusicPlayer::pause ()
415 // pause the music only if we are really playing (else risk to pause a background music!)
418 SoundMngr
->pauseMusic();
422 _PauseTime
= CTime::getLocalTime() - _PlayStart
;
424 NLGUI::CDBManager::getInstance()->getDbProp("UI:TEMP:MP3_PLAYING")->setValueBool(false);
428 // ***************************************************************************
430 void CMusicPlayer::stop ()
435 // stop the music only if we are really playing (else risk to stop a background music!)
436 if (_State
!= Stopped
)
437 SoundMngr
->stopMusic(0);
445 NLGUI::CDBManager::getInstance()->getDbProp("UI:TEMP:MP3_PLAYING")->setValueBool(false);
448 // ***************************************************************************
450 void CMusicPlayer::previous ()
454 // Point the previous song
456 if (_CurrentSongIndex
== 0)
457 index
= (uint
)_Songs
.size()-1;
459 index
= _CurrentSongIndex
-1;
465 // ***************************************************************************
467 void CMusicPlayer::next ()
471 sint index
= _CurrentSongIndex
+1;
472 if (index
== _Songs
.size())
479 // ***************************************************************************
480 void CMusicPlayer::updatePlayingInfo(const std::string info
)
482 CViewText
*pVT
= dynamic_cast<CViewText
*>(CWidgetManager::getInstance()->getElementFromId("ui:interface:mp3_player:screen:text"));
489 // ***************************************************************************
490 void CMusicPlayer::clearPlayingInfo()
494 updatePlayingInfo(CI18N::get("uiNoFiles"));
498 updatePlayingInfo("");
502 // ***************************************************************************
503 void CMusicPlayer::update ()
507 if (_State
!= Stopped
)
515 if (MusicPlayerWorker
.isRunning() || !_SongUpdateQueue
.empty())
520 if (_State
== Playing
)
523 TTime dur
= (CTime::getLocalTime() - _PlayStart
) / 1000;
524 uint min
= (dur
/ 60) % 60;
526 uint hour
= dur
/ 3600;
528 std::string
title(toString("%02d:%02d", min
, sec
));
529 if (hour
> 0) title
= toString("%02d:", hour
) + title
;
530 title
+= " " + _CurrentSong
.Title
;
531 updatePlayingInfo(title
);
534 if (SoundMngr
->isMusicEnded ())
536 // select next song from playlist
537 sint index
= _CurrentSongIndex
+ 1;
538 if (isRepeatEnabled() || index
< _Songs
.size())
540 if (index
== _Songs
.size())
544 if (isShuffleEnabled())
545 shuffleAndRebuildPlaylist();
552 // remove active highlight from playlist
553 updatePlaylist(_CurrentSongIndex
, false);
557 // restart from top on next 'play'
558 _CurrentSongIndex
= 0;
564 // ***************************************************************************
565 static void addFromPlaylist(const std::string
&playlist
, const std::vector
<std::string
> &extensions
, std::vector
<std::string
> &filenames
)
567 static uint8 utf8Header
[] = { 0xefu
, 0xbbu
, 0xbfu
};
571 // Get the path of the playlist
572 string basePlaylist
= CFile::getPath (playlist
);
573 FILE *file
= nlfopen (playlist
, "r");
575 bool useUtf8
= CFile::getExtension(playlist
) == "m3u8";
579 while (fgets (line
, 512, file
))
581 string lineStr
= trim(std::string(line
));
583 // id a UTF-8 BOM header is present, parse as UTF-8
584 if (!useUtf8
&& lineStr
.length() >= 3 && memcmp(line
, utf8Header
, 3) == 0)
587 lineStr
= trim(std::string(line
+ 3));
592 lineStr
= NLMISC::mbcsToUtf8(line
); // Attempt local codepage first
594 lineStr
= CUtfStringView::fromAscii(std::string(line
)); // Fallback
595 lineStr
= trim(lineStr
);
598 lineStr
= CUtfStringView(lineStr
).toUtf8(true); // Re-encode external string
600 // Not a comment line
601 if (lineStr
[0] != '#')
603 std::string filename
= CPath::makePathAbsolute(CFile::getPath(lineStr
), basePlaylist
) + CFile::getFilename(lineStr
);
604 std::string ext
= toLowerAscii(CFile::getExtension(filename
));
605 if (std::find(extensions
.begin(), extensions
.end(), ext
) != extensions
.end())
607 if (CFile::fileExists(filename
))
608 filenames
.push_back(filename
);
610 nlwarning("Ignore non-existing file '%s'", filename
.c_str());
614 nlwarning("Ingnore invalid extension '%s'", filename
.c_str());
622 void CMusicPlayer::createPlaylistFromMusic()
624 std::vector
<std::string
> extensions
;
625 SoundMngr
->getMixer()->getMusicExtensions(extensions
);
627 // no format supported
628 if (extensions
.empty())
630 // in the very unlikely scenario
631 static const string
message("Sound driver has no support for music.");
632 CInterfaceManager::getInstance()->displaySystemInfo(message
, "SYS");
633 nlinfo("%s", message
.c_str());
636 std::string newPath
= CPath::makePathAbsolute(CPath::standardizePath(ClientCfg
.MediaPlayerDirectory
), CPath::getCurrentPath(), true);
638 join(extensions
, ", ", extlist
);
639 extlist
+= ", m3u, m3u8";
641 std::string
msg(CI18N::get("uiMk_system6"));
642 msg
+= ": " + newPath
+ " (" + extlist
+ ")";
643 CInterfaceManager::getInstance()->displaySystemInfo(msg
, "SYS");
644 nlinfo("%s", msg
.c_str());
646 // Recursive scan for files from media directory
647 vector
<string
> filesToProcess
;
648 CPath::getPathContent (newPath
, true, false, true, filesToProcess
);
651 std::vector
<std::string
> filenames
;
652 std::vector
<std::string
> playlists
;
654 for (i
= 0; i
< filesToProcess
.size(); ++i
)
656 std::string ext
= toLowerAscii(CFile::getExtension(filesToProcess
[i
]));
657 if (std::find(extensions
.begin(), extensions
.end(), ext
) != extensions
.end())
659 filenames
.push_back(filesToProcess
[i
]);
661 else if (ext
== "m3u" || ext
== "m3u8")
663 playlists
.push_back(filesToProcess
[i
]);
667 // Add songs from playlists
668 for (i
= 0; i
< playlists
.size(); ++i
)
670 addFromPlaylist(playlists
[i
], extensions
, filenames
);
673 // Sort songs by filename
674 sort(filenames
.begin(), filenames
.end());
676 playSongs(filenames
);
679 // ***************************************************************************
680 class CMusicPlayerPlaySongs
: public IActionHandler
683 virtual void execute(CCtrlBase
* /* pCaller */, const string
&Params
)
687 // Do not show warning on volume change as its restored at startup
688 if (Params
.find("volume") == std::string::npos
)
689 CInterfaceManager::getInstance()->messageBox (CI18N::get ("uiMP3SoundDisabled"));
694 if (Params
== "play_songs")
696 MusicPlayer
.createPlaylistFromMusic();
698 else if (Params
== "update_playlist")
700 if (MusicPlayer
.isShuffleEnabled())
701 MusicPlayer
.shuffleAndRebuildPlaylist();
703 MusicPlayer
.rebuildPlaylist();
705 else if (Params
== "previous")
706 MusicPlayer
.previous();
707 else if (Params
== "play")
709 else if (Params
== "stop")
711 else if (Params
== "pause")
713 else if (Params
== "next")
717 string volume
= getParam(Params
, "volume");
720 CInterfaceExprValue result
;
721 if (CInterfaceExpr::eval (volume
, result
))
723 if (result
.toDouble ())
725 float value
= (float)result
.getDouble() / 255.f
;
727 SoundMngr
->setUserMusicVolume (value
);
732 string song
= getParam(Params
, "song");
736 fromString(song
, index
);
737 MusicPlayer
.play(index
);
742 REGISTER_ACTION_HANDLER( CMusicPlayerPlaySongs
, "music_player");