Skip to content

Commit b9313dc

Browse files
authored
Merge pull request #1743 from avanwinkle/audio-service-menu
Audio Service Menu and FAST Audio Board Support
2 parents 54294f2 + 83b51fb commit b9313dc

File tree

11 files changed

+316
-126
lines changed

11 files changed

+316
-126
lines changed

mpf/config_spec.yaml

+3-1
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,7 @@ fast_aud:
663663
port: list|str|auto
664664
baud: single|int|230400
665665
debug: single|bool|false
666+
optional: single|bool|false
666667
console_log: single|enum(none,basic,full)|none
667668
file_log: single|enum(none,basic,full)|basic
668669
pin1_pulse_time: single|ms|100
@@ -692,7 +693,7 @@ fast_aud:
692693
headphones_levels_list: list|int|None
693694
headphones_level: single|enum(headphones,line)|headphones
694695
mute_speakers_with_headphones: single|bool|true
695-
link_main_to_main: single|bool|true
696+
link_main_to_main: single|bool|false
696697
link_sub_to_main: single|bool|true
697698
link_headphones_to_main: single|bool|false
698699

@@ -1812,6 +1813,7 @@ sound_system:
18121813
sound_system_tracks:
18131814
type: single|enum(standard,sound_loop,playlist)|standard
18141815
volume: single|gain|0.5
1816+
label: single|str|None
18151817
events_when_played: list|event_posted|None
18161818
events_when_stopped: list|event_posted|None
18171819
events_when_paused: list|event_posted|None

mpf/modes/service/code/service.py

+137
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ def _load_menu_entries(self) -> List[ServiceMenuEntry]:
122122
ServiceMenuEntry("Audits Menu", self._audits_menu),
123123
ServiceMenuEntry("Adjustments Menu", self._adjustments_menu),
124124
ServiceMenuEntry("Utilities Menu", self._utilities_menu),
125+
ServiceMenuEntry("Audio Menu", self._audio_menu)
125126

126127
]
127128
return entries
@@ -184,6 +185,28 @@ def _load_adjustments_menu_entries(self) -> List[ServiceMenuEntry]:
184185
async def _adjustments_menu(self):
185186
await self._make_menu(self._load_adjustments_menu_entries())
186187

188+
# Audio
189+
def _load_audio_menu_entries(self) -> List[ServiceMenuEntry]:
190+
"""Return the audio menu items with label and callback."""
191+
items = [
192+
ServiceMenuEntry("Software Levels", self._volume_menu)
193+
]
194+
195+
self.debug_log("Looking for platform volumes: %s", self.machine.hardware_platforms)
196+
for p, platform in self.machine.hardware_platforms.items():
197+
# TODO: Define an AudioInterface base class
198+
if getattr(platform, "audio_interface", None):
199+
self.debug_log("Found '%s' platform audio for volume: %s", p, platform)
200+
# TODO: find a good way to get a name of a platform
201+
name = p.title()
202+
items.append(ServiceMenuEntry(f"{name} Levels", partial(self._volume_menu, platform)))
203+
else:
204+
self.debug_log("Platform '%s' has no audio to configure volume: %s", p, platform)
205+
return items
206+
207+
async def _audio_menu(self):
208+
await self._make_menu(self._load_audio_menu_entries())
209+
187210
# Utilities
188211
def _load_utilities_menu_entries(self) -> List[ServiceMenuEntry]:
189212
"""Return the utilities menu items with label and callback."""
@@ -433,6 +456,120 @@ async def _light_test_menu(self):
433456

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

