Skip to content

Commit 54018a6

Browse files
committed
cmdline: implement programme/object selection
1 parent ec86bff commit 54018a6

File tree

2 files changed

+285
-60
lines changed

2 files changed

+285
-60
lines changed

ear/cmdline/render_file.py

Lines changed: 169 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
from __future__ import print_function
22
import argparse
33
import sys
4+
from attr import attrs, attrib, Factory
45
import scipy.sparse
56
from itertools import chain
67
from ..core import bs2051, layout, Renderer
78
from ..core.monitor import PeakMonitor
89
from ..core.metadata_processing import preprocess_rendering_items
10+
from ..core.select_items import select_rendering_items
911
from ..fileio import openBw64, openBw64Adm
12+
from ..fileio.adm.elements import AudioProgramme, AudioObject
1013
from ..fileio.bw64.chunks import FormatInfoChunk
1114
import warnings
1215
from ..fileio.adm.exceptions import AdmUnknownAttribute
@@ -17,81 +20,188 @@ def handle_strict(args):
1720
warnings.filterwarnings("error", category=AdmUnknownAttribute)
1821

1922

20-
def process_file(input_file, output_file, target_layout, speakers_file, output_gain_db, fail_on_overload, enable_block_duration_fix, config=None):
21-
if config is None:
22-
config = {}
23+
@attrs
24+
class OfflineRenderDriver(object):
25+
"""Obtain and store ancillary rendering parameters, and use them to perform file-to-file rendering."""
2326

24-
spkr_layout = bs2051.get_layout(target_layout)
27+
target_layout = attrib()
28+
speakers_file = attrib()
29+
output_gain_db = attrib()
30+
fail_on_overload = attrib()
31+
enable_block_duration_fix = attrib()
32+
config = attrib(default=Factory(dict))
2533

26-
if speakers_file is not None:
27-
real_layout = layout.load_real_layout(speakers_file)
28-
spkr_layout, upmix = spkr_layout.with_real_layout(real_layout)
29-
spkr_layout.check_upmix_matrix(upmix)
30-
upmix = scipy.sparse.csc_matrix(upmix.T)
31-
n_channels = upmix.shape[1]
32-
else:
33-
upmix = None
34-
n_channels = len(spkr_layout.channels)
34+
programme_id = attrib(default=None)
35+
complementary_object_ids = attrib(default=Factory(list))
3536

36-
renderer = Renderer(spkr_layout, **config)
37+
blocksize = 8192
3738

38-
output_gain_linear = 10.0 ** (output_gain_db / 20.0)
39+
@classmethod
40+
def add_args(cls, parser):
41+
"""Add arguments to an ArgumentParser that can be used by from_args."""
42+
formats_string = ", ".join(bs2051.layout_names)
43+
parser.add_argument("-s", "--system", required=True, metavar="target_system",
44+
help="Target output system, accoring to ITU-R BS.2051. "
45+
"Available systems are: {}".format(formats_string))
46+
47+
parser.add_argument("-l", "--layout", type=argparse.FileType("r"), metavar="layout_file",
48+
help="Layout config file")
49+
parser.add_argument("--output-gain-db", type=float, metavar="gain_db", default=0,
50+
help="output gain in dB (default: 0)")
51+
parser.add_argument("--fail-on-overload", "-c", action="store_true",
52+
help="fail if an overload condition is detected in the output")
53+
parser.add_argument("--enable-block-duration-fix", action="store_true",
54+
help="automatically try to fix faulty block format durations")
55+
56+
parser.add_argument("--programme", metavar="id",
57+
help="select an audioProgramme to render by ID")
58+
parser.add_argument("--comp-object", metavar="id", action="append", default=[],
59+
help="select an audioObject by ID from a complementary group")
60+
61+
@classmethod
62+
def from_args(cls, args):
63+
return cls(
64+
target_layout=args.system,
65+
speakers_file=args.layout,
66+
output_gain_db=args.output_gain_db,
67+
fail_on_overload=args.fail_on_overload,
68+
enable_block_duration_fix=args.enable_block_duration_fix,
69+
programme_id=args.programme,
70+
complementary_object_ids=args.comp_object,
71+
)
72+
73+
def load_output_layout(self):
74+
"""Load the specified layout.
75+
76+
Returns:
77+
layout (Layout): loudspeaker layout
78+
upmix (sparse array or None): optional matrix to apply after rendering
79+
n_channels (int): number of channels required in output file
80+
"""
81+
spkr_layout = bs2051.get_layout(self.target_layout)
82+
83+
if self.speakers_file is not None:
84+
real_layout = layout.load_real_layout(self.speakers_file)
85+
spkr_layout, upmix = spkr_layout.with_real_layout(real_layout)
86+
spkr_layout.check_upmix_matrix(upmix)
87+
upmix = scipy.sparse.csc_matrix(upmix.T)
88+
n_channels = upmix.shape[1]
89+
else:
90+
upmix = None
91+
n_channels = len(spkr_layout.channels)
3992

