Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Audio Service Menu and FAST Audio Board Support #1743

Merged
merged 8 commits into from
Jan 22, 2024
4 changes: 3 additions & 1 deletion mpf/config_spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,7 @@ fast_aud:
port: list|str|auto
baud: single|int|230400
debug: single|bool|false
optional: single|bool|false
console_log: single|enum(none,basic,full)|none
file_log: single|enum(none,basic,full)|basic
pin1_pulse_time: single|ms|100
Expand Down Expand Up @@ -692,7 +693,7 @@ fast_aud:
headphones_levels_list: list|int|None
headphones_level: single|enum(headphones,line)|headphones
mute_speakers_with_headphones: single|bool|true
link_main_to_main: single|bool|true
link_main_to_main: single|bool|false
link_sub_to_main: single|bool|true
link_headphones_to_main: single|bool|false

Expand Down Expand Up @@ -1812,6 +1813,7 @@ sound_system:
sound_system_tracks:
type: single|enum(standard,sound_loop,playlist)|standard
volume: single|gain|0.5
label: single|str|None
events_when_played: list|event_posted|None
events_when_stopped: list|event_posted|None
events_when_paused: list|event_posted|None
Expand Down
137 changes: 137 additions & 0 deletions mpf/modes/service/code/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def _load_menu_entries(self) -> List[ServiceMenuEntry]:
ServiceMenuEntry("Audits Menu", self._audits_menu),
ServiceMenuEntry("Adjustments Menu", self._adjustments_menu),
ServiceMenuEntry("Utilities Menu", self._utilities_menu),
ServiceMenuEntry("Audio Menu", self._audio_menu)

]
return entries
Expand Down Expand Up @@ -184,6 +185,28 @@ def _load_adjustments_menu_entries(self) -> List[ServiceMenuEntry]:
async def _adjustments_menu(self):
await self._make_menu(self._load_adjustments_menu_entries())

# Audio
def _load_audio_menu_entries(self) -> List[ServiceMenuEntry]:
"""Return the audio menu items with label and callback."""
items = [
ServiceMenuEntry("Software Levels", self._volume_menu)
]

self.debug_log("Looking for platform volumes: %s", self.machine.hardware_platforms)
for p, platform in self.machine.hardware_platforms.items():
# TODO: Define an AudioInterface base class
if getattr(platform, "audio_interface", None):
self.debug_log("Found '%s' platform audio for volume: %s", p, platform)
# TODO: find a good way to get a name of a platform
name = p.title()
items.append(ServiceMenuEntry(f"{name} Levels", partial(self._volume_menu, platform)))
else:
self.debug_log("Platform '%s' has no audio to configure volume: %s", p, platform)
return items

async def _audio_menu(self):
await self._make_menu(self._load_audio_menu_entries())

# Utilities
def _load_utilities_menu_entries(self) -> List[ServiceMenuEntry]:
"""Return the utilities menu items with label and callback."""
Expand Down Expand Up @@ -433,6 +456,120 @@ async def _light_test_menu(self):

self.machine.events.post("service_light_test_stop")

async def _volume_menu(self, platform=None):
position = 0
if platform:
item_configs = platform.audio_interface.amps
else:
item_configs = self.machine.config["sound_system"]["tracks"]
items = [{
**config,
"name": config.get("name", track),
"label": config.get("label", track),
"is_platform": bool(platform),
# TODO: Give each software track a 'name' property
"value": self.machine.variables.get_machine_var(f"{config['name'] if platform else track}_volume") or config['volume']
} for track, config in item_configs.items()]

# do not crash if no items
if not items: # pragma: no cover
return

# Convert floats to ints for systems that use 0.0-1.0 for volume
for item in items:
if isinstance(item['value'], float):
item['value'] = int(item['value'] * 100)

# If supported on hardware platform, add option to write to firmware
if platform and hasattr(platform.audio_interface, "save_settings_to_firmware"):
items.append({
"name": "write_to_firmware",
"label": "Write Settings",
"is_platform": True,
"value": "Confirm",
"levels_list": ["Confirm", "Saved"]
})

