gstlal  1.4.1
gstlal_play
1 #!/usr/bin/env python
2 #
3 # Copyright (C) 2011--2013 Kipp Cannon, Chad Hanna, Drew Keppel
4 #
5 # This program is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation; either version 2 of the License, or (at your
8 # option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
13 # Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 
19 
20 from optparse import OptionParser
21 import sys
22 
23 import gi
24 gi.require_version('Gst', '1.0')
25 from gi.repository import GObject, Gst
26 GObject.threads_init()
27 Gst.init(None)
28 
29 import lal
30 
31 from glue.ligolw import utils as ligolw_utils
32 from gstlal import datasource
33 from gstlal import multirate_datasource
34 from gstlal import pipeparts
35 from gstlal import simplehandler
36 from glue.ligolw.utils import segments as ligolw_segments
37 from glue.ligolw import lsctables, ligolw
38 
39 ### This program will play data in a variety of ways
40 ###
41 ### This program will play data in a variety of ways. Its input is anything
42 ### supported by :any:`datasource`. You can additionally whiten the data
43 ### or apply a band pass filtering. It can direct its output to either your
44 ### sound card, various audio file formats, or stderr/stdout in tab delimited
45 ### ASCII text.
46 ###
47 ### Graph of the gsreamer pipeline
48 ### ------------------------------
49 ###
50 ### - gray boxes are optional and depend on the command line given
51 ###
52 ### .. graphviz::
53 ###
54 ### digraph G {
55 ### // graph properties
56 ###
57 ### rankdir=LR;
58 ### compound=true;
59 ### node [shape=record fontsize=10 fontname="Verdana"];
60 ### edge [fontsize=8 fontname="Verdana"];
61 ###
62 ### // nodes
63 ###
64 ### "mkbasicsrc()" [URL="\ref datasource.mkbasicsrc()"];
65 ### "whitened_multirate_src()" [label="whitened_multirate_src()", URL="\ref multirate_datasource.mkwhitened_multirate_src()", style=filled, color=lightgrey];
66 ### "mkresample()" [URL="\ref pipeparts.mkresample()", style=filled, color=lightgrey];
67 ### "mkcapsfilter()" [URL="\ref pipeparts.mkcapsfilter()", style=filled, color=lightgrey];
68 ### "mkaudiochebband()" [URL="\ref pipeparts.mkaudiochebband()", style=filled, color=lightgrey];
69 ### "mkaudiocheblimit()" [URL="\ref pipeparts.mkaudiocheblimit()", style=filled, color=lightgrey];
70 ### "mkaudioconvert()" [URL="\ref pipeparts.mkaudioconvert()"];
71 ### "mkaudioamplify()" [URL="\ref pipeparts.mkaudioamplify()"];
72 ### "mkautoaudiosink()" [URL="\ref pipeparts.mkautoaudiosink()", style=filled, color=lightgrey];
73 ### "mkwavenc()" [URL="\ref pipeparts.mkwavenc()", style=filled, color=lightgrey];
74 ### "mkflacenc()" [URL="\ref pipeparts.mkflacenc()", style=filled, color=lightgrey];
75 ### "mkvorbisenc()" [URL="\ref pipeparts.mkvorbisenc()", style=filled, color=lightgrey];
76 ### "mkfilesink()" [URL="\ref pipeparts.mkfilesink()", style=filled, color=lightgrey];
77 ### "mknxydumpsink()" [URL="\ref pipeparts.mknxydumpsink()", style=filled, color=lightgrey];
78 ###
79 ### // connections
80 ###
81 ### "mkbasicsrc()" -> "mkresample()" [label=" --whiten not provided"];
82 ### "mkresample()" -> "mkcapsfilter()";
83 ### "mkcapsfilter()" -> "mkaudioconvert()" [label=" neither --low-pass-filter nor --high-pass-filter provided"];
84 ### "mkcapsfilter()" -> "mkaudiochebband()" [label=" --low-pass-filter and --high-pass-filter provided"];
85 ### "mkcapsfilter()" -> "mkaudiocheblimit()" [label=" --low-pass-filter or --high-pass-filter provided"];
86 ###
87 ### "mkbasicsrc()" -> "whitened_multirate_src()" [label=" --whiten provided"];
88 ### "whitened_multirate_src()" -> "mkaudioconvert()" [label=" neither --low-pass-filter nor --high-pass-filter provided"];
89 ### "whitened_multirate_src()" -> "mkaudiochebband()" [label=" --low-pass-filter and --high-pass-filter provided"];
90 ### "whitened_multirate_src()" -> "mkaudiocheblimit()" [label=" --low-pass-filter or --high-pass-filter provided"];
91 ###
92 ### "mkaudiochebband()" -> "mkaudioconvert()";
93 ### "mkaudiocheblimit()" -> "mkaudioconvert()";
94 ###
95 ### "mkaudioconvert()" -> "mkaudioamplify()";
96 ###
97 ### "mkaudioamplify()" -> "mkautoaudiosink()" [label=" --output not provided"];
98 ### "mkaudioamplify()" -> "mkwavenc()" [label=" --output ends with '.wav'"];
99 ### "mkaudioamplify()" -> "mkflacenc()" [label=" --output ends with '.flac'"];
100 ### "mkaudioamplify()" -> "mkvorbisenc()" [label=" --output ends with '.ogg'"];
101 ### "mkaudioamplify()" -> "mknxydumpsink()" [label=" --output ends with '.txt' or is /dev/stderr or /dev/stdout"];
102 ### "mkwavenc()" -> "mkfilesink()";
103 ### "mkvorbisenc()" -> "mkfilesink()";
104 ### "mkflacenc()" -> "mkfilesink()";
105 ### }
106 ###
107 ### Usage cases
108 ### -----------
109 ###
110 ### See :py:func:`datasource.append_options` for additional usage cases for datasource specific command line options
111 ###
112 ### 1. Viewing low latency data in stdout (e.g. on CIT cluster) Note ctrl+c kills this::
113 ###
114 ### $ gstlal_play --data-source framexmit --channel-name=L1=FAKE-STRAIN \
115 ### --output /dev/stdout
116 ###
117 ### 2. Pipe low latency data to an ogg file narrowing in on the sweet spot and
118 ### add amplification to make it audible. Note ctrl+c kills this::
119 ###
120 ### $ gstlal_play --data-source framexmit --channel-name=L1=FAKE-STRAIN \
121 ### --high-pass-filter 40 --low-pass-filter 1000 --amplification 1e21 --output test.ogg
122 ###
123 ### 3. Write injection time series from an xml file into an ASCII delimited text file::
124 ###
125 ### $ gstlal_play --data-source silence --gps-start-time 966383960 \
126 ### --gps-end-time 966384960 --channel-name=L1=FAKE-STRAIN \
127 ### --injections=BNS-MDC1-FIXEDMASS.xml --output test.txt
128 ###
129 ### 4. Other things are certainly possible. Please add some!
130 ###
131 
132 
133 @lsctables.use_in
134 class LIGOLWContentHandler(ligolw.LIGOLWContentHandler):
135  pass
136 
137 
138 def parse_command_line():
139 
140  parser = OptionParser(description = __doc__)
141 
142  #
143  # First append the datasource common options
144  #
145 
146  datasource.append_options(parser)
147 
148  parser.add_option("--output", metavar = "filename", help = "Set the filename in which to save the output. If not given, output is sent to the default audio device. The filename's extension determines the format, the following are recognized: .wav, .flac, .ogg, .txt, /dev/stdout, /dev/stderr")
149  parser.add_option("--sample-format", metavar = "name", help = "Force a specific sample format for the output. If not specified, the format is chosen by auto-negotiation with the encoder. Allowed values are any GStreamer-recognized format that is compatible with the requested encoder. Examples include \"F32LE\", \"F64LE\".")
150  parser.add_option("--rate", metavar = "Hz", type = "int", default = 4096, help = "Downsample input to this sample rate. Default = 4096 Hz. Must be <= input sample rate or else you will get a caps negotiation error.")
151  parser.add_option("--whiten", action = "store_true", help = "Whiten the time series (default = do not whiten).")
152  parser.add_option("--veto-segments-file", metavar = "filename", help = "Set the name of the LIGO light-weight XML file from which to load vetoes (optional). Must coincide with --whiten")
153  parser.add_option("--veto-segments-name", metavar = "name", help = "Set the name of the segments to extract from the segment tables and use as the veto list. Must coincide with --whiten", default = "vetoes")
154  parser.add_option("--reference-psd", metavar = "file", help = "When whitening, normalize the time series to the spectrum described in this XML file. If this option is not given, the spectrum is measured from the data.")
155  parser.add_option("--low-pass-filter", metavar = "Hz", type = "float", help = "Low pass filter frequency (default = no low-pass filter). Low-pass filter is applied after whitening.")
156  parser.add_option("--high-pass-filter", metavar = "Hz", type = "float", help = "High pass filter frequency (default = no high-pass filter). High-pass filter is applied after whitening.")
157  parser.add_option("--amplification", metavar = "num", type = "float", default = 1.0, help = "Amplify the timeseries this much (default = no amplification). Amplification is applied after low- and high-pass filtering. For unwhitened h(t) that is bandpassed to the most sensitive region you might need to set this to 1e20 to make it audible")
158  parser.add_option("-v", "--verbose", action = "store_true", help = "Be verbose.")
159 
160  #
161  # parse the arguments and sanity check
162  #
163 
164  options, filenames = parser.parse_args()
165 
166  if options.low_pass_filter is not None and options.high_pass_filter is not None:
167  if options.low_pass_filter <= options.high_pass_filter:
168  raise ValueError("--low-pass-filter must be > --high-pass-filter")
169 
170  if options.reference_psd or options.veto_segments_file and not options.whiten:
171  raise ValueError("--reference-psd and --veto-segments-file requires --whiten")
172 
173  if len(options.channel_name) > 1:
174  raise ValueError("only one --channel-name allowed")
175 
176  return options, filenames
177 
178 
179 # parsing and setting up some core structures
180 options, filenames = parse_command_line()
181 
182 gw_data_source_info = datasource.GWDataSourceInfo(options)
183 instrument, = gw_data_source_info.channel_dict
184 
185 if options.reference_psd is not None:
186  psd = lal.series.read_psd_xmldoc(ligolw_utils.load_filename(options.reference_psd, verbose = options.verbose, contenthandler = lal.series.PSDContentHandler))[instrument]
187 else:
188  psd = None
189 
190 if options.veto_segments_file is not None:
191  veto_segments = ligolw_segments.segmenttable_get_by_name(ligolw_utils.load_filename(options.veto_segments_file, verbose = options.verbose, contenthandler = LIGOLWContentHandler), options.veto_segments_name).coalesce()[instrument]
192 else:
193  veto_segments = None
194 
195 # building the event loop and pipeline
196 mainloop = GObject.MainLoop()
197 pipeline = Gst.Pipeline("gstlal_play")
198 handler = simplehandler.Handler(mainloop, pipeline)
199 
200 #
201 # the pipeline
202 #
203 # A basic src
204 head, _, _ = datasource.mkbasicsrc(pipeline, gw_data_source_info, instrument, verbose = options.verbose)
205 
206 # if whitening, leverage mkwhitened_multirate_src() otherwise just resample
207 if options.whiten:
208  head = multirate_datasource.mkwhitened_multirate_src(pipeline, head, [options.rate], instrument, psd, track_psd = True, veto_segments = veto_segments)[options.rate]
209 else:
210  head = pipeparts.mkresample(pipeline, head, quality = 9)
211  head = pipeparts.mkcapsfilter(pipeline, head, "audio/x-raw, rate=%d" % options.rate)
212 
213 # handle filtering
214 if options.high_pass_filter is not None and options.low_pass_filter is not None:
215  head = pipeparts.mkaudiochebband(pipeline, head, lower_frequency = options.high_pass_filter, upper_frequency = options.low_pass_filter)
216 elif options.high_pass_filter is not None:
217  head = pipeparts.mkaudiocheblimit(pipeline, head, cutoff = options.high_pass_filter, mode = "high-pass")
218 elif options.low_pass_filter is not None:
219  head = pipeparts.mkaudiocheblimit(pipeline, head, cutoff = options.low_pass_filter, mode = "low-pass")
220 
221 # necessary audio convert and amplify
222 head = pipeparts.mkaudioconvert(pipeline, pipeparts.mkaudioamplify(pipeline, head, options.amplification))
223 if options.sample_format is not None:
224  head = pipeparts.mkcapsfilter(pipeline, head, "audio/x-raw, format=%s" % options.sample_format)
225 
226 if options.output is None:
227  pipeparts.mkautoaudiosink(pipeline, head)
228 elif options.output.endswith(".wav"):
229  pipeparts.mkfilesink(pipeline, pipeparts.mkwavenc(pipeline, head), options.output)
230 elif options.output.endswith(".flac"):
231  pipeparts.mkfilesink(pipeline, pipeparts.mkflacenc(pipeline, head), options.output)
232 elif options.output.endswith(".ogg"):
233  head = pipeparts.mkoggmux(pipeline, pipeparts.mkvorbisenc(pipeline, head))
234  pipeparts.mkfilesink(pipeline, head, options.output)
235 elif options.output.endswith(".txt") or options.output in ("/dev/stdout", "/dev/stderr"):
236  pipeparts.mknxydumpsink(pipeline, head, options.output)
237 else:
238  raise ValueError("unrecognized format for --output")
239 
240 # Allow Ctrl+C or sig term to gracefully shut down the program for online
241 # sources, otherwise it will just kill it
242 if gw_data_source_info.data_source in ("lvshm", "framexmit"):# what about nds online?
243  simplehandler.OneTimeSignalHandler(pipeline)
244 
245 # Seek
246 if pipeline.set_state(Gst.State.READY) == Gst.StateChangeReturn.FAILURE:
247  raise RuntimeError("pipeline failed to enter READY state")
248 if gw_data_source_info.data_source not in ("lvshm", "framexmit"):# what about nds online?
249  datasource.pipeline_seek_for_gps(pipeline, gw_data_source_info.seg[0], gw_data_source_info.seg[1])
250 
251 # run
252 if pipeline.set_state(Gst.State.PLAYING) == Gst.StateChangeReturn.FAILURE:
253  raise RuntimeError("pipeline failed to enter PLAYING state")
254 mainloop.run()