diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index 88bb2e39..653af8f8 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -92,6 +92,7 @@ assetdata assetfiles assetmanager + assetname assetpack assetpackage assetpackput @@ -1021,6 +1022,7 @@ lzma lzmamodule macappstore + macmusicapp macos macpath mailcap @@ -1223,6 +1225,7 @@ origwrapper ortho osascript + osmusic ostype otherplayer otherspawn @@ -1247,6 +1250,7 @@ partyqueue partyval passnode + passthrough passwd patcomp pathlib diff --git a/assets/.asset_manifest_1.json b/assets/.asset_manifest_1.json index 636e7b5c..62e08ec8 100644 --- a/assets/.asset_manifest_1.json +++ b/assets/.asset_manifest_1.json @@ -51,6 +51,8 @@ "ba_data/python/ba/__pycache__/_tournament.cpython-37.opt-1.pyc", "ba_data/python/ba/__pycache__/deprecated.cpython-37.opt-1.pyc", "ba_data/python/ba/__pycache__/internal.cpython-37.opt-1.pyc", + "ba_data/python/ba/__pycache__/macmusicapp.cpython-37.opt-1.pyc", + "ba_data/python/ba/__pycache__/osmusic.cpython-37.opt-1.pyc", "ba_data/python/ba/_account.py", "ba_data/python/ba/_achievement.py", "ba_data/python/ba/_activity.py", @@ -101,6 +103,8 @@ "ba_data/python/ba/_tournament.py", "ba_data/python/ba/deprecated.py", "ba_data/python/ba/internal.py", + "ba_data/python/ba/macmusicapp.py", + "ba_data/python/ba/osmusic.py", "ba_data/python/ba/ui/__init__.py", "ba_data/python/ba/ui/__pycache__/__init__.cpython-37.opt-1.pyc", "ba_data/python/bacommon/__init__.py", @@ -425,11 +429,11 @@ "ba_data/python/bastd/ui/soundtrack/__pycache__/browser.cpython-37.opt-1.pyc", "ba_data/python/bastd/ui/soundtrack/__pycache__/edit.cpython-37.opt-1.pyc", "ba_data/python/bastd/ui/soundtrack/__pycache__/entrytypeselect.cpython-37.opt-1.pyc", - "ba_data/python/bastd/ui/soundtrack/__pycache__/itunes.cpython-37.opt-1.pyc", + "ba_data/python/bastd/ui/soundtrack/__pycache__/macmusicapp.cpython-37.opt-1.pyc", "ba_data/python/bastd/ui/soundtrack/browser.py", "ba_data/python/bastd/ui/soundtrack/edit.py", "ba_data/python/bastd/ui/soundtrack/entrytypeselect.py", - "ba_data/python/bastd/ui/soundtrack/itunes.py", + "ba_data/python/bastd/ui/soundtrack/macmusicapp.py", "ba_data/python/bastd/ui/specialoffer.py", "ba_data/python/bastd/ui/store/__init__.py", "ba_data/python/bastd/ui/store/__pycache__/__init__.cpython-37.opt-1.pyc", diff --git a/assets/Makefile b/assets/Makefile index a23585ec..889b42bb 100644 --- a/assets/Makefile +++ b/assets/Makefile @@ -163,6 +163,7 @@ SCRIPT_TARGETS_PY_1 = \ build/ba_data/python/ba/_apputils.py \ build/ba_data/python/ba/_coopsession.py \ build/ba_data/python/ba/_appdelegate.py \ + build/ba_data/python/ba/macmusicapp.py \ build/ba_data/python/ba/internal.py \ build/ba_data/python/ba/_coopgame.py \ build/ba_data/python/ba/_meta.py \ @@ -196,6 +197,7 @@ SCRIPT_TARGETS_PY_1 = \ build/ba_data/python/ba/_multiteamsession.py \ build/ba_data/python/ba/_actor.py \ build/ba_data/python/ba/_powerup.py \ + build/ba_data/python/ba/osmusic.py \ build/ba_data/python/ba/_campaign.py \ build/ba_data/python/ba/_lobby.py \ build/ba_data/python/ba/_stats.py \ @@ -285,8 +287,8 @@ SCRIPT_TARGETS_PY_1 = \ build/ba_data/python/bastd/ui/playlist/editgame.py \ build/ba_data/python/bastd/ui/playlist/editcontroller.py \ build/ba_data/python/bastd/ui/playlist/addgame.py \ + build/ba_data/python/bastd/ui/soundtrack/macmusicapp.py \ build/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py \ - build/ba_data/python/bastd/ui/soundtrack/itunes.py \ build/ba_data/python/bastd/ui/soundtrack/__init__.py \ build/ba_data/python/bastd/ui/soundtrack/edit.py \ build/ba_data/python/bastd/ui/soundtrack/browser.py \ @@ -401,6 +403,7 @@ SCRIPT_TARGETS_PYC_1 = \ build/ba_data/python/ba/__pycache__/_apputils.cpython-37.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_coopsession.cpython-37.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_appdelegate.cpython-37.opt-1.pyc \ + build/ba_data/python/ba/__pycache__/macmusicapp.cpython-37.opt-1.pyc \ build/ba_data/python/ba/__pycache__/internal.cpython-37.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_coopgame.cpython-37.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_meta.cpython-37.opt-1.pyc \ @@ -434,6 +437,7 @@ SCRIPT_TARGETS_PYC_1 = \ build/ba_data/python/ba/__pycache__/_multiteamsession.cpython-37.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_actor.cpython-37.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_powerup.cpython-37.opt-1.pyc \ + build/ba_data/python/ba/__pycache__/osmusic.cpython-37.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_campaign.cpython-37.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_lobby.cpython-37.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_stats.cpython-37.opt-1.pyc \ @@ -523,8 +527,8 @@ SCRIPT_TARGETS_PYC_1 = \ build/ba_data/python/bastd/ui/playlist/__pycache__/editgame.cpython-37.opt-1.pyc \ build/ba_data/python/bastd/ui/playlist/__pycache__/editcontroller.cpython-37.opt-1.pyc \ build/ba_data/python/bastd/ui/playlist/__pycache__/addgame.cpython-37.opt-1.pyc \ + build/ba_data/python/bastd/ui/soundtrack/__pycache__/macmusicapp.cpython-37.opt-1.pyc \ build/ba_data/python/bastd/ui/soundtrack/__pycache__/entrytypeselect.cpython-37.opt-1.pyc \ - build/ba_data/python/bastd/ui/soundtrack/__pycache__/itunes.cpython-37.opt-1.pyc \ build/ba_data/python/bastd/ui/soundtrack/__pycache__/__init__.cpython-37.opt-1.pyc \ build/ba_data/python/bastd/ui/soundtrack/__pycache__/edit.cpython-37.opt-1.pyc \ build/ba_data/python/bastd/ui/soundtrack/__pycache__/browser.cpython-37.opt-1.pyc \ @@ -735,6 +739,11 @@ build/ba_data/python/ba/__pycache__/_appdelegate.cpython-37.opt-1.pyc: \ @echo Compiling script: $^ @rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@ +build/ba_data/python/ba/__pycache__/macmusicapp.cpython-37.opt-1.pyc: \ + build/ba_data/python/ba/macmusicapp.py + @echo Compiling script: $^ + @rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@ + build/ba_data/python/ba/__pycache__/internal.cpython-37.opt-1.pyc: \ build/ba_data/python/ba/internal.py @echo Compiling script: $^ @@ -900,6 +909,11 @@ build/ba_data/python/ba/__pycache__/_powerup.cpython-37.opt-1.pyc: \ @echo Compiling script: $^ @rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@ +build/ba_data/python/ba/__pycache__/osmusic.cpython-37.opt-1.pyc: \ + build/ba_data/python/ba/osmusic.py + @echo Compiling script: $^ + @rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@ + build/ba_data/python/ba/__pycache__/_campaign.cpython-37.opt-1.pyc: \ build/ba_data/python/ba/_campaign.py @echo Compiling script: $^ @@ -1345,13 +1359,13 @@ build/ba_data/python/bastd/ui/playlist/__pycache__/addgame.cpython-37.opt-1.pyc: @echo Compiling script: $^ @rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@ -build/ba_data/python/bastd/ui/soundtrack/__pycache__/entrytypeselect.cpython-37.opt-1.pyc: \ - build/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py +build/ba_data/python/bastd/ui/soundtrack/__pycache__/macmusicapp.cpython-37.opt-1.pyc: \ + build/ba_data/python/bastd/ui/soundtrack/macmusicapp.py @echo Compiling script: $^ @rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@ -build/ba_data/python/bastd/ui/soundtrack/__pycache__/itunes.cpython-37.opt-1.pyc: \ - build/ba_data/python/bastd/ui/soundtrack/itunes.py +build/ba_data/python/bastd/ui/soundtrack/__pycache__/entrytypeselect.cpython-37.opt-1.pyc: \ + build/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py @echo Compiling script: $^ @rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@ diff --git a/assets/src/ba_data/python/ba/__init__.py b/assets/src/ba_data/python/ba/__init__.py index f5b12972..e4b941fe 100644 --- a/assets/src/ba_data/python/ba/__init__.py +++ b/assets/src/ba_data/python/ba/__init__.py @@ -59,7 +59,7 @@ from ba._gameresults import TeamGameResults from ba._lang import Lstr, setlanguage, get_valid_languages from ba._map import Map, getmaps from ba._session import Session -from ba._server import Server +from ba._server import ServerController from ba._stats import PlayerScoredMessage, PlayerRecord, Stats from ba._team import Team from ba._teamgame import TeamGameActivity @@ -89,7 +89,7 @@ from ba.ui import Window, UIController, uicleanupcheck app: App -# Change everything's listed module to ba (instead of ba.foo.bar.etc). +# Change everything's listed module to simply 'ba' (instead of 'ba.foo.bar'). def _simplify_module_names() -> None: for attr, obj in globals().items(): if not attr.startswith('_'): diff --git a/assets/src/ba_data/python/ba/_app.py b/assets/src/ba_data/python/ba/_app.py index 2d0fd15b..4c123b82 100644 --- a/assets/src/ba_data/python/ba/_app.py +++ b/assets/src/ba_data/python/ba/_app.py @@ -31,8 +31,7 @@ if TYPE_CHECKING: from ba import _lang, _meta from ba.ui import UICleanupCheck from bastd.actor import spazappearance - from typing import (Optional, Dict, Tuple, Set, Any, List, Type, Tuple, - Callable) + from typing import Optional, Dict, Set, Any, Type, Tuple, Callable, List class App: @@ -268,9 +267,7 @@ class App: the single shared instance. """ # pylint: disable=too-many-statements - from ba._music import MusicPlayMode - - # _test_https() + from ba._music import MusicController # Config. self.config_file_healthy = False @@ -281,8 +278,7 @@ class App: self.fg_state = 0 # Environment stuff. - # (pulling these into attrs so we can type-check them) - + # (pulling these into attrs so we can type-check them and provide docs) env = _ba.env() self._build_number: int = env['build_number'] assert isinstance(self._build_number, int) @@ -332,7 +328,7 @@ class App: self.last_ad_completion_time: Optional[float] = None self.last_ad_was_short = False self.did_weak_call_warning = False - self.ran_on_launch = False + self.ran_on_app_launch = False # If we try to run promo-codes due to launch-args/etc we might # not be signed in yet; go ahead and queue them up in that case. @@ -354,15 +350,8 @@ class App: # Co-op Campaigns. self.campaigns: Dict[str, ba.Campaign] = {} - # Server-Mode. - self.server: Optional[ba.Server] = None - # self.server_config: Dict[str, Any] = {} - # self.server_config_dirty = False - # self.run_server_wait_timer: Optional[ba.Timer] = None - # self.server_playlist_fetch: Optional[Dict[str, Any]] = None - # self.next_server_account_warn_time: Optional[float] = None - # self.launched_server = False - # self.run_server_first_run = True + # Server Mode. + self.server: Optional[ba.ServerController] = None # Ads. self.last_ad_network = 'unknown' @@ -372,14 +361,7 @@ class App: self.attempted_first_ad = False # Music. - self.music: Optional[ba.Node] = None - self.music_mode: ba.MusicPlayMode = MusicPlayMode.REGULAR - self.music_player: Optional[ba.MusicPlayer] = None - self.music_player_type: Optional[Type[ba.MusicPlayer]] = None - self.music_types: Dict[ba.MusicPlayMode, Optional[ba.MusicType]] = { - MusicPlayMode.REGULAR: None, - MusicPlayMode.TEST: None - } + self.music = MusicController() # Language. self.language_target: Optional[_lang.AttrDict] = None @@ -453,13 +435,12 @@ class App: self.large_ui = env['interface_type'] == 'large' self.toolbars = env.get('toolbar_test', True) - def on_launch(self) -> None: + def on_app_launch(self) -> None: """Runs after the app finishes bootstrapping. (internal)""" # FIXME: Break this up. # pylint: disable=too-many-statements - # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=cyclic-import from ba import _apputils @@ -468,7 +449,6 @@ class App: from ba import _achievement from ba import _map from ba import _meta - from ba import _music from ba import _campaign from bastd import appdelegate from bastd import maps as stdmaps @@ -483,11 +463,6 @@ class App: _achievement.init_achievements() spazappearance.register_appearances() _campaign.init_campaigns() - if _ba.env()['platform'] == 'android': - self.music_player_type = _music.InternalMusicPlayer - elif _ba.env()['platform'] == 'mac' and hasattr( - _ba, 'mac_music_app_init'): - self.music_player_type = _music.MacMusicAppMusicPlayer # FIXME: This should not be hard-coded. for maptype in [ @@ -566,17 +541,7 @@ class App: # to disk. _appconfig.commit_app_config(force=True) - # If we're using a non-default playlist, lets go ahead and get our - # music-player going since it may hitch (better while we're faded - # out than later). - try: - if ('Soundtrack' in cfg and cfg['Soundtrack'] not in [ - '__default__', 'Default Soundtrack' - ]): - _music.get_music_player() - except Exception: - from ba import _error - _error.print_exception('error prepping music-player') + self.music.on_app_launch() launch_count = cfg.get('launchCount', 0) launch_count += 1 @@ -632,7 +597,7 @@ class App: _ba.pushcall(do_auto_sign_in) - self.ran_on_launch = True + self.ran_on_app_launch = True # from ba._dependency import test_depset # test_depset() @@ -744,10 +709,8 @@ class App: def handle_app_resume(self) -> None: """Run when the app resumes from a suspended state.""" - # If there's music playing externally, make sure we aren't playing - # ours. - from ba import _music - _music.handle_app_resume() + self.music.handle_app_resume() + self.fg_state += 1 # Mark our cached tourneys as invalid so anyone using them knows @@ -827,8 +790,7 @@ class App: def shutdown(self) -> None: """(internal)""" - if self.music_player is not None: - self.music_player.shutdown() + self.music.on_app_shutdown() def handle_deep_link(self, url: str) -> None: """Handle a deep link URL.""" diff --git a/assets/src/ba_data/python/ba/_music.py b/assets/src/ba_data/python/ba/_music.py index 4230a40f..80368f49 100644 --- a/assets/src/ba_data/python/ba/_music.py +++ b/assets/src/ba_data/python/ba/_music.py @@ -22,16 +22,14 @@ from __future__ import annotations import copy -import os -import random -import threading from typing import TYPE_CHECKING +from dataclasses import dataclass from enum import Enum import _ba if TYPE_CHECKING: - from typing import Callable, Any, List, Optional, Dict, Union, Tuple + from typing import Callable, Any, Optional, Dict, Union, Type class MusicType(Enum): @@ -72,6 +70,329 @@ class MusicPlayMode(Enum): TEST = 'test' +@dataclass +class AssetSoundtrackEntry: + """A music entry using an internal asset. + + Category: App Classes + """ + assetname: str + volume: float = 1.0 + loop: bool = True + + +# What gets played by default for our different music types: +ASSET_SOUNDTRACK_ENTRIES: Dict[MusicType, AssetSoundtrackEntry] = { + MusicType.MENU: + AssetSoundtrackEntry('menuMusic'), + MusicType.VICTORY: + AssetSoundtrackEntry('victoryMusic', volume=1.2, loop=False), + MusicType.CHAR_SELECT: + AssetSoundtrackEntry('charSelectMusic', volume=0.4), + MusicType.RUN_AWAY: + AssetSoundtrackEntry('runAwayMusic', volume=1.2), + MusicType.ONSLAUGHT: + AssetSoundtrackEntry('runAwayMusic', volume=1.2), + MusicType.KEEP_AWAY: + AssetSoundtrackEntry('runAwayMusic', volume=1.2), + MusicType.RACE: + AssetSoundtrackEntry('runAwayMusic', volume=1.2), + MusicType.EPIC_RACE: + AssetSoundtrackEntry('slowEpicMusic', volume=1.2), + MusicType.SCORES: + AssetSoundtrackEntry('scoresEpicMusic', volume=0.6, loop=False), + MusicType.GRAND_ROMP: + AssetSoundtrackEntry('grandRompMusic', volume=1.2), + MusicType.TO_THE_DEATH: + AssetSoundtrackEntry('toTheDeathMusic', volume=1.2), + MusicType.CHOSEN_ONE: + AssetSoundtrackEntry('survivalMusic', volume=0.8), + MusicType.FORWARD_MARCH: + AssetSoundtrackEntry('forwardMarchMusic', volume=0.8), + MusicType.FLAG_CATCHER: + AssetSoundtrackEntry('flagCatcherMusic', volume=1.2), + MusicType.SURVIVAL: + AssetSoundtrackEntry('survivalMusic', volume=0.8), + MusicType.EPIC: + AssetSoundtrackEntry('slowEpicMusic', volume=1.2), + MusicType.SPORTS: + AssetSoundtrackEntry('sportsMusic', volume=0.8), + MusicType.HOCKEY: + AssetSoundtrackEntry('sportsMusic', volume=0.8), + MusicType.FOOTBALL: + AssetSoundtrackEntry('sportsMusic', volume=0.8), + MusicType.FLYING: + AssetSoundtrackEntry('flyingMusic', volume=0.8), + MusicType.SCARY: + AssetSoundtrackEntry('scaryMusic', volume=0.8), + MusicType.MARCHING: + AssetSoundtrackEntry('whenJohnnyComesMarchingHomeMusic', volume=0.8), +} + + +class MusicController: + """Controller for overall music playback in the app. + + Category: App Classes + """ + + def __init__(self) -> None: + # pylint: disable=cyclic-import + self._music_node: Optional[_ba.Node] = None + self._music_mode: MusicPlayMode = MusicPlayMode.REGULAR + self._music_player: Optional[MusicPlayer] = None + self._music_player_type: Optional[Type[MusicPlayer]] = None + self.music_types: Dict[MusicPlayMode, Optional[MusicType]] = { + MusicPlayMode.REGULAR: None, + MusicPlayMode.TEST: None + } + + # Set up custom music players for platforms that support them. + # FIXME: should generalize this to support arbitrary players per + # platform (which can be discovered via ba_meta). + # Our standard asset playback should probably just be one of them + # instead of a special case. + if self.supports_soundtrack_entry_type('musicFile'): + from ba.osmusic import OSMusicPlayer + self._music_player_type = OSMusicPlayer + elif self.supports_soundtrack_entry_type('iTunesPlaylist'): + from ba.macmusicapp import MacMusicAppMusicPlayer + self._music_player_type = MacMusicAppMusicPlayer + + def on_app_launch(self) -> None: + """Should be called by app on_app_launch().""" + + # If we're using a non-default playlist, lets go ahead and get our + # music-player going since it may hitch (better while we're faded + # out than later). + try: + cfg = _ba.app.config + if ('Soundtrack' in cfg and cfg['Soundtrack'] not in [ + '__default__', 'Default Soundtrack' + ]): + self.get_music_player() + except Exception: + from ba import _error + _error.print_exception('error prepping music-player') + + def on_app_shutdown(self) -> None: + """Should be called when the app is shutting down.""" + if self._music_player is not None: + self._music_player.shutdown() + + def have_music_player(self) -> bool: + """Returns whether a music player is present.""" + return self._music_player_type is not None + + def get_music_player(self) -> MusicPlayer: + """Returns the system music player, instantiating if necessary.""" + if self._music_player is None: + if self._music_player_type is None: + raise Exception("no music player type set") + self._music_player = self._music_player_type() + return self._music_player + + def music_volume_changed(self, val: float) -> None: + """Should be called when changing the music volume.""" + if self._music_player is not None: + self._music_player.set_volume(val) + + def set_music_play_mode(self, + mode: MusicPlayMode, + force_restart: bool = False) -> None: + """Sets music play mode; used for soundtrack testing/etc.""" + old_mode = self._music_mode + self._music_mode = mode + if old_mode != self._music_mode or force_restart: + + # If we're switching into test mode we don't + # actually play anything until its requested. + # If we're switching *out* of test mode though + # we want to go back to whatever the normal song was. + if mode is MusicPlayMode.REGULAR: + mtype = self.music_types[MusicPlayMode.REGULAR] + self.do_play_music(None if mtype is None else mtype.value) + + def supports_soundtrack_entry_type(self, entry_type: str) -> bool: + """Return whether provided soundtrack entry type is supported here.""" + uas = _ba.env()['user_agent_string'] + assert isinstance(uas, str) + + # FIXME: Generalize this. + if entry_type == 'iTunesPlaylist': + return 'Mac' in uas + if entry_type in ('musicFile', 'musicFolder'): + return ('android' in uas + and _ba.android_get_external_storage_path() is not None) + if entry_type == 'default': + return True + return False + + def get_soundtrack_entry_type(self, entry: Any) -> str: + """Given a soundtrack entry, returns its type, taking into + account what is supported locally.""" + try: + if entry is None: + entry_type = 'default' + + # Simple string denotes iTunesPlaylist (legacy format). + elif isinstance(entry, str): + entry_type = 'iTunesPlaylist' + + # For other entries we expect type and name strings in a dict. + elif (isinstance(entry, dict) and 'type' in entry + and isinstance(entry['type'], str) and 'name' in entry + and isinstance(entry['name'], str)): + entry_type = entry['type'] + else: + raise Exception("invalid soundtrack entry: " + str(entry) + + " (type " + str(type(entry)) + ")") + if self.supports_soundtrack_entry_type(entry_type): + return entry_type + raise Exception("invalid soundtrack entry:" + str(entry)) + except Exception as exc: + print('EXC on get_soundtrack_entry_type', exc) + return 'default' + + def get_soundtrack_entry_name(self, entry: Any) -> str: + """Given a soundtrack entry, returns its name.""" + try: + if entry is None: + raise Exception('entry is None') + + # Simple string denotes an iTunesPlaylist name (legacy entry). + if isinstance(entry, str): + return entry + + # For other entries we expect type and name strings in a dict. + if (isinstance(entry, dict) and 'type' in entry + and isinstance(entry['type'], str) and 'name' in entry + and isinstance(entry['name'], str)): + return entry['name'] + raise Exception("invalid soundtrack entry:" + str(entry)) + except Exception: + from ba import _error + _error.print_exception() + return 'default' + + def handle_app_resume(self) -> None: + """Should be run when the app resumes from a suspended state.""" + if _ba.is_os_playing_music(): + self.do_play_music(None) + + def do_play_music(self, + musictype: Union[MusicType, str, None], + continuous: bool = False, + mode: MusicPlayMode = MusicPlayMode.REGULAR, + testsoundtrack: Dict[str, Any] = None) -> None: + """Plays the requested music type/mode. + + For most cases, setmusic() is the proper call to use, which itself + calls this. Certain cases, however, such as soundtrack testing, may + require calling this directly. + """ + + # We can be passed a MusicType or the string value corresponding + # to one. + if musictype is not None: + try: + musictype = MusicType(musictype) + except ValueError: + print(f"Invalid music type: '{musictype}'") + musictype = None + + with _ba.Context('ui'): + + # If they don't want to restart music and we're already + # playing what's requested, we're done. + if continuous and self.music_types[mode] is musictype: + return + self.music_types[mode] = musictype + + # If the OS tells us there's currently music playing, + # all our operations default to playing nothing. + if _ba.is_os_playing_music(): + musictype = None + + # If we're not in the mode this music is being set for, + # don't actually change what's playing. + if mode != self._music_mode: + return + + # Some platforms have a special music-player for things like iTunes + # soundtracks, mp3s, etc. if this is the case, attempt to grab an + # entry for this music-type, and if we have one, have the + # music-player play it. If not, we'll play game music ourself. + if musictype is not None and self._music_player_type is not None: + if testsoundtrack is not None: + soundtrack = testsoundtrack + else: + soundtrack = self._get_user_soundtrack() + entry = soundtrack.get(musictype.value) + else: + entry = None + + # Go through music-player. + if entry is not None: + self._play_music_player_music(entry) + + # Handle via internal music. + else: + self._play_internal_music(musictype) + + def _get_user_soundtrack(self) -> Dict[str, Any]: + """Return current user soundtrack or empty dict otherwise.""" + cfg = _ba.app.config + soundtrack: Dict[str, Any] = {} + soundtrackname = cfg.get('Soundtrack') + if soundtrackname is not None and soundtrackname != '__default__': + try: + soundtrack = cfg.get('Soundtracks', {})[soundtrackname] + except Exception as exc: + print(f"Error looking up user soundtrack: {exc}") + soundtrack = {} + return soundtrack + + def _play_music_player_music(self, entry: Any) -> None: + + # Stop any existing internal music. + if self._music_node is not None: + self._music_node.delete() + self._music_node = None + + # Do the thing. + self.get_music_player().play(entry) + + def _play_internal_music(self, musictype: Optional[MusicType]) -> None: + + # Stop any existing music-player playback. + if self._music_player is not None: + self._music_player.stop() + + # Stop any existing internal music. + if self._music_node: + self._music_node.delete() + self._music_node = None + + # Start up new internal music. + if musictype is not None: + + entry = ASSET_SOUNDTRACK_ENTRIES.get(musictype) + if entry is None: + print(f"Unknown music: '{musictype}'") + entry = ASSET_SOUNDTRACK_ENTRIES[MusicType.FLAG_CATCHER] + + self._music_node = _ba.newnode( + type='sound', + attrs={ + 'sound': _ba.getsound(entry.assetname), + 'positional': False, + 'music': True, + 'volume': entry.volume * 5.0, + 'loop': entry.loop + }) + + class MusicPlayer: """Wrangles soundtrack music playback. @@ -123,7 +444,7 @@ class MusicPlayer: def shutdown(self) -> None: """Shutdown music playback completely.""" - self.on_shutdown() + self.on_app_shutdown() def on_select_entry(self, callback: Callable[[Any], None], current_entry: Any, selection_target_name: str) -> Any: @@ -142,7 +463,7 @@ class MusicPlayer: def on_stop(self) -> None: """Called when the music should stop.""" - def on_shutdown(self) -> None: + def on_app_shutdown(self) -> None: """Called on final app shutdown.""" def _update_play_state(self) -> None: @@ -159,618 +480,33 @@ class MusicPlayer: self._actually_playing = False -class InternalMusicPlayer(MusicPlayer): - """Music player that talks to internal c layer functionality. - - (internal)""" - - def __init__(self) -> None: - super().__init__() - self._want_to_play = False - self._actually_playing = False - - def on_select_entry(self, callback: Callable[[Any], None], - current_entry: Any, selection_target_name: str) -> Any: - # pylint: disable=cyclic-import - from bastd.ui.soundtrack.entrytypeselect import ( - SoundtrackEntryTypeSelectWindow) - return SoundtrackEntryTypeSelectWindow(callback, current_entry, - selection_target_name) - - def on_set_volume(self, volume: float) -> None: - _ba.music_player_set_volume(volume) - - class _PickFolderSongThread(threading.Thread): - - def __init__(self, path: str, - callback: Callable[[Union[str, List[str]], Optional[str]], - None]): - super().__init__() - self._callback = callback - self._path = path - - def run(self) -> None: - from ba import _lang - from ba._general import Call - try: - _ba.set_thread_name("BA_PickFolderSongThread") - all_files: List[str] = [] - valid_extensions = [ - '.' + x for x in get_valid_music_file_extensions() - ] - for root, _subdirs, filenames in os.walk(self._path): - for fname in filenames: - if any(fname.lower().endswith(ext) - for ext in valid_extensions): - all_files.insert( - random.randrange(len(all_files) + 1), - root + '/' + fname) - if not all_files: - raise Exception( - _lang.Lstr(resource='internal.noMusicFilesInFolderText' - ).evaluate()) - _ba.pushcall(Call(self._callback, all_files, None), - from_other_thread=True) - except Exception as exc: - from ba import _error - _error.print_exception() - try: - err_str = str(exc) - except Exception: - err_str = '' - _ba.pushcall(Call(self._callback, self._path, err_str), - from_other_thread=True) - - def on_play(self, entry: Any) -> None: - entry_type = get_soundtrack_entry_type(entry) - name = get_soundtrack_entry_name(entry) - assert name is not None - if entry_type == 'musicFile': - self._want_to_play = self._actually_playing = True - _ba.music_player_play(name) - elif entry_type == 'musicFolder': - - # Launch a thread to scan this folder and give us a random - # valid file within. - self._want_to_play = True - self._actually_playing = False - self._PickFolderSongThread(name, self._on_play_folder_cb).start() - - def _on_play_folder_cb(self, - result: Union[str, List[str]], - error: Optional[str] = None) -> None: - from ba import _lang - if error is not None: - rstr = (_lang.Lstr( - resource='internal.errorPlayingMusicText').evaluate()) - if isinstance(result, str): - err_str = (rstr.replace('${MUSIC}', os.path.basename(result)) + - '; ' + str(error)) - else: - err_str = (rstr.replace('${MUSIC}', '') + '; ' + - str(error)) - _ba.screenmessage(err_str, color=(1, 0, 0)) - return - - # There's a chance a stop could have been issued before our thread - # returned. If that's the case, don't play. - if not self._want_to_play: - print('_on_play_folder_cb called with _want_to_play False') - else: - self._actually_playing = True - _ba.music_player_play(result) - - def on_stop(self) -> None: - self._want_to_play = False - self._actually_playing = False - _ba.music_player_stop() - - def on_shutdown(self) -> None: - _ba.music_player_shutdown() - - -# For internal music player. -# FIXME: this only applies to Android currently. -def get_valid_music_file_extensions() -> List[str]: - """Return file extensions for types playable on this device.""" - return ['mp3', 'ogg', 'm4a', 'wav', 'flac', 'mid'] - - -class MacMusicAppThread(threading.Thread): - """Thread which wrangles iTunes/Music.app playback""" - - def __init__(self) -> None: - super().__init__() - self._commands_available = threading.Event() - self._commands: List[List] = [] - self._volume = 1.0 - self._current_playlist: Optional[str] = None - self._orig_volume: Optional[int] = None - - def run(self) -> None: - """Run the Music.app thread.""" - from ba._general import Call - from ba._lang import Lstr - from ba._enums import TimeType - _ba.set_thread_name("BA_MacMusicAppThread") - _ba.mac_music_app_init() - - # Let's mention to the user we're launching Music.app in case - # it causes any funny business (this used to background the app - # sometimes, though I think that is fixed now) - def do_print() -> None: - _ba.timer(1.0, - Call(_ba.screenmessage, Lstr(resource='usingItunesText'), - (0, 1, 0)), - timetype=TimeType.REAL) - - _ba.pushcall(do_print, from_other_thread=True) - - # Here we grab this to force the actual launch. - _ba.mac_music_app_get_volume() - _ba.mac_music_app_get_library_source() - done = False - while not done: - self._commands_available.wait() - self._commands_available.clear() - - # We're not protecting this list with a mutex but we're - # just using it as a simple queue so it should be fine. - while self._commands: - cmd = self._commands.pop(0) - if cmd[0] == 'DIE': - - self._handle_die_command() - done = True - break - if cmd[0] == 'PLAY': - self._handle_play_command(target=cmd[1]) - elif cmd[0] == 'GET_PLAYLISTS': - self._handle_get_playlists_command(target=cmd[1]) - - del cmd # Allows the command data/callback/etc to be freed. - - def set_volume(self, volume: float) -> None: - """Set volume to a value between 0 and 1.""" - old_volume = self._volume - self._volume = volume - - # If we've got nothing we're supposed to be playing, - # don't touch itunes/music. - if self._current_playlist is None: - return - - # If volume is going to zero, stop actually playing - # but don't clear playlist. - if old_volume > 0.0 and volume == 0.0: - try: - assert self._orig_volume is not None - _ba.mac_music_app_stop() - _ba.mac_music_app_set_volume(self._orig_volume) - except Exception as exc: - print('Error stopping iTunes music:', exc) - elif self._volume > 0: - - # If volume was zero, store pre-playing volume and start - # playing. - if old_volume == 0.0: - self._orig_volume = _ba.mac_music_app_get_volume() - self._update_mac_music_app_volume() - if old_volume == 0.0: - self._play_current_playlist() - - def play_playlist(self, musictype: Optional[str]) -> None: - """Play the given playlist.""" - self._commands.append(['PLAY', musictype]) - self._commands_available.set() - - def shutdown(self) -> None: - """Request that the player shuts down.""" - self._commands.append(['DIE']) - self._commands_available.set() - self.join() - - def get_playlists(self, callback: Callable[[Any], None]) -> None: - """Request the list of playlists.""" - self._commands.append(['GET_PLAYLISTS', callback]) - self._commands_available.set() - - def _handle_get_playlists_command( - self, target: Callable[[List[str]], None]) -> None: - from ba._general import Call - try: - playlists = _ba.mac_music_app_get_playlists() - playlists = [ - p for p in playlists if p not in [ - 'Music', 'Movies', 'TV Shows', 'Podcasts', 'iTunes\xa0U', - 'Books', 'Genius', 'iTunes DJ', 'Music Videos', - 'Home Videos', 'Voice Memos', 'Audiobooks' - ] - ] - playlists.sort(key=lambda x: x.lower()) - except Exception as exc: - print('Error getting iTunes playlists:', exc) - playlists = [] - _ba.pushcall(Call(target, playlists), from_other_thread=True) - - def _handle_play_command(self, target: Optional[str]) -> None: - if target is None: - if self._current_playlist is not None and self._volume > 0: - try: - assert self._orig_volume is not None - _ba.mac_music_app_stop() - _ba.mac_music_app_set_volume(self._orig_volume) - except Exception as exc: - print('Error stopping iTunes music:', exc) - self._current_playlist = None - else: - # If we've got something playing with positive - # volume, stop it. - if self._current_playlist is not None and self._volume > 0: - try: - assert self._orig_volume is not None - _ba.mac_music_app_stop() - _ba.mac_music_app_set_volume(self._orig_volume) - except Exception as exc: - print('Error stopping iTunes music:', exc) - - # Set our playlist and play it if our volume is up. - self._current_playlist = target - if self._volume > 0: - self._orig_volume = (_ba.mac_music_app_get_volume()) - self._update_mac_music_app_volume() - self._play_current_playlist() - - def _handle_die_command(self) -> None: - - # Only stop if we've actually played something - # (we don't want to kill music the user has playing). - if self._current_playlist is not None and self._volume > 0: - try: - assert self._orig_volume is not None - _ba.mac_music_app_stop() - _ba.mac_music_app_set_volume(self._orig_volume) - except Exception as exc: - print('Error stopping iTunes music:', exc) - - def _play_current_playlist(self) -> None: - try: - from ba import _lang - from ba._general import Call - assert self._current_playlist is not None - if _ba.mac_music_app_play_playlist(self._current_playlist): - pass - else: - _ba.pushcall(Call( - _ba.screenmessage, - _lang.get_resource('playlistNotFoundText') + ': \'' + - self._current_playlist + '\'', (1, 0, 0)), - from_other_thread=True) - except Exception: - from ba import _error - _error.print_exception( - f"error playing playlist {self._current_playlist}") - - def _update_mac_music_app_volume(self) -> None: - _ba.mac_music_app_set_volume( - max(0, min(100, int(100.0 * self._volume)))) - - -class MacMusicAppMusicPlayer(MusicPlayer): - """A music-player that utilizes iTunes/Music.app for playback. - - Allows selecting playlists as entries. - """ - - def __init__(self) -> None: - super().__init__() - self._thread = MacMusicAppThread() - self._thread.start() - - def on_select_entry(self, callback: Callable[[Any], None], - current_entry: Any, selection_target_name: str) -> Any: - # pylint: disable=cyclic-import - from bastd.ui.soundtrack import entrytypeselect as etsel - return etsel.SoundtrackEntryTypeSelectWindow(callback, current_entry, - selection_target_name) - - def on_set_volume(self, volume: float) -> None: - self._thread.set_volume(volume) - - def get_playlists(self, callback: Callable) -> None: - """Asynchronously fetch the list of available iTunes playlists.""" - self._thread.get_playlists(callback) - - def on_play(self, entry: Any) -> None: - entry_type = get_soundtrack_entry_type(entry) - if entry_type == 'iTunesPlaylist': - self._thread.play_playlist(get_soundtrack_entry_name(entry)) - else: - print('MacMusicAppMusicPlayer passed unrecognized entry type:', - entry_type) - - def on_stop(self) -> None: - self._thread.play_playlist(None) - - def on_shutdown(self) -> None: - self._thread.shutdown() - - -def have_music_player() -> bool: - """Returns whether a music player is present.""" - return _ba.app.music_player_type is not None - - -def get_music_player() -> MusicPlayer: - """Returns the system music player, instantiating if necessary.""" - app = _ba.app - if app.music_player is None: - if app.music_player_type is None: - raise Exception("no music player type set") - app.music_player = app.music_player_type() - return app.music_player - - -def music_volume_changed(val: float) -> None: - """Should be called when changing the music volume.""" - app = _ba.app - if app.music_player is not None: - app.music_player.set_volume(val) - - -def set_music_play_mode(mode: MusicPlayMode, - force_restart: bool = False) -> None: - """Sets music play mode; used for soundtrack testing/etc.""" - app = _ba.app - old_mode = app.music_mode - app.music_mode = mode - if old_mode != app.music_mode or force_restart: - - # If we're switching into test mode we don't - # actually play anything until its requested. - # If we're switching *out* of test mode though - # we want to go back to whatever the normal song was. - if mode is MusicPlayMode.REGULAR: - mtype = app.music_types[MusicPlayMode.REGULAR] - do_play_music(None if mtype is None else mtype.value) - - -def supports_soundtrack_entry_type(entry_type: str) -> bool: - """Return whether the provided soundtrack entry type is supported here.""" - uas = _ba.app.user_agent_string - if entry_type == 'iTunesPlaylist': - return 'Mac' in uas - if entry_type in ('musicFile', 'musicFolder'): - return ('android' in uas - and _ba.android_get_external_storage_path() is not None) - if entry_type == 'default': - return True - return False - - -def get_soundtrack_entry_type(entry: Any) -> str: - """Given a soundtrack entry, returns its type, taking into - account what is supported locally.""" - try: - if entry is None: - entry_type = 'default' - - # Simple string denotes iTunesPlaylist (legacy format). - elif isinstance(entry, str): - entry_type = 'iTunesPlaylist' - - # For other entries we expect type and name strings in a dict. - elif (isinstance(entry, dict) and 'type' in entry - and isinstance(entry['type'], str) and 'name' in entry - and isinstance(entry['name'], str)): - entry_type = entry['type'] - else: - raise Exception("invalid soundtrack entry: " + str(entry) + - " (type " + str(type(entry)) + ")") - if supports_soundtrack_entry_type(entry_type): - return entry_type - raise Exception("invalid soundtrack entry:" + str(entry)) - except Exception as exc: - print('EXC on get_soundtrack_entry_type', exc) - return 'default' - - -def get_soundtrack_entry_name(entry: Any) -> str: - """Given a soundtrack entry, returns its name.""" - try: - if entry is None: - raise Exception('entry is None') - - # Simple string denotes an iTunesPlaylist name (legacy entry). - if isinstance(entry, str): - return entry - - # For other entries we expect type and name strings in a dict. - if (isinstance(entry, dict) and 'type' in entry - and isinstance(entry['type'], str) and 'name' in entry - and isinstance(entry['name'], str)): - return entry['name'] - raise Exception("invalid soundtrack entry:" + str(entry)) - except Exception: - from ba import _error - _error.print_exception() - return 'default' - - def setmusic(musictype: Optional[MusicType], continuous: bool = False) -> None: - """Set or stop the current music based on a string musictype. + """Tell the game to play (or stop playing) a certain type of music. category: Gameplay Functions - This function will handle loading and playing sound media as necessary, + This function will handle loading and playing sound assets as necessary, and also supports custom user soundtracks on specific platforms so the user can override particular game music with their own. Pass None to stop music. - if 'continuous' is True the musictype passed is the same as what is already + if 'continuous' is True and musictype is the same as what is already playing, the playing track will not be restarted. """ from ba import _gameutils # All we do here now is set a few music attrs on the current globals # node. The foreground globals' current playing music then gets fed to - # the do_play_music call below. This way we can seamlessly support custom - # soundtracks in replays/etc since we're replaying an attr value set; - # not an actual sound node create. + # the do_play_music call in our music controller. This way we can + # seamlessly support custom soundtracks in replays/etc since we're being + # driven purely by node data. gnode = _gameutils.sharedobj('globals') gnode.music_continuous = continuous gnode.music = '' if musictype is None else musictype.value gnode.music_count += 1 -def handle_app_resume() -> None: - """Should be run when the app resumes from a suspended state.""" - if _ba.is_os_playing_music(): - do_play_music(None) - - -def do_play_music(musictype: Union[MusicType, str, None], - continuous: bool = False, - mode: MusicPlayMode = MusicPlayMode.REGULAR, - testsoundtrack: Dict[str, Any] = None) -> None: - """Plays the requested music type/mode. - - For most cases setmusic() is the proper call to use, which itself calls - this. Certain cases, however, such as soundtrack testing, may require - calling this directly. - """ - - # We can be passed a MusicType or the string value of one. - if musictype is not None: - try: - musictype = MusicType(musictype) - except ValueError: - print(f"Invalid music type: '{musictype}'") - musictype = None - - app = _ba.app - with _ba.Context('ui'): - - # If they don't want to restart music and we're already - # playing what's requested, we're done. - if continuous and app.music_types[mode] is musictype: - return - app.music_types[mode] = musictype - - # If the OS tells us there's currently music playing, - # all our operations default to playing nothing. - if _ba.is_os_playing_music(): - musictype = None - - # If we're not in the mode this music is being set for, - # don't actually change what's playing. - if mode != app.music_mode: - return - - # Some platforms have a special music-player for things like iTunes - # soundtracks, mp3s, etc. if this is the case, attempt to grab an - # entry for this music-type, and if we have one, have the music-player - # play it. If not, we'll play game music ourself. - if musictype is not None and app.music_player_type is not None: - if testsoundtrack is not None: - soundtrack = testsoundtrack - else: - soundtrack = _get_user_soundtrack() - entry = soundtrack.get(musictype.value) - else: - entry = None - - # Go through music-player. - if entry is not None: - _play_music_player_music(entry) - - # Handle via internal music. - else: - _play_internal_music(musictype) - - -def _get_user_soundtrack() -> Dict[str, Any]: - """Return current user soundtrack or empty dict otherwise.""" - cfg = _ba.app.config - soundtrack: Dict[str, Any] = {} - soundtrackname = cfg.get('Soundtrack') - if soundtrackname is not None and soundtrackname != '__default__': - try: - soundtrack = cfg.get('Soundtracks', {})[soundtrackname] - except Exception as exc: - print(f"Error looking up user soundtrack: {exc}") - soundtrack = {} - return soundtrack - - -def _play_music_player_music(entry: Any) -> None: - app = _ba.app - - # Stop any existing internal music. - if app.music is not None: - app.music.delete() - app.music = None - - # Do the thing. - get_music_player().play(entry) - - -def _play_internal_music(musictype: Optional[MusicType]) -> None: - app = _ba.app - - # Stop any existing music-player playback. - if app.music_player is not None: - app.music_player.stop() - - # Stop any existing internal music. - if app.music: - app.music.delete() - app.music = None - - # Start up new internal music. - if musictype is not None: - - # Filenames/volume/loop for our built-in music. - musicinfos: Dict[MusicType, Tuple[str, float, bool]] = { - MusicType.MENU: ('menuMusic', 5.0, True), - MusicType.VICTORY: ('victoryMusic', 6.0, False), - MusicType.CHAR_SELECT: ('charSelectMusic', 2.0, True), - MusicType.RUN_AWAY: ('runAwayMusic', 6.0, True), - MusicType.ONSLAUGHT: ('runAwayMusic', 6.0, True), - MusicType.KEEP_AWAY: ('runAwayMusic', 6.0, True), - MusicType.RACE: ('runAwayMusic', 6.0, True), - MusicType.EPIC_RACE: ('slowEpicMusic', 6.0, True), - MusicType.SCORES: ('scoresEpicMusic', 3.0, False), - MusicType.GRAND_ROMP: ('grandRompMusic', 6.0, True), - MusicType.TO_THE_DEATH: ('toTheDeathMusic', 6.0, True), - MusicType.CHOSEN_ONE: ('survivalMusic', 4.0, True), - MusicType.FORWARD_MARCH: ('forwardMarchMusic', 4.0, True), - MusicType.FLAG_CATCHER: ('flagCatcherMusic', 6.0, True), - MusicType.SURVIVAL: ('survivalMusic', 4.0, True), - MusicType.EPIC: ('slowEpicMusic', 6.0, True), - MusicType.SPORTS: ('sportsMusic', 4.0, True), - MusicType.HOCKEY: ('sportsMusic', 4.0, True), - MusicType.FOOTBALL: ('sportsMusic', 4.0, True), - MusicType.FLYING: ('flyingMusic', 4.0, True), - MusicType.SCARY: ('scaryMusic', 4.0, True), - MusicType.MARCHING: - ('whenJohnnyComesMarchingHomeMusic', 4.0, True), - } - musicinfo = musicinfos.get(musictype) - if musicinfo is None: - print(f"Unknown music: '{musictype}'") - filename = 'flagCatcherMusic' - volume = 6.0 - loop = True - else: - filename, volume, loop = musicinfo - - app.music = _ba.newnode(type='sound', - attrs={ - 'sound': _ba.getsound(filename), - 'positional': False, - 'music': True, - 'volume': volume, - 'loop': loop - }) +def do_play_music(*args: Any, **keywds: Any) -> None: + """A passthrough used by the C++ layer.""" + _ba.app.music.do_play_music(*args, **keywds) diff --git a/assets/src/ba_data/python/ba/_server.py b/assets/src/ba_data/python/ba/_server.py index 99ddd9bb..e7e3a8c2 100644 --- a/assets/src/ba_data/python/ba/_server.py +++ b/assets/src/ba_data/python/ba/_server.py @@ -46,14 +46,14 @@ def _cmd(command_data: bytes) -> None: if command is ServerCommand.CONFIG: assert isinstance(payload, ServerConfig) assert _ba.app.server is None - _ba.app.server = Server(payload) + _ba.app.server = ServerController(payload) return assert _ba.app.server is not None print('WOULD DO OTHER SERVER COMMAND') -class Server: +class ServerController: """Overall controller for the app in server mode. Category: App Classes @@ -79,8 +79,6 @@ class Server: self._config_server() - # Launch the server only the first time through; - # after that it will be self-sustaining. self._next_server_account_warn_time = time.time() + 10.0 # Now sit around until we're signed in and then @@ -197,6 +195,9 @@ class Server: signed_in = _ba.get_account_state() == 'signed_in' if not signed_in: + + # Signing in to the local server account should not take long; + # complain if it does... curtime = time.time() if curtime > self._next_server_account_warn_time: print('Still waiting for account sign-in...') @@ -205,7 +206,6 @@ class Server: can_launch = False # If we're trying to fetch a playlist, we do that first. - # if self._server_playlist_fetch is not None: if self._playlist_fetch_running: # Send request if we haven't. diff --git a/assets/src/ba_data/python/ba/internal.py b/assets/src/ba_data/python/ba/internal.py index 6ff81f15..4a9f5386 100644 --- a/assets/src/ba_data/python/ba/internal.py +++ b/assets/src/ba_data/python/ba/internal.py @@ -53,11 +53,7 @@ from ba._messages import PlayerProfilesChangedMessage from ba._meta import get_game_types from ba._modutils import show_user_scripts from ba._multiteamsession import DEFAULT_TEAM_COLORS, DEFAULT_TEAM_NAMES -from ba._music import (have_music_player, music_volume_changed, do_play_music, - get_soundtrack_entry_name, get_soundtrack_entry_type, - get_music_player, set_music_play_mode, - supports_soundtrack_entry_type, - get_valid_music_file_extensions, MacMusicAppMusicPlayer) +from ba._music import do_play_music from ba._netutils import serverget, serverput, get_ip_address_type from ba._powerup import get_default_powerup_distribution from ba._profile import (get_player_profile_colors, get_player_profile_icon, diff --git a/assets/src/ba_data/python/ba/macmusicapp.py b/assets/src/ba_data/python/ba/macmusicapp.py new file mode 100644 index 00000000..816e0200 --- /dev/null +++ b/assets/src/ba_data/python/ba/macmusicapp.py @@ -0,0 +1,251 @@ +# Copyright (c) 2011-2020 Eric Froemling +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ----------------------------------------------------------------------------- +"""Music playback functionality using the Mac Music (formerly iTunes) app.""" +from __future__ import annotations + +import threading +from typing import TYPE_CHECKING + +import _ba +from ba._music import MusicPlayer + +if TYPE_CHECKING: + from typing import List, Optional, Callable, Any + + +class MacMusicAppMusicPlayer(MusicPlayer): + """A music-player that utilizes the macOS Music.app for playback. + + Allows selecting playlists as entries. + """ + + def __init__(self) -> None: + super().__init__() + self._thread = _MacMusicAppThread() + self._thread.start() + + def on_select_entry(self, callback: Callable[[Any], None], + current_entry: Any, selection_target_name: str) -> Any: + # pylint: disable=cyclic-import + from bastd.ui.soundtrack import entrytypeselect as etsel + return etsel.SoundtrackEntryTypeSelectWindow(callback, current_entry, + selection_target_name) + + def on_set_volume(self, volume: float) -> None: + self._thread.set_volume(volume) + + def get_playlists(self, callback: Callable) -> None: + """Asynchronously fetch the list of available iTunes playlists.""" + self._thread.get_playlists(callback) + + def on_play(self, entry: Any) -> None: + music = _ba.app.music + entry_type = music.get_soundtrack_entry_type(entry) + if entry_type == 'iTunesPlaylist': + self._thread.play_playlist(music.get_soundtrack_entry_name(entry)) + else: + print('MacMusicAppMusicPlayer passed unrecognized entry type:', + entry_type) + + def on_stop(self) -> None: + self._thread.play_playlist(None) + + def on_app_shutdown(self) -> None: + self._thread.shutdown() + + +class _MacMusicAppThread(threading.Thread): + """Thread which wrangles Music.app playback""" + + def __init__(self) -> None: + super().__init__() + self._commands_available = threading.Event() + self._commands: List[List] = [] + self._volume = 1.0 + self._current_playlist: Optional[str] = None + self._orig_volume: Optional[int] = None + + def run(self) -> None: + """Run the Music.app thread.""" + from ba._general import Call + from ba._lang import Lstr + from ba._enums import TimeType + _ba.set_thread_name("BA_MacMusicAppThread") + _ba.mac_music_app_init() + + # Let's mention to the user we're launching Music.app in case + # it causes any funny business (this used to background the app + # sometimes, though I think that is fixed now) + def do_print() -> None: + _ba.timer(1.0, + Call(_ba.screenmessage, Lstr(resource='usingItunesText'), + (0, 1, 0)), + timetype=TimeType.REAL) + + _ba.pushcall(do_print, from_other_thread=True) + + # Here we grab this to force the actual launch. + _ba.mac_music_app_get_volume() + _ba.mac_music_app_get_library_source() + done = False + while not done: + self._commands_available.wait() + self._commands_available.clear() + + # We're not protecting this list with a mutex but we're + # just using it as a simple queue so it should be fine. + while self._commands: + cmd = self._commands.pop(0) + if cmd[0] == 'DIE': + self._handle_die_command() + done = True + break + if cmd[0] == 'PLAY': + self._handle_play_command(target=cmd[1]) + elif cmd[0] == 'GET_PLAYLISTS': + self._handle_get_playlists_command(target=cmd[1]) + + del cmd # Allows the command data/callback/etc to be freed. + + def set_volume(self, volume: float) -> None: + """Set volume to a value between 0 and 1.""" + old_volume = self._volume + self._volume = volume + + # If we've got nothing we're supposed to be playing, + # don't touch itunes/music. + if self._current_playlist is None: + return + + # If volume is going to zero, stop actually playing + # but don't clear playlist. + if old_volume > 0.0 and volume == 0.0: + try: + assert self._orig_volume is not None + _ba.mac_music_app_stop() + _ba.mac_music_app_set_volume(self._orig_volume) + except Exception as exc: + print('Error stopping iTunes music:', exc) + elif self._volume > 0: + + # If volume was zero, store pre-playing volume and start + # playing. + if old_volume == 0.0: + self._orig_volume = _ba.mac_music_app_get_volume() + self._update_mac_music_app_volume() + if old_volume == 0.0: + self._play_current_playlist() + + def play_playlist(self, musictype: Optional[str]) -> None: + """Play the given playlist.""" + self._commands.append(['PLAY', musictype]) + self._commands_available.set() + + def shutdown(self) -> None: + """Request that the player shuts down.""" + self._commands.append(['DIE']) + self._commands_available.set() + self.join() + + def get_playlists(self, callback: Callable[[Any], None]) -> None: + """Request the list of playlists.""" + self._commands.append(['GET_PLAYLISTS', callback]) + self._commands_available.set() + + def _handle_get_playlists_command( + self, target: Callable[[List[str]], None]) -> None: + from ba._general import Call + try: + playlists = _ba.mac_music_app_get_playlists() + playlists = [ + p for p in playlists if p not in [ + 'Music', 'Movies', 'TV Shows', 'Podcasts', 'iTunes\xa0U', + 'Books', 'Genius', 'iTunes DJ', 'Music Videos', + 'Home Videos', 'Voice Memos', 'Audiobooks' + ] + ] + playlists.sort(key=lambda x: x.lower()) + except Exception as exc: + print('Error getting iTunes playlists:', exc) + playlists = [] + _ba.pushcall(Call(target, playlists), from_other_thread=True) + + def _handle_play_command(self, target: Optional[str]) -> None: + if target is None: + if self._current_playlist is not None and self._volume > 0: + try: + assert self._orig_volume is not None + _ba.mac_music_app_stop() + _ba.mac_music_app_set_volume(self._orig_volume) + except Exception as exc: + print('Error stopping iTunes music:', exc) + self._current_playlist = None + else: + # If we've got something playing with positive + # volume, stop it. + if self._current_playlist is not None and self._volume > 0: + try: + assert self._orig_volume is not None + _ba.mac_music_app_stop() + _ba.mac_music_app_set_volume(self._orig_volume) + except Exception as exc: + print('Error stopping iTunes music:', exc) + + # Set our playlist and play it if our volume is up. + self._current_playlist = target + if self._volume > 0: + self._orig_volume = (_ba.mac_music_app_get_volume()) + self._update_mac_music_app_volume() + self._play_current_playlist() + + def _handle_die_command(self) -> None: + + # Only stop if we've actually played something + # (we don't want to kill music the user has playing). + if self._current_playlist is not None and self._volume > 0: + try: + assert self._orig_volume is not None + _ba.mac_music_app_stop() + _ba.mac_music_app_set_volume(self._orig_volume) + except Exception as exc: + print('Error stopping iTunes music:', exc) + + def _play_current_playlist(self) -> None: + try: + from ba import _lang + from ba._general import Call + assert self._current_playlist is not None + if _ba.mac_music_app_play_playlist(self._current_playlist): + pass + else: + _ba.pushcall(Call( + _ba.screenmessage, + _lang.get_resource('playlistNotFoundText') + ': \'' + + self._current_playlist + '\'', (1, 0, 0)), + from_other_thread=True) + except Exception: + from ba import _error + _error.print_exception( + f"error playing playlist {self._current_playlist}") + + def _update_mac_music_app_volume(self) -> None: + _ba.mac_music_app_set_volume( + max(0, min(100, int(100.0 * self._volume)))) diff --git a/assets/src/ba_data/python/ba/osmusic.py b/assets/src/ba_data/python/ba/osmusic.py new file mode 100644 index 00000000..6caacaf6 --- /dev/null +++ b/assets/src/ba_data/python/ba/osmusic.py @@ -0,0 +1,150 @@ +# Copyright (c) 2011-2020 Eric Froemling +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ----------------------------------------------------------------------------- +"""Music playback using OS functionality exposed through the C++ layer.""" +from __future__ import annotations + +import os +import random +import threading +from typing import TYPE_CHECKING + +import _ba +from ba._music import MusicPlayer + +if TYPE_CHECKING: + from typing import Callable, Any, Union, List, Optional + + +class OSMusicPlayer(MusicPlayer): + """Music player that talks to internal C++ layer for functionality. + + (internal)""" + + def __init__(self) -> None: + super().__init__() + self._want_to_play = False + self._actually_playing = False + + @classmethod + def get_valid_music_file_extensions(cls) -> List[str]: + """Return file extensions for types playable on this device.""" + # FIXME: should ask the C++ layer for these; just hard-coding for now. + return ['mp3', 'ogg', 'm4a', 'wav', 'flac', 'mid'] + + def on_select_entry(self, callback: Callable[[Any], None], + current_entry: Any, selection_target_name: str) -> Any: + # pylint: disable=cyclic-import + from bastd.ui.soundtrack.entrytypeselect import ( + SoundtrackEntryTypeSelectWindow) + return SoundtrackEntryTypeSelectWindow(callback, current_entry, + selection_target_name) + + def on_set_volume(self, volume: float) -> None: + _ba.music_player_set_volume(volume) + + def on_play(self, entry: Any) -> None: + music = _ba.app.music + entry_type = music.get_soundtrack_entry_type(entry) + name = music.get_soundtrack_entry_name(entry) + assert name is not None + if entry_type == 'musicFile': + self._want_to_play = self._actually_playing = True + _ba.music_player_play(name) + elif entry_type == 'musicFolder': + + # Launch a thread to scan this folder and give us a random + # valid file within. + self._want_to_play = True + self._actually_playing = False + _PickFolderSongThread(name, self.get_valid_music_file_extensions(), + self._on_play_folder_cb).start() + + def _on_play_folder_cb(self, + result: Union[str, List[str]], + error: Optional[str] = None) -> None: + from ba import _lang + if error is not None: + rstr = (_lang.Lstr( + resource='internal.errorPlayingMusicText').evaluate()) + if isinstance(result, str): + err_str = (rstr.replace('${MUSIC}', os.path.basename(result)) + + '; ' + str(error)) + else: + err_str = (rstr.replace('${MUSIC}', '') + '; ' + + str(error)) + _ba.screenmessage(err_str, color=(1, 0, 0)) + return + + # There's a chance a stop could have been issued before our thread + # returned. If that's the case, don't play. + if not self._want_to_play: + print('_on_play_folder_cb called with _want_to_play False') + else: + self._actually_playing = True + _ba.music_player_play(result) + + def on_stop(self) -> None: + self._want_to_play = False + self._actually_playing = False + _ba.music_player_stop() + + def on_app_shutdown(self) -> None: + _ba.music_player_shutdown() + + +class _PickFolderSongThread(threading.Thread): + + def __init__(self, path: str, valid_extensions: List[str], + callback: Callable[[Union[str, List[str]], Optional[str]], + None]): + super().__init__() + self._valid_extensions = valid_extensions + self._callback = callback + self._path = path + + def run(self) -> None: + from ba import _lang + from ba._general import Call + try: + _ba.set_thread_name("BA_PickFolderSongThread") + all_files: List[str] = [] + valid_extensions = ['.' + x for x in self._valid_extensions] + for root, _subdirs, filenames in os.walk(self._path): + for fname in filenames: + if any(fname.lower().endswith(ext) + for ext in valid_extensions): + all_files.insert(random.randrange(len(all_files) + 1), + root + '/' + fname) + if not all_files: + raise Exception( + _lang.Lstr(resource='internal.noMusicFilesInFolderText'). + evaluate()) + _ba.pushcall(Call(self._callback, all_files, None), + from_other_thread=True) + except Exception as exc: + from ba import _error + _error.print_exception() + try: + err_str = str(exc) + except Exception: + err_str = '' + _ba.pushcall(Call(self._callback, self._path, err_str), + from_other_thread=True) diff --git a/assets/src/ba_data/python/bastd/ui/settings/audio.py b/assets/src/ba_data/python/bastd/ui/settings/audio.py index ab7c0b81..0a18c4dc 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/audio.py +++ b/assets/src/ba_data/python/bastd/ui/settings/audio.py @@ -40,9 +40,10 @@ class AudioSettingsWindow(ba.Window): # pylint: disable=too-many-statements # pylint: disable=too-many-locals # pylint: disable=cyclic-import - from ba.internal import have_music_player, music_volume_changed - from bastd.ui import popup as popup_ui - from bastd.ui import config as cfgui + from bastd.ui.popup import PopupMenu + from bastd.ui.config import ConfigNumberEdit + + music = ba.app.music # If they provided an origin-widget, scale up from that. scale_origin: Optional[Tuple[float, float]] @@ -60,8 +61,8 @@ class AudioSettingsWindow(ba.Window): width = 460.0 height = 210.0 - # Update: hard-coding head-relative audio to true now, so not showing - # options. + # Update: hard-coding head-relative audio to true now, + # so not showing options. # show_vr_head_relative_audio = True if ba.app.vr_mode else False show_vr_head_relative_audio = False @@ -69,7 +70,7 @@ class AudioSettingsWindow(ba.Window): height += 70 show_soundtracks = False - if have_music_player(): + if music.have_music_player(): show_soundtracks = True height += spacing * 2.0 @@ -111,7 +112,7 @@ class AudioSettingsWindow(ba.Window): size=(60, 60), label=ba.charstr(ba.SpecialChar.BACK)) - self._sound_volume_numedit = svne = cfgui.ConfigNumberEdit( + self._sound_volume_numedit = svne = ConfigNumberEdit( parent=self._root_widget, position=(40, v), xoffset=10, @@ -124,7 +125,7 @@ class AudioSettingsWindow(ba.Window): ba.widget(edit=svne.plusbutton, right_widget=_ba.get_special_widget('party_button')) v -= spacing - self._music_volume_numedit = cfgui.ConfigNumberEdit( + self._music_volume_numedit = ConfigNumberEdit( parent=self._root_widget, position=(40, v), xoffset=10, @@ -133,7 +134,7 @@ class AudioSettingsWindow(ba.Window): minval=0.0, maxval=1.0, increment=0.1, - callback=music_volume_changed, + callback=music.music_volume_changed, changesound=False) v -= 0.5 * spacing @@ -151,7 +152,7 @@ class AudioSettingsWindow(ba.Window): h_align="left", v_align="center") - popup = popup_ui.PopupMenu( + popup = PopupMenu( parent=self._root_widget, position=(290, v), width=120, diff --git a/assets/src/ba_data/python/bastd/ui/soundtrack/browser.py b/assets/src/ba_data/python/bastd/ui/soundtrack/browser.py index 68c5b5f0..101bcaa7 100644 --- a/assets/src/ba_data/python/bastd/ui/soundtrack/browser.py +++ b/assets/src/ba_data/python/bastd/ui/soundtrack/browser.py @@ -311,7 +311,7 @@ class SoundtrackBrowserWindow(ba.Window): self._refresh(select_soundtrack=test_name) def _select(self, name: str, index: int) -> None: - from ba.internal import do_play_music + music = ba.app.music self._selected_soundtrack_index = index self._selected_soundtrack = name cfg = ba.app.config @@ -322,9 +322,10 @@ class SoundtrackBrowserWindow(ba.Window): ba.playsound(ba.getsound('gunCocking')) cfg['Soundtrack'] = self._selected_soundtrack cfg.commit() + # Just play whats already playing.. this'll grab it from the # new soundtrack. - do_play_music(ba.app.music_types[ba.MusicPlayMode.REGULAR]) + music.do_play_music(music.music_types[ba.MusicPlayMode.REGULAR]) def _back(self) -> None: # pylint: disable=cyclic-import diff --git a/assets/src/ba_data/python/bastd/ui/soundtrack/edit.py b/assets/src/ba_data/python/bastd/ui/soundtrack/edit.py index 6453dd42..b96ca296 100644 --- a/assets/src/ba_data/python/bastd/ui/soundtrack/edit.py +++ b/assets/src/ba_data/python/bastd/ui/soundtrack/edit.py @@ -291,7 +291,8 @@ class SoundtrackEditWindow(ba.Window): @classmethod def _restore_editor(cls, state: Dict[str, Any], musictype: str, entry: Any) -> None: - from ba.internal import get_soundtrack_entry_type + music = ba.app.music + # Apply the change and recreate the window. soundtrack = state['soundtrack'] existing_entry = (None if musictype not in soundtrack else @@ -303,7 +304,7 @@ class SoundtrackEditWindow(ba.Window): if entry is not None: entry = copy.deepcopy(entry) - entry_type = get_soundtrack_entry_type(entry) + entry_type = music.get_soundtrack_entry_type(entry) if entry_type == 'default': # For 'default' entries simply exclude them from the list. if musictype in soundtrack: @@ -316,7 +317,7 @@ class SoundtrackEditWindow(ba.Window): def _get_entry(self, song_type: str, entry: Any, selection_target_name: str) -> None: - from ba.internal import get_music_player + music = ba.app.music if selection_target_name != '': selection_target_name = "'" + selection_target_name + "'" state = { @@ -326,12 +327,12 @@ class SoundtrackEditWindow(ba.Window): 'last_edited_song_type': song_type } ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.main_menu_window = (get_music_player().select_entry( + ba.app.main_menu_window = (music.get_music_player().select_entry( ba.Call(self._restore_editor, state, song_type), entry, selection_target_name).get_root_widget()) def _test(self, song_type: ba.MusicType) -> None: - from ba.internal import set_music_play_mode, do_play_music + music = ba.app.music # Warn if volume is zero. if ba.app.config.resolve("Music Volume") < 0.01: @@ -339,28 +340,27 @@ class SoundtrackEditWindow(ba.Window): ba.screenmessage(ba.Lstr(resource=self._r + '.musicVolumeZeroWarning'), color=(1, 0.5, 0)) - set_music_play_mode(ba.MusicPlayMode.TEST) - do_play_music(song_type, - mode=ba.MusicPlayMode.TEST, - testsoundtrack=self._soundtrack) + music.set_music_play_mode(ba.MusicPlayMode.TEST) + music.do_play_music(song_type, + mode=ba.MusicPlayMode.TEST, + testsoundtrack=self._soundtrack) def _get_entry_button_display_name(self, entry: Any) -> Union[str, ba.Lstr]: - from ba.internal import (get_soundtrack_entry_type, - get_soundtrack_entry_name) - etype = get_soundtrack_entry_type(entry) + music = ba.app.music + etype = music.get_soundtrack_entry_type(entry) ename: Union[str, ba.Lstr] if etype == 'default': ename = ba.Lstr(resource=self._r + '.defaultGameMusicText') elif etype in ('musicFile', 'musicFolder'): - ename = os.path.basename(get_soundtrack_entry_name(entry)) + ename = os.path.basename(music.get_soundtrack_entry_name(entry)) else: - ename = get_soundtrack_entry_name(entry) + ename = music.get_soundtrack_entry_name(entry) return ename def _get_entry_button_display_icon_type(self, entry: Any) -> Optional[str]: - from ba.internal import get_soundtrack_entry_type - etype = get_soundtrack_entry_type(entry) + music = ba.app.music + etype = music.get_soundtrack_entry_type(entry) if etype == 'musicFile': return 'file' if etype == 'musicFolder': @@ -368,17 +368,18 @@ class SoundtrackEditWindow(ba.Window): return None def _cancel(self) -> None: - from ba.internal import set_music_play_mode from bastd.ui.soundtrack import browser as stb + music = ba.app.music + # Resets music back to normal. - set_music_play_mode(ba.MusicPlayMode.REGULAR) + music.set_music_play_mode(ba.MusicPlayMode.REGULAR) ba.containerwidget(edit=self._root_widget, transition='out_right') ba.app.main_menu_window = (stb.SoundtrackBrowserWindow( transition='in_left').get_root_widget()) def _do_it(self) -> None: - from ba.internal import set_music_play_mode from bastd.ui.soundtrack import browser as stb + music = ba.app.music cfg = ba.app.config new_name = cast(str, ba.textwidget(query=self._text_field)) if (new_name != self._soundtrack_name @@ -413,7 +414,7 @@ class SoundtrackEditWindow(ba.Window): ba.containerwidget(edit=self._root_widget, transition='out_right') # Resets music back to normal. - set_music_play_mode(ba.MusicPlayMode.REGULAR, force_restart=True) + music.set_music_play_mode(ba.MusicPlayMode.REGULAR, force_restart=True) ba.app.main_menu_window = (stb.SoundtrackBrowserWindow( transition='in_left').get_root_widget()) diff --git a/assets/src/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py b/assets/src/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py index 4a228c30..cd0444ef 100644 --- a/assets/src/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py +++ b/assets/src/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py @@ -39,8 +39,7 @@ class SoundtrackEntryTypeSelectWindow(ba.Window): current_entry: Any, selection_target_name: str, transition: str = 'in_right'): - from ba.internal import (get_soundtrack_entry_type, - supports_soundtrack_entry_type) + music = ba.app.music self._r = 'editSoundtrackWindow' self._callback = callback @@ -50,11 +49,13 @@ class SoundtrackEntryTypeSelectWindow(ba.Window): self._height = 220 spacing = 80 + # FIXME: Generalize this so new custom soundtrack types can add + # themselves here. do_default = True - do_mac_music_app_playlist = supports_soundtrack_entry_type( + do_mac_music_app_playlist = music.supports_soundtrack_entry_type( 'iTunesPlaylist') - do_music_file = supports_soundtrack_entry_type('musicFile') - do_music_folder = supports_soundtrack_entry_type('musicFolder') + do_music_file = music.supports_soundtrack_entry_type('musicFile') + do_music_folder = music.supports_soundtrack_entry_type('musicFolder') if do_mac_music_app_playlist: self._height += spacing @@ -96,7 +97,7 @@ class SoundtrackEntryTypeSelectWindow(ba.Window): v = self._height - 155 - current_entry_type = get_soundtrack_entry_type(current_entry) + current_entry_type = music.get_soundtrack_entry_type(current_entry) if do_default: btn = ba.buttonwidget(parent=self._root_widget, @@ -147,23 +148,23 @@ class SoundtrackEntryTypeSelectWindow(ba.Window): v -= spacing def _on_mac_music_app_playlist_press(self) -> None: - from ba.internal import (get_soundtrack_entry_type, - get_soundtrack_entry_name) - from bastd.ui.soundtrack import itunes + music = ba.app.music + from bastd.ui.soundtrack import macmusicapp ba.containerwidget(edit=self._root_widget, transition='out_left') current_playlist_entry: Optional[str] - if get_soundtrack_entry_type(self._current_entry) == 'iTunesPlaylist': - current_playlist_entry = get_soundtrack_entry_name( + if (music.get_soundtrack_entry_type( + self._current_entry) == 'iTunesPlaylist'): + current_playlist_entry = music.get_soundtrack_entry_name( self._current_entry) else: current_playlist_entry = None - ba.app.main_menu_window = (itunes.MacMusicAppPlaylistSelectWindow( + ba.app.main_menu_window = (macmusicapp.MacMusicAppPlaylistSelectWindow( self._callback, current_playlist_entry, self._current_entry).get_root_widget()) def _on_music_file_press(self) -> None: - from ba.internal import get_valid_music_file_extensions + from ba.osmusic import OSMusicPlayer from bastd.ui import fileselector ba.containerwidget(edit=self._root_widget, transition='out_left') base_path = _ba.android_get_external_storage_path() @@ -171,7 +172,8 @@ class SoundtrackEntryTypeSelectWindow(ba.Window): base_path, callback=self._music_file_selector_cb, show_base_path=False, - valid_file_extensions=get_valid_music_file_extensions(), + valid_file_extensions=( + OSMusicPlayer.get_valid_music_file_extensions()), allow_folders=False).get_root_widget()) def _on_music_folder_press(self) -> None: diff --git a/assets/src/ba_data/python/bastd/ui/soundtrack/itunes.py b/assets/src/ba_data/python/bastd/ui/soundtrack/macmusicapp.py similarity index 96% rename from assets/src/ba_data/python/bastd/ui/soundtrack/itunes.py rename to assets/src/ba_data/python/bastd/ui/soundtrack/macmusicapp.py index 3e968509..27b2d3c6 100644 --- a/assets/src/ba_data/python/bastd/ui/soundtrack/itunes.py +++ b/assets/src/ba_data/python/bastd/ui/soundtrack/macmusicapp.py @@ -18,7 +18,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # ----------------------------------------------------------------------------- -"""UI functionality related to using iTunes for soundtracks.""" +"""UI functionality related to using the macOS Music app for soundtracks.""" from __future__ import annotations @@ -36,7 +36,7 @@ class MacMusicAppPlaylistSelectWindow(ba.Window): def __init__(self, callback: Callable[[Any], Any], existing_playlist: Optional[str], existing_entry: Any): - from ba.internal import get_music_player, MacMusicAppMusicPlayer + from ba.macmusicapp import MacMusicAppMusicPlayer self._r = 'editSoundtrackWindow' self._callback = callback self._existing_playlist = existing_playlist @@ -83,7 +83,7 @@ class MacMusicAppPlaylistSelectWindow(ba.Window): text=ba.Lstr(resource=self._r + '.fetchingITunesText'), color=(0.6, 0.9, 0.6, 1.0), scale=0.8) - musicplayer = get_music_player() + musicplayer = ba.app.music.get_music_player() assert isinstance(musicplayer, MacMusicAppMusicPlayer) musicplayer.get_playlists(self._playlists_cb) ba.containerwidget(edit=self._root_widget, diff --git a/docs/ba_module.md b/docs/ba_module.md index 1862d293..7e74d5c7 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -137,7 +137,7 @@
  • ba.AppDelegate
  • ba.Campaign
  • ba.MusicPlayer
  • -
  • ba.Server
  • +
  • ba.ServerController
  • User Interface Classes