Merge branch '138-toggle-free-look-with-hotkey' into main/gingo-test
[ryzomcore.git] / ryzom / client / src / interface_v3 / music_player.cpp
blobab79c96c14c9d794dafb81c1c6e59b8505b8fa4d
1 // Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
2 // Copyright (C) 2010-2020 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 CGroupList *pList = dynamic_cast<CGroupList *>(CWidgetManager::getInstance()->getElementFromId(MP3_PLAYER_PLAYLIST_LIST));
162 if (!pList)
164 nlwarning("MusicPlayer UI not found (%s)", MP3_PLAYER_PLAYLIST_LIST);
165 return;
168 uint limit = pList->getMaxElements();
169 if (filenames.size() > limit)
171 CInterfaceManager::getInstance()->displaySystemInfo(toString("Found %u, limit %u", filenames.size(), limit), "SYS");
174 _Songs.clear();
175 for (uint i = 0; (i < limit) && (i < filenames.size()); i++)
177 _Songs.push_back(CSongs(filenames[i], CFile::getFilename(filenames[i]), 0.f));
180 // reset song index if out of bounds
181 if (_CurrentSongIndex > _Songs.size())
182 _CurrentSongIndex = 0;
184 if (isShuffleEnabled())
185 shuffleAndRebuildPlaylist();
186 else
187 rebuildPlaylist();
189 // If pause, stop, else play will resume
190 if (_State == Paused || _Songs.empty())
191 stop();
193 // get song title/duration using worker thread
194 MusicPlayerWorker.getSongsInfo(filenames);
197 // ***************************************************************************
198 void CMusicPlayer::updatePlaylist(uint index, bool state)
200 if (index >= _Songs.size()) return;
202 std::string rowId = toString("%s:s%d:bg", MP3_PLAYER_PLAYLIST_LIST, index);
203 CInterfaceElement *pIE = dynamic_cast<CInterfaceElement*>(CWidgetManager::getInstance()->getElementFromId(rowId));
204 if (pIE) pIE->setActive(state);
207 void CMusicPlayer::updatePlaylist(sint prevIndex)
209 if (prevIndex >= 0 && prevIndex < _Songs.size())
211 updatePlaylist(prevIndex, false);
214 updatePlaylist(_CurrentSongIndex, true);
217 // ***************************************************************************
218 // called from worker thread
219 void CMusicPlayer::updateSong(const std::string filename, const std::string title, float length)
221 CAutoMutex<CUnfairMutex> mutex(MusicPlayerMutex);
223 _SongUpdateQueue.push_back(CSongs(filename, title, length));
226 // ***************************************************************************
227 // called from GUI
228 void CMusicPlayer::updateSongs()
230 CAutoMutex<CUnfairMutex> mutex(MusicPlayerMutex);
231 if (!_SongUpdateQueue.empty())
233 for(uint i = 0; i < _SongUpdateQueue.size(); ++i)
235 updateSong(_SongUpdateQueue[i]);
237 _SongUpdateQueue.clear();
241 // ***************************************************************************
242 void CMusicPlayer::updateSong(const CSongs &song)
244 uint index = 0;
245 while(index < _Songs.size())
247 if (_Songs[index].Filename == song.Filename)
249 _Songs[index].Title = song.Title;
250 _Songs[index].Length = song.Length;
251 break;
254 ++index;
256 if (index == _Songs.size())
258 nlwarning("Unknown song file '%s'", song.Filename.c_str());
259 return;
262 std::string rowId(toString("%s:s%d", MP3_PLAYER_PLAYLIST_LIST, index));
263 CInterfaceGroup *pIG = dynamic_cast<CInterfaceGroup*>(CWidgetManager::getInstance()->getElementFromId(rowId));
264 if (!pIG)
266 nlwarning("Playlist row '%s' not found", rowId.c_str());
267 return;
270 CViewText *pVT;
271 pVT = dynamic_cast<CViewText *>(pIG->getView(TEMPLATE_PLAYLIST_SONG_TITLE));
272 if (pVT)
274 pVT->setHardText(song.Title);
276 else
278 nlwarning("title element '%s' not found", TEMPLATE_PLAYLIST_SONG_TITLE);
281 pVT = dynamic_cast<CViewText *>(pIG->getView(TEMPLATE_PLAYLIST_SONG_DURATION));
282 if (pVT)
284 uint min = (sint32)(song.Length / 60) % 60;
285 uint sec = (sint32)(song.Length) % 60;
286 uint hour = song.Length / 3600;
287 std::string duration(toString("%02d:%02d", min, sec));
288 if (hour > 0)
289 duration = toString("%02d:", hour) + duration;
291 pVT->setHardText(duration);
293 else
295 nlwarning("duration element '%s' not found", TEMPLATE_PLAYLIST_SONG_DURATION);
299 // ***************************************************************************
300 void CMusicPlayer::shuffleAndRebuildPlaylist()
302 std::random_shuffle(_Songs.begin(), _Songs.end());
303 rebuildPlaylist();
306 // ***************************************************************************
307 void CMusicPlayer::rebuildPlaylist()
309 CGroupList *pList = dynamic_cast<CGroupList *>(CWidgetManager::getInstance()->getElementFromId(MP3_PLAYER_PLAYLIST_LIST));
310 if (pList)
312 pList->clearGroups();
313 pList->setDynamicDisplaySize(true);
314 bool found = _CurrentSong.Filename.empty();
315 for (uint i=0; i < _Songs.size(); ++i)
317 if (!found && _CurrentSong.Filename == _Songs[i].Filename)
319 found = true;
320 _CurrentSongIndex = i;
323 std::string duration("--:--");
324 if (_Songs[i].Length > 0)
326 uint min = (sint32)(_Songs[i].Length / 60) % 60;
327 uint sec = (sint32)(_Songs[i].Length) % 60;
328 uint hour = _Songs[i].Length / 3600;
329 duration = toString("%02d:%02d", min, sec);
330 if (hour > 0)
331 duration = toString("%02d:", hour) + duration;
334 vector< pair<string, string> > vParams;
335 vParams.push_back(pair<string, string>("id", "s" + toString(i)));
336 vParams.push_back(pair<string, string>("index", toString(i)));
337 CInterfaceGroup *pNew = CWidgetManager::getInstance()->getParser()->createGroupInstance(TEMPLATE_PLAYLIST_SONG, pList->getId(), vParams);
338 if (pNew)
340 CViewText *pVT = dynamic_cast<CViewText *>(pNew->getView(TEMPLATE_PLAYLIST_SONG_TITLE));
341 if (pVT)
343 pVT->setText(_Songs[i].Title);
346 pVT = dynamic_cast<CViewText *>(pNew->getView(TEMPLATE_PLAYLIST_SONG_DURATION));
347 if (pVT)
349 pVT->setText(duration);
352 pNew->setParent(pList);
353 pList->addChild(pNew);
356 pList->invalidateCoords();
359 updatePlaylist();
363 // ***************************************************************************
365 void CMusicPlayer::play (sint index)
367 if(!SoundMngr)
368 return;
370 if (_Songs.empty())
372 index = 0;
373 createPlaylistFromMusic();
376 if (_Songs.empty())
378 stop();
379 return;
382 sint prevSongIndex = _CurrentSongIndex;
384 if (index >= 0 && index < (sint)_Songs.size())
386 if (_State == Paused)
388 stop();
391 _CurrentSongIndex = index;
392 _PauseTime = 0;
395 if (!_Songs.empty())
397 nlassert (_CurrentSongIndex<_Songs.size());
399 /* If the player is paused, resume, else, play the current song */
400 if (_State == Paused)
402 SoundMngr->resumeMusic();
404 else
406 SoundMngr->playMusic(_Songs[_CurrentSongIndex].Filename, 0, true, false, false);
407 _PauseTime = 0;
410 _State = Playing;
411 _PlayStart = CTime::getLocalTime() - _PauseTime;
413 _CurrentSong = _Songs[_CurrentSongIndex];
415 updatePlaylist(prevSongIndex);
417 NLGUI::CDBManager::getInstance()->getDbProp("UI:TEMP:MP3_PLAYING")->setValueBool(true);
421 // ***************************************************************************
423 void CMusicPlayer::pause ()
425 if(!SoundMngr)
426 return;
428 // pause the music only if we are really playing (else risk to pause a background music!)
429 if(_State==Playing)
431 SoundMngr->pauseMusic();
432 _State = Paused;
434 if (_PlayStart > 0)
435 _PauseTime = CTime::getLocalTime() - _PlayStart;
437 NLGUI::CDBManager::getInstance()->getDbProp("UI:TEMP:MP3_PLAYING")->setValueBool(false);
441 // ***************************************************************************
443 void CMusicPlayer::stop ()
445 if(!SoundMngr)
446 return;
448 // stop the music only if we are really playing (else risk to stop a background music!)
449 if (_State != Stopped)
450 SoundMngr->stopMusic(0);
452 _State = Stopped;
453 _PlayStart = 0;
454 _PauseTime = 0;
456 clearPlayingInfo();
458 NLGUI::CDBManager::getInstance()->getDbProp("UI:TEMP:MP3_PLAYING")->setValueBool(false);
461 // ***************************************************************************
463 void CMusicPlayer::previous ()
465 if (!_Songs.empty())
467 // Point the previous song
468 sint index;
469 if (_CurrentSongIndex == 0)
470 index = (uint)_Songs.size()-1;
471 else
472 index = _CurrentSongIndex-1;
474 play(index);
478 // ***************************************************************************
480 void CMusicPlayer::next ()
482 if (!_Songs.empty())
484 sint index = _CurrentSongIndex+1;
485 if (index == _Songs.size())
486 index = 0;
488 play(index);
492 // ***************************************************************************
493 void CMusicPlayer::updatePlayingInfo(const std::string info)
495 CViewText *pVT = dynamic_cast<CViewText*>(CWidgetManager::getInstance()->getElementFromId("ui:interface:mp3_player:screen:text"));
496 if (pVT)
498 pVT->setText(info);
502 // ***************************************************************************
503 void CMusicPlayer::clearPlayingInfo()
505 if (_Songs.empty())
507 updatePlayingInfo(CI18N::get("uiNoFiles"));
509 else
511 updatePlayingInfo("");
515 // ***************************************************************************
516 void CMusicPlayer::update ()
518 if(!SoundMngr)
520 if (_State != Stopped)
522 _State = Stopped;
523 clearPlayingInfo();
525 return;
528 if (MusicPlayerWorker.isRunning() || !_SongUpdateQueue.empty())
530 updateSongs();
533 if (_State == Playing)
536 TTime dur = (CTime::getLocalTime() - _PlayStart) / 1000;
537 uint min = (dur / 60) % 60;
538 uint sec = dur % 60;
539 uint hour = dur / 3600;
541 std::string title(toString("%02d:%02d", min, sec));
542 if (hour > 0) title = toString("%02d:", hour) + title;
543 title += " " + _CurrentSong.Title;
544 updatePlayingInfo(title);
547 if (SoundMngr->isMusicEnded ())
549 // select next song from playlist
550 sint index = _CurrentSongIndex + 1;
551 if (isRepeatEnabled() || index < _Songs.size())
553 if (index == _Songs.size())
555 index = 0;
557 if (isShuffleEnabled())
558 shuffleAndRebuildPlaylist();
561 play(index);
563 else
565 // remove active highlight from playlist
566 updatePlaylist(_CurrentSongIndex, false);
568 stop();
570 // restart from top on next 'play'
571 _CurrentSongIndex = 0;
577 // ***************************************************************************
578 static void addFromPlaylist(const std::string &playlist, const std::vector<std::string> &extensions, std::vector<std::string> &filenames)
580 static uint8 utf8Header[] = { 0xefu, 0xbbu, 0xbfu };
582 // Add playlist
583 uint i;
584 // Get the path of the playlist
585 string basePlaylist = CFile::getPath (playlist);
586 FILE *file = nlfopen (playlist, "r");
588 bool useUtf8 = CFile::getExtension(playlist) == "m3u8";
589 if (file)
591 char line[512];
592 while (fgets (line, 512, file))
594 string lineStr = trim(std::string(line));
596 // id a UTF-8 BOM header is present, parse as UTF-8
597 if (!useUtf8 && lineStr.length() >= 3 && memcmp(line, utf8Header, 3) == 0)
599 useUtf8 = true;
600 lineStr = trim(std::string(line + 3));
603 if (!useUtf8)
605 lineStr = NLMISC::mbcsToUtf8(line); // Attempt local codepage first
606 if (lineStr.empty())
607 lineStr = CUtfStringView::fromAscii(std::string(line)); // Fallback
608 lineStr = trim(lineStr);
611 lineStr = CUtfStringView(lineStr).toUtf8(true); // Re-encode external string
613 // Not a comment line
614 if (lineStr[0] != '#')
616 std::string filename = CPath::makePathAbsolute(CFile::getPath(lineStr), basePlaylist) + CFile::getFilename(lineStr);
617 std::string ext = toLowerAscii(CFile::getExtension(filename));
618 if (std::find(extensions.begin(), extensions.end(), ext) != extensions.end())
620 if (CFile::fileExists(filename))
621 filenames.push_back(filename);
622 else
623 nlwarning("Ignore non-existing file '%s'", filename.c_str());
625 else
627 nlwarning("Ingnore invalid extension '%s'", filename.c_str());
631 fclose (file);
635 void CMusicPlayer::createPlaylistFromMusic()
637 std::vector<std::string> extensions;
638 SoundMngr->getMixer()->getMusicExtensions(extensions);
640 // no format supported
641 if (extensions.empty())
643 // in the very unlikely scenario
644 static const string message("Sound driver has no support for music.");
645 CInterfaceManager::getInstance()->displaySystemInfo(message, "SYS");
646 nlinfo("%s", message.c_str());
647 return;
649 std::string newPath = CPath::makePathAbsolute(CPath::standardizePath(ClientCfg.MediaPlayerDirectory), CPath::getCurrentPath(), true);
650 std::string extlist;
651 join(extensions, ", ", extlist);
652 extlist += ", m3u, m3u8";
654 std::string msg(CI18N::get("uiMk_system6"));
655 msg += ": " + newPath + " (" + extlist + ")";
656 CInterfaceManager::getInstance()->displaySystemInfo(msg, "SYS");
657 nlinfo("%s", msg.c_str());
659 // Recursive scan for files from media directory
660 vector<string> filesToProcess;
661 CPath::getPathContent (newPath, true, false, true, filesToProcess);
663 uint i;
664 std::vector<std::string> filenames;
665 std::vector<std::string> playlists;
667 for (i = 0; i < filesToProcess.size(); ++i)
669 std::string ext = toLowerAscii(CFile::getExtension(filesToProcess[i]));
670 if (std::find(extensions.begin(), extensions.end(), ext) != extensions.end())
672 filenames.push_back(filesToProcess[i]);
674 else if (ext == "m3u" || ext == "m3u8")
676 playlists.push_back(filesToProcess[i]);
680 // Add songs from playlists
681 for (i = 0; i < playlists.size(); ++i)
683 addFromPlaylist(playlists[i], extensions, filenames);
686 // Sort songs by filename
687 sort(filenames.begin(), filenames.end());
689 playSongs(filenames);
692 // ***************************************************************************
693 class CMusicPlayerPlaySongs: public IActionHandler
695 public:
696 virtual void execute(CCtrlBase * /* pCaller */, const string &Params)
698 if(!SoundMngr)
700 // Do not show warning on volume change as its restored at startup
701 if (Params.find("volume") == std::string::npos)
702 CInterfaceManager::getInstance()->messageBox (CI18N::get ("uiMP3SoundDisabled"));
704 return;
707 if (Params == "play_songs")
709 MusicPlayer.createPlaylistFromMusic();
711 else if (Params == "update_playlist")
713 if (MusicPlayer.isShuffleEnabled())
714 MusicPlayer.shuffleAndRebuildPlaylist();
716 MusicPlayer.rebuildPlaylist();
718 else if (Params == "previous")
719 MusicPlayer.previous();
720 else if (Params == "play")
721 MusicPlayer.play();
722 else if (Params == "stop")
723 MusicPlayer.stop();
724 else if (Params == "pause")
725 MusicPlayer.pause();
726 else if (Params == "next")
727 MusicPlayer.next();
728 else
730 string volume = getParam(Params, "volume");
731 if (!volume.empty())
733 CInterfaceExprValue result;
734 if (CInterfaceExpr::eval (volume, result))
736 if (result.toDouble ())
738 float value = (float)result.getDouble() / 255.f;
739 clamp (value, 0, 1);
740 SoundMngr->setUserMusicVolume (value);
745 string song = getParam(Params, "song");
746 if (!song.empty())
748 sint index=0;
749 fromString(song, index);
750 MusicPlayer.play(index);
755 REGISTER_ACTION_HANDLER( CMusicPlayerPlaySongs, "music_player");