Cleaned up music subsystem

This commit is contained in:
Eric Froemling 2020-04-24 16:59:04 -07:00
parent ffc21de05a
commit cff5046fe9
16 changed files with 859 additions and 737 deletions

View File

@ -92,6 +92,7 @@
<w>assetdata</w> <w>assetdata</w>
<w>assetfiles</w> <w>assetfiles</w>
<w>assetmanager</w> <w>assetmanager</w>
<w>assetname</w>
<w>assetpack</w> <w>assetpack</w>
<w>assetpackage</w> <w>assetpackage</w>
<w>assetpackput</w> <w>assetpackput</w>
@ -1021,6 +1022,7 @@
<w>lzma</w> <w>lzma</w>
<w>lzmamodule</w> <w>lzmamodule</w>
<w>macappstore</w> <w>macappstore</w>
<w>macmusicapp</w>
<w>macos</w> <w>macos</w>
<w>macpath</w> <w>macpath</w>
<w>mailcap</w> <w>mailcap</w>
@ -1223,6 +1225,7 @@
<w>origwrapper</w> <w>origwrapper</w>
<w>ortho</w> <w>ortho</w>
<w>osascript</w> <w>osascript</w>
<w>osmusic</w>
<w>ostype</w> <w>ostype</w>
<w>otherplayer</w> <w>otherplayer</w>
<w>otherspawn</w> <w>otherspawn</w>
@ -1247,6 +1250,7 @@
<w>partyqueue</w> <w>partyqueue</w>
<w>partyval</w> <w>partyval</w>
<w>passnode</w> <w>passnode</w>
<w>passthrough</w>
<w>passwd</w> <w>passwd</w>
<w>patcomp</w> <w>patcomp</w>
<w>pathlib</w> <w>pathlib</w>

View File

@ -51,6 +51,8 @@
"ba_data/python/ba/__pycache__/_tournament.cpython-37.opt-1.pyc", "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__/deprecated.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/internal.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/_account.py",
"ba_data/python/ba/_achievement.py", "ba_data/python/ba/_achievement.py",
"ba_data/python/ba/_activity.py", "ba_data/python/ba/_activity.py",
@ -101,6 +103,8 @@
"ba_data/python/ba/_tournament.py", "ba_data/python/ba/_tournament.py",
"ba_data/python/ba/deprecated.py", "ba_data/python/ba/deprecated.py",
"ba_data/python/ba/internal.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/__init__.py",
"ba_data/python/ba/ui/__pycache__/__init__.cpython-37.opt-1.pyc", "ba_data/python/ba/ui/__pycache__/__init__.cpython-37.opt-1.pyc",
"ba_data/python/bacommon/__init__.py", "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__/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__/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__/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/browser.py",
"ba_data/python/bastd/ui/soundtrack/edit.py", "ba_data/python/bastd/ui/soundtrack/edit.py",
"ba_data/python/bastd/ui/soundtrack/entrytypeselect.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/specialoffer.py",
"ba_data/python/bastd/ui/store/__init__.py", "ba_data/python/bastd/ui/store/__init__.py",
"ba_data/python/bastd/ui/store/__pycache__/__init__.cpython-37.opt-1.pyc", "ba_data/python/bastd/ui/store/__pycache__/__init__.cpython-37.opt-1.pyc",

View File

@ -163,6 +163,7 @@ SCRIPT_TARGETS_PY_1 = \
build/ba_data/python/ba/_apputils.py \ build/ba_data/python/ba/_apputils.py \
build/ba_data/python/ba/_coopsession.py \ build/ba_data/python/ba/_coopsession.py \
build/ba_data/python/ba/_appdelegate.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/internal.py \
build/ba_data/python/ba/_coopgame.py \ build/ba_data/python/ba/_coopgame.py \
build/ba_data/python/ba/_meta.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/_multiteamsession.py \
build/ba_data/python/ba/_actor.py \ build/ba_data/python/ba/_actor.py \
build/ba_data/python/ba/_powerup.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/_campaign.py \
build/ba_data/python/ba/_lobby.py \ build/ba_data/python/ba/_lobby.py \
build/ba_data/python/ba/_stats.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/editgame.py \
build/ba_data/python/bastd/ui/playlist/editcontroller.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/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/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/__init__.py \
build/ba_data/python/bastd/ui/soundtrack/edit.py \ build/ba_data/python/bastd/ui/soundtrack/edit.py \
build/ba_data/python/bastd/ui/soundtrack/browser.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__/_apputils.cpython-37.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_coopsession.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__/_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__/internal.cpython-37.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_coopgame.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 \ 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__/_multiteamsession.cpython-37.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_actor.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__/_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__/_campaign.cpython-37.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_lobby.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 \ 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__/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__/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/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__/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__/__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__/edit.cpython-37.opt-1.pyc \
build/ba_data/python/bastd/ui/soundtrack/__pycache__/browser.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: $^ @echo Compiling script: $^
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@ @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/__pycache__/internal.cpython-37.opt-1.pyc: \
build/ba_data/python/ba/internal.py build/ba_data/python/ba/internal.py
@echo Compiling script: $^ @echo Compiling script: $^
@ -900,6 +909,11 @@ build/ba_data/python/ba/__pycache__/_powerup.cpython-37.opt-1.pyc: \
@echo Compiling script: $^ @echo Compiling script: $^
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@ @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/__pycache__/_campaign.cpython-37.opt-1.pyc: \
build/ba_data/python/ba/_campaign.py build/ba_data/python/ba/_campaign.py
@echo Compiling script: $^ @echo Compiling script: $^
@ -1345,13 +1359,13 @@ build/ba_data/python/bastd/ui/playlist/__pycache__/addgame.cpython-37.opt-1.pyc:
@echo Compiling script: $^ @echo Compiling script: $^
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@ @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/__pycache__/macmusicapp.cpython-37.opt-1.pyc: \
build/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py build/ba_data/python/bastd/ui/soundtrack/macmusicapp.py
@echo Compiling script: $^ @echo Compiling script: $^
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@ @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/__pycache__/entrytypeselect.cpython-37.opt-1.pyc: \
build/ba_data/python/bastd/ui/soundtrack/itunes.py build/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py
@echo Compiling script: $^ @echo Compiling script: $^
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@ @rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@

