chiark / gitweb /
misc/ab-chop: Fix the licensing notice.
[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###
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.
62import sys as SYS
63import os as OS
64import shutil as SH
65import fnmatch as FN
66import random as R
67
68SR = R.SystemRandom()
69
70## GObject and GStreamer.
71import gobject as G
72import gst as GS
73
74###--------------------------------------------------------------------------
75### GStreamer utilities.
76
77def 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
90def 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
98def 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 print
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
125def 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.
153input = SYS.argv[1]
154caps = GS.caps_from_string(SYS.argv[2])
155output = 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.
159tmp = OS.tmpnam()
160OS.mkdir(tmp)
161try:
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'
241finally:
242 SH.rmtree(tmp)