self._update_volume_slide(items, position)

while True:
key = await self._get_key()
if key == 'ESC':
break
if key == 'UP':
position += 1
if position >= len(items):
position = 0
self._update_volume_slide(items, position)
elif key == 'DOWN':
position -= 1
if position < 0:
position = len(items) - 1
self._update_volume_slide(items, position)
elif key == 'ENTER':
# change setting
await self._volume_change(items, position, platform, focus_change="enter")

self.machine.events.post("service_volume_stop")


def _update_volume_slide(self, items, position, is_change=False, focus_change=None):
config = items[position]
event = "service_volume_{}".format("edit" if is_change else "start")
# The 'focus_change' argument can be used to start/stop sound files playing
# during the service menu, to test volume.
self.machine.events.post(event,
settings_label=config["label"],
value_label=config["value"],
track=config["name"],
is_platform=config["is_platform"],
focus_change=focus_change)

async def _volume_change(self, items, position, platform, focus_change=None):
self._update_volume_slide(items, position, focus_change=focus_change)
if items[position].get("levels_list"):
values = items[position]["levels_list"]
else:
# Use ints for values to avoid floating-point comparisons
values = [int((0.05 * i) * 100) for i in range(0,21)]
value_position = values.index(items[position]["value"])
self._update_volume_slide(items, position, is_change=True)

while True:
key = await self._get_key()
new_value = None
if key == 'ESC':
self._update_volume_slide(items, position, focus_change="exit")
break
if key == 'UP':
value_position += 1
if value_position >= len(values):
value_position = 0
new_value = values[value_position]
elif key == 'DOWN':
value_position -= 1
if value_position < 0:
value_position = len(values) - 1
new_value = values[value_position]
if new_value is not None:
items[position]['value'] = new_value
# Check for a firmware update
if items[position]['name'] == "write_to_firmware":
if new_value == "Saved":
platform.audio_interface.save_settings_to_firmware()
# Remove the options from the list
values = ['Saved']
items[position]['levels_list'] = values
else:
# Internally tracked values divide by 100 to store a float.
# External (hardware) values, use the value units provided
# TODO: Create an Amp/Track class to internalize this method.
if not items[position].get("levels_list"):
new_value = new_value / 100
self.machine.variables.set_machine_var(f"{items[position]['name']}_volume", new_value, persist=True)
self._update_volume_slide(items, position, is_change=True)

