Commit | Line | Data |
---|---|---|
583b7e4a MW |
1 | #! /usr/bin/python |
2 | ### | |
3 | ### A simple program for doing blind A/B audio comparisons | |
4 | ### | |
5 | ### (c) 2010 Mark Wooding | |
6 | ### | |
7 | ||
8 | ###----- Licensing notice --------------------------------------------------- | |
9 | ### | |
3edb59b1 MW |
10 | ### This file is part of the `autoys' audio tools collection. |
11 | ### | |
12 | ### `autoys' is free software; you can redistribute it and/or modify | |
583b7e4a MW |
13 | ### it under the terms of the GNU General Public License as published by |
14 | ### the Free Software Foundation; either version 2 of the License, or | |
15 | ### (at your option) any later version. | |
16 | ### | |
3edb59b1 | 17 | ### `autoys' is distributed in the hope that it will be useful, |
583b7e4a MW |
18 | ### but WITHOUT ANY WARRANTY; without even the implied warranty of |
19 | ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
20 | ### GNU General Public License for more details. | |
21 | ### | |
22 | ### You should have received a copy of the GNU General Public License | |
3edb59b1 | 23 | ### along with `autoys'; if not, write to the Free Software Foundation, |
583b7e4a MW |
24 | ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
25 | ||
26 | ###----- Usage -------------------------------------------------------------- | |
27 | ### | |
28 | ### The command line syntax is: | |
29 | ### | |
30 | ### ab-chop INPUT CAPS OUTPUT PIPELINE... | |
31 | ### | |
32 | ### This means that we should read INPUT, decode it (using a GStreamer | |
33 | ### `decodebin', so it should be able to handle most things you care to throw | |
34 | ### at it), and then re-encode it according to each PIPELINE in turn, decode | |
35 | ### /that/ again, and stash the resulting raw PCM data. When we've finished, | |
36 | ### we line up the PCM data streams side-by-side, chop them into chunks, and | |
37 | ### then stitch chunks from randomly chosen streams together to make a new | |
38 | ### PCM stream. Finally, we encode that mixed-up stream as FLAC, and write | |
39 | ### it to OUTPUT. It also writes a file OUTPUT.sequence which is a list of | |
40 | ### numbers indicating which pipeline each chunk of the original came from. | |
41 | ### | |
42 | ### The motivation is that we want to test encoder quality. So you take a | |
43 | ### reference source (as good as you can find), and use that as your INPUT. | |
44 | ### You then write GStreamer pipeline fragments for the encoders you want to | |
45 | ### compare; say `identity' if you want the unmodified original reference to | |
46 | ### be mixed in. | |
47 | ### | |
48 | ### The only tricky bit is the CAPS, which is a GStreamer capabilities string | |
49 | ### describing the raw PCM format to use as an intermediate representation. | |
50 | ### (This is far too low-level and cumbersome for real use, but it's OK for | |
51 | ### now.) You need to say something like | |
52 | ### | |
53 | ### audio/x-raw-int,width=16,rate=44100,channels=2,depth=16, | |
54 | ### endianness=1234,signed=true | |
55 | ### | |
56 | ### for standard CD audio. | |
57 | ||
58 | ###-------------------------------------------------------------------------- | |
59 | ### External dependencies. | |
60 | ||
61 | ## Standard Python libraries. | |
62 | import sys as SYS | |
63 | import os as OS | |
64 | import shutil as SH | |
65 | import fnmatch as FN | |
66 | import random as R | |
67 | ||
68 | SR = R.SystemRandom() | |
69 | ||
70 | ## GObject and GStreamer. | |
71 | import gobject as G | |
72 | import gst as GS | |
73 | ||
74 | ###-------------------------------------------------------------------------- | |
75 | ### GStreamer utilities. | |
76 | ||
77 | def link_on_demand(src, sink, sinkpad = None, cap = None): | |
78 | """ | |
79 | Link SINK to SRC when a pad appears. | |
80 | ||
81 | More precisely, when SRC reports that a pad with media type matching the | |
82 | `fnmatch' pattern CAP has appeared, link the pad of SINK named SINKPAD (or | |
83 | some sensible pad by default). | |
84 | """ | |
85 | def _link(src, srcpad): | |
86 | if cap is None or FN.fnmatchcase(srcpad.get_caps()[0].get_name(), cap): | |
87 | src.link_pads(srcpad.get_name(), sink, sinkpad) | |
88 | src.connect('pad-added', _link) | |
89 | ||
90 | def make_element(factory, name = None, **props): | |
91 | """ | |
92 | Return an element made by FACTORY with properties specified by PROPS. | |
93 | """ | |
94 | elt = GS.element_factory_make(factory, name) | |
95 | elt.set_properties(**props) | |
96 | return elt | |
97 | ||
98 | def dump_pipeline(pipe, indent = 0): | |
99 | done = {} | |
100 | q = [] | |
101 | for e in pipe.iterate_sources(): | |
102 | q = [e] | |
103 | while q: | |
104 | e, q = q[0], q[1:] | |
105 | if e in done: | |
106 | continue | |
107 | done[e] = True | |
108 | ||
109 | print '%s%s %s' % (' '*indent, type(e).__name__, e.get_name()) | |
110 | for p in e.pads(): | |
111 | c = p.get_negotiated_caps() | |
112 | peer = p.get_peer() | |
113 | print '%s Pad %s %s (%s)' % \ | |
114 | (' '*(indent + 1), | |
115 | p.get_name(), | |
116 | peer and ('<-> %s.%s' % (peer.get_parent().get_name(), | |
117 | peer.get_name())) | |
118 | or 'unconnected', | |
119 | c and c.to_string() or 'no-negotiated-caps') | |
120 | if peer: | |
121 | q.append(peer.get_parent()) | |
122 | if isinstance(e, GS.Bin): | |
123 | dump_pipeline(e, indent + 1) | |
124 | ||
125 | def run_pipe(pipe, what): | |
126 | """ | |
127 | Run a GStreamer pipeline PIPE until it finishes. | |
128 | """ | |
129 | loop = G.MainLoop() | |
130 | bus = pipe.get_bus() | |
131 | bus.add_signal_watch() | |
132 | def _bus_message(bus, msg): | |
133 | if msg.type == GS.MESSAGE_ERROR: | |
134 | SYS.stderr.write('error from pipeline: %s\n' % msg) | |
135 | SYS.exit(1) | |
136 | elif msg.type == GS.MESSAGE_STATE_CHANGED and \ | |
137 | msg.src == pipe and \ | |
138 | msg.structure['new-state'] == GS.STATE_PAUSED: | |
139 | dump_pipeline(pipe) | |
140 | elif msg.type == GS.MESSAGE_EOS: | |
141 | loop.quit() | |
142 | bus.connect('message', _bus_message) | |
143 | ||
144 | pipe.set_state(GS.STATE_PLAYING) | |
145 | loop.run() | |
146 | GS.DEBUG_BIN_TO_DOT_FILE(pipe, 3, what) | |
147 | pipe.set_state(GS.STATE_NULL) | |
148 | ||
149 | ###-------------------------------------------------------------------------- | |
150 | ### Main program. | |
151 | ||
152 | ## Read the command line arguments. | |
153 | input = SYS.argv[1] | |
154 | caps = GS.caps_from_string(SYS.argv[2]) | |
155 | output = SYS.argv[3] | |
156 | ||
157 | ## We want a temporary place to keep things. This provokes a warning, but | |
158 | ## `mkdir' is atomic and sane so it's not a worry. | |
159 | tmp = OS.tmpnam() | |
160 | OS.mkdir(tmp) | |
161 | try: | |
162 | ||
163 | ## First step: produce raw PCM files from the original source and the | |
164 | ## requested encoders. | |
165 | q = 0 | |
166 | temps = [] | |
167 | for i in SYS.argv[4:]: | |
168 | temp = OS.path.join(tmp, '%d.raw' % q) | |
169 | temps.append(temp) | |
170 | pipe = GS.Pipeline() | |
171 | origin = make_element('filesrc', location = input) | |
172 | decode_1 = make_element('decodebin') | |
173 | convert_1 = make_element('audioconvert') | |
174 | encode = GS.parse_bin_from_description(i, True) | |
175 | decode_2 = make_element('decodebin') | |
176 | convert_2 = make_element('audioconvert') | |
177 | target = make_element('filesink', location = temp) | |
178 | pipe.add(origin, decode_1, convert_1, encode, | |
179 | decode_2, convert_2, target) | |
180 | origin.link(decode_1) | |
181 | link_on_demand(decode_1, convert_1) | |
182 | ##convert_1.link(encode, GS.caps_from_string('audio/x-raw-float, channels=2')) | |
183 | convert_1.link(encode) | |
184 | encode.link(decode_2) | |
185 | link_on_demand(decode_2, convert_2) | |
186 | convert_2.link(target, caps) | |
187 | ||
188 | run_pipe(pipe, 'input-%d' % q) | |
189 | del pipe | |
190 | print 'done %s' % i | |
191 | q += 1 | |
192 | step = 1763520 | |
193 | lens = [OS.stat(i).st_size for i in temps] | |
194 | blocks = (max(*lens) + step - 1)//step | |
195 | while True: | |
196 | seq = [] | |
197 | done = {} | |
198 | for i in xrange(blocks): | |
199 | j = SR.randrange(q) | |
200 | done[j] = True | |
201 | seq.append(j) | |
202 | ok = True | |
203 | for i in xrange(q): | |
204 | if i not in done: | |
205 | ok = False | |
206 | break | |
207 | if ok: | |
208 | break | |
209 | ff = [open(i, 'rb') for i in temps] | |
210 | mix = OS.path.join(tmp, 'mix.raw') | |
211 | out = open(mix, 'wb') | |
212 | pos = 0 | |
213 | for i in seq: | |
214 | f = ff[i] | |
215 | f.seek(pos) | |
216 | buf = f.read(step) | |
217 | out.write(buf) | |
218 | if len(buf) < step: | |
219 | break | |
220 | pos += step | |
221 | out.close() | |
222 | for f in ff: | |
223 | f.close() | |
224 | ||
225 | f = open(output + '.sequence', 'w') | |
226 | f.write(', '.join([str(i) for i in seq]) + '\n') | |
227 | f.close() | |
228 | ||
229 | pipe = GS.Pipeline() | |
230 | origin = make_element('filesrc', location = mix) | |
231 | convert = make_element('audioconvert') | |
232 | encode = make_element('flacenc', quality = 8) | |
233 | target = make_element('filesink', location = output) | |
234 | pipe.add(origin, convert, encode, target) | |
235 | origin.link(convert, caps) | |
236 | GS.element_link_many(convert, encode, target) | |
237 | ||
238 | run_pipe(pipe, 'output') | |
239 | del pipe | |
240 | print 'all done' | |
241 | finally: | |
242 | SH.rmtree(tmp) |