You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[FEATURE] GUI to demonstrate and control SOF components on target hardware #9223
There is currently no easy way to demonstrate SOF on target hardware, especially to non-developers and people who are not familiar with DSP.
Proposed Solution:
Develop a generic GUI and TUI that interacts with SOF components on target HW, utilizing sof-ctl for real time control.
This UI will be developed in python, with the GUI utilizing the python GTK library.
The initial features for the first version of the UI (Est. completion Mid July) will be:
Real time control of audio playback and record through ALSA
Real time control of Volume, EQ, and DRC SOF components
Tested functionality on NXP target HW
The features for the final version (completed Late August) will be:
All previous features
Clean/professional graphics and intuitive control
Functionality on all supported HW (some may only work through the TUI)
Clear interfaces and procedures for adding new SOF components to the UI
Alternatives
alsamixer and sof-ctl provide some of these functionalities, however utilizing them on their own has a high lack of customization and is also not very presentable to an interested party.
Additional Context
Hi SOF community, I'm a new GSoC contributor for SOF through the Linux Foundation this summer and will be developing this feature as my project for that. I'm very excited to get started.
@alexb3103 btw @singalsu has octave scripts that generate the coefficients for all modules (to change processing settings at runtime). I've also got some python that parses topology and creates a Gtk4 listview of all objects (I will clean up and post it soon).
@alexb3103 I was going to paste here, but GH wont allow me to post python. I will create a staging repo for python tooling that can share common libraries. In the mean time I've pasted the topology2 parser here.
#!/usr/bin/python3# SPDX-License-Identifier: BSD-3-Clauseimportosimportre# import the Gtk moduleimportgigi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
fromgi.repositoryimportGtk, Adw, Gio, Gdk, GLib, GObjectglb_search_dirs= [""]
glb_root_objects= []
glb_root_tokens= []
# keywords to search for in the topology filetplg_keywords= ["DefineAttribute",
"DefineArgument",
"Class",
"Control",
"attributes",
"constraints",
"[", "]",
"{", "}",
".",
"!",
"Object",
"constructor",
"immutable",
"unique",
"deprecated",
"mandatory",
"Define",
"IncludeByKey"]
defobj_name(line):
"""Extracts the object name from a line."""start=line.find('"')
end=line[start+1:].find('"')
name=line[start+1:start+1+end]
returnnamedeffile_name(line):
"""Extracts the file name from a line."""start=line.find('<')
end=line[start+1:].find('>')
name=line[start+1:start+1+end]
returnnamedefsearch_dir(line):
"""Extracts the search directory name from a line."""start=line.find('<searchdir:') # 11chars longend=line[start+11:].find('>')
name=line[start+11:start+11+end]
returnname# get the start and end index of a sectiondefsection_get_limits(tokens, idx):
"""Get the start and end index of a section."""count=0start=0end=0foriinrange(idx, len(tokens)):
token=tokens[i].get("token")
iftoken=="{":
count+=1ifcount==1:
start=icontinueeliftoken=="}":
count-=1ifcount==0:
end=ibreakreturnstart, end# get the start and end index of an arraydefarray_get_limits(tokens, idx):
"""Get the start and end index of an array."""count=0start=0end=0foriinrange(idx, len(tokens)):
token=tokens[i].get("token")
iftoken=="[":
count+=1ifcount==1:
start=icontinueeliftoken=="]":
count-=1ifcount==0:
end=ibreakreturnstart, end# get the preamble comments for the objectdefsection_get_preamble(tokens, idx):
"""Get any preceding comments."""foriinrange(idx-1, -1, -1):
token=tokens[i].get("token")
iftoken=="}":
breakreturni+1defopen_file_from_directories(filename):
""" Attempts to open a file by searching through a list of directories. Args: filename: The name of the file to open. search_dirs: A list of directory paths to search in. Returns: file object: If the file is found and successfully opened, returns a file object. None: If the file cannot be found or opened in any of the directories. """fordirectoryinglb_search_dirs:
filepath=os.path.join(directory, filename)
ifos.path.exists(filepath): # Check if the file existstry:
file=open(filepath, "r", encoding="utf-8") # Open the file in read modereturnfile# Return the file object if opened successfullyexceptOSErrorase:
print(f"Error opening file '{filename}' in '{filepath}': {e}")
returnNone# File not found or couldn't be opened in any directorydefload_file_into_buffer(filename):
"""Loads the entire content of a text file into a string buffer."""filename=filename.strip("'\"")
withopen_file_from_directories(filename) asf: # Use appropriate encodingbuffer=f.read()
returnbufferdeftokenize_with_metadata(filename, text):
"""Tokenizes text, preserving all tokens with detailed metadata."""# Escape keywords for regex and sort by length (longest first)pattern="|".join(map(re.escape, sorted(tplg_keywords, key=len, reverse=True)))
pattern=f"({pattern})"# Capture keywordscurrent_line_number=1key_check=Falseforlineintext.splitlines(keepends=False): # Discard newline charactersstring=""line=line.expandtabs(4)
# lines starting with # are whole line commentsifre.match(r"\s*#", line):
glb_root_tokens.append({
"token": line[line.find('#'):].strip(),
"type": "comment",
"line_number": current_line_number,
"file": filename
})
current_line_number+=1continuematches=re.finditer(pattern+r"|\S+", line) # \S+ matches any non-whitespaceformatchinmatches:
token=match.group().strip()
ifnottoken:
continue# Skip empty tokensifstring!="":
string+=" "+tokeniftoken.endswith("\"") ortoken.endswith("'"):
glb_root_tokens.append({
"token": string,
"type": "string",
"line_number": current_line_number,
"file": filename
})
string=""continue# Identify token type (keyword, comment, or other)token_type="keyword"iftokenintplg_keywordselse"other"iftoken.startswith("#"):
glb_root_tokens.append({
"token": line[match.start():].strip(),
"type": "comment",
"line_number": current_line_number,
"file": filename
})
breakeliftoken.startswith("\"") ortoken.startswith("'"):
token_type="string"ifnottoken.endswith("\"") andnottoken.endswith("'"):
string=tokencontinueeliftoken.startswith("<searchdir:"):
token_type="searchdir"glb_search_dirs.append(search_dir(line))
eliftoken.startswith("<"):
token_type="file"buffer=load_file_into_buffer(file_name(token))
tokenize_with_metadata(file_name(token), buffer)
eliftoken.startswith("IncludeByKey"):
token_type="key"eliftoken.startswith("["):
token_type="array"eliftoken.startswith("]"):
token_type="eoa"eliftoken.startswith("{"):
token_type="section"eliftoken.startswith("}"):
token_type="eos"# Store token info with line number and positionglb_root_tokens.append({
"token": token.strip(),
"type": token_type,
"line_number": current_line_number,
"file": filename
})
current_line_number+=1returnglb_root_tokensclasst2Base(GObject.Object):
name=GObject.Property(type=str)
def__init__(self, preamble, depth):
super().__init__()
self.tokens= []
self.head= []
self.tail= []
self.objects= []
self.cond_objects= []
self.preamble=preambleself.depth=depthself.type=Noneself.value=Noneself.ui_data=Nonedefprint(self):
print("Base")
fortokeninself.tokens:
print(token)
defdraw(self):
passdefwrite(self):
pass# get the define value by recursively searching the objects for the keydefget_define(self, key):
forobjectinself.objects:
value=object.get_define(key)
ifvalue!=None:
returnvalueifisinstance(object, t2Attribute):
if (object.tokens[object.attr_idx].get("token") ==key):
returnobject.tokens[object.value_idx].get("token")
returnNone# print the tokens in topology formatdefprint_tplg_tokens(self, tokens):
if(len(tokens) ==0):
returnlast_line=int(tokens[0].get("line_number"))
last_file=tokens[0].get("file")
line=""strip=False# for each token in the objectforiinrange(len(tokens)):
this_token=tokens[i].get("token")
this_line=int(tokens[i].get("line_number"))
this_type=tokens[i].get("type")
this_file=tokens[i].get("file")
# if the line number or file changes print the lineifthis_file!=last_file:
print(f"{line}", end="\n")
line=""last_line=this_line# if the line number changes print the lineforiinrange(last_line, this_line):
print(f"{line}", end="\n")
line=""# if the token is a comment add it to the lineif (this_type=="comment"):
# if the line is empty add the commentif (line!=""):
line=f"{line}\t{this_token}"else:
# if the line is not empty then add the comment after tabforiinrange(0, self.depth):
line=f"{line}{"\t"}"line=f"{line}{this_token}"last_line=this_linecontinue# handle special token typesmatchthis_token:
# new section or arraycase"{"|"[":
ifline=="":
foriinrange(0, self.depth):
line=f"{line}{"\t"}"line=f"{line}{this_token}"else:
line=f"{line}{this_token}"self.depth+=1# end of section or arraycase"}"|"]":
line=""self.depth-=1foriinrange(0, self.depth):
line=f"{line}{"\t"}"line=f"{line}{this_token}"# join two strings with a .case".":
line=line.rstrip()
line=f"{line}{this_token}"strip=True# join postfix stringcase"!":
foriinrange(0, self.depth):
line=f"{line}{"\t"}"line=f"{line}{this_token}"strip=True# default casecase _:
# if the last token was a . then join the stringsifstrip:
strip=Falseline=f"{line}{this_token}"else:
# if the line is empty add the tokenifline=="":
foriinrange(0, self.depth):
line=f"{line}{"\t"}"line=f"{line}{this_token}"else:
# if the line is not empty then add the token after tabline=f"{line}\t{this_token}"# update the last line and filelast_line=this_linelast_file=this_file# print the last tokens and lineprint(f"{line}", end="\n")
# print the section in topology formatdefprint_tplg_object(self):
# print the headself.print_tplg_tokens(self.head)
# print the tokens in the object recursivelyforobjectinself.objects:
object.print_tplg()
self.print_tplg_tokens(self.tokens)
# print the tailself.print_tplg_tokens(self.tail)
# print the conditional objectsforobjectinself.cond_objects:
object.print_tplg()
defprint_tplg(self):
self.print_tplg_object()
# print the section in topology formatdefprint_objects(self):
# print the headprint(" "*self.depth, self.name, self.value)
# print the tokens in the object recursivelyforobjectinself.objects:
object.print_object()
# print the conditional objects#for object in self.cond_objects:# object.print_tplg()defprint_object(self):
self.print_objects()
deft2_child_model(obj, data): # Function to create child models on demandifisinstance(obj, Gtk.TreeListRow):
obj=obj.get_item()
ifobj.objects!= []:
child_store=Gio.ListStore(item_type=t2Base)
forchildinobj.objects:
child_store.append(child)
returnGtk.TreeListModel.new(child_store, False, False, t2_child_model, None) # Use new directlyreturnNone# No children# Array definition in topology using []classt2Array(t2Base):
def__init__(self, parent_tokens, preamble, parent_depth):
print("->New Array")
super().__init__(preamble, parent_depth)
# get the array limitsarray_start, array_end=array_get_limits(parent_tokens, preamble)
# skip "{" tokenarray_start+=1# set the tokens for this objectself.tokens=parent_tokens[array_start:array_end]
self.tail=parent_tokens[array_end:array_end+1]
self.head=parent_tokens[0:array_start]
# set the nameself.name=self.head[preamble+1].get("token")
# remove our tokens from the listforiinrange(0, array_end+1):
parent_tokens.remove(parent_tokens[0])
# get the next objects in the arraywhileTrue:
object=tokenizer_next(self.tokens, self.depth+1)
ifobject==None:
breakself.objects.append(object)
classt2Attribute(t2Base):
def__init__(self, parent_tokens, preamble, parent_depth):
print("->New Attribute")
super().__init__(preamble, parent_depth)
self.attr_idx=0self.value_idx=0self.type="Attribute"# get the attribute and value tokenscount=self.get_tuple(parent_tokens)
# validate attr and valueifself.attr_idx<0orself.attr_idx>count:
raiseValueError("attr index ", self.attr_idx, " out of range", count)
ifself.value_idx<0orself.value_idx>count:
raiseValueError("value index ", self.value_idx, " out of range", count)
# set the tokens for this objectself.tokens=parent_tokens[0:count]
# set the name and valueself.name=self.tokens[self.attr_idx].get("token")
self.value=self.tokens[self.value_idx].get("token")
# remove our tokens from the listforiinrange(0, len(self.tokens)):
parent_tokens.remove(parent_tokens[0])
# get the attribute and value tokens alongside any comments for this tupledefget_tuple(self, tokens):
count=0end=len(tokens)
# preceding commentswhilecount<endandtokens[count].get("type") =="comment":
count+=1# attributeifcount<end:
self.attr_idx=countcount+=1# valueifcount<end:
current_line=tokens[count].get("line_number")
self.value_idx=countcount+=1# trailing comments on the same linewhilecount<endandtokens[count].get("type") =="comment"andcurrent_line==tokens[count].get("line_number"):
count+=1returncountclasst2Section(t2Base):
def__init__(self, parent_tokens, preamble, parent_depth):
print("->New Section")
super().__init__(preamble, parent_depth)
# get the section limitssect_start, sect_end=section_get_limits(parent_tokens, preamble)
# skip "{" tokensect_start+=1self.start=0self.tokens=parent_tokens[sect_start:sect_end]
self.tail=parent_tokens[sect_end:sect_end+1]
self.head=parent_tokens[0:sect_start]
# set the namefortokeninself.head:
#print("head", self.head)if (token.get("type") =="string"ortoken.get("type") =="other"):
self.name+=token.get("token")
ifself.name=="":
self.name=self.head[preamble].get("token")
self.name=self.name.strip("\"")
#print("**Section", self.name)#print("head", self.head)#print("preamble", preamble) # remove our tokens from the listforiinrange(0, sect_end+1):
parent_tokens.remove(parent_tokens[0])
# get the next objects in the sectionwhileTrue:
object=tokenizer_next(self.tokens, self.depth+1)
ifobject==None:
breakself.objects.append(object)
## Define {# NUM_HDMIS 3#}classt2Define(t2Section):
def__init__(self, parent_tokens, preamble, parent_depth):
print("->New Define")
super().__init__(parent_tokens, preamble, parent_depth)
self.type="Define"# DefineAttribute."priority" {classt2DefineAttribute(t2Section):
def__init__(self, parent_tokens, preamble, parent_depth):
print("->New DefineAttribute")
super().__init__(parent_tokens, preamble, parent_depth)
self.type="DefineAttribute"# Class.Widget."pipeline" {classt2Class(t2Section):
def__init__(self, parent_tokens, preamble, parent_depth):
print("->New Class")
super().__init__(parent_tokens, preamble, parent_depth)
self.type="Class"# Class.Widget."pipeline" {classt2Object(t2Section):
def__init__(self, parent_tokens, preamble, parent_depth):
print("->New Object")
super().__init__(parent_tokens, preamble, parent_depth)
self.type="Object"# get the value of a key in the objectsdefkey_get_value(key):
forobjectinglb_root_objects:
value=object.get_define(key)
#todo remove the none checkif (value!=None):
returnvalueclasst2IncludeKey(t2Section):
def__init__(self, parent_tokens, preamble, parent_depth):
t2Base.__init__(self, preamble, parent_depth)
self.type="IncludeByKey"valid_condition=Falsekey_tokens= []
# get the section limitssect_start, sect_end=section_get_limits(parent_tokens, preamble)
# skip "{" tokensect_start+=1self.start=0self.tokens=parent_tokens[sect_start:sect_end]
self.tail=parent_tokens[sect_end:sect_end+1]
self.head=parent_tokens[0:sect_start]
# get the keyself.key=self.head[2+preamble].get("token")
# remove our tokens from the listforiinrange(0, sect_end+1):
parent_tokens.remove(parent_tokens[0])
# get the key valueself.key_value=key_get_value(self.key)
ifself.key_value==None:
print(f"Key {self.key} not found")
returnself.key_value=self.key_value.strip("'\"")
# get the condition and statementi=0whilei<len(self.tokens):
condition=self.tokens[i].get("token").strip("'\"")
# check if the condition matches the keyifcondition.isnumeric():
# numeric key to matchnum_condition=int(condition)
num_key=int(self.key_value)
ifnum_condition==num_key:
valid_condition=Trueelse:
valid_condition=Falseelifre.match(r"\[\d+-\d+\]$", condition):
# range key to match [x-y]start, end=map(int, condition[1:-1].split("-"))
# Check if the number is within the rangenumber=int(self.key_value)
valid_condition=start<=number<=endelse:
# string key to matchifcondition==self.key_value:
valid_condition=Trueelse:
valid_condition=Falsei+=1if (i>=len(self.tokens)):
break# get the statementstatement=self.tokens[i].get("token")
# check if the statement is a sectionis_section=Trueifstatement=="{"elseFalse# ternary operatorifis_section:
# Skip the sectionsect_start, i=section_get_limits(self.tokens, i)
# check if the condition is validifnotvalid_condition:
# Skip the statementi+=1continue# Process the statementifnotis_section:
key_tokens= []
buffer=load_file_into_buffer(statement)
#todo remove return valuekey_tokens=tokenize_with_metadata(statement, buffer)
else:
key_tokens=self.tokens[sect_start+1:i]
# get the next objects in the sectionwhileTrue:
object=tokenizer_next(key_tokens, self.depth)
ifobject==None:
breakself.cond_objects.append(object)
# go to the next tokeni+=1# get the next object in the tokensdeftokenizer_next(tokens, depth):
# preamble is the number of comments before the objectpreamble=0is_attribute=0# get the next object in the tokenswhile (tokensandlen(tokens) >preamble):
# get the next tokentoken=tokens[preamble] # cant pop on recursiontoken_name=token.get("token")
token_type=token.get("type")
#print("%%:" + " " * depth, preamble, is_attribute, token)# skip comments but keep track of the preamble and create a new attributematchtoken_type:
case"comment":
preamble+=1is_attribute=0continuecase"other"|"string":
is_attribute+=1ifis_attribute>1:
returnt2Attribute(tokens, preamble, depth)
case _:
is_attribute=0# check the token name and create the objectmatchtoken_name:
case"Define":
returnt2Define(tokens, preamble, depth)
case"DefineAttribute":
returnt2DefineAttribute(tokens, preamble, depth)
case"Class":
returnt2Class(tokens, preamble, depth)
case"Object":
returnt2Object(tokens, preamble, depth)
case"IncludeByKey":
returnt2IncludeKey(tokens, preamble, depth)
case"attributes"|"constraints":
returnt2Section(tokens, preamble, depth)
case"!":
returnt2Array(tokens, preamble, depth)
case"{":
returnt2Section(tokens, preamble, depth)
case _:
preamble+=1continue;
# no more tokensreturnNonedeftplg_reset():
glb_root_objects.clear()
glb_root_tokens.clear()
glb_search_dirs.clear()
glb_search_dirs.append("")
defimport_tplg_as_objects(filename, verbose):
tplg_reset()
# Load the topology file into a bufferbuffer=load_file_into_buffer(filename)
tokens=tokenize_with_metadata(filename, buffer)
# get the next object in the tokenswhileTrue:
object=tokenizer_next(tokens, 0)
ifobject==None:
breakglb_root_objects.append(object)
# print the objectsifverbose:
forobjectinglb_root_objects:
object.print_tplg()
returnglb_root_objects
@alexb3103 btw @singalsu has octave scripts that generate the coefficients for all modules (to change processing settings at runtime). I've also got some python that parses topology and creates a Gtk4 listview of all objects (I will clean up and post it soon).
Yep. It should be quite straightforward to convert the Matlab/Octave scripts to Python SciPy and NumPy code. In first step retrieving current settings blobs and displaying them graphically as frequency responses etc. would also be quite simple. I have for some component blobs a decoder script.
Cutoff date for v2.12 features today. @alexb3103@lgirdwood can we close this? It seems the #9406 PR was merged, right. If there's more development, can we perhaps file new tickets to follow-up that work (this issue starts to have a lot of history).
Activity
lgirdwood commentedon Jun 12, 2024
@alexb3103 btw @singalsu has octave scripts that generate the coefficients for all modules (to change processing settings at runtime). I've also got some python that parses topology and creates a Gtk4 listview of all objects (I will clean up and post it soon).
lgirdwood commentedon Jun 24, 2024
@alexb3103 I was going to paste here, but GH wont allow me to post python. I will create a staging repo for python tooling that can share common libraries. In the mean time I've pasted the topology2 parser here.
alexb3103 commentedon Jun 27, 2024
Wow, this is incredibly useful, I appreciate it a lot. Definitely something for further along in the project but it'll be invaluable for sure.
Let me know when you create the repo, very open contributing to that as well if I make anything useful for it.
singalsu commentedon Jul 24, 2024
Yep. It should be quite straightforward to convert the Matlab/Octave scripts to Python SciPy and NumPy code. In first step retrieving current settings blobs and displaying them graphically as frequency responses etc. would also be quite simple. I have for some component blobs a decoder script.
kv2019i commentedon Dec 13, 2024
Cutoff date for v2.12 features today. @alexb3103 @lgirdwood can we close this? It seems the #9406 PR was merged, right. If there's more development, can we perhaps file new tickets to follow-up that work (this issue starts to have a lot of history).
kv2019i commentedon Jan 3, 2025
Closing as per above comment.