Skip to content

Commit 3ae1637

Browse files
committed
Implement gui and tui for sof demonstration
Implements a gui and tui that can be used to easily demonstrate SOF on target HW. See README and README-dev for more information on functionality and purpose. Signed-off-by: Alexander Brown <alex.brown.3103@gmail.com>
1 parent ae4f314 commit 3ae1637

File tree

8 files changed

+932
-0
lines changed

8 files changed

+932
-0
lines changed

tools/demo-gui/README-dev.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
## Developing sof-gui and tui
2+
3+
The architecture of the SOF UIs is simple, and designed to make implementing new features extremely easy.
4+
5+
Controller engine "sof_controller_engine.py", that handles all interaction with Linux and SOF.
6+
7+
GUI "sof_demo_gui.py", links a GTK gui with the controller engine.
8+
9+
TUI "sof_demo_tui.py", links a text UI to the controller engine.
10+
11+
eq_configs and audios folders to contain example audios and EQ commands.
12+
13+
Pipeline within topology folder, named:
14+
```sof-<hardware-name>-gui-components-wm8960.tplg```
15+
16+
## Adding a new component to the UIs
17+
18+
There are three main things that need to be edited to add a new component:
19+
20+
### Controller engine
21+
22+
Provide required sof_ctl calls and other necessary logic to control the component. Update execute_command to contain the desired commands. Ensure that autodetection is used for commands so that the implementation is generic.
23+
24+
### GUI and TUI
25+
26+
Add new buttons to the init method or gui that provide the needed functionality for the component. These should be designed to call methods in controller engine that will then interact with SOF and Linux
27+
28+
### Pipeline
29+
30+
See relevant documentation for pipeline development. Ensure any control needed is exposed through the pipeline. Also ensure the pipeline is set to build for your target HW within the cmakefiles.
31+
32+
## Next steps for overall UI development
33+
34+
Add DRC and other base level SOF components.
35+
36+
Add real time EQ config generation, so the user could control low, mid, and high controls in real time. This would require a new EQ component that supports smooth real time control.
37+
38+
Add graphics and other quality of life functions to the GUI.
39+
40+
Create a version of sof-ctl that provides direct Python bindings to communicate with SOF components, rather than needing a Linux command.