View File

@ -59,7 +59,7 @@ from ba._gameresults import TeamGameResults
from ba._lang import Lstr, setlanguage, get_valid_languages from ba._lang import Lstr, setlanguage, get_valid_languages
from ba._map import Map, getmaps from ba._map import Map, getmaps
from ba._session import Session from ba._session import Session
from ba._server import Server from ba._server import ServerController
from ba._stats import PlayerScoredMessage, PlayerRecord, Stats from ba._stats import PlayerScoredMessage, PlayerRecord, Stats
from ba._team import Team from ba._team import Team
from ba._teamgame import TeamGameActivity from ba._teamgame import TeamGameActivity
@ -89,7 +89,7 @@ from ba.ui import Window, UIController, uicleanupcheck
app: App 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: def _simplify_module_names() -> None:
for attr, obj in globals().items(): for attr, obj in globals().items():
if not attr.startswith('_'): if not attr.startswith('_'):

View File

@ -31,8 +31,7 @@ if TYPE_CHECKING:
from ba import _lang, _meta from ba import _lang, _meta
from ba.ui import UICleanupCheck from ba.ui import UICleanupCheck
from bastd.actor import spazappearance from bastd.actor import spazappearance
from typing import (Optional, Dict, Tuple, Set, Any, List, Type, Tuple, from typing import Optional, Dict, Set, Any, Type, Tuple, Callable, List
Callable)
class App: class App:
@ -268,9 +267,7 @@ class App:
the single shared instance. the single shared instance.
""" """
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
from ba._music import MusicPlayMode from ba._music import MusicController
# _test_https()
# Config. # Config.
self.config_file_healthy = False self.config_file_healthy = False
@ -281,8 +278,7 @@ class App:
self.fg_state = 0 self.fg_state = 0
# Environment stuff. # 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() env = _ba.env()
self._build_number: int = env['build_number'] self._build_number: int = env['build_number']
assert isinstance(self._build_number, int) assert isinstance(self._build_number, int)
@ -332,7 +328,7 @@ class App:
self.last_ad_completion_time: Optional[float] = None self.last_ad_completion_time: Optional[float] = None
self.last_ad_was_short = False self.last_ad_was_short = False
self.did_weak_call_warning = 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 # 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. # not be signed in yet; go ahead and queue them up in that case.
@ -354,15 +350,8 @@ class App:
# Co-op Campaigns. # Co-op Campaigns.
self.campaigns: Dict[str, ba.Campaign] = {} self.campaigns: Dict[str, ba.Campaign] = {}
# Server-Mode. # Server Mode.
self.server: Optional[ba.Server] = None self.server: Optional[ba.ServerController] = 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
# Ads. # Ads.
self.last_ad_network = 'unknown' self.last_ad_network = 'unknown'
@ -372,14 +361,7 @@ class App:
self.attempted_first_ad = False self.attempted_first_ad = False
# Music. # Music.
self.music: Optional[ba.Node] = None self.music = MusicController()
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
}
# Language. # Language.
self.language_target: Optional[_lang.AttrDict] = None self.language_target: Optional[_lang.AttrDict] = None
@ -453,13 +435,12 @@ class App:
self.large_ui = env['interface_type'] == 'large' self.large_ui = env['interface_type'] == 'large'
self.toolbars = env.get('toolbar_test', True) self.toolbars = env.get('toolbar_test', True)
def on_launch(self) -> None: def on_app_launch(self) -> None:
"""Runs after the app finishes bootstrapping. """Runs after the app finishes bootstrapping.
(internal)""" (internal)"""
# FIXME: Break this up. # FIXME: Break this up.
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from ba import _apputils from ba import _apputils
@ -468,7 +449,6 @@ class App:
from ba import _achievement from ba import _achievement
from ba import _map from ba import _map
from ba import _meta from ba import _meta
from ba import _music
from ba import _campaign from ba import _campaign
from bastd import appdelegate from bastd import appdelegate
from bastd import maps as stdmaps from bastd import maps as stdmaps
@ -483,11 +463,6 @@ class App:
_achievement.init_achievements() _achievement.init_achievements()
spazappearance.register_appearances() spazappearance.register_appearances()
_campaign.init_campaigns() _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. # FIXME: This should not be hard-coded.
for maptype in [ for maptype in [
@ -566,17 +541,7 @@ class App:
# to disk. # to disk.
_appconfig.commit_app_config(force=True) _appconfig.commit_app_config(force=True)
# If we're using a non-default playlist, lets go ahead and get our self.music.on_app_launch()
# 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')
launch_count = cfg.get('launchCount', 0) launch_count = cfg.get('launchCount', 0)
launch_count += 1 launch_count += 1
@ -632,7 +597,7 @@ class App:
_ba.pushcall(do_auto_sign_in) _ba.pushcall(do_auto_sign_in)
self.ran_on_launch = True self.ran_on_app_launch = True
# from ba._dependency import test_depset # from ba._dependency import test_depset
# test_depset() # test_depset()
@ -744,10 +709,8 @@ class App:
def handle_app_resume(self) -> None: def handle_app_resume(self) -> None:
"""Run when the app resumes from a suspended state.""" """Run when the app resumes from a suspended state."""
# If there's music playing externally, make sure we aren't playing self.music.handle_app_resume()
# ours.
from ba import _music
_music.handle_app_resume()
self.fg_state += 1 self.fg_state += 1
# Mark our cached tourneys as invalid so anyone using them knows # Mark our cached tourneys as invalid so anyone using them knows
@ -827,8 +790,7 @@ class App:
def shutdown(self) -> None: def shutdown(self) -> None:
"""(internal)""" """(internal)"""
if self.music_player is not None: self.music.on_app_shutdown()
self.music_player.shutdown()
def handle_deep_link(self, url: str) -> None: def handle_deep_link(self, url: str) -> None:
"""Handle a deep link URL.""" """Handle a deep link URL."""

View File

@ -22,16 +22,14 @@
from __future__ import annotations from __future__ import annotations
import copy import copy
import os
import random
import threading
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dataclasses import dataclass
from enum import Enum from enum import Enum
import _ba import _ba
if TYPE_CHECKING: 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): class MusicType(Enum):
@ -72,6 +70,329 @@ class MusicPlayMode(Enum):
TEST = 'test' 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: class MusicPlayer:
"""Wrangles soundtrack music playback. """Wrangles soundtrack music playback.
@ -123,7 +444,7 @@ class MusicPlayer:
def shutdown(self) -> None: def shutdown(self) -> None:
"""Shutdown music playback completely.""" """Shutdown music playback completely."""
self.on_shutdown() self.on_app_shutdown()
def on_select_entry(self, callback: Callable[[Any], None], def on_select_entry(self, callback: Callable[[Any], None],
current_entry: Any, selection_target_name: str) -> Any: current_entry: Any, selection_target_name: str) -> Any:
@ -142,7 +463,7 @@ class MusicPlayer:
def on_stop(self) -> None: def on_stop(self) -> None:
"""Called when the music should stop.""" """Called when the music should stop."""
def on_shutdown(self) -> None: def on_app_shutdown(self) -> None:
"""Called on final app shutdown.""" """Called on final app shutdown."""
def _update_play_state(self) -> None: def _update_play_state(self) -> None:
@ -159,618 +480,33 @@ class MusicPlayer:
self._actually_playing = False 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 = '<ENCERR4523>'
_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}', '<multiple>') + '; ' +
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: 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 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 and also supports custom user soundtracks on specific platforms so the
user can override particular game music with their own. user can override particular game music with their own.
Pass None to stop music. 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. playing, the playing track will not be restarted.
""" """
from ba import _gameutils from ba import _gameutils
# All we do here now is set a few music attrs on the current globals # 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 # node. The foreground globals' current playing music then gets fed to
# the do_play_music call below. This way we can seamlessly support custom # the do_play_music call in our music controller. This way we can
# soundtracks in replays/etc since we're replaying an attr value set; # seamlessly support custom soundtracks in replays/etc since we're being
# not an actual sound node create. # driven purely by node data.
gnode = _gameutils.sharedobj('globals') gnode = _gameutils.sharedobj('globals')
gnode.music_continuous = continuous gnode.music_continuous = continuous
gnode.music = '' if musictype is None else musictype.value gnode.music = '' if musictype is None else musictype.value
gnode.music_count += 1 gnode.music_count += 1
def handle_app_resume() -> None: def do_play_music(*args: Any, **keywds: Any) -> None:
"""Should be run when the app resumes from a suspended state.""" """A passthrough used by the C++ layer."""
if _ba.is_os_playing_music(): _ba.app.music.do_play_music(*args, **keywds)
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
})

