11from __future__ import print_function
22import argparse
33import sys
4+ from attr import attrs , attrib , Factory
45import scipy .sparse
56from itertools import chain
67from ..core import bs2051 , layout , Renderer
78from ..core .monitor import PeakMonitor
89from ..core .metadata_processing import preprocess_rendering_items
10+ from ..core .select_items import select_rendering_items
911from ..fileio import openBw64 , openBw64Adm
12+ from ..fileio .adm .elements import AudioProgramme , AudioObject
1013from ..fileio .bw64 .chunks import FormatInfoChunk
1114import warnings
1215from ..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
71192def 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