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