View File

@ -46,14 +46,14 @@ def _cmd(command_data: bytes) -> None:
if command is ServerCommand.CONFIG: if command is ServerCommand.CONFIG:
assert isinstance(payload, ServerConfig) assert isinstance(payload, ServerConfig)
assert _ba.app.server is None assert _ba.app.server is None
_ba.app.server = Server(payload) _ba.app.server = ServerController(payload)
return return
assert _ba.app.server is not None assert _ba.app.server is not None
print('WOULD DO OTHER SERVER COMMAND') print('WOULD DO OTHER SERVER COMMAND')
class Server: class ServerController:
"""Overall controller for the app in server mode. """Overall controller for the app in server mode.
Category: App Classes Category: App Classes
@ -79,8 +79,6 @@ class Server:
self._config_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 self._next_server_account_warn_time = time.time() + 10.0
# Now sit around until we're signed in and then # Now sit around until we're signed in and then
@ -197,6 +195,9 @@ class Server:
signed_in = _ba.get_account_state() == 'signed_in' signed_in = _ba.get_account_state() == 'signed_in'
if not signed_in: if not signed_in:
# Signing in to the local server account should not take long;
# complain if it does...
curtime = time.time() curtime = time.time()
if curtime > self._next_server_account_warn_time: if curtime > self._next_server_account_warn_time:
print('Still waiting for account sign-in...') print('Still waiting for account sign-in...')
@ -205,7 +206,6 @@ class Server:
can_launch = False can_launch = False
# If we're trying to fetch a playlist, we do that first. # 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: if self._playlist_fetch_running:
# Send request if we haven't. # Send request if we haven't.

