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

View File

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

View File

@ -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('_'):

View File

@ -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."""

View File

@ -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 = '<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:
"""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)

View File

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

View File

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

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-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,

View File

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

View File

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

View File

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

View File

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

View File

@ -137,7 +137,7 @@
<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_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>
<h4><a name="class_category_User_Interface_Classes">User Interface Classes</a></h4>
<ul>
@ -3251,11 +3251,17 @@ another <a href="#class_ba_Activity">ba.Activity</a>.</p>
</p>
<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>
<dt><h4><a name="method_ba_MusicPlayer____init__">&lt;constructor&gt;</a></dt></h4><dd>
<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>
<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>
@ -3277,12 +3283,6 @@ signify that the default soundtrack should be used..</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>
<dt><h4><a name="method_ba_MusicPlayer__on_stop">on_stop()</a></dt></h4><dd>
<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>
</dl>
<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>
<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>
<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>
<dt><h4><a name="method_ba_Server____init__">&lt;constructor&gt;</a></dt></h4><dd>
<p><span>ba.Server(config: ServerConfig)</span></p>
<dt><h4><a name="method_ba_ServerController____init__">&lt;constructor&gt;</a></dt></h4><dd>
<p><span>ba.ServerController(config: ServerConfig)</span></p>
</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>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>
</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>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>
<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>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
user can override particular game music with their own.</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>
<hr>