Files
3d_audio/lib_audio_dsp/test/json/test_json_parser.py
Steven Dan d8b2974133 init
2025-12-11 09:43:42 +08:00

408 lines
12 KiB
Python

# Copyright 2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
from pathlib import Path
import annotated_types
from audio_dsp.design.parse_json import Graph, make_pipeline, insert_forks, DspJson, pipeline_to_dspjson
from audio_dsp.models.stage import all_models
from audio_dsp.stages import all_stages
from typing import get_origin, get_args, Literal
from types import UnionType
def find_autoforks(graph):
for node in graph.nodes:
if 'AutoFork' in node.placement.name:
return True
assert False, "No AutoFork node found in the graph after insert_forks."
def test_no_shared_edge():
json_str = """
{
"name": "No shared edge",
"fs": 44100,
"inputs": [{
"name": "inputs",
"channels": 1
}],
"nodes": [
{
"op_type": "VolumeControl",
"config": {},
"placement": {
"input": [["inputs", 0]],
"name": "VolumeControl_1",
"thread": 0
}
}
],
"outputs": [{
"name": "outputs",
"input": [["VolumeControl_1", 0]]
}]
}
"""
graph = Graph.model_validate_json(json_str)
dsp_json = DspJson(ir_version=1, producer_name="test", producer_version="1.0", graph=graph)
a = make_pipeline(dsp_json)
a.draw(Path("test_no_shared_edge"))
new_graph = insert_forks(graph)
print(f"Before insert_forks: {graph.model_dump_json()}")
print(f"After insert_forks: {new_graph.model_dump_json()}")
dsp_json = DspJson(ir_version=1, producer_name="pipeline_to_dspjson", producer_version="1.0", graph=new_graph)
new_json = pipeline_to_dspjson(a)
assert dsp_json.graph == new_json.graph, "Pipeline JSON does not match original"
for node in graph.nodes:
if 'AutoFork' in node.placement.name:
assert False, "AutoFork node found in the graph after insert_forks, but not needed."
def test_shared_edge_from_graph_input():
json_str = """
{
"name": "Shared edge from graph input",
"fs": 44100,
"inputs": [{
"name": "inputs",
"channels": 1
}],
"nodes": [
{
"op_type": "VolumeControl",
"config": {},
"placement": {
"input": [["inputs", 0]],
"name": "VolumeControl_A",
"thread": 0
}
},
{
"op_type": "Mixer",
"config": {},
"placement": {
"input": [["inputs", 0]],
"name": "Mixer_B",
"thread": 0
}
}
],
"outputs": [{
"name": "outputs",
"input": [["VolumeControl_A", 0]]
}]
}
"""
graph = Graph.model_validate_json(json_str)
dsp_json = DspJson(ir_version=1, producer_name="test", producer_version="1.0", graph=graph)
a = make_pipeline(dsp_json)
a.draw(Path("test_shared_edge_from_graph_input"))
new_graph = insert_forks(graph)
print(f"Before insert_forks: {graph.model_dump_json()}")
print(f"After insert_forks: {new_graph.model_dump_json()}")
find_autoforks(new_graph)
def test_shared_edge_from_producer_node():
json_str = """
{
"name": "Shared edge from producer node",
"fs": 44100,
"inputs": [{
"name": "inputs",
"channels": 1
}],
"nodes": [
{
"op_type": "VolumeControl",
"config": {},
"placement": {
"input": [["inputs", 0]],
"name": "Producer",
"thread": 0
}
},
{
"op_type": "Mixer",
"config": {},
"placement": {
"input": [["Producer", 0]],
"name": "Consumer_1",
"thread": 0
}
},
{
"op_type": "Mixer",
"config": {},
"placement": {
"input": [["Producer", 0]],
"name": "Consumer_2",
"thread": 0
}
}
],
"outputs": [{
"name": "outputs",
"input": [["Consumer_1", 0]]
}]
}
"""
graph = Graph.model_validate_json(json_str)
dsp_json = DspJson(ir_version=1, producer_name="test", producer_version="1.0", graph=graph)
a = make_pipeline(dsp_json)
a.draw(Path("test_shared_edge_from_producer_node"))
new_graph = insert_forks(graph)
print(f"Before insert_forks: {graph.model_dump_json()}")
print(f"After insert_forks: {new_graph.model_dump_json()}")
find_autoforks(new_graph)
def test_shared_edge_with_graph_output():
json_str = """
{
"name": "Shared edge with graph output",
"fs": 44100,
"inputs": [{
"name": "inputs",
"channels": 1
}],
"nodes": [
{
"op_type": "VolumeControl",
"config": {},
"placement": {
"input": [["inputs", 0]],
"name": "Producer",
"thread": 0
}
},
{
"op_type": "Mixer",
"config": {},
"placement": {
"input": [["Producer", 0]],
"name": "Consumer",
"thread": 0
}
}
],
"outputs": [{
"name": "outputs",
"input": [["Producer", 0]]
}]
}
"""
graph = Graph.model_validate_json(json_str)
dsp_json = DspJson(ir_version=1, producer_name="test", producer_version="1.0", graph=graph)
a = make_pipeline(dsp_json)
a.draw(Path("test_shared_edge_with_graph_output"))
new_graph = insert_forks(graph)
print(f"Before insert_forks: {graph.model_dump_json()}")
print(f"After insert_forks: {new_graph.model_dump_json()}")
find_autoforks(new_graph)
def test_again():
json_str = """
{
"name": "Stereo Mixer with Volume",
"fs": 48000,
"inputs": [{
"name": "stereo_in",
"channels": 2
}],
"nodes": [
{"op_type": "Mixer", "placement": {"input": [["stereo_in", 0], ["stereo_in", 1]], "name": "Mixer", "thread": 0}},
{"op_type": "VolumeControl", "placement": {"input": [["Mixer", 0]], "name": "Volume", "thread": 0}}
],
"outputs": [{
"name": "stereo_out",
"input": [["Volume", 0], ["Volume", 0]]
}]
}
"""
graph = Graph.model_validate_json(json_str)
dsp_json = DspJson(ir_version=1, producer_name="test", producer_version="1.0", graph=graph)
a = make_pipeline(dsp_json)
a.draw(Path("test_again"))
new_graph = insert_forks(graph)
print(f"Before insert_forks: {graph.model_dump_json()}")
print(f"After insert_forks: {new_graph.model_dump_json()}")
find_autoforks(new_graph)
def test_multiple_inputs_outputs_non_shared():
"""
Test a graph with two inputs (one mono and one stereo) and two outputs (one mono and one stereo)
where no edge is shared between consumers.
"""
json_str = """
{
"name": "Multiple Inputs and Outputs Non-Shared Test",
"fs": 48000,
"inputs": [
{"name": "mono_in", "channels": 1},
{"name": "stereo_in", "channels": 2}
],
"nodes": [
{
"op_type": "VolumeControl",
"config": {},
"placement": {
"input": [["mono_in", 0]],
"name": "VolumeControl_A",
"thread": 0
}
},
{
"op_type": "Mixer",
"config": {},
"placement": {
"input": [["stereo_in", 0], ["stereo_in", 1]],
"name": "Mixer_B",
"thread": 0
}
}
],
"outputs": [
{"name": "mono_out", "input": [["VolumeControl_A", 0]]},
{"name": "stereo_out", "input": [["Mixer_B", 0], ["VolumeControl_A", 0]]}
]
}
"""
graph = Graph.model_validate_json(json_str)
dsp_json = DspJson(ir_version=1, producer_name="test", producer_version="1.0", graph=graph)
a = make_pipeline(dsp_json)
a.draw(Path("test_multiple_inputs_outputs_non_shared"))
new_graph = insert_forks(graph)
print("Test: Multiple Inputs and Outputs Non-Shared Test")
print(f"Before insert_forks: {graph.model_dump_json(indent=2)}")
print(f"After insert_forks: {new_graph.model_dump_json(indent=2)}")
find_autoforks(new_graph)
def test_multiple_inputs_outputs_shared():
"""
Test a graph with two inputs and two outputs where a node produces an output
that is shared by both graph outputs. A Fork should be inserted.
"""
json_str = """
{
"name": "Multiple Inputs and Outputs Shared Test",
"fs": 48000,
"inputs": [
{"name": "input1", "channels": 1},
{"name": "input2", "channels": 1}
],
"nodes": [
{
"op_type": "Mixer",
"config": {},
"placement": {
"input": [["input1", 0], ["input2", 0]],
"name": "Mixer",
"thread": 0
}
}
],
"outputs": [
{"name": "output1", "input": [["Mixer", 0]]},
{"name": "output2", "input": [["Mixer", 0]]}
]
}
"""
graph = Graph.model_validate_json(json_str)
dsp_json = DspJson(ir_version=1, producer_name="test", producer_version="1.0", graph=graph)
a = make_pipeline(dsp_json)
a.draw(Path("test_multiple_inputs_outputs_shared"))
new_graph = insert_forks(graph)
print("Test: Multiple Inputs and Outputs Shared Test")
print(f"Before insert_forks: {graph.model_dump_json(indent=2)}")
print(f"After insert_forks: {new_graph.model_dump_json(indent=2)}")
find_autoforks(new_graph)
def test_all_stages_models():
all_m = all_models()
all_s = all_stages()
failed = False
for s in all_s:
if s.startswith("_") or s in ["DSPThreadStage", "PipelineStage"]:
continue
try:
assert s in all_m, f"Stage {s} not found in all models"
assert s == all_m[s].model_fields["op_type"].default, f"Stage {s} op_type mismatch"
except AssertionError as e:
print(e)
failed = True
continue
if "biquad" in s.lower() or "parametric" in s.lower():
continue
if "parameters" not in all_m[s].model_fields:
continue
try:
if type(all_s[s].set_parameters.__annotations__["parameters"]) is UnionType:
set_params_input = get_args(all_s[s].set_parameters.__annotations__["parameters"])
model_params_type = all_m[s].model_fields["parameters"].default_factory
assert model_params_type in set_params_input, f"Stage {s} set_parameters input type mismatch"
else:
set_params_input = all_s[s].set_parameters.__annotations__["parameters"].__name__
model_params_type = all_m[s].model_fields["parameters"].default_factory.__name__
assert set_params_input == model_params_type, f"Stage {s} set_parameters input type mismatch"
except AssertionError as e:
print(e)
failed = True
for field, value in type(all_m[s].model_fields["parameters"].default_factory()).model_fields.items():
try:
if get_origin(value.annotation) is list:
item_type = get_args(value.annotation)[0]
meta = get_args(item_type)[1].metadata
elif get_origin(value.annotation) is Literal:
continue # Skip Literal types
else:
meta = value.metadata
min = [g.ge for g in meta if isinstance(g, annotated_types.Ge)] or [
g.gt for g in meta if isinstance(g, annotated_types.Gt)]
max = [g.le for g in meta if isinstance(g, annotated_types.Le)] or [
g.lt for g in meta if isinstance(g, annotated_types.Lt)]
assert min, f"Minimum not defined for field {field} in {s}"
if field not in ["position", "delay"]:
assert max, f"Maximum not defined for field {field} in {s}"
assert min[0] < max[0], f"Range not correct for field {field} in {s}"
except AssertionError as e:
print(e)
failed = True
continue
assert not failed, "Some stages failed the test."
if __name__ == "__main__":
test_shared_edge_from_graph_input()