40-
output_monitor = PeakMonitor(n_channels)
93+
return spkr_layout, upmix, n_channels
4194

42-
blocksize = 8192
43-
with openBw64Adm(input_file, enable_block_duration_fix) as infile:
44-
selected_items = preprocess_rendering_items(infile.selected_items)
45-
renderer.set_rendering_items(selected_items)
95+
@property
96+
def output_gain_linear(self):
97+
return 10.0 ** (self.output_gain_db / 20.0)
98+
99+
@classmethod
100+
def lookup_adm_element(cls, adm, element_id, element_type, element_type_name):
101+
"""Lookup an element in adm by type and ID, with nice error messages."""
102+
if element_id is None:
103+
return None
104+
105+
try:
106+
element = adm[element_id]
107+
except KeyError:
108+
raise KeyError("could not find {element_type_name} with ID {element_id}".format(
109+
element_type_name=element_type_name, element_id=element_id,
110+
))
111+
112+
if not isinstance(element, element_type):
113+
raise ValueError("{element_id} is not an {element_type_name}".format(
114+
element_type_name=element_type_name, element_id=element_id,
115+
))
116+
117+
return element
118+
119+
def get_audio_programme(self, adm):
120+
return self.lookup_adm_element(adm, self.programme_id, AudioProgramme, "audioProgramme")
121+
122+
def get_complementary_objects(self, adm):
123+
return [self.lookup_adm_element(adm, obj_id, AudioObject, "audioObject")
124+
for obj_id in self.complementary_object_ids]
125+
126+
def get_rendering_items(self, adm):
127+
"""Get rendering items from the input file adm.
128+
129+
Parameters:
130+
adm (ADM): ADM to get the RenderingItems from
46131
47-
formatInfo = FormatInfoChunk(formatTag=1,
48-
channelCount=n_channels,
49-
sampleRate=infile.sampleRate,
50-
bitsPerSample=infile.bitdepth)
51-
with openBw64(output_file, 'w', formatInfo=formatInfo) as outfile:
52-
for input_samples in chain(infile.iter_sample_blocks(blocksize), [None]):
53-
if input_samples is None:
54-
output_samples = renderer.get_tail(infile.sampleRate, infile.channels)
55-
else:
56-
output_samples = renderer.render(infile.sampleRate, input_samples)
132+
Returns:
133+
list of RenderingItem: selected rendering items
134+
"""
135+
audio_programme = self.get_audio_programme(adm)
136+
comp_objects = self.get_complementary_objects(adm)
137+
selected_items = select_rendering_items(
138+
adm,
139+
audio_programme=audio_programme,
140+
selected_complementary_objects=comp_objects)
57141

58-
output_samples *= output_gain_linear
142+
return preprocess_rendering_items(selected_items)
59143