459+
async def _volume_menu(self, platform=None):
460+
position = 0
461+
if platform:
462+
item_configs = platform.audio_interface.amps
463+
else:
464+
item_configs = self.machine.config["sound_system"]["tracks"]
465+
items = [{
466+
**config,
467+
"name": config.get("name", track),
468+
"label": config.get("label", track),
469+
"is_platform": bool(platform),
470+
# TODO: Give each software track a 'name' property
471+
"value": self.machine.variables.get_machine_var(f"{config['name'] if platform else track}_volume") or config['volume']
472+
} for track, config in item_configs.items()]
473+
474+
# do not crash if no items
475+
if not items: # pragma: no cover
476+
return
477+
478+
# Convert floats to ints for systems that use 0.0-1.0 for volume
479+
for item in items:
480+
if isinstance(item['value'], float):
481+
item['value'] = int(item['value'] * 100)
482+
483+
# If supported on hardware platform, add option to write to firmware
484+
if platform and hasattr(platform.audio_interface, "save_settings_to_firmware"):
485+
items.append({
486+
"name": "write_to_firmware",
487+
"label": "Write Settings",
488+
"is_platform": True,
489+
"value": "Confirm",
490+
"levels_list": ["Confirm", "Saved"]
491+
})
492+
493+
self._update_volume_slide(items, position)
494+
495+
while True:
496+
key = await self._get_key()
497+
if key == 'ESC':
498+
break
499+
if key == 'UP':
500+
position += 1
501+
if position >= len(items):
502+
position = 0
503+
self._update_volume_slide(items, position)
504+
elif key == 'DOWN':
505+
position -= 1
506+
if position < 0:
507+
position = len(items) - 1
508+
self._update_volume_slide(items, position)
509+
elif key == 'ENTER':
510+
# change setting
511+
await self._volume_change(items, position, platform, focus_change="enter")
512+
513+
self.machine.events.post("service_volume_stop")
514+
515+
516+
def _update_volume_slide(self, items, position, is_change=False, focus_change=None):
517+
config = items[position]
518+
event = "service_volume_{}".format("edit" if is_change else "start")
519+
# The 'focus_change' argument can be used to start/stop sound files playing
520+
# during the service menu, to test volume.
521+
self.machine.events.post(event,
522+
settings_label=config["label"],
523+
value_label=config["value"],
524+
track=config["name"],
525+
is_platform=config["is_platform"],
526+
focus_change=focus_change)
527+
528+
async def _volume_change(self, items, position, platform, focus_change=None):
529+
self._update_volume_slide(items, position, focus_change=focus_change)
530+
if items[position].get("levels_list"):
531+
values = items[position]["levels_list"]
532+
else:
533+
# Use ints for values to avoid floating-point comparisons
534+
values = [int((0.05 * i) * 100) for i in range(0,21)]
535+
value_position = values.index(items[position]["value"])
536+
self._update_volume_slide(items, position, is_change=True)
537+
538+
while True:
539+
key = await self._get_key()
540+
new_value = None
541+
if key == 'ESC':
542+
self._update_volume_slide(items, position, focus_change="exit")
543+
break
544+
if key == 'UP':
545+
value_position += 1
546+
if value_position >= len(values):
547+
value_position = 0
548+
new_value = values[value_position]
549+
elif key == 'DOWN':
550+
value_position -= 1
551+
if value_position < 0:
552+
value_position = len(values) - 1
553+
new_value = values[value_position]
554+
if new_value is not None:
555+
items[position]['value'] = new_value
556+
# Check for a firmware update
557+
if items[position]['name'] == "write_to_firmware":
558+
if new_value == "Saved":
559+
platform.audio_interface.save_settings_to_firmware()
560+
# Remove the options from the list
561+
values = ['Saved']
562+
items[position]['levels_list'] = values
563+
else:
564+
# Internally tracked values divide by 100 to store a float.
565+
# External (hardware) values, use the value units provided
566+
# TODO: Create an Amp/Track class to internalize this method.
567+
if not items[position].get("levels_list"):
568+
new_value = new_value / 100
569+
self.machine.variables.set_machine_var(f"{items[position]['name']}_volume", new_value, persist=True)
570+
self._update_volume_slide(items, position, is_change=True)
571+
572+
# AUDIT Menu
436573
def _load_audit_menu_entries(self) -> List[ServiceMenuEntry]:
437574
"""Return the audit menu items with label and callback."""
438575
return [

mpf/modes/service_dmd/config/service_dmd.yaml

+15
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,21 @@ slide_player:
9393
service_settings:
9494
action: remove
9595

96+
# volume:
97+
service_volume_start:
98+
service_settings:
99+
action: play
100+
priority: 2
101+
service_settings_edit:
102+
action: remove
103+
service_volume_edit:
104+
service_settings_edit:
105+
action: play
106+
priority: 3
107+
service_volume_stop:
108+
service_settings:
109+
action: remove
110+
96111
# audits:
97112
service_audits_menu_show:
98113
service_audits_menu:

mpf/platforms/fast/communicators/aud.py

+26-21
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,17 @@ def __init__(self, platform, processor, config):
2929
self.amps = {
3030
'main':
3131
{'cmd': 'AV',
32-
'volume': '00',
32+
'volume': 0,
3333
'enabled': False,
3434
},
3535
'sub':
3636
{'cmd': 'AS',
37-
'volume': '00',
37+
'volume': 0,
3838
'enabled': False,
3939
},
4040
'headphones':
4141
{'cmd': 'AH',
42-
'volume': '00',
42+
'volume': 0,
4343
'enabled': False,
4444
},
4545
}
@@ -54,10 +54,12 @@ async def init(self):
5454

5555
async def soft_reset(self):
5656
self.update_config(send_now=True)
57-
for amp in self.amps:
58-
self.set_volume(amp, int(self.amps[amp]['volume'], 16))
57+
for amp_name in self.amps:
58+
self.set_volume(amp_name, self.amps[amp_name]['volume'])
5959

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

86-
def set_volume(self, amp, volume, send_now=True):
87-
if amp not in self.amps:
88-
raise AssertionError(f"Invalid amp {amp}")
88+
def set_volume(self, amp_name, volume, send_now=True):
89+
if amp_name not in self.amps:
90+
raise AssertionError(f"Invalid amp {amp_name}")
8991

90-
volume = self._volume_to_hw(volume)
91-
self.amps[amp]['volume'] = volume
92+
# Track the internal (realtime) volume of the amp, which may be
93+
# different than the stored (machine var) volume during ducking
94+
self.amps[amp_name]['volume'] = volume
9295
if send_now:
93-
self.send_and_forget(f"{self.amps[amp]['cmd']}:{volume}")
96+
hw_volume = self._volume_to_hw(volume)
97+
self.send_and_forget(f"{self.amps[amp_name]['cmd']}:{hw_volume}")
9498

95-
def get_volume(self, amp):
96-
return int(self.amps[amp]['volume'], 16)
99+
def get_volume(self, amp_name):
100+
"""Get the current internal volume of the amp."""
101+
return self.amps[amp_name]['volume']
97102

98-
def enable_amp(self, amp, send_now=True):
99-
if amp not in self.amps:
100-
raise AssertionError(f"Invalid amp {amp}")
101-
self.amps[amp]['enabled'] = True
103+
def enable_amp(self, amp_name, send_now=True):
104+
if amp_name not in self.amps:
105+
raise AssertionError(f"Invalid amp {amp_name}")
106+
self.amps[amp_name]['enabled'] = True
102107
self.update_config(send_now)
103108

104-
def disable_amp(self, amp, send_now=True):
105-
if amp not in self.amps:
106-
raise AssertionError(f"Invalid amp {amp}")
107-
self.amps[amp]['enabled'] = False
109+
def disable_amp(self, amp_name, send_now=True):
110+
if amp_name not in self.amps:
111+
raise AssertionError(f"Invalid amp {amp_name}")
112+
self.amps[amp_name]['enabled'] = False
108113
self.update_config(send_now)
109114

110115
def set_phones_level(self, mode, send_now=True):

mpf/platforms/fast/communicators/base.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ async def soft_reset(self):
7373

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

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

8993
# if we are in production mode, retry
9094
await asyncio.sleep(.1)
91-
self.log.warning("Connection to %s failed. Will retry.", port)
95+
self.log.warning("Connection to port %s failed. Will retry.", port)
9296
else:
9397
# we got a connection
9498
self.log.info(f"Connected to {port} at {self.config['baud']}bps")

mpf/platforms/fast/fast.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,6 @@ async def initialize(self):
117117
self.machine.events.add_async_handler('init_phase_1', self.soft_reset)
118118
self.machine.events.add_handler('init_phase_3', self._start_communicator_tasks)
119119
self.machine.events.add_handler('machine_reset_phase_2', self._init_complete)
120-
self.audio_interface = self.configure_audio_interface()
121120
await self._connect_to_hardware()
122121

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

0 commit comments

Comments
 (0)