View File

@ -53,11 +53,7 @@ from ba._messages import PlayerProfilesChangedMessage
from ba._meta import get_game_types from ba._meta import get_game_types
from ba._modutils import show_user_scripts from ba._modutils import show_user_scripts
from ba._multiteamsession import DEFAULT_TEAM_COLORS, DEFAULT_TEAM_NAMES from ba._multiteamsession import DEFAULT_TEAM_COLORS, DEFAULT_TEAM_NAMES
from ba._music import (have_music_player, music_volume_changed, do_play_music, from ba._music import 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._netutils import serverget, serverput, get_ip_address_type from ba._netutils import serverget, serverput, get_ip_address_type
from ba._powerup import get_default_powerup_distribution from ba._powerup import get_default_powerup_distribution
from ba._profile import (get_player_profile_colors, get_player_profile_icon, from ba._profile import (get_player_profile_colors, get_player_profile_icon,

View File

@ -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))))

View File

@ -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}', '<multiple>') + '; ' +
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 = '<ENCERR4523>'
_ba.pushcall(Call(self._callback, self._path, err_str),
from_other_thread=True)

View File

@ -40,9 +40,10 @@ class AudioSettingsWindow(ba.Window):
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from ba.internal import have_music_player, music_volume_changed from bastd.ui.popup import PopupMenu
from bastd.ui import popup as popup_ui from bastd.ui.config import ConfigNumberEdit
from bastd.ui import config as cfgui
music = ba.app.music
# If they provided an origin-widget, scale up from that. # If they provided an origin-widget, scale up from that.
scale_origin: Optional[Tuple[float, float]] scale_origin: Optional[Tuple[float, float]]
@ -60,8 +61,8 @@ class AudioSettingsWindow(ba.Window):
width = 460.0 width = 460.0
height = 210.0 height = 210.0
# Update: hard-coding head-relative audio to true now, so not showing # Update: hard-coding head-relative audio to true now,
# options. # so not showing options.
# show_vr_head_relative_audio = True if ba.app.vr_mode else False # show_vr_head_relative_audio = True if ba.app.vr_mode else False
show_vr_head_relative_audio = False show_vr_head_relative_audio = False
@ -69,7 +70,7 @@ class AudioSettingsWindow(ba.Window):
height += 70 height += 70
show_soundtracks = False show_soundtracks = False
if have_music_player(): if music.have_music_player():
show_soundtracks = True show_soundtracks = True
height += spacing * 2.0 height += spacing * 2.0
@ -111,7 +112,7 @@ class AudioSettingsWindow(ba.Window):
size=(60, 60), size=(60, 60),
label=ba.charstr(ba.SpecialChar.BACK)) label=ba.charstr(ba.SpecialChar.BACK))
self._sound_volume_numedit = svne = cfgui.ConfigNumberEdit( self._sound_volume_numedit = svne = ConfigNumberEdit(
parent=self._root_widget, parent=self._root_widget,
position=(40, v), position=(40, v),
xoffset=10, xoffset=10,
@ -124,7 +125,7 @@ class AudioSettingsWindow(ba.Window):
ba.widget(edit=svne.plusbutton, ba.widget(edit=svne.plusbutton,
right_widget=_ba.get_special_widget('party_button')) right_widget=_ba.get_special_widget('party_button'))
v -= spacing v -= spacing
self._music_volume_numedit = cfgui.ConfigNumberEdit( self._music_volume_numedit = ConfigNumberEdit(
parent=self._root_widget, parent=self._root_widget,
position=(40, v), position=(40, v),
xoffset=10, xoffset=10,
@ -133,7 +134,7 @@ class AudioSettingsWindow(ba.Window):
minval=0.0, minval=0.0,
maxval=1.0, maxval=1.0,
increment=0.1, increment=0.1,
callback=music_volume_changed, callback=music.music_volume_changed,
changesound=False) changesound=False)
v -= 0.5 * spacing v -= 0.5 * spacing
@ -151,7 +152,7 @@ class AudioSettingsWindow(ba.Window):
h_align="left", h_align="left",
v_align="center") v_align="center")
popup = popup_ui.PopupMenu( popup = PopupMenu(
parent=self._root_widget, parent=self._root_widget,
position=(290, v), position=(290, v),
width=120, width=120,

View File

@ -311,7 +311,7 @@ class SoundtrackBrowserWindow(ba.Window):
self._refresh(select_soundtrack=test_name) self._refresh(select_soundtrack=test_name)
def _select(self, name: str, index: int) -> None: 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_index = index
self._selected_soundtrack = name self._selected_soundtrack = name
cfg = ba.app.config cfg = ba.app.config
@ -322,9 +322,10 @@ class SoundtrackBrowserWindow(ba.Window):
ba.playsound(ba.getsound('gunCocking')) ba.playsound(ba.getsound('gunCocking'))
cfg['Soundtrack'] = self._selected_soundtrack cfg['Soundtrack'] = self._selected_soundtrack
cfg.commit() cfg.commit()
# Just play whats already playing.. this'll grab it from the # Just play whats already playing.. this'll grab it from the
# new soundtrack. # 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: def _back(self) -> None:
# pylint: disable=cyclic-import # pylint: disable=cyclic-import

View File

@ -291,7 +291,8 @@ class SoundtrackEditWindow(ba.Window):
@classmethod @classmethod
def _restore_editor(cls, state: Dict[str, Any], musictype: str, def _restore_editor(cls, state: Dict[str, Any], musictype: str,
entry: Any) -> None: entry: Any) -> None:
from ba.internal import get_soundtrack_entry_type music = ba.app.music
# Apply the change and recreate the window. # Apply the change and recreate the window.
soundtrack = state['soundtrack'] soundtrack = state['soundtrack']
existing_entry = (None if musictype not in soundtrack else existing_entry = (None if musictype not in soundtrack else
@ -303,7 +304,7 @@ class SoundtrackEditWindow(ba.Window):
if entry is not None: if entry is not None:
entry = copy.deepcopy(entry) entry = copy.deepcopy(entry)
entry_type = get_soundtrack_entry_type(entry) entry_type = music.get_soundtrack_entry_type(entry)
if entry_type == 'default': if entry_type == 'default':
# For 'default' entries simply exclude them from the list. # For 'default' entries simply exclude them from the list.
if musictype in soundtrack: if musictype in soundtrack:
@ -316,7 +317,7 @@ class SoundtrackEditWindow(ba.Window):
def _get_entry(self, song_type: str, entry: Any, def _get_entry(self, song_type: str, entry: Any,
selection_target_name: str) -> None: selection_target_name: str) -> None:
from ba.internal import get_music_player music = ba.app.music
if selection_target_name != '': if selection_target_name != '':
selection_target_name = "'" + selection_target_name + "'" selection_target_name = "'" + selection_target_name + "'"
state = { state = {
@ -326,12 +327,12 @@ class SoundtrackEditWindow(ba.Window):
'last_edited_song_type': song_type 'last_edited_song_type': song_type
} }
ba.containerwidget(edit=self._root_widget, transition='out_left') 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, ba.Call(self._restore_editor, state, song_type), entry,
selection_target_name).get_root_widget()) selection_target_name).get_root_widget())
def _test(self, song_type: ba.MusicType) -> None: 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. # Warn if volume is zero.
if ba.app.config.resolve("Music Volume") < 0.01: if ba.app.config.resolve("Music Volume") < 0.01:
@ -339,28 +340,27 @@ class SoundtrackEditWindow(ba.Window):
ba.screenmessage(ba.Lstr(resource=self._r + ba.screenmessage(ba.Lstr(resource=self._r +
'.musicVolumeZeroWarning'), '.musicVolumeZeroWarning'),
color=(1, 0.5, 0)) color=(1, 0.5, 0))
set_music_play_mode(ba.MusicPlayMode.TEST) music.set_music_play_mode(ba.MusicPlayMode.TEST)
do_play_music(song_type, music.do_play_music(song_type,
mode=ba.MusicPlayMode.TEST, mode=ba.MusicPlayMode.TEST,
testsoundtrack=self._soundtrack) testsoundtrack=self._soundtrack)
def _get_entry_button_display_name(self, def _get_entry_button_display_name(self,
entry: Any) -> Union[str, ba.Lstr]: entry: Any) -> Union[str, ba.Lstr]:
from ba.internal import (get_soundtrack_entry_type, music = ba.app.music
get_soundtrack_entry_name) etype = music.get_soundtrack_entry_type(entry)
etype = get_soundtrack_entry_type(entry)
ename: Union[str, ba.Lstr] ename: Union[str, ba.Lstr]
if etype == 'default': if etype == 'default':
ename = ba.Lstr(resource=self._r + '.defaultGameMusicText') ename = ba.Lstr(resource=self._r + '.defaultGameMusicText')
elif etype in ('musicFile', 'musicFolder'): 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: else:
ename = get_soundtrack_entry_name(entry) ename = music.get_soundtrack_entry_name(entry)
return ename return ename
def _get_entry_button_display_icon_type(self, entry: Any) -> Optional[str]: def _get_entry_button_display_icon_type(self, entry: Any) -> Optional[str]:
from ba.internal import get_soundtrack_entry_type music = ba.app.music
etype = get_soundtrack_entry_type(entry) etype = music.get_soundtrack_entry_type(entry)
if etype == 'musicFile': if etype == 'musicFile':
return 'file' return 'file'
if etype == 'musicFolder': if etype == 'musicFolder':
@ -368,17 +368,18 @@ class SoundtrackEditWindow(ba.Window):
return None return None
def _cancel(self) -> None: def _cancel(self) -> None:
from ba.internal import set_music_play_mode
from bastd.ui.soundtrack import browser as stb from bastd.ui.soundtrack import browser as stb
music = ba.app.music
# Resets music back to normal. # 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.containerwidget(edit=self._root_widget, transition='out_right')
ba.app.main_menu_window = (stb.SoundtrackBrowserWindow( ba.app.main_menu_window = (stb.SoundtrackBrowserWindow(
transition='in_left').get_root_widget()) transition='in_left').get_root_widget())
def _do_it(self) -> None: def _do_it(self) -> None:
from ba.internal import set_music_play_mode
from bastd.ui.soundtrack import browser as stb from bastd.ui.soundtrack import browser as stb
music = ba.app.music
cfg = ba.app.config cfg = ba.app.config
new_name = cast(str, ba.textwidget(query=self._text_field)) new_name = cast(str, ba.textwidget(query=self._text_field))
if (new_name != self._soundtrack_name if (new_name != self._soundtrack_name
@ -413,7 +414,7 @@ class SoundtrackEditWindow(ba.Window):
ba.containerwidget(edit=self._root_widget, transition='out_right') ba.containerwidget(edit=self._root_widget, transition='out_right')
# Resets music back to normal. # 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( ba.app.main_menu_window = (stb.SoundtrackBrowserWindow(
transition='in_left').get_root_widget()) transition='in_left').get_root_widget())