60-
if upmix is not None:
61-
output_samples *= upmix
144+
def render_input_file(self, infile, spkr_layout, upmix=None):
145+
"""Get sample blocks of the input file after rendering.
62146
63-
output_monitor.process(output_samples)
64-
outfile.write(output_samples)
147+
Parameters:
148+
infile (Bw64AdmReader): file to read from
149+
spkr_layout (Layout): layout to render to
150+
upmix (sparse array or None): optional upmix to apply
65151
66-
output_monitor.warn_overloaded()
67-
if fail_on_overload and output_monitor.has_overloaded():
68-
sys.exit("error: output overloaded")
152+
Yields:
153+
2D sample blocks
154+
"""
155+
renderer = Renderer(spkr_layout, **self.config)
156+
renderer.set_rendering_items(self.get_rendering_items(infile.adm))
157+
158+
for input_samples in chain(infile.iter_sample_blocks(self.blocksize), [None]):
159+
if input_samples is None:
160+
output_samples = renderer.get_tail(infile.sampleRate, infile.channels)
161+
else:
162+
output_samples = renderer.render(infile.sampleRate, input_samples)
163+
164+
output_samples *= self.output_gain_linear
165+
166+
if upmix is not None:
167+
output_samples *= upmix
168+
169+
yield output_samples
170+
171+
def run(self, input_file, output_file):
172+
"""Render input_file to output_file."""
173+
spkr_layout, upmix, n_channels = self.load_output_layout()
174+
175+
output_monitor = PeakMonitor(n_channels)
176+
177+
with openBw64Adm(input_file, self.enable_block_duration_fix) as infile:
178+
formatInfo = FormatInfoChunk(formatTag=1,
179+
channelCount=n_channels,
180+
sampleRate=infile.sampleRate,
181+
bitsPerSample=infile.bitdepth)
182+
with openBw64(output_file, "w", formatInfo=formatInfo) as outfile:
183+
for output_block in self.render_input_file(infile, spkr_layout, upmix):
184+
output_monitor.process(output_block)
185+
outfile.write(output_block)
186+
187+
output_monitor.warn_overloaded()
188+
if self.fail_on_overload and output_monitor.has_overloaded():
189+
sys.exit("error: output overloaded")
69190

70191

71192
def parse_command_line():
72-
parser = argparse.ArgumentParser(description='EBU ADM renderer')
193+
parser = argparse.ArgumentParser(description="EBU ADM renderer")
73194

74-
parser.add_argument('-d', '--debug',
195+
parser.add_argument("-d", "--debug",
75196
help="print debug information when an error occurs",
76197
action="store_true")
77198

78-
parser.add_argument('input_file')
79-
parser.add_argument('output_file')
80-
81-
formats_string = ', '.join(bs2051.layout_names)
82-
parser.add_argument('-s', '--system', required=True, metavar="target_system",
83-
help='Target output system, accoring to ITU-R BS.2051. '
84-
'Available systems are: {}'.format(formats_string))
85-
86-
parser.add_argument('-l', '--layout', type=argparse.FileType('r'), metavar='layout_file',
87-
help='Layout config file')
88-
parser.add_argument('--output-gain-db', type=float, metavar='gain_db', default=0,
89-
help='output gain in dB (default: 0)')
90-
parser.add_argument('--fail-on-overload', "-c", action='store_true',
91-
help='fail if an overload condition is detected in the output')
92-
parser.add_argument('--enable-block-duration-fix', action='store_true',
93-
help='automatically try to fix faulty block format durations')
94-
parser.add_argument('--strict',
199+
OfflineRenderDriver.add_args(parser)
200+
201+
parser.add_argument("input_file")
202+
parser.add_argument("output_file")
203+
204+
parser.add_argument("--strict",
95205
help="treat unknown ADM attributes as errors",
96206
action="store_true")
97207

@@ -105,14 +215,13 @@ def main():
105215
handle_strict(args)
106216

107217
try:
108-
process_file(args.input_file, args.output_file, args.system, args.layout,
109-
args.output_gain_db, args.fail_on_overload, args.enable_block_duration_fix)
218+
OfflineRenderDriver.from_args(args).run(args.input_file, args.output_file)
110219
except Exception as error:
111220
if args.debug:
112221
raise
113222
else:
114223
sys.exit(str(error))
115224

116225

117-
if __name__ == '__main__':
226+
if __name__ == "__main__":
118227
main()

0 commit comments

Comments
 (0)