1 // Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
2 // Copyright (C) 2010-2020 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
)
161 CGroupList
*pList
= dynamic_cast<CGroupList
*>(CWidgetManager::getInstance()->getElementFromId(MP3_PLAYER_PLAYLIST_LIST
));
164 nlwarning("MusicPlayer UI not found (%s)", MP3_PLAYER_PLAYLIST_LIST
);
168 uint limit
= pList
->getMaxElements();
169 if (filenames
.size() > limit
)
171 CInterfaceManager::getInstance()->displaySystemInfo(toString("Found %u, limit %u", filenames
.size(), limit
), "SYS");
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();
189 // If pause, stop, else play will resume
190 if (_State
== Paused
|| _Songs
.empty())
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 // ***************************************************************************
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
)
245 while(index
< _Songs
.size())
247 if (_Songs
[index
].Filename
== song
.Filename
)
249 _Songs
[index
].Title
= song
.Title
;
250 _Songs
[index
].Length
= song
.Length
;
256 if (index
== _Songs
.size())
258 nlwarning("Unknown song file '%s'", song
.Filename
.c_str());
262 std::string
rowId(toString("%s:s%d", MP3_PLAYER_PLAYLIST_LIST
, index
));
263 CInterfaceGroup
*pIG
= dynamic_cast<CInterfaceGroup
*>(CWidgetManager::getInstance()->getElementFromId(rowId
));
266 nlwarning("Playlist row '%s' not found", rowId
.c_str());
271 pVT
= dynamic_cast<CViewText
*>(pIG
->getView(TEMPLATE_PLAYLIST_SONG_TITLE
));
274 pVT
->setHardText(song
.Title
);
278 nlwarning("title element '%s' not found", TEMPLATE_PLAYLIST_SONG_TITLE
);
281 pVT
= dynamic_cast<CViewText
*>(pIG
->getView(TEMPLATE_PLAYLIST_SONG_DURATION
));
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
));
289 duration
= toString("%02d:", hour
) + duration
;
291 pVT
->setHardText(duration
);
295 nlwarning("duration element '%s' not found", TEMPLATE_PLAYLIST_SONG_DURATION
);
299 // ***************************************************************************
300 void CMusicPlayer::shuffleAndRebuildPlaylist()
302 std::random_shuffle(_Songs
.begin(), _Songs
.end());
306 // ***************************************************************************
307 void CMusicPlayer::rebuildPlaylist()
309 CGroupList
*pList
= dynamic_cast<CGroupList
*>(CWidgetManager::getInstance()->getElementFromId(MP3_PLAYER_PLAYLIST_LIST
));
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
)
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
);
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
);
340 CViewText
*pVT
= dynamic_cast<CViewText
*>(pNew
->getView(TEMPLATE_PLAYLIST_SONG_TITLE
));
343 pVT
->setText(_Songs
[i
].Title
);
346 pVT
= dynamic_cast<CViewText
*>(pNew
->getView(TEMPLATE_PLAYLIST_SONG_DURATION
));
349 pVT
->setText(duration
);
352 pNew
->setParent(pList
);
353 pList
->addChild(pNew
);
356 pList
->invalidateCoords();
363 // ***************************************************************************
365 void CMusicPlayer::play (sint index
)
373 createPlaylistFromMusic();
382 sint prevSongIndex
= _CurrentSongIndex
;
384 if (index
>= 0 && index
< (sint
)_Songs
.size())
386 if (_State
== Paused
)
391 _CurrentSongIndex
= index
;
397 nlassert (_CurrentSongIndex
<_Songs
.size());
399 /* If the player is paused, resume, else, play the current song */
400 if (_State
== Paused
)
402 SoundMngr
->resumeMusic();
406 SoundMngr
->playMusic(_Songs
[_CurrentSongIndex
].Filename
, 0, true, false, false);
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 ()
428 // pause the music only if we are really playing (else risk to pause a background music!)
431 SoundMngr
->pauseMusic();
435 _PauseTime
= CTime::getLocalTime() - _PlayStart
;
437 NLGUI::CDBManager::getInstance()->getDbProp("UI:TEMP:MP3_PLAYING")->setValueBool(false);
441 // ***************************************************************************
443 void CMusicPlayer::stop ()
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);
458 NLGUI::CDBManager::getInstance()->getDbProp("UI:TEMP:MP3_PLAYING")->setValueBool(false);
461 // ***************************************************************************
463 void CMusicPlayer::previous ()
467 // Point the previous song
469 if (_CurrentSongIndex
== 0)
470 index
= (uint
)_Songs
.size()-1;
472 index
= _CurrentSongIndex
-1;
478 // ***************************************************************************
480 void CMusicPlayer::next ()
484 sint index
= _CurrentSongIndex
+1;
485 if (index
== _Songs
.size())
492 // ***************************************************************************
493 void CMusicPlayer::updatePlayingInfo(const std::string info
)
495 CViewText
*pVT
= dynamic_cast<CViewText
*>(CWidgetManager::getInstance()->getElementFromId("ui:interface:mp3_player:screen:text"));
502 // ***************************************************************************
503 void CMusicPlayer::clearPlayingInfo()
507 updatePlayingInfo(CI18N::get("uiNoFiles"));
511 updatePlayingInfo("");
515 // ***************************************************************************
516 void CMusicPlayer::update ()
520 if (_State
!= Stopped
)
528 if (MusicPlayerWorker
.isRunning() || !_SongUpdateQueue
.empty())
533 if (_State
== Playing
)
536 TTime dur
= (CTime::getLocalTime() - _PlayStart
) / 1000;
537 uint min
= (dur
/ 60) % 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())
557 if (isShuffleEnabled())
558 shuffleAndRebuildPlaylist();
565 // remove active highlight from playlist
566 updatePlaylist(_CurrentSongIndex
, false);
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
};
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";
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)
600 lineStr
= trim(std::string(line
+ 3));
605 lineStr
= NLMISC::mbcsToUtf8(line
); // Attempt local codepage first
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
);
623 nlwarning("Ignore non-existing file '%s'", filename
.c_str());
627 nlwarning("Ingnore invalid extension '%s'", filename
.c_str());
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());
649 std::string newPath
= CPath::makePathAbsolute(CPath::standardizePath(ClientCfg
.MediaPlayerDirectory
), CPath::getCurrentPath(), true);
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
);
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
696 virtual void execute(CCtrlBase
* /* pCaller */, const string
&Params
)
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"));
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")
722 else if (Params
== "stop")
724 else if (Params
== "pause")
726 else if (Params
== "next")
730 string volume
= getParam(Params
, "volume");
733 CInterfaceExprValue result
;
734 if (CInterfaceExpr::eval (volume
, result
))
736 if (result
.toDouble ())
738 float value
= (float)result
.getDouble() / 255.f
;
740 SoundMngr
->setUserMusicVolume (value
);
745 string song
= getParam(Params
, "song");
749 fromString(song
, index
);
750 MusicPlayer
.play(index
);
755 REGISTER_ACTION_HANDLER( CMusicPlayerPlaySongs
, "music_player");