View File

@ -39,8 +39,7 @@ class SoundtrackEntryTypeSelectWindow(ba.Window):
current_entry: Any, current_entry: Any,
selection_target_name: str, selection_target_name: str,
transition: str = 'in_right'): transition: str = 'in_right'):
from ba.internal import (get_soundtrack_entry_type, music = ba.app.music
supports_soundtrack_entry_type)
self._r = 'editSoundtrackWindow' self._r = 'editSoundtrackWindow'
self._callback = callback self._callback = callback
@ -50,11 +49,13 @@ class SoundtrackEntryTypeSelectWindow(ba.Window):
self._height = 220 self._height = 220
spacing = 80 spacing = 80
# FIXME: Generalize this so new custom soundtrack types can add
# themselves here.
do_default = True do_default = True
do_mac_music_app_playlist = supports_soundtrack_entry_type( do_mac_music_app_playlist = music.supports_soundtrack_entry_type(
'iTunesPlaylist') 'iTunesPlaylist')
do_music_file = supports_soundtrack_entry_type('musicFile') do_music_file = music.supports_soundtrack_entry_type('musicFile')
do_music_folder = supports_soundtrack_entry_type('musicFolder') do_music_folder = music.supports_soundtrack_entry_type('musicFolder')
if do_mac_music_app_playlist: if do_mac_music_app_playlist:
self._height += spacing self._height += spacing
@ -96,7 +97,7 @@ class SoundtrackEntryTypeSelectWindow(ba.Window):
v = self._height - 155 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: if do_default:
btn = ba.buttonwidget(parent=self._root_widget, btn = ba.buttonwidget(parent=self._root_widget,
@ -147,23 +148,23 @@ class SoundtrackEntryTypeSelectWindow(ba.Window):
v -= spacing v -= spacing
def _on_mac_music_app_playlist_press(self) -> None: def _on_mac_music_app_playlist_press(self) -> None:
from ba.internal import (get_soundtrack_entry_type, music = ba.app.music
get_soundtrack_entry_name) from bastd.ui.soundtrack import macmusicapp
from bastd.ui.soundtrack import itunes
ba.containerwidget(edit=self._root_widget, transition='out_left') ba.containerwidget(edit=self._root_widget, transition='out_left')
current_playlist_entry: Optional[str] current_playlist_entry: Optional[str]
if get_soundtrack_entry_type(self._current_entry) == 'iTunesPlaylist': if (music.get_soundtrack_entry_type(
current_playlist_entry = get_soundtrack_entry_name( self._current_entry) == 'iTunesPlaylist'):
current_playlist_entry = music.get_soundtrack_entry_name(
self._current_entry) self._current_entry)
else: else:
current_playlist_entry = None current_playlist_entry = None
ba.app.main_menu_window = (itunes.MacMusicAppPlaylistSelectWindow( ba.app.main_menu_window = (macmusicapp.MacMusicAppPlaylistSelectWindow(
self._callback, current_playlist_entry, self._callback, current_playlist_entry,
self._current_entry).get_root_widget()) self._current_entry).get_root_widget())
def _on_music_file_press(self) -> None: 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 from bastd.ui import fileselector
ba.containerwidget(edit=self._root_widget, transition='out_left') ba.containerwidget(edit=self._root_widget, transition='out_left')
base_path = _ba.android_get_external_storage_path() base_path = _ba.android_get_external_storage_path()
@ -171,7 +172,8 @@ class SoundtrackEntryTypeSelectWindow(ba.Window):
base_path, base_path,
callback=self._music_file_selector_cb, callback=self._music_file_selector_cb,
show_base_path=False, 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()) allow_folders=False).get_root_widget())
def _on_music_folder_press(self) -> None: def _on_music_folder_press(self) -> None:

