mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-21 22:33:58 +08:00
Cleaned up music subsystem
This commit is contained in:
parent
ffc21de05a
commit
cff5046fe9
4
.idea/dictionaries/ericf.xml
generated
4
.idea/dictionaries/ericf.xml
generated
@ -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>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 $@
|
||||
|
||||
|
||||
@ -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('_'):
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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,
|
||||
|
||||
251
assets/src/ba_data/python/ba/macmusicapp.py
Normal file
251
assets/src/ba_data/python/ba/macmusicapp.py
Normal 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))))
|
||||
150
assets/src/ba_data/python/ba/osmusic.py
Normal file
150
assets/src/ba_data/python/ba/osmusic.py
Normal 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)
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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,
|
||||
@ -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__"><constructor></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__"><constructor></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__"><constructor></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) -> 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) -> 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) -> 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) -> 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><top level class></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__"><constructor></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__"><constructor></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__"><constructor></a></dt></h4><dd>
|
||||
<p><span>ba.Server(config: ServerConfig)</span></p>
|
||||
<dt><h4><a name="method_ba_ServerController____init__"><constructor></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) -> 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) -> 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) -> 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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user