tools/demo-gui/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
## sof-demo-gui
2+
3+
### sof-demo-gui.py - sof-demo-tui.py
4+
User input logic and display handling
5+
6+
### sof-controller-engine
7+
Controller to abstract the GUI and TUI control.
8+
9+
Handles the linking of user input and sof_ctl generically of the control type.
10+
11+
### How to use the interfaces
12+
13+
Build sof-ctl for target and copy it to the gui folder base directory within your local repo.
14+
15+
If you have audio on your local machine that you wish to demonstrate using the GUI, add it to the audios subfolder.
16+
Also, you can specify audio paths on the target with the command line arg --audio-path "path"
17+
18+
If you would like to include eq configs, they are stored in tools/ctl/ipc3. Copy them from there to the eq_configs folder.
19+
20+
Copy entire GUI folder to target hardware.
21+
22+
Next, ensure that the
23+
```sof-<hardware-name>-gui-components-wm8960.tplg```
24+
is built and loaded as SOF's topology. Make sure this is built for your target hardware.
25+
26+
After this, run either the GUI or TUI on a board with SOF loaded. This can be done using the command:
27+
```python3 sof-demo-gui```
28+
or
29+
```python3 sof-demo-tui```
30+
31+
The interfaces themselves are self-explanatory, as they are made to be plug and play on all SOF supporting systems.
32+
33+
The features currently supported are:
34+
Playback and Record with ALSA
35+
Volume control using the SOF component
36+
EQ component with realtime control
37+
Generic implementation with autodetection of SOF cards and commands
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
3+
import subprocess
4+
import os
5+
import signal
6+
import re
7+
import math
8+
9+
# Global variables to store the aplay/arecord process, paused state, current file, detected device, volume control, and EQ numid
10+
aplay_process = None
11+
arecord_process = None
12+
paused = None
13+
current_file = None
14+
device_string = None
15+
volume_control = None
16+
eq_numid = None
17+
18+
extra_audio_paths = []
19+
20+
def initialize_device():
21+
global device_string, volume_control, eq_numid
22+
23+
try:
24+
output = subprocess.check_output(["aplay", "-l"], text=True, stderr=subprocess.DEVNULL)
25+
26+
match = re.search(r"card (\d+):.*\[.*sof.*\]", output, re.IGNORECASE)
27+
if match:
28+
card_number = match.group(1)
29+
device_string = f"hw:{card_number}"
30+
print(f"Detected SOF card: {device_string}")
31+
else:
32+
print("No SOF card found.")
33+
raise RuntimeError("SOF card not found. Ensure the device is connected and recognized by the system.")
34+
35+
controls_output = subprocess.check_output(["amixer", f"-D{device_string}", "controls"], text=True, stderr=subprocess.DEVNULL)
36+
37+
volume_match = re.search(r"numid=(\d+),iface=MIXER,name='(.*Master GUI Playback Volume.*)'", controls_output)
38+
if volume_match:
39+
volume_control = volume_match.group(2)
40+
print(f"Detected Volume Control: {volume_control}")
41+
else:
42+
print("Master GUI Playback Volume control not found.")
43+
raise RuntimeError("Volume control not found.")
44+
45+
eq_match = re.search(r"numid=(\d+),iface=MIXER,name='EQIIR1\.0 eqiir_coef_1'", controls_output)
46+
if eq_match:
47+
eq_numid = eq_match.group(1)
48+
print(f"Detected EQ numid: {eq_numid}")
49+
else:
50+
print("EQ control not found.")
51+
raise RuntimeError("EQ control not found.")
52+
53+
except subprocess.CalledProcessError as e:
54+
print(f"Failed to run device detection commands: {e}")
55+
raise
56+
57+
def scale_volume(user_volume):
58+
normalized_volume = user_volume / 100.0
59+
scaled_volume = 31 * (math.sqrt(normalized_volume))
60+
return int(round(scaled_volume))
61+
62+
def scan_for_files(directory_name: str, file_extension: str, extra_paths: list = None):
63+
found_files = []
64+
dir_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), directory_name)
65+
66+
if os.path.exists(dir_path):
67+
found_files.extend([f for f in os.listdir(dir_path) if f.endswith(file_extension)])
68+
else:
69+
print(f"Error: The '{directory_name}' directory is missing. It should be located in the same folder as this script.")
70+
71+
if extra_paths:
72+
for path in extra_paths:
73+
if os.path.exists(path):
74+
found_files.extend([f for f in os.listdir(path) if f.endswith(file_extension)])
75+
else:
76+
print(f"Warning: The directory '{path}' does not exist.")
77+
78+
return found_files
79+
80+
def execute_command(command: str, data: int = 0, file_name: str = None):
81+
if device_string is None or volume_control is None or eq_numid is None:
82+
raise RuntimeError("Device not initialized. Call initialize_device() first.")
83+
84+
command_switch = {
85+
'volume': lambda x: handle_volume(data),
86+
'eq': lambda x: handle_eq(file_name),
87+
'play': lambda x: handle_play(file_name),
88+
'pause': lambda x: handle_pause(),
89+
'record': lambda x: handle_record(start=data, filename=file_name)
90+
}
91+
92+
command_function = command_switch.get(command, lambda x: handle_unknown_command(data))
93+
command_function(data)
94+
95+
def handle_volume(data: int):
96+
amixer_command = f"amixer -D{device_string} cset name='{volume_control}' {data}"
97+
try:
98+
subprocess.run(amixer_command, shell=True, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
99+
except subprocess.CalledProcessError as e:
100+
print(f"Failed to set volume: {e}")
101+
102+
def handle_eq(eq_file_name: str):
103+
ctl_command = f"./sof-ctl -D{device_string} -n {eq_numid} -s {eq_file_name}"
104+
try:
105+
subprocess.run(ctl_command, shell=True, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
106+
except subprocess.CalledProcessError as e:
107+
print(f"Failed to apply EQ settings: {e}")
108+
109+
def handle_play(play_file_name: str):
110+
global aplay_process, paused, current_file
111+
112+
if paused and paused is not None and current_file == play_file_name:
113+
os.kill(aplay_process.pid, signal.SIGCONT)
114+
print("Playback resumed.")
115+
paused = False
116+
return
117+
118+
if aplay_process is not None:
119+
if aplay_process.poll() is None:
120+
if current_file == play_file_name:
121+
print("Playback is already in progress.")
122+
return
123+
else:
124+
os.kill(aplay_process.pid, signal.SIGKILL)
125+
print("Stopping current playback to play a new file.")
126+
else:
127+
print("Previous process is not running, starting new playback.")
128+
129+
default_audio_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'audios')
130+
file_path = next((os.path.join(path, play_file_name) for path in [default_audio_dir] + extra_audio_paths if os.path.exists(os.path.join(path, play_file_name))), None)
131+
132+
if file_path is None:
133+
print(f"Error: File '{play_file_name}' not found in the default 'audios' directory or any provided paths.")
134+
return
135+
136+
aplay_command = f"aplay -D{device_string} '{file_path}'"
137+
138+
try:
139+
aplay_process = subprocess.Popen(aplay_command, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
140+
current_file = play_file_name
141+
print(f"Playing file: {play_file_name}.")
142+
paused = False
143+
except subprocess.CalledProcessError as e:
144+
print(f"Failed to play file: {e}")
145+
146+
def handle_pause():
147+
global aplay_process, paused
148+
149+
if aplay_process is None:
150+
print("No playback process to pause.")
151+
return
152+
153+
if aplay_process.poll() is not None:
154+
print("Playback process has already finished.")
155+
return
156+
157+
try:
158+
os.kill(aplay_process.pid, signal.SIGSTOP)
159+
paused = True
160+
print("Playback paused.")
161+
except Exception as e:
162+
print(f"Failed to pause playback: {e}")
163+
164+
def handle_record(start: bool, filename: str):
165+
global arecord_process
166+
167+
if start:
168+
if arecord_process is not None and arecord_process.poll() is None:
169+
print("Recording is already in progress.")
170+
return
171+
172+
if not filename:
173+
print("No filename provided for recording.")
174+
return
175+
176+
record_command = f"arecord -D{device_string} -f cd -t wav {filename}"
177+
178+
try:
179+
arecord_process = subprocess.Popen(record_command, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
180+
print(f"Started recording: {filename}")
181+
except subprocess.CalledProcessError as e:
182+
print(f"Failed to start recording: {e}")
183+
184+
else:
185+
if arecord_process is None or arecord_process.poll() is not None:
186+
print("No recording process to stop.")
187+
return
188+
189+
try:
190+
os.kill(arecord_process.pid, signal.SIGINT)
191+
arecord_process = None
192+
print(f"Stopped recording.")
193+
except Exception as e:
194+
print(f"Failed to stop recording: {e}")
195+
196+
def handle_unknown_command(data: int):
197+
print(f"Unknown command: {data}")

0 commit comments

Comments
 (0)