View File

@ -18,7 +18,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
"""UI functionality related to using iTunes for soundtracks.""" """UI functionality related to using the macOS Music app for soundtracks."""
from __future__ import annotations from __future__ import annotations
@ -36,7 +36,7 @@ class MacMusicAppPlaylistSelectWindow(ba.Window):
def __init__(self, callback: Callable[[Any], Any], def __init__(self, callback: Callable[[Any], Any],
existing_playlist: Optional[str], existing_entry: 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._r = 'editSoundtrackWindow'
self._callback = callback self._callback = callback
self._existing_playlist = existing_playlist self._existing_playlist = existing_playlist
@ -83,7 +83,7 @@ class MacMusicAppPlaylistSelectWindow(ba.Window):
text=ba.Lstr(resource=self._r + '.fetchingITunesText'), text=ba.Lstr(resource=self._r + '.fetchingITunesText'),
color=(0.6, 0.9, 0.6, 1.0), color=(0.6, 0.9, 0.6, 1.0),
scale=0.8) scale=0.8)
musicplayer = get_music_player() musicplayer = ba.app.music.get_music_player()
assert isinstance(musicplayer, MacMusicAppMusicPlayer) assert isinstance(musicplayer, MacMusicAppMusicPlayer)
musicplayer.get_playlists(self._playlists_cb) musicplayer.get_playlists(self._playlists_cb)
ba.containerwidget(edit=self._root_widget, ba.containerwidget(edit=self._root_widget,

View File

@ -137,7 +137,7 @@
<li><a href="#class_ba_AppDelegate">ba.AppDelegate</a></li> <li><a href="#class_ba_AppDelegate">ba.AppDelegate</a></li>
<li><a href="#class_ba_Campaign">ba.Campaign</a></li> <li><a href="#class_ba_Campaign">ba.Campaign</a></li>
<li><a href="#class_ba_MusicPlayer">ba.MusicPlayer</a></li> <li><a href="#class_ba_MusicPlayer">ba.MusicPlayer</a></li>
<li><a href="#class_ba_Server">ba.Server</a></li> <li><a href="#class_ba_ServerController">ba.ServerController</a></li>
</ul> </ul>
<h4><a name="class_category_User_Interface_Classes">User Interface Classes</a></h4> <h4><a name="class_category_User_Interface_Classes">User Interface Classes</a></h4>
<ul> <ul>
@ -3251,11 +3251,17 @@ another <a href="#class_ba_Activity">ba.Activity</a>.</p>
</p> </p>
<h3>Methods:</h3> <h3>Methods:</h3>
<h5><a href="#method_ba_MusicPlayer____init__">&lt;constructor&gt;</a>, <a href="#method_ba_MusicPlayer__on_play">on_play()</a>, <a href="#method_ba_MusicPlayer__on_select_entry">on_select_entry()</a>, <a href="#method_ba_MusicPlayer__on_set_volume">on_set_volume()</a>, <a href="#method_ba_MusicPlayer__on_shutdown">on_shutdown()</a>, <a href="#method_ba_MusicPlayer__on_stop">on_stop()</a>, <a href="#method_ba_MusicPlayer__play">play()</a>, <a href="#method_ba_MusicPlayer__select_entry">select_entry()</a>, <a href="#method_ba_MusicPlayer__set_volume">set_volume()</a>, <a href="#method_ba_MusicPlayer__shutdown">shutdown()</a>, <a href="#method_ba_MusicPlayer__stop">stop()</a></h5> <h5><a href="#method_ba_MusicPlayer____init__">&lt;constructor&gt;</a>, <a href="#method_ba_MusicPlayer__on_app_shutdown">on_app_shutdown()</a>, <a href="#method_ba_MusicPlayer__on_play">on_play()</a>, <a href="#method_ba_MusicPlayer__on_select_entry">on_select_entry()</a>, <a href="#method_ba_MusicPlayer__on_set_volume">on_set_volume()</a>, <a href="#method_ba_MusicPlayer__on_stop">on_stop()</a>, <a href="#method_ba_MusicPlayer__play">play()</a>, <a href="#method_ba_MusicPlayer__select_entry">select_entry()</a>, <a href="#method_ba_MusicPlayer__set_volume">set_volume()</a>, <a href="#method_ba_MusicPlayer__shutdown">shutdown()</a>, <a href="#method_ba_MusicPlayer__stop">stop()</a></h5>
<dl> <dl>
<dt><h4><a name="method_ba_MusicPlayer____init__">&lt;constructor&gt;</a></dt></h4><dd> <dt><h4><a name="method_ba_MusicPlayer____init__">&lt;constructor&gt;</a></dt></h4><dd>
<p><span>ba.MusicPlayer()</span></p> <p><span>ba.MusicPlayer()</span></p>
</dd>
<dt><h4><a name="method_ba_MusicPlayer__on_app_shutdown">on_app_shutdown()</a></dt></h4><dd>
<p><span>on_app_shutdown(self) -&gt; None</span></p>
<p>Called on final app shutdown.</p>
</dd> </dd>
<dt><h4><a name="method_ba_MusicPlayer__on_play">on_play()</a></dt></h4><dd> <dt><h4><a name="method_ba_MusicPlayer__on_play">on_play()</a></dt></h4><dd>
<p><span>on_play(self, entry: Any) -&gt; None</span></p> <p><span>on_play(self, entry: Any) -&gt; None</span></p>
@ -3277,12 +3283,6 @@ signify that the default soundtrack should be used..</p>
<p>Called when the volume should be changed.</p> <p>Called when the volume should be changed.</p>
</dd>
<dt><h4><a name="method_ba_MusicPlayer__on_shutdown">on_shutdown()</a></dt></h4><dd>
<p><span>on_shutdown(self) -&gt; None</span></p>
<p>Called on final app shutdown.</p>
</dd> </dd>
<dt><h4><a name="method_ba_MusicPlayer__on_stop">on_stop()</a></dt></h4><dd> <dt><h4><a name="method_ba_MusicPlayer__on_stop">on_stop()</a></dt></h4><dd>
<p><span>on_stop(self) -&gt; None</span></p> <p><span>on_stop(self) -&gt; None</span></p>
@ -3975,7 +3975,7 @@ cause the powerup box to make a sound and disappear or whatnot.</p>
</dd> </dd>
</dl> </dl>
<hr> <hr>
<h2><strong><a name="class_ba_Server">ba.Server</a></strong></h3> <h2><strong><a name="class_ba_ServerController">ba.ServerController</a></strong></h3>
<p><em>&lt;top level class&gt;</em> <p><em>&lt;top level class&gt;</em>
</p> </p>
<p>Overall controller for the app in server mode.</p> <p>Overall controller for the app in server mode.</p>
@ -3984,13 +3984,13 @@ cause the powerup box to make a sound and disappear or whatnot.</p>
</p> </p>
<h3>Methods:</h3> <h3>Methods:</h3>
<h5><a href="#method_ba_Server____init__">&lt;constructor&gt;</a>, <a href="#method_ba_Server__handle_transition">handle_transition()</a>, <a href="#method_ba_Server__launch_server_session">launch_server_session()</a></h5> <h5><a href="#method_ba_ServerController____init__">&lt;constructor&gt;</a>, <a href="#method_ba_ServerController__handle_transition">handle_transition()</a>, <a href="#method_ba_ServerController__launch_server_session">launch_server_session()</a></h5>
<dl> <dl>
<dt><h4><a name="method_ba_Server____init__">&lt;constructor&gt;</a></dt></h4><dd> <dt><h4><a name="method_ba_ServerController____init__">&lt;constructor&gt;</a></dt></h4><dd>
<p><span>ba.Server(config: ServerConfig)</span></p> <p><span>ba.ServerController(config: ServerConfig)</span></p>
</dd> </dd>
<dt><h4><a name="method_ba_Server__handle_transition">handle_transition()</a></dt></h4><dd> <dt><h4><a name="method_ba_ServerController__handle_transition">handle_transition()</a></dt></h4><dd>
<p><span>handle_transition(self) -&gt; bool</span></p> <p><span>handle_transition(self) -&gt; bool</span></p>
<p>Handle transitioning to a new <a href="#class_ba_Session">ba.Session</a> or quitting the app.</p> <p>Handle transitioning to a new <a href="#class_ba_Session">ba.Session</a> or quitting the app.</p>
@ -4001,7 +4001,7 @@ Should return True if action will be handled by us; False if the
session should just continue on it's merry way.</p> session should just continue on it's merry way.</p>
</dd> </dd>
<dt><h4><a name="method_ba_Server__launch_server_session">launch_server_session()</a></dt></h4><dd> <dt><h4><a name="method_ba_ServerController__launch_server_session">launch_server_session()</a></dt></h4><dd>
<p><span>launch_server_session(self) -&gt; None</span></p> <p><span>launch_server_session(self) -&gt; None</span></p>
<p>Kick off a host-session based on the current server config.</p> <p>Kick off a host-session based on the current server config.</p>
@ -5774,17 +5774,17 @@ are applied to the Widget.</p>
<h2><strong><a name="function_ba_setmusic">ba.setmusic()</a></strong></h3> <h2><strong><a name="function_ba_setmusic">ba.setmusic()</a></strong></h3>
<p><span>setmusic(musictype: Optional[MusicType], continuous: bool = False) -&gt; None</span></p> <p><span>setmusic(musictype: Optional[MusicType], continuous: bool = False) -&gt; None</span></p>
<p>Set or stop the current music based on a string musictype.</p> <p>Tell the game to play (or stop playing) a certain type of music.</p>
<p>Category: <a href="#function_category_Gameplay_Functions">Gameplay Functions</a></p> <p>Category: <a href="#function_category_Gameplay_Functions">Gameplay Functions</a></p>
<p>This function will handle loading and playing sound media as necessary, <p>This function will handle loading and playing sound assets as necessary,
and also supports custom user soundtracks on specific platforms so the and also supports custom user soundtracks on specific platforms so the
user can override particular game music with their own.</p> user can override particular game music with their own.</p>
<p>Pass None to stop music.</p> <p>Pass None to stop music.</p>
<p>if 'continuous' is True the musictype passed is the same as what is already <p>if 'continuous' is True and musictype is the same as what is already
playing, the playing track will not be restarted.</p> playing, the playing track will not be restarted.</p>
<hr> <hr>