# AUDIT Menu
def _load_audit_menu_entries(self) -> List[ServiceMenuEntry]:
"""Return the audit menu items with label and callback."""
return [
Expand Down
15 changes: 15 additions & 0 deletions mpf/modes/service_dmd/config/service_dmd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,21 @@ slide_player:
service_settings:
action: remove

# volume:
service_volume_start:
service_settings:
action: play
priority: 2
service_settings_edit:
action: remove
service_volume_edit:
service_settings_edit:
action: play
priority: 3
service_volume_stop:
service_settings:
action: remove

# audits:
service_audits_menu_show:
service_audits_menu:
Expand Down
47 changes: 26 additions & 21 deletions mpf/platforms/fast/communicators/aud.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,17 @@ def __init__(self, platform, processor, config):
self.amps = {
'main':
{'cmd': 'AV',
'volume': '00',
'volume': 0,
'enabled': False,
},
'sub':
{'cmd': 'AS',
'volume': '00',
'volume': 0,
'enabled': False,
},
'headphones':
{'cmd': 'AH',
'volume': '00',
'volume': 0,
'enabled': False,
},
}
Expand All @@ -54,10 +54,12 @@ async def init(self):

async def soft_reset(self):
self.update_config(send_now=True)
for amp in self.amps:
self.set_volume(amp, int(self.amps[amp]['volume'], 16))
for amp_name in self.amps:
self.set_volume(amp_name, self.amps[amp_name]['volume'])

def _volume_to_hw(self, volume):
"""Always store and pass volume levels as decimals (0-64), and use this
method to convert to hex strings when sending via serial to AUD board."""
volume = int(volume)
assert 0 <= volume <= 63, f"Invalid volume {volume}"
return f"{volume:02X}"
Expand All @@ -83,28 +85,31 @@ def update_config(self, send_now=True):
if send_now:
self.send_config_to_board()

def set_volume(self, amp, volume, send_now=True):
if amp not in self.amps:
raise AssertionError(f"Invalid amp {amp}")
def set_volume(self, amp_name, volume, send_now=True):
if amp_name not in self.amps:
raise AssertionError(f"Invalid amp {amp_name}")

volume = self._volume_to_hw(volume)
self.amps[amp]['volume'] = volume
# Track the internal (realtime) volume of the amp, which may be
# different than the stored (machine var) volume during ducking
self.amps[amp_name]['volume'] = volume
if send_now:
self.send_and_forget(f"{self.amps[amp]['cmd']}:{volume}")
hw_volume = self._volume_to_hw(volume)
self.send_and_forget(f"{self.amps[amp_name]['cmd']}:{hw_volume}")

def get_volume(self, amp):
return int(self.amps[amp]['volume'], 16)
def get_volume(self, amp_name):
"""Get the current internal volume of the amp."""
return self.amps[amp_name]['volume']

def enable_amp(self, amp, send_now=True):
if amp not in self.amps:
raise AssertionError(f"Invalid amp {amp}")
self.amps[amp]['enabled'] = True
def enable_amp(self, amp_name, send_now=True):
if amp_name not in self.amps:
raise AssertionError(f"Invalid amp {amp_name}")
self.amps[amp_name]['enabled'] = True
self.update_config(send_now)

def disable_amp(self, amp, send_now=True):
if amp not in self.amps:
raise AssertionError(f"Invalid amp {amp}")
self.amps[amp]['enabled'] = False
def disable_amp(self, amp_name, send_now=True):
if amp_name not in self.amps:
raise AssertionError(f"Invalid amp {amp_name}")
self.amps[amp_name]['enabled'] = False
self.update_config(send_now)

def set_phones_level(self, mode, send_now=True):
Expand Down
6 changes: 5 additions & 1 deletion mpf/platforms/fast/communicators/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ async def soft_reset(self):

async def connect(self):
for port in self.config['port']:
# If this is an auto-detect that failed to detect, the port will
# just be 'auto' and there's no reason to try and connect to it.
if port == 'auto':
continue
self.log.info(f"Trying to connect to {port} at {self.config['baud']}bps")
success = False

Expand All @@ -88,7 +92,7 @@ async def connect(self):

# if we are in production mode, retry
await asyncio.sleep(.1)
self.log.warning("Connection to %s failed. Will retry.", port)
self.log.warning("Connection to port %s failed. Will retry.", port)
else:
# we got a connection
self.log.info(f"Connected to {port} at {self.config['baud']}bps")
Expand Down
5 changes: 4 additions & 1 deletion mpf/platforms/fast/fast.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ async def initialize(self):
self.machine.events.add_async_handler('init_phase_1', self.soft_reset)
self.machine.events.add_handler('init_phase_3', self._start_communicator_tasks)
self.machine.events.add_handler('machine_reset_phase_2', self._init_complete)
self.audio_interface = self.configure_audio_interface()
await self._connect_to_hardware()

async def soft_reset(self, **kwargs):
Expand Down Expand Up @@ -275,6 +274,10 @@ async def _connect_to_hardware(self): # TODO move to class methods?
try:
await communicator.connect()
except SerialException as e:
if config.get("optional"):
self.info_log("Unable to connect to %s on port %s, flagged as optional so ignoring", port, config['port'])
del(self.serial_connections[port])
continue
raise MpfRuntimeError("Could not open serial port {}. Is something else connected to the port? "
"Did the port number or your computer change? Do you have permissions to the port? "
"".format(port), 1, self.log.name) from e
Expand Down
Loading