Fix css style order when using external css files
[ryzomcore.git] / ryzom / client / src / interface_v3 / music_player.cpp
blobe211bb9d261007ea1e5adec2e5a0f09be452f829
1 // Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
2 // Copyright (C) 2010 Winch Gate Property Limited
3 //
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>
7 //
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/>.
23 #include "stdpch.h"
25 #include "music_player.h"
26 #include "nel/gui/action_handler.h"
27 #include "../input.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"
35 using namespace std;
36 using namespace NLMISC;
37 using namespace NL3D;
39 #ifdef NL_OS_WINDOWS
40 extern HINSTANCE HInstance;
41 #endif
43 extern UDriver *Driver;
45 // xml element ids
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"
50 // ui state
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
60 private:
61 bool _Running;
62 IThread *_Thread;
64 std::vector<std::string> _Files;
66 public:
67 CMusicPlayerWorker(): _Running(false), _Thread(NULL)
71 ~CMusicPlayerWorker()
73 _Running = false;
74 if (_Thread)
76 _Thread->terminate();
77 delete _Thread;
78 _Thread = NULL;
82 bool isRunning() const { return _Running; }
84 void run()
86 _Running = true;
88 uint i = 0;
89 while(_Running && SoundMngr && i < _Files.size())
91 // get copy incase _Files changes
92 std::string filename(_Files[i]);
94 std::string title;
95 float length;
97 if (SoundMngr->getMixer()->getSongTitle(filename, title, length))
99 MusicPlayer.updateSong(filename, title, length);
102 ++i;
105 _Running = false;
106 _Files.clear();
109 // called from GUI
110 void getSongsInfo(const std::vector<std::string> &filenames)
112 if (_Thread)
114 stopThread();
117 _Files = filenames;
119 _Thread = IThread::create(this);
120 nlassert(_Thread != NULL);
121 _Thread->start();
124 void stopThread()
126 _Running = false;
127 if (_Thread)
129 _Thread->wait();
130 delete _Thread;
131 _Thread = NULL;
136 static CMusicPlayerWorker MusicPlayerWorker;
138 // ***************************************************************************
140 CMusicPlayer::CMusicPlayer ()
142 _CurrentSongIndex = 0;
143 _State = Stopped;
144 _PlayStart = 0;
145 _PauseTime = 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)
161 _Songs.clear();
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();
173 else
174 rebuildPlaylist();
176 // If pause, stop, else play will resume
177 if (_State == Paused || _Songs.empty())
178 stop();
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 // ***************************************************************************
214 // called from GUI
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)
231 uint index = 0;
232 while(index < _Songs.size())
234 if (_Songs[index].Filename == song.Filename)
236 _Songs[index].Title = song.Title;
237 _Songs[index].Length = song.Length;
238 break;
241 ++index;
243 if (index == _Songs.size())
245 nlwarning("Unknown song file '%s'", song.Filename.c_str());
246 return;
249 std::string rowId(toString("%s:s%d", MP3_PLAYER_PLAYLIST_LIST, index));
250 CInterfaceGroup *pIG = dynamic_cast<CInterfaceGroup*>(CWidgetManager::getInstance()->getElementFromId(rowId));
251 if (!pIG)
253 nlwarning("Playlist row '%s' not found", rowId.c_str());
254 return;
257 CViewText *pVT;
258 pVT = dynamic_cast<CViewText *>(pIG->getView(TEMPLATE_PLAYLIST_SONG_TITLE));
259 if (pVT)
261 pVT->setHardText(song.Title);
263 else
265 nlwarning("title element '%s' not found", TEMPLATE_PLAYLIST_SONG_TITLE);
268 pVT = dynamic_cast<CViewText *>(pIG->getView(TEMPLATE_PLAYLIST_SONG_DURATION));
269 if (pVT)
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));
275 if (hour > 0)
276 duration = toString("%02d:", hour) + duration;
278 pVT->setHardText(duration);
280 else
282 nlwarning("duration element '%s' not found", TEMPLATE_PLAYLIST_SONG_DURATION);
286 // ***************************************************************************
287 void CMusicPlayer::shuffleAndRebuildPlaylist()
289 std::random_shuffle(_Songs.begin(), _Songs.end());
290 rebuildPlaylist();
293 // ***************************************************************************
294 void CMusicPlayer::rebuildPlaylist()
296 CGroupList *pList = dynamic_cast<CGroupList *>(CWidgetManager::getInstance()->getElementFromId(MP3_PLAYER_PLAYLIST_LIST));
297 if (pList)
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)
306 found = true;
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);
317 if (hour > 0)
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);
325 if (pNew)
327 CViewText *pVT = dynamic_cast<CViewText *>(pNew->getView(TEMPLATE_PLAYLIST_SONG_TITLE));
328 if (pVT)
330 pVT->setText(_Songs[i].Title);
333 pVT = dynamic_cast<CViewText *>(pNew->getView(TEMPLATE_PLAYLIST_SONG_DURATION));
334 if (pVT)
336 pVT->setText(duration);
339 pNew->setParent(pList);
340 pList->addChild(pNew);
343 pList->invalidateCoords();
346 updatePlaylist();
350 // ***************************************************************************
352 void CMusicPlayer::play (sint index)
354 if(!SoundMngr)
355 return;
357 if (_Songs.empty())
359 index = 0;
360 createPlaylistFromMusic();
363 if (_Songs.empty())
365 stop();
366 return;
369 sint prevSongIndex = _CurrentSongIndex;
371 if (index >= 0 && index < (sint)_Songs.size())
373 if (_State == Paused)
375 stop();
378 _CurrentSongIndex = index;
379 _PauseTime = 0;
382 if (!_Songs.empty())
384 nlassert (_CurrentSongIndex<_Songs.size());
386 /* If the player is paused, resume, else, play the current song */
387 if (_State == Paused)
389 SoundMngr->resumeMusic();
391 else
393 SoundMngr->playMusic(_Songs[_CurrentSongIndex].Filename, 0, true, false, false);
394 _PauseTime = 0;
397 _State = Playing;
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 ()
412 if(!SoundMngr)
413 return;
415 // pause the music only if we are really playing (else risk to pause a background music!)
416 if(_State==Playing)
418 SoundMngr->pauseMusic();
419 _State = Paused;
421 if (_PlayStart > 0)
422 _PauseTime = CTime::getLocalTime() - _PlayStart;
424 NLGUI::CDBManager::getInstance()->getDbProp("UI:TEMP:MP3_PLAYING")->setValueBool(false);
428 // ***************************************************************************
430 void CMusicPlayer::stop ()
432 if(!SoundMngr)
433 return;
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);
439 _State = Stopped;
440 _PlayStart = 0;
441 _PauseTime = 0;
443 clearPlayingInfo();
445 NLGUI::CDBManager::getInstance()->getDbProp("UI:TEMP:MP3_PLAYING")->setValueBool(false);
448 // ***************************************************************************
450 void CMusicPlayer::previous ()
452 if (!_Songs.empty())
454 // Point the previous song
455 sint index;
456 if (_CurrentSongIndex == 0)
457 index = (uint)_Songs.size()-1;
458 else
459 index = _CurrentSongIndex-1;
461 play(index);
465 // ***************************************************************************
467 void CMusicPlayer::next ()
469 if (!_Songs.empty())
471 sint index = _CurrentSongIndex+1;
472 if (index == _Songs.size())
473 index = 0;
475 play(index);
479 // ***************************************************************************
480 void CMusicPlayer::updatePlayingInfo(const std::string info)
482 CViewText *pVT = dynamic_cast<CViewText*>(CWidgetManager::getInstance()->getElementFromId("ui:interface:mp3_player:screen:text"));
483 if (pVT)
485 pVT->setText(info);
489 // ***************************************************************************
490 void CMusicPlayer::clearPlayingInfo()
492 if (_Songs.empty())
494 updatePlayingInfo(CI18N::get("uiNoFiles"));
496 else
498 updatePlayingInfo("");
502 // ***************************************************************************
503 void CMusicPlayer::update ()
505 if(!SoundMngr)
507 if (_State != Stopped)
509 _State = Stopped;
510 clearPlayingInfo();
512 return;
515 if (MusicPlayerWorker.isRunning() || !_SongUpdateQueue.empty())
517 updateSongs();
520 if (_State == Playing)
523 TTime dur = (CTime::getLocalTime() - _PlayStart) / 1000;
524 uint min = (dur / 60) % 60;
525 uint sec = dur % 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())
542 index = 0;
544 if (isShuffleEnabled())
545 shuffleAndRebuildPlaylist();
548 play(index);
550 else
552 // remove active highlight from playlist
553 updatePlaylist(_CurrentSongIndex, false);
555 stop();
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 };
569 // Add playlist
570 uint i;
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";
576 if (file)
578 char line[512];
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)
586 useUtf8 = true;
587 lineStr = trim(std::string(line + 3));
590 if (!useUtf8)
592 lineStr = NLMISC::mbcsToUtf8(line); // Attempt local codepage first
593 if (lineStr.empty())
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);
609 else
610 nlwarning("Ignore non-existing file '%s'", filename.c_str());
612 else
614 nlwarning("Ingnore invalid extension '%s'", filename.c_str());
618 fclose (file);
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());
634 return;
636 std::string newPath = CPath::makePathAbsolute(CPath::standardizePath(ClientCfg.MediaPlayerDirectory), CPath::getCurrentPath(), true);
637 std::string extlist;
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);
650 uint i;
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
682 public:
683 virtual void execute(CCtrlBase * /* pCaller */, const string &Params)
685 if(!SoundMngr)
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"));
691 return;
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")
708 MusicPlayer.play();
709 else if (Params == "stop")
710 MusicPlayer.stop();
711 else if (Params == "pause")
712 MusicPlayer.pause();
713 else if (Params == "next")
714 MusicPlayer.next();
715 else
717 string volume = getParam(Params, "volume");
718 if (!volume.empty())
720 CInterfaceExprValue result;
721 if (CInterfaceExpr::eval (volume, result))
723 if (result.toDouble ())
725 float value = (float)result.getDouble() / 255.f;
726 clamp (value, 0, 1);
727 SoundMngr->setUserMusicVolume (value);
732 string song = getParam(Params, "song");
733 if (!song.empty())
735 sint index=0;
736 fromString(song, index);
737 MusicPlayer.play(index);
742 REGISTER_ACTION_HANDLER( CMusicPlayerPlaySongs, "music_player");