From dc03c210daa26344ab9a173c01244566779765fc Mon Sep 17 00:00:00 2001 From: Alexander Brown Date: Sun, 25 Aug 2024 00:49:26 -0400 Subject: [PATCH] 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 --- tools/demo-gui/README-dev.md | 40 ++++ tools/demo-gui/README.md | 37 +++ .../demo-gui/sof_controller_engine.py | 197 ++++++++++++++++ tools/demo-gui/demo-gui/sof_demo_gui.py | 213 ++++++++++++++++++ tools/demo-gui/demo-gui/sof_demo_tui.py | 184 +++++++++++++++ tools/topology/topology1/CMakeLists.txt | 1 + .../sof/pipe-gui-components-capture.m4 | 146 ++++++++++++ .../sof/pipe-gui-components-playback.m4 | 114 ++++++++++ 8 files changed, 932 insertions(+) create mode 100644 tools/demo-gui/README-dev.md create mode 100644 tools/demo-gui/README.md create mode 100644 tools/demo-gui/demo-gui/sof_controller_engine.py create mode 100644 tools/demo-gui/demo-gui/sof_demo_gui.py create mode 100644 tools/demo-gui/demo-gui/sof_demo_tui.py create mode 100644 tools/topology/topology1/sof/pipe-gui-components-capture.m4 create mode 100644 tools/topology/topology1/sof/pipe-gui-components-playback.m4 diff --git a/tools/demo-gui/README-dev.md b/tools/demo-gui/README-dev.md new file mode 100644 index 000000000000..455d3bf6b582 --- /dev/null +++ b/tools/demo-gui/README-dev.md @@ -0,0 +1,40 @@ +## Developing sof-gui and tui + +The architecture of the SOF UIs is simple, and designed to make implementing new features extremely easy. + +Controller engine "sof_controller_engine.py", that handles all interaction with Linux and SOF. + +GUI "sof_demo_gui.py", links a GTK gui with the controller engine. + +TUI "sof_demo_tui.py", links a text UI to the controller engine. + +eq_configs and audios folders to contain example audios and EQ commands. + +Pipeline within topology folder, named: +```sof--gui-components-wm8960.tplg``` + +## Adding a new component to the UIs + +There are three main things that need to be edited to add a new component: + +### Controller engine + +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. + +### GUI and TUI + +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 + +### Pipeline + +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. + +## Next steps for overall UI development + +Add DRC and other base level SOF components. + +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. + +Add graphics and other quality of life functions to the GUI. + +Create a version of sof-ctl that provides direct Python bindings to communicate with SOF components, rather than needing a Linux command. diff --git a/tools/demo-gui/README.md b/tools/demo-gui/README.md new file mode 100644 index 000000000000..279937dfa0aa --- /dev/null +++ b/tools/demo-gui/README.md @@ -0,0 +1,37 @@ +## sof-demo-gui + +### sof-demo-gui.py - sof-demo-tui.py +User input logic and display handling + +### sof-controller-engine +Controller to abstract the GUI and TUI control. + +Handles the linking of user input and sof_ctl generically of the control type. + +### How to use the interfaces + +Build sof-ctl for target and copy it to the gui folder base directory within your local repo. + +If you have audio on your local machine that you wish to demonstrate using the GUI, add it to the audios subfolder. +Also, you can specify audio paths on the target with the command line arg --audio-path "path" + +If you would like to include eq configs, they are stored in tools/ctl/ipc3. Copy them from there to the eq_configs folder. + +Copy entire GUI folder to target hardware. + +Next, ensure that the +```sof--gui-components-wm8960.tplg``` +is built and loaded as SOF's topology. Make sure this is built for your target hardware. + +After this, run either the GUI or TUI on a board with SOF loaded. This can be done using the command: +```python3 sof-demo-gui``` +or +```python3 sof-demo-tui``` + +The interfaces themselves are self-explanatory, as they are made to be plug and play on all SOF supporting systems. + +The features currently supported are: +Playback and Record with ALSA +Volume control using the SOF component +EQ component with realtime control +Generic implementation with autodetection of SOF cards and commands diff --git a/tools/demo-gui/demo-gui/sof_controller_engine.py b/tools/demo-gui/demo-gui/sof_controller_engine.py new file mode 100644 index 000000000000..25d40b820b8a --- /dev/null +++ b/tools/demo-gui/demo-gui/sof_controller_engine.py @@ -0,0 +1,197 @@ +# SPDX-License-Identifier: BSD-3-Clause + +import subprocess +import os +import signal +import re +import math + +# Global variables to store the aplay/arecord process, paused state, current file, detected device, volume control, and EQ numid +aplay_process = None +arecord_process = None +paused = None +current_file = None +device_string = None +volume_control = None +eq_numid = None + +extra_audio_paths = [] + +def initialize_device(): + global device_string, volume_control, eq_numid + + try: + output = subprocess.check_output(["aplay", "-l"], text=True, stderr=subprocess.DEVNULL) + + match = re.search(r"card (\d+):.*\[.*sof.*\]", output, re.IGNORECASE) + if match: + card_number = match.group(1) + device_string = f"hw:{card_number}" + print(f"Detected SOF card: {device_string}") + else: + print("No SOF card found.") + raise RuntimeError("SOF card not found. Ensure the device is connected and recognized by the system.") + + controls_output = subprocess.check_output(["amixer", f"-D{device_string}", "controls"], text=True, stderr=subprocess.DEVNULL) + + volume_match = re.search(r"numid=(\d+),iface=MIXER,name='(.*Master Playback Volume.*)'", controls_output) + if volume_match: + volume_control = volume_match.group(2) + print(f"Detected Volume Control: {volume_control}") + else: + print("Master GUI Playback Volume control not found.") + raise RuntimeError("Volume control not found.") + + eq_match = re.search(r"numid=(\d+),iface=MIXER,name='EQIIR1\.0 eqiir_coef_1'", controls_output) + if eq_match: + eq_numid = eq_match.group(1) + print(f"Detected EQ numid: {eq_numid}") + else: + print("EQ control not found.") + raise RuntimeError("EQ control not found.") + + except subprocess.CalledProcessError as e: + print(f"Failed to run device detection commands: {e}") + raise + +def scale_volume(user_volume): + normalized_volume = user_volume / 100.0 + scaled_volume = 31 * (math.sqrt(normalized_volume)) + return int(round(scaled_volume)) + +def scan_for_files(directory_name: str, file_extension: str, extra_paths: list = None): + found_files = [] + dir_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), directory_name) + + if os.path.exists(dir_path): + found_files.extend([f for f in os.listdir(dir_path) if f.endswith(file_extension)]) + else: + print(f"Error: The '{directory_name}' directory is missing. It should be located in the same folder as this script.") + + if extra_paths: + for path in extra_paths: + if os.path.exists(path): + found_files.extend([f for f in os.listdir(path) if f.endswith(file_extension)]) + else: + print(f"Warning: The directory '{path}' does not exist.") + + return found_files + +def execute_command(command: str, data: int = 0, file_name: str = None): + if device_string is None or volume_control is None or eq_numid is None: + raise RuntimeError("Device not initialized. Call initialize_device() first.") + + command_switch = { + 'volume': lambda x: handle_volume(data), + 'eq': lambda x: handle_eq(file_name), + 'play': lambda x: handle_play(file_name), + 'pause': lambda x: handle_pause(), + 'record': lambda x: handle_record(start=data, filename=file_name) + } + + command_function = command_switch.get(command, lambda x: handle_unknown_command(data)) + command_function(data) + +def handle_volume(data: int): + amixer_command = f"amixer -D{device_string} cset name='{volume_control}' {data}" + try: + subprocess.run(amixer_command, shell=True, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError as e: + print(f"Failed to set volume: {e}") + +def handle_eq(eq_file_name: str): + ctl_command = f"./sof-ctl -D{device_string} -n {eq_numid} -s {eq_file_name}" + try: + subprocess.run(ctl_command, shell=True, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError as e: + print(f"Failed to apply EQ settings: {e}") + +def handle_play(play_file_name: str): + global aplay_process, paused, current_file + + if paused and paused is not None and current_file == play_file_name: + os.kill(aplay_process.pid, signal.SIGCONT) + print("Playback resumed.") + paused = False + return + + if aplay_process is not None: + if aplay_process.poll() is None: + if current_file == play_file_name: + print("Playback is already in progress.") + return + else: + os.kill(aplay_process.pid, signal.SIGKILL) + print("Stopping current playback to play a new file.") + else: + print("Previous process is not running, starting new playback.") + + default_audio_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'audios') + 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) + + if file_path is None: + print(f"Error: File '{play_file_name}' not found in the default 'audios' directory or any provided paths.") + return + + aplay_command = f"aplay -D{device_string} '{file_path}'" + + try: + aplay_process = subprocess.Popen(aplay_command, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + current_file = play_file_name + print(f"Playing file: {play_file_name}.") + paused = False + except subprocess.CalledProcessError as e: + print(f"Failed to play file: {e}") + +def handle_pause(): + global aplay_process, paused + + if aplay_process is None: + print("No playback process to pause.") + return + + if aplay_process.poll() is not None: + print("Playback process has already finished.") + return + + try: + os.kill(aplay_process.pid, signal.SIGSTOP) + paused = True + print("Playback paused.") + except Exception as e: + print(f"Failed to pause playback: {e}") + +def handle_record(start: bool, filename: str): + global arecord_process + + if start: + if arecord_process is not None and arecord_process.poll() is None: + print("Recording is already in progress.") + return + + if not filename: + print("No filename provided for recording.") + return + + record_command = f"arecord -D{device_string} -f cd -t wav {filename}" + + try: + arecord_process = subprocess.Popen(record_command, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + print(f"Started recording: {filename}") + except subprocess.CalledProcessError as e: + print(f"Failed to start recording: {e}") + + else: + if arecord_process is None or arecord_process.poll() is not None: + print("No recording process to stop.") + return + + try: + os.kill(arecord_process.pid, signal.SIGINT) + arecord_process = None + print(f"Stopped recording.") + except Exception as e: + print(f"Failed to stop recording: {e}") + +def handle_unknown_command(data: int): + print(f"Unknown command: {data}") diff --git a/tools/demo-gui/demo-gui/sof_demo_gui.py b/tools/demo-gui/demo-gui/sof_demo_gui.py new file mode 100644 index 000000000000..37174de7b1a7 --- /dev/null +++ b/tools/demo-gui/demo-gui/sof_demo_gui.py @@ -0,0 +1,213 @@ +# SPDX-License-Identifier: BSD-3-Clause + +import gi +import os +import argparse +import sof_controller_engine as sof_ctl +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gdk + +class MyWindow(Gtk.Window): + def __init__(self, audio_paths, config_paths): + super().__init__(title="SOF Demo Gui") + self.set_resizable(False) + self.set_border_width(10) + + css_provider = Gtk.CssProvider() + css = """ + * { + font-family: "Segoe UI", "Arial", sans-serif; + font-size: 14px; + color: #FFFFFF; + } + window { + background-color: #2C3E50; + } + frame { + border: 1px solid #34495E; + border-radius: 5px; + padding: 10px; + margin: 5px; + background-color: #34495E; + } + button, togglebutton { + background-color: #3b3b3b; + background: #3b3b3b; + border: none; + padding: 10px; + color: #3b3b3b; + } + button:hover, togglebutton:hover { + background-color: #A9A9A9; + color: #A9A9A9; + } + togglebutton:checked { + background-color: #A9A9A9; + color: #A9A9A9; + } + scale { + background-color: #34495E; + border-radius: 5px; + } + label { + color: #A9A9A9; + } + headerbar { + background-color: #2C3E50; + color: #2C3E50; + } + headerbar.titlebar { + background: #2C3E50; + } + """ + css_provider.load_from_data(css.encode('utf-8')) + screen = Gdk.Screen.get_default() + Gtk.StyleContext.add_provider_for_screen(screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + + main_grid = Gtk.Grid() + main_grid.set_row_spacing(10) + main_grid.set_column_spacing(10) + self.add(main_grid) + + control_frame = Gtk.Frame(label="Playback and Volume Control") + control_grid = Gtk.Grid() + control_grid.set_row_spacing(10) + control_grid.set_column_spacing(10) + control_frame.add(control_grid) + main_grid.attach(control_frame, 0, 0, 1, 1) + + self.play_pause_button = Gtk.ToggleButton(label="Play") + self.play_pause_button.connect("toggled", self.on_play_pause_toggled) + control_grid.attach(self.play_pause_button, 0, 0, 1, 1) + + volume_adjustment = Gtk.Adjustment(value=100, lower=0, upper=100, step_increment=1, page_increment=5, page_size=0) + self.volume_button = Gtk.Scale(name="volume", orientation=Gtk.Orientation.HORIZONTAL, adjustment=volume_adjustment) + self.volume_button.set_digits(0) + self.volume_button.set_hexpand(True) + self.volume_button.connect("value-changed", self.on_volume_changed) + control_grid.attach(self.volume_button, 1, 0, 1, 1) + + file_frame = Gtk.Frame(label="File Selection") + file_grid = Gtk.Grid() + file_grid.set_row_spacing(10) + file_grid.set_column_spacing(10) + file_frame.add(file_grid) + main_grid.attach(file_frame, 0, 1, 1, 1) + + self.wav_dropdown = Gtk.ComboBoxText() + self.wav_dropdown.connect("changed", self.on_wav_file_selected) + wav_label = Gtk.Label(label="Select WAV File") + wav_label.set_margin_top(5) + wav_label.set_margin_bottom(5) + file_grid.attach(wav_label, 0, 0, 1, 1) + file_grid.attach(self.wav_dropdown, 1, 0, 1, 1) + + self.eq_dropdown = Gtk.ComboBoxText() + eq_label = Gtk.Label(label="Select EQ Config") + eq_label.set_margin_top(5) + eq_label.set_margin_bottom(5) + file_grid.attach(eq_label, 0, 1, 1, 1) + file_grid.attach(self.eq_dropdown, 1, 1, 1, 1) + + self.apply_eq_button = Gtk.Button(label="Apply EQ Config") + self.apply_eq_button.connect("clicked", self.on_apply_eq_clicked) + file_grid.attach(self.apply_eq_button, 1, 2, 1, 1) + + record_frame = Gtk.Frame(label="Recording Control") + record_grid = Gtk.Grid() + record_grid.set_row_spacing(10) + record_grid.set_column_spacing(10) + record_grid.set_hexpand(True) + record_grid.set_vexpand(True) + record_frame.add(record_grid) + main_grid.attach(record_frame, 0, 2, 1, 1) + + self.record_button = Gtk.ToggleButton(label="Record") + self.record_button.connect("toggled", self.on_record_toggled) + self.record_button.set_hexpand(True) + self.record_button.set_vexpand(True) + record_grid.attach(self.record_button, 0, 0, 1, 1) + + self.record_index = 1 + + self.scan_and_populate_dropdowns(audio_paths, config_paths) + sof_ctl.initialize_device() + + def scan_and_populate_dropdowns(self, audio_paths, config_paths): + wav_files = sof_ctl.scan_for_files('audios', '.wav', extra_paths=audio_paths) + + self.wav_dropdown.remove_all() + for wav_file in wav_files: + self.wav_dropdown.append_text(wav_file) + + if wav_files: + self.wav_dropdown.set_active(0) + self.selected_wav_file = wav_files[0] + + eq_files = sof_ctl.scan_for_files('eq_configs', '.txt', extra_paths=config_paths) + + self.eq_dropdown.remove_all() + for eq_file in eq_files: + self.eq_dropdown.append_text(eq_file) + + if eq_files: + self.eq_dropdown.set_active(0) + self.selected_eq_file = eq_files[0] + + def on_volume_changed(self, widget): + user_volume = widget.get_value() + scaled_volume = sof_ctl.scale_volume(user_volume) + sof_ctl.execute_command(command="volume", data=scaled_volume) + + def on_play_pause_toggled(self, widget): + if widget.get_active(): + widget.set_label("Pause") + sof_ctl.execute_command(command="play", file_name=self.selected_wav_file) + else: + widget.set_label("Play") + sof_ctl.execute_command(command="pause") + + def on_record_toggled(self, widget): + if widget.get_active(): + widget.set_label("Stop Recording") + filename = self.generate_sequential_filename() + sof_ctl.execute_command(command="record", data=True, file_name=filename) + else: + widget.set_label("Record") + sof_ctl.execute_command(command="record", data=False) + + def generate_sequential_filename(self): + recordings_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'recordings') + + if not os.path.exists(recordings_dir): + os.makedirs(recordings_dir) + + filename = os.path.join(recordings_dir, f"recording_{self.record_index}.wav") + self.record_index += 1 + + return filename + + def on_wav_file_selected(self, widget): + self.selected_wav_file = widget.get_active_text() + + def on_apply_eq_clicked(self, widget): + self.selected_eq_file = self.eq_dropdown.get_active_text() + if self.selected_eq_file: + eq_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'eq_configs', self.selected_eq_file) + sof_ctl.execute_command(command="eq", file_name=eq_file_path) + +def main(): + parser = argparse.ArgumentParser(description="SOF GUI Application") + parser.add_argument('--audio-path', action='append', help="Additional path to search for audio files (.wav)") + parser.add_argument('--config-path', action='append', help="Additional path to search for EQ config files (.txt)") + args = parser.parse_args() + + sof_ctl.extra_audio_paths = args.audio_path if args.audio_path else [] + + win = MyWindow(audio_paths=args.audio_path, config_paths=args.config_path) + win.connect("destroy", Gtk.main_quit) + win.show_all() + Gtk.main() + +if __name__ == "__main__": + main() diff --git a/tools/demo-gui/demo-gui/sof_demo_tui.py b/tools/demo-gui/demo-gui/sof_demo_tui.py new file mode 100644 index 000000000000..3fb387d684e1 --- /dev/null +++ b/tools/demo-gui/demo-gui/sof_demo_tui.py @@ -0,0 +1,184 @@ +# SPDX-License-Identifier: BSD-3-Clause + +import os +import sof_controller_engine as sof_ctl +import argparse + +os.chdir(os.path.dirname(os.path.abspath(__file__))) + +class TextBasedUI: + def __init__(self, audio_paths, config_paths): + self.selected_wav_file = None + self.selected_eq_file = None + self.volume = 15 + self.is_playing = False + self.is_recording = False + self.scan_and_populate_files(audio_paths, config_paths) + sof_ctl.initialize_device() + self.run_ui() + + def scan_and_populate_files(self, audio_paths, config_paths): + self.wav_files = sof_ctl.scan_for_files('audios', '.wav', extra_paths=audio_paths) + + if self.wav_files: + self.selected_wav_file = self.wav_files[0] + print(f"Default WAV file selected: {self.selected_wav_file}") + + self.eq_files = sof_ctl.scan_for_files('eq_configs', '.txt', extra_paths=config_paths) + + if self.eq_files: + self.selected_eq_file = self.eq_files[0] + print(f"Default EQ config selected: {self.selected_eq_file}") + + def display_menu(self): + print("\nSOF Text-Based UI") + print("==================") + print("1. Select a WAV file") + if self.selected_wav_file: + print(f" Currently selected: {self.selected_wav_file}") + else: + print(" No WAV file selected") + + print("2. Select an EQ Config") + if self.selected_eq_file: + print(f" Currently selected: {self.selected_eq_file}") + else: + print(" No EQ file selected") + + print(f"3. Set Volume (Current: {self.volume})") + print(f"4. Play/Pause (Current: {'Playing' if self.is_playing else 'Paused'})") + print(f"5. Start/Stop Recording (Current: {'Recording' if self.is_recording else 'Not Recording'})") + print("6. Apply EQ Config") + print("7. Quit") + print("==================") + + def run_ui(self): + while True: + self.display_menu() + choice = input("Enter your choice (1-7): ").strip() + + if choice == '1': + self.select_wav_file() + elif choice == '2': + self.select_eq_file() + elif choice == '3': + self.set_volume() + elif choice == '4': + self.toggle_play_pause() + elif choice == '5': + self.toggle_record() + elif choice == '6': + self.apply_eq_config() + elif choice == '7': + print("Exiting...") + break + else: + print("Invalid choice, please try again.") + + def select_wav_file(self): + if not self.wav_files: + print("No .wav files found in the current directory.") + return + + print("\nSelect a WAV file:") + for idx, wav_file in enumerate(self.wav_files): + print(f"{idx + 1}. {wav_file}") + + file_choice = input(f"Enter the number of the file (1-{len(self.wav_files)}): ").strip() + + try: + file_index = int(file_choice) - 1 + if 0 <= file_index < len(self.wav_files): + self.selected_wav_file = self.wav_files[file_index] + print(f"Selected file: {self.selected_wav_file}") + else: + print("Invalid selection.") + except ValueError: + print("Invalid input, please enter a number.") + + def select_eq_file(self): + if not self.eq_files: + print("No EQ config files found in the eq_configs directory.") + return + + print("\nSelect an EQ Config file:") + for idx, eq_file in enumerate(self.eq_files): + print(f"{idx + 1}. {eq_file}") + + file_choice = input(f"Enter the number of the file (1-{len(self.eq_files)}): ").strip() + + try: + file_index = int(file_choice) - 1 + if 0 <= file_index < len(self.eq_files): + self.selected_eq_file = self.eq_files[file_index] + print(f"Selected EQ Config: {self.selected_eq_file}") + else: + print("Invalid selection.") + except ValueError: + print("Invalid input, please enter a number.") + + def set_volume(self): + volume_input = input("Enter volume level (0-100): ").strip() + + try: + user_volume = int(volume_input) + if 0 <= user_volume <= 100: + self.volume = user_volume + scaled_volume = sof_ctl.scale_volume(user_volume) + sof_ctl.execute_command(command="volume", data=scaled_volume) + print(f"Volume set to {self.volume}.") + else: + print("Volume must be between 0 and 100.") + except ValueError: + print("Invalid input, please enter a number.") + + def toggle_play_pause(self): + if not self.selected_wav_file: + print("No WAV file selected.") + return + + if not self.is_playing: + sof_ctl.execute_command(command="play", file_name=self.selected_wav_file) + self.is_playing = True + print("Playing...") + else: + sof_ctl.execute_command(command="pause") + self.is_playing = False + print("Paused.") + + def toggle_record(self): + recordings_dir = os.path.join(os.getcwd(), 'recordings') + + if not self.is_recording: + filename = input(f"Enter filename to save the recording (default: {recordings_dir}/record.wav): ").strip() + if not filename: + filename = "record.wav" + + full_path = os.path.join(recordings_dir, filename) + sof_ctl.execute_command(command="record", data=True, file_name=full_path) + self.is_recording = True + else: + sof_ctl.execute_command(command="record", data=False) + self.is_recording = False + + def apply_eq_config(self): + if not self.selected_eq_file: + print("No EQ config file selected.") + return + + eq_file_path = os.path.join(os.getcwd(), 'eq_configs', self.selected_eq_file) + sof_ctl.execute_command(command="eq", file_name=eq_file_path) + print(f"Applied EQ config: {self.selected_eq_file}") + +def main(): + parser = argparse.ArgumentParser(description="SOF TUI Application") + parser.add_argument('--audio-path', action='append', help="Additional path to search for audio files (.wav)") + parser.add_argument('--config-path', action='append', help="Additional path to search for EQ config files (.txt)") + args = parser.parse_args() + + sof_ctl.extra_audio_paths = args.audio_path if args.audio_path else [] + + TextBasedUI(audio_paths=args.audio_path, config_paths=args.config_path) + +if __name__ == "__main__": + main() diff --git a/tools/topology/topology1/CMakeLists.txt b/tools/topology/topology1/CMakeLists.txt index 38e6c3563600..f267c129c3f1 100644 --- a/tools/topology/topology1/CMakeLists.txt +++ b/tools/topology/topology1/CMakeLists.txt @@ -55,6 +55,7 @@ set(TPLGS "sof-imx8-wm8960\;sof-imx8mp-eq-iir-wm8962\;-DCODEC=wm8962\;-DRATE=48000\;-DPPROC=eq-iir-volume\;-DSAI_INDEX=3\;-DDMA_DOMAIN" "sof-imx8-wm8960\;sof-imx8mp-eq-fir-wm8960\;-DCODEC=wm8960\;-DRATE=48000\;-DPPROC=eq-fir-volume\;-DSAI_INDEX=3\;-DDMA_DOMAIN" "sof-imx8-wm8960\;sof-imx8mp-eq-fir-wm8962\;-DCODEC=wm8962\;-DRATE=48000\;-DPPROC=eq-fir-volume\;-DSAI_INDEX=3\;-DDMA_DOMAIN" + "sof-imx8-wm8960\;sof-imx8mp-gui-components-wm8960\;-DCODEC=wm8960\;-DRATE=48000\;-DPPROC=gui-components\;-DSAI_INDEX=3\;-DDMA_DOMAIN" "sof-imx8-wm8960\;sof-imx8mp-drc-wm8960\;-DCODEC=wm8960\;-DRATE=48000\;-DPPROC=drc\;-DSAI_INDEX=3\;-DDMA_DOMAIN" ## end i.MX8MP topologies diff --git a/tools/topology/topology1/sof/pipe-gui-components-capture.m4 b/tools/topology/topology1/sof/pipe-gui-components-capture.m4 new file mode 100644 index 000000000000..ee8a66f6efd4 --- /dev/null +++ b/tools/topology/topology1/sof/pipe-gui-components-capture.m4 @@ -0,0 +1,146 @@ +# Capture EQ Pipeline and PCM, 48 kHz +# +# Pipeline Endpoints for connection are :- +# +# host PCM_C <-- B0 <-- Volume 0 <-- B1 <--EQ_IIR 0 <-- B2 <-- sink DAI0 + +# Include topology builder +include(`utils.m4') +include(`buffer.m4') +include(`pcm.m4') +include(`pga.m4') +include(`dai.m4') +include(`pipeline.m4') +include(`bytecontrol.m4') +include(`mixercontrol.m4') +include(`eq_iir.m4') + +ifdef(`PGA_NAME', `', `define(PGA_NAME, N_PGA(0))') +define(`CONTROL_NAME_VOLUME', Capture Volume) +define(`CONTROL_NAME_SWITCH', Capture Switch) +define(`CONTROL_NAME', `CONTROL_NAME_VOLUME') + +# +# Controls +# + +# Volume Mixer control with max value of 32 +C_CONTROLMIXER(Master Capture Volume, PIPELINE_ID, + CONTROLMIXER_OPS(volsw, + 256 binds the mixer control to volume get/put handlers, + 256, 256), + CONTROLMIXER_MAX(, 70), + false, + CONTROLMIXER_TLV(TLV 80 steps from -50dB to +20dB for 1dB, vtlv_m50s1), + Channel register and shift for Front Left/Right, + VOLUME_CHANNEL_MAP) + +undefine(`CONTROL_NAME') +define(`CONTROL_NAME', `CONTROL_NAME_SWITCH') + +# Switch type Mixer Control with max value of 1 +C_CONTROLMIXER(Capture Switch, PIPELINE_ID, + CONTROLMIXER_OPS(volsw, 259 binds the mixer control to switch get/put handlers, 259, 259), + CONTROLMIXER_MAX(max 1 indicates switch type control, 1), + false, + , + Channel register and shift for Front Left/Right, + SWITCH_CHANNEL_MAP, + "1", "1") + +# Volume Configuration +define(DEF_PGA_TOKENS, concat(`pga_tokens_', PIPELINE_ID)) +define(DEF_PGA_CONF, concat(`pga_conf_', PIPELINE_ID)) + +W_VENDORTUPLES(DEF_PGA_TOKENS, sof_volume_tokens, +LIST(` ', `SOF_TKN_VOLUME_RAMP_STEP_TYPE "0"' + ` ', `SOF_TKN_VOLUME_RAMP_STEP_MS "250"')) + +W_DATA(DEF_PGA_CONF, DEF_PGA_TOKENS) + +# By default, use 40 Hz highpass response with +0 dB gain for 48khz +ifdef(`DMICPROC_FILTER1', , `define(DMICPROC_FILTER1, eq_iir_coef_highpass_40hz_0db_48khz.m4)') +define(DEF_EQIIR_PRIV, DMICPROC_FILTER1) +include(DMICPROC_FILTER1) + +define(DEF_EQIIR_COEF, concat(`eqiir_coef_', PIPELINE_ID)) + +# EQ Bytes control with max value of 255 +C_CONTROLBYTES(DEF_EQIIR_COEF, PIPELINE_ID, + CONTROLBYTES_OPS(bytes, + 258 binds the mixer control to bytes get/put handlers, + 258, 258), + CONTROLBYTES_EXTOPS( + 258 binds the mixer control to bytes get/put handlers, + 258, 258), + , , , + CONTROLBYTES_MAX(, 1024), + , + DEF_EQIIR_PRIV) + +# +# Components and Buffers +# + +# Host "Highpass Capture" PCM +# with 0 sink and 2 source periods +W_PCM_CAPTURE(PCM_ID, Highpass Capture, 0, 2, SCHEDULE_CORE) + +# "Volume" has 2 source and 2 sink periods +W_PGA(0, PIPELINE_FORMAT, 2, 2, DEF_PGA_CONF, SCHEDULE_CORE, + LIST(` ', "CONTROL_NAME_VOLUME", + "CONTROL_NAME_SWITCH")) + +# "EQ 0" has 2 sink period and x source periods +W_EQ_IIR(0, PIPELINE_FORMAT, 2, DAI_PERIODS, SCHEDULE_CORE, + LIST(` ', "DEF_EQIIR_COEF")) + +# Capture Buffers +W_BUFFER(0, COMP_BUFFER_SIZE(2, + COMP_SAMPLE_SIZE(PIPELINE_FORMAT), PIPELINE_CHANNELS, + COMP_PERIOD_FRAMES(PCM_MAX_RATE, SCHEDULE_PERIOD)), PLATFORM_PASS_MEM_CAP) + +W_BUFFER(1, COMP_BUFFER_SIZE(2, + COMP_SAMPLE_SIZE(PIPELINE_FORMAT), PIPELINE_CHANNELS, + COMP_PERIOD_FRAMES(PCM_MAX_RATE, SCHEDULE_PERIOD)), PLATFORM_PASS_MEM_CAP) + +W_BUFFER(2, COMP_BUFFER_SIZE(DAI_PERIODS, + COMP_SAMPLE_SIZE(PIPELINE_FORMAT), PIPELINE_CHANNELS, + COMP_PERIOD_FRAMES(PCM_MAX_RATE, SCHEDULE_PERIOD)), PLATFORM_PASS_MEM_CAP) + +# +# Pipeline Graph +# +# host PCM_C <-- B0 <-- Volume 0 <-- B1 <--EQ_IIR 0 <-- B2 <-- sink DAI0 + +P_GRAPH(pipe-eq-iir-volume-capture, PIPELINE_ID, + LIST(` ', + `dapm(N_PCMC(PCM_ID), N_BUFFER(0))', + `dapm(N_BUFFER(0), PGA_NAME)', + `dapm(PGA_NAME, N_BUFFER(1))', + `dapm(N_BUFFER(1), N_EQ_IIR(0))', + `dapm(N_EQ_IIR(0), N_BUFFER(2))')) + +undefine(`PGA_NAME') +undefine(`CONTROL_NAME') +undefine(`CONTROL_NAME_VOLUME') +undefine(`CONTROL_NAME_SWITCH') + +# +# Pipeline Source and Sinks +# +indir(`define', concat(`PIPELINE_SINK_', PIPELINE_ID), N_BUFFER(2)) +indir(`define', concat(`PIPELINE_PCM_', PIPELINE_ID), Highpass Capture PCM_ID) + +# +# PCM Configuration +# + +PCM_CAPABILITIES(Highpass Capture PCM_ID, CAPABILITY_FORMAT_NAME(PIPELINE_FORMAT), PCM_MIN_RATE, + PCM_MAX_RATE, PIPELINE_CHANNELS, PIPELINE_CHANNELS, + 2, 16, 192, 16384, 65536, 65536) + +undefine(`DEF_PGA_TOKENS') +undefine(`DEF_PGA_CONF') +undefine(`DEF_EQIIR_COEF') +undefine(`DEF_EQIIR_PRIV') diff --git a/tools/topology/topology1/sof/pipe-gui-components-playback.m4 b/tools/topology/topology1/sof/pipe-gui-components-playback.m4 new file mode 100644 index 000000000000..c54b168e1245 --- /dev/null +++ b/tools/topology/topology1/sof/pipe-gui-components-playback.m4 @@ -0,0 +1,114 @@ +# Pipeline to include all relevant components for the SOF GUI + +# Include topology builder +include(`utils.m4') +include(`buffer.m4') +include(`pcm.m4') +include(`pga.m4') +include(`dai.m4') +include(`mixercontrol.m4') +include(`bytecontrol.m4') +include(`pipeline.m4') +include(`eq_iir.m4') + +# +# Controls +# +# Volume Mixer control with max value of 32 +C_CONTROLMIXER(Master Playback Volume, PIPELINE_ID, + CONTROLMIXER_OPS(volsw, 256 binds the mixer control to volume get/put handlers, 256, 256), + CONTROLMIXER_MAX(, 32), + false, + CONTROLMIXER_TLV(TLV 32 steps from -64dB to 0dB for 2dB, vtlv_m64s2), + Channel register and shift for Front Left/Right, + VOLUME_CHANNEL_MAP) + +# +# Volume configuration +# + +define(DEF_PGA_TOKENS, concat(`pga_tokens_', PIPELINE_ID)) +define(DEF_PGA_CONF, concat(`pga_conf_', PIPELINE_ID)) + +W_VENDORTUPLES(DEF_PGA_TOKENS, sof_volume_tokens, +LIST(` ', `SOF_TKN_VOLUME_RAMP_STEP_TYPE "2"' + ` ', `SOF_TKN_VOLUME_RAMP_STEP_MS "20"')) + +W_DATA(DEF_PGA_CONF, DEF_PGA_TOKENS) + +# +# IIR EQ +# +define(DEF_EQIIR_COEF, concat(`eqiir_coef_', PIPELINE_ID)) +define(DEF_EQIIR_PRIV, concat(`eqiir_priv_', PIPELINE_ID)) + +# define filter. eq_iir_coef_flat.m4 is set by default +ifdef(`PIPELINE_FILTER1', , `define(PIPELINE_FILTER1, eq_iir_coef_flat.m4)') +include(PIPELINE_FILTER1) + +# EQ Bytes control with max value of 255 +C_CONTROLBYTES(DEF_EQIIR_COEF, PIPELINE_ID, + CONTROLBYTES_OPS(bytes, 258 binds the mixer control to bytes get/put handlers, 258, 258), + CONTROLBYTES_EXTOPS(258 binds the mixer control to bytes get/put handlers, 258, 258), + , , , + CONTROLBYTES_MAX(, 1024), + , + DEF_EQIIR_PRIV) + +# +# Components and Buffers +# + +# Host "Passthrough Playback" PCM +# with 2 sink and 0 source periods +W_PCM_PLAYBACK(PCM_ID, Passthrough Playback, 2, 0, SCHEDULE_CORE) + +# "Volume" has 2 source and x sink periods +W_PGA(0, PIPELINE_FORMAT, DAI_PERIODS, 2, DEF_PGA_CONF, SCHEDULE_CORE, + LIST(` ', "PIPELINE_ID Master Playback Volume")) + +# "EQ 0" has 2 sink period and 2 source periods +W_EQ_IIR(0, PIPELINE_FORMAT, 2, 2, SCHEDULE_CORE, + LIST(` ', "DEF_EQIIR_COEF")) + +# Playback Buffers +W_BUFFER(0, COMP_BUFFER_SIZE(2, + COMP_SAMPLE_SIZE(PIPELINE_FORMAT), PIPELINE_CHANNELS, COMP_PERIOD_FRAMES(PCM_MAX_RATE, SCHEDULE_PERIOD)), + PLATFORM_HOST_MEM_CAP) +W_BUFFER(1, COMP_BUFFER_SIZE(2, + COMP_SAMPLE_SIZE(PIPELINE_FORMAT), PIPELINE_CHANNELS, COMP_PERIOD_FRAMES(PCM_MAX_RATE, SCHEDULE_PERIOD)), + PLATFORM_HOST_MEM_CAP) +W_BUFFER(2, COMP_BUFFER_SIZE(DAI_PERIODS, + COMP_SAMPLE_SIZE(DAI_FORMAT), PIPELINE_CHANNELS, COMP_PERIOD_FRAMES(PCM_MAX_RATE, SCHEDULE_PERIOD)), + PLATFORM_DAI_MEM_CAP) + +# +# Pipeline Graph +# +# host PCM_P --> B0 --> EQ 0 --> B1 --> Volume 0 --> B2 --> sink DAI0 + +P_GRAPH(pipe-eq-iir-volume-playback, PIPELINE_ID, + LIST(` ', + `dapm(N_BUFFER(0), N_PCMP(PCM_ID))', + `dapm(N_EQ_IIR(0), N_BUFFER(0))', + `dapm(N_BUFFER(1), N_EQ_IIR(0))', + `dapm(N_PGA(0), N_BUFFER(1))', + `dapm(N_BUFFER(2), N_PGA(0))')) + +# +# Pipeline Source and Sinks +# +indir(`define', concat(`PIPELINE_SOURCE_', PIPELINE_ID), N_BUFFER(2)) +indir(`define', concat(`PIPELINE_PCM_', PIPELINE_ID), Passthrough Playback PCM_ID) + + +# +# PCM Configuration + +# +PCM_CAPABILITIES(Passthrough Playback PCM_ID, CAPABILITY_FORMAT_NAME(PIPELINE_FORMAT), PCM_MIN_RATE, PCM_MAX_RATE, 2, PIPELINE_CHANNELS, 2, 16, 192, 16384, 65536, 65536) + +undefine(`DEF_PGA_TOKENS') +undefine(`DEF_PGA_CONF') +undefine(`DEF_EQIIR_COEF') +undefine(`DEF_EQIIR_PRIV')