X-Git-Url: https://git.distorted.org.uk/~mdw/autoys/blobdiff_plain/3589c4a40f6610b6a8a3b36c71eb09b9e0e33616..487d44e5b8c92ac02e6ab4e967fddd898cf2d1c9:/gremlin/gremlin.in diff --git a/gremlin/gremlin.in b/gremlin/gremlin.in old mode 100755 new mode 100644 index e32d084..c0383ed --- a/gremlin/gremlin.in +++ b/gremlin/gremlin.in @@ -7,18 +7,20 @@ ###----- Licensing notice --------------------------------------------------- ### -### This program is free software; you can redistribute it and/or modify +### This file is part of the `autoys' audio tools collection. +### +### `autoys' is free software; you can redistribute it and/or modify ### it under the terms of the GNU General Public License as published by ### the Free Software Foundation; either version 2 of the License, or ### (at your option) any later version. ### -### This program is distributed in the hope that it will be useful, +### `autoys' is distributed in the hope that it will be useful, ### but WITHOUT ANY WARRANTY; without even the implied warranty of ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ### GNU General Public License for more details. ### ### You should have received a copy of the GNU General Public License -### along with this program; if not, write to the Free Software Foundation, +### along with `autoys'; if not, write to the Free Software Foundation, ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. ###-------------------------------------------------------------------------- @@ -39,20 +41,18 @@ import shutil as SH import optparse as OP import threading as TH import shlex as L -from math import sqrt +from math import sqrt, ceil from contextlib import contextmanager ## eyeD3 tag fettling. -import eyeD3 as E3 +import eyed3 as E3 -## Gstreamer. It picks up command-line arguments -- most notably `--help' -- -## and processes them itself. Of course, its help is completely wrong. This -## kludge is due to Jonas Wagner. -_argv, SYS.argv = SYS.argv, [] -import gobject as G -import gio as GIO -import gst as GS -SYS.argv = _argv +## Gstreamer. +import gi +gi.require_version('GLib', '2.0'); from gi.repository import GLib as G +gi.require_version('Gio', '2.0'); from gi.repository import Gio as GIO +gi.require_version('Gst', '1.0'); from gi.repository import Gst as GS +GS.init([]) ## Python Imaging. from PIL import Image as I @@ -245,7 +245,7 @@ class ProgressEyecandy (object): ## Work out -- well, guess -- the time remaining. if cur: t = T.time() - eta = me._fmt_time((t - me._start)*(max - cur)/cur) + eta = me._fmt_time(ceil((t - me._start)*(max - cur)/cur)) else: eta = '???' @@ -409,6 +409,8 @@ class FormatParser (P.ParserElement): named format and its superclasses. """ + name = 'format-spec' + ## We cache the parser elements we generate to avoid enormous consing. CACHE = {} @@ -706,10 +708,28 @@ Policy << (And | Or | Accept | Convert) def make_element(factory, name = None, **props): "Return a new element from the FACTORY with the given NAME and PROPS." - elt = GS.element_factory_make(factory, name) + elt = GS.ElementFactory.make(factory, name) + if elt is None: raise ValueError, 'failed to make `%s\' element' % factory elt.set_properties(**props) return elt +def link_elements(elts): + "Link the elements ELTS together, in order." + e0 = None + for e1 in elts: + if e0 is not None: e0.link(e1) + e0 = e1 + +def bin_children(bin): + "Iterate over the (direct) children of a BIN." + iter = bin.iterate_elements() + while True: + rc, elt = iter.next() + if rc == GS.IteratorResult.DONE: break + elif rc != GS.IteratorResult.OK: + raise ValueError, 'iteration failed (%s)' % rc + else: yield elt + class GStreamerProgressEyecandy (ProgressEyecandy): """ Provide amusement while GStreamer is busy doing something. @@ -750,12 +770,10 @@ class GStreamerProgressEyecandy (ProgressEyecandy): ## time, because (particularly with VBR-encoded MP3 inputs) the estimated ## duration can change as we progress. Hopefully it settles down fairly ## soon. - try: - t, hunoz = me._elt.query_position(GS.FORMAT_TIME) - end, hukairz = me._elt.query_duration(GS.FORMAT_TIME) - return t, end - except GS.QueryError: - return None, None + ok, t = me._elt.query_position(GS.Format.TIME) + if ok: ok, end = me._elt.query_duration(GS.Format.TIME) + if ok: return t, end + else: return None, None def __enter__(me): "Enter context: attach progress meter display." @@ -765,7 +783,7 @@ class GStreamerProgressEyecandy (ProgressEyecandy): return ## Update regularly. The pipeline runs asynchronously. - me._id = G.timeout_add(200, me._update) + me._id = G.timeout_add(100, me._update) def __exit__(me, ty, val, tb): "Leave context: remove display and report completion or failure." @@ -801,45 +819,55 @@ class AudioIdentifier (object): demand. """ - def __init__(me, file, mime): - "Initialize the object suitably for identifying FILE." - - ## Make some initial GStreamer objects. We'll want the pipeline later if - ## we need to analyse a poorly tagged MP3 stream, so save it away. - me._pipe = GS.Pipeline() - me._file = file - bus = me._pipe.get_bus() - bus.add_signal_watch() - loop = G.MainLoop() + def _prepare_pipeline(me): + pipe = GS.Pipeline() + bus = pipe.get_bus() ## The basic recognition kit is based around `decodebin'. We must keep ## it happy by giving it sinks for the streams it's found, which it ## announces asynchronously. - source = make_element('filesrc', 'file', location = file) + source = make_element('filesrc', 'file', location = me._file) decoder = make_element('decodebin', 'decode') sink = make_element('fakesink') def decoder_pad_arrived(elt, pad): - if pad.get_caps()[0].get_name().startswith('audio/'): + if pad.get_current_caps()[0].get_name().startswith('audio/'): elt.link_pads(pad.get_name(), sink, 'sink') - dpaid = decoder.connect('pad-added', decoder_pad_arrived) - me._pipe.add(source, decoder, sink) - GS.element_link_many(source, decoder) + decoder.connect('pad-added', decoder_pad_arrived) + for i in [source, decoder, sink]: pipe.add(i) + link_elements([source, decoder]) + + ## Done. + return pipe, bus, decoder, sink + + def __init__(me, file, mime): + "Initialize the object suitably for identifying FILE." + + me._file = file + pipe, bus, decoder, sink = me._prepare_pipeline() + + ## Make some initial GStreamer objects. We'll want the pipeline later if + ## we need to analyse a poorly tagged MP3 stream, so save it away. + loop = G.MainLoop() ## Arrange to collect tags from the pipeline's bus as they're reported. - ## If we reuse the pipeline later, we'll want different bus-message - ## handling, so make sure we can take the signal handler away. tags = {} fail = [] def bus_message(bus, msg): - if msg.type == GS.MESSAGE_ERROR: - fail[:] = (ValueError, msg.structure['debug'], None) + ty, s = msg.type, msg.get_structure() + if ty == GS.MessageType.ERROR: + fail[:] = (ValueError, s['debug'], None) loop.quit() - elif msg.type == GS.MESSAGE_STATE_CHANGED: - if msg.structure['new-state'] == GS.STATE_PAUSED and \ - msg.src == me._pipe: + elif ty == GS.MessageType.STATE_CHANGED: + if s['new-state'] == GS.State.PAUSED and \ + msg.src == pipe: loop.quit() - elif msg.type == GS.MESSAGE_TAG: - tags.update(msg.structure) + elif ty == GS.MessageType.TAG: + tt = s['taglist'] + for i in xrange(tt.n_tags()): + t = tt.nth_tag_name(i) + if tt.get_tag_size(t) != 1: continue + v = tt.get_value_index(t, 0) + tags[t] = v bmid = bus.connect('message', bus_message) ## We want to identify the kind of stream this is. (Hmm. The MIME type @@ -851,7 +879,7 @@ class AudioIdentifier (object): ## things from being too awful.) me.cap = None me.dcap = None - for e in decoder.elements(): + for e in bin_children(decoder): if e.get_factory().get_name() == 'typefind': tfelt = e break @@ -860,12 +888,13 @@ class AudioIdentifier (object): ## Crank up most of the heavy machinery. The message handler will stop ## the loop when things seem to be sufficiently well underway. - me._pipe.set_state(GS.STATE_PAUSED) + bus.add_signal_watch() + pipe.set_state(GS.State.PAUSED) loop.run() bus.disconnect(bmid) - decoder.disconnect(dpaid) + bus.remove_signal_watch() if fail: - me._pipe.set_state(GS.STATE_NULL) + pipe.set_state(GS.State.NULL) raise fail[0], fail[1], fail[2] ## Store the collected tags. @@ -873,9 +902,9 @@ class AudioIdentifier (object): ## Gather the capabilities. The `typefind' element knows the input data ## type. The 'decodebin' knows the raw data type. - me.cap = tfelt.get_pad('src').get_negotiated_caps()[0] + me.cap = tfelt.get_static_pad('src').get_allowed_caps()[0] me.mime = set([mime, me.cap.get_name()]) - me.dcap = sink.get_pad('sink').get_negotiated_caps()[0] + me.dcap = sink.get_static_pad('sink').get_allowed_caps()[0] ## If we found a plausible bitrate then stash it. Otherwise note that we ## failed. If anybody asks then we'll work it out then. @@ -884,17 +913,11 @@ class AudioIdentifier (object): elif 'bitrate' in tags and tags['bitrate'] >= 80000: me._bitrate = tags['bitrate']/1000 else: - me._bitrate = None - - ## The bitrate computation wants the file size. Ideally we'd want the - ## total size of the frames' contents, but that seems hard to dredge - ## out. If the framing overhead is small, this should be close enough - ## for our purposes. - me._bytes = OS.stat(file).st_size - - def __del__(me): - "Close the pipeline down so we don't leak file descriptors." - me._pipe.set_state(GS.STATE_NULL) + ok, n = pipe.query_duration(GS.Format.BYTES) + if ok: ok, t = pipe.query_duration(GS.Format.TIME) + if ok: me._bitrate = int((8e6*n)/t) + else: me._bitrate = None + pipe.set_state(GS.State.NULL) @property def bitrate(me): @@ -908,36 +931,48 @@ class AudioIdentifier (object): if me._bitrate is not None: return me._bitrate - ## Make up a new main loop. + ## Make up a new pipeline and main loop. + pipe, bus, _, _ = me._prepare_pipeline() loop = G.MainLoop() ## Watch for bus messages. We'll stop when we reach the end of the ## stream: then we'll have a clear idea of how long the track was. fail = [] def bus_message(bus, msg): - if msg.type == GS.MESSAGE_ERROR: - fail[:] = (ValueError, msg.structure['debug'], None) + ty, s = msg.type, msg.get_structure() + if ty == GS.MessageType.ERROR: + fail[:] = (ValueError, s['debug'], None) loop.quit() - elif msg.type == GS.MESSAGE_EOS: + elif ty == GS.MessageType.EOS: loop.quit() - bus = me._pipe.get_bus() + bus = pipe.get_bus() bmid = bus.connect('message', bus_message) ## Get everything moving, and keep the user amused while we work. - me._pipe.set_state(GS.STATE_PLAYING) - with GStreamerProgressEyecandy(filestatus(file, 'measure bitrate') % - me._pipe, - silentp = True): + bus.add_signal_watch() + pipe.set_state(GS.State.PLAYING) + with GStreamerProgressEyecandy(filestatus(me._file, 'measure bitrate'), + pipe, silentp = True): loop.run() + bus.remove_signal_watch() bus.disconnect(bmid) if fail: - me._pipe.set_state(GS.STATE_NULL) + pipe.set_state(GS.State.NULL) raise fail[0], fail[1], fail[2] + STATUS.clear() + + ## The bitrate computation wants the file size. Ideally we'd want the + ## total size of the frames' contents, but that seems hard to dredge + ## out. If the framing overhead is small, this should be close enough + ## for our purposes. + bytes = OS.stat(me._file).st_size ## Now we should be able to find out our position accurately and work out ## a bitrate. Cache it in case anybody asks again. - t, hukairz = me._pipe.query_position(GS.FORMAT_TIME) - me._bitrate = int(8*me._bytes*1e6/t) + ok, t = pipe.query_position(GS.Format.TIME) + assert ok, 'failed to discover bitrate' + me._bitrate = int(8*bytes*1e6/t) + pipe.set_state(GS.State.NULL) ## Done. return me._bitrate @@ -986,10 +1021,10 @@ class AudioFormat (BaseFormat): """ elts = me.encoder_chain() bin = GS.Bin() - bin.add(*elts) - GS.element_link_many(*elts) - bin.add_pad(GS.GhostPad('sink', elts[0].get_pad('sink'))) - bin.add_pad(GS.GhostPad('src', elts[-1].get_pad('src'))) + for i in elts: bin.add(i) + link_elements(elts) + bin.add_pad(GS.GhostPad('sink', elts[0].get_static_pad('sink'))) + bin.add_pad(GS.GhostPad('src', elts[-1].get_static_pad('src'))) return bin def convert(me, master, id, target): @@ -1002,7 +1037,6 @@ class AudioFormat (BaseFormat): ## Construct the necessary equipment. pipe = GS.Pipeline() bus = pipe.get_bus() - bus.add_signal_watch() loop = G.MainLoop() ## Make sure that there isn't anything in the way of our output. We're @@ -1023,9 +1057,9 @@ class AudioFormat (BaseFormat): convert = make_element('audioconvert', 'convert') encoder = me.encoder() sink = make_element('filesink', 'sink', location = new) - pipe.add(source, decoder, convert, encoder, sink) - GS.element_link_many(source, decoder) - GS.element_link_many(convert, encoder, sink) + for i in [source, decoder, convert, encoder, sink]: pipe.add(i) + link_elements([source, decoder]) + link_elements([convert, encoder, sink]) ## Some decoders (e.g., the AC3 decoder) include channel-position ## indicators in their output caps. The Vorbis encoder interferes with @@ -1053,7 +1087,7 @@ class AudioFormat (BaseFormat): ## our encoding chain. For now, we'll hope that there's only one audio ## stream in there, and just throw everything else away. def decoder_pad_arrived(elt, pad): - if pad.get_caps()[0].get_name().startswith('audio/'): + if pad.get_current_caps()[0].get_name().startswith('audio/'): if dcap: elt.link_pads_filtered(pad.get_name(), convert, 'sink', dcap) else: @@ -1063,20 +1097,23 @@ class AudioFormat (BaseFormat): ## Watch the bus for completion messages. fail = [] def bus_message(bus, msg): - if msg.type == GS.MESSAGE_ERROR: - fail[:] = (ValueError, msg.structure['debug'], None) + if msg.type == GS.MessageType.ERROR: + fail[:] = (ValueError, msg.get_structure()['debug'], None) loop.quit() - elif msg.type == GS.MESSAGE_EOS: + elif msg.type == GS.MessageType.EOS: loop.quit() bmid = bus.connect('message', bus_message) ## Get everything ready and let it go. - pipe.set_state(GS.STATE_PLAYING) + bus.add_signal_watch() + pipe.set_state(GS.State.PLAYING) with GStreamerProgressEyecandy(filestatus(master, 'convert to %s' % me.NAME), pipe): loop.run() - pipe.set_state(GS.STATE_NULL) + pipe.set_state(GS.State.NULL) + bus.remove_signal_watch() + bus.disconnect(bmid) if fail: raise fail[0], fail[1], fail[2] @@ -1105,8 +1142,8 @@ class OggVorbisFormat (AudioFormat): for q, br in me.QMAP: if br >= me.bitrate: break - else: - raise ValueError, 'no suitable quality setting found' + else: + raise ValueError, 'no suitable quality setting found' encprops['quality'] = q/10.0 return [make_element('vorbisenc', **encprops), make_element('oggmux')] @@ -1122,8 +1159,13 @@ class MP3Format (AudioFormat): def encoder_chain(me): encprops = {} - if me.bitrate is not None: encprops['vbr_mean_bitrate'] = me.bitrate - return [make_element('lame', vbr = 4, **encprops), + if me.bitrate is not None: + encprops['bitrate'] = me.bitrate + encprops['target'] = 'bitrate' + else: + encprops['quality'] = 4 + encprops['target'] = 'quality' + return [make_element('lamemp3enc', quality = 4, **encprops), make_element('xingmux'), make_element('id3v2mux')] @@ -1134,13 +1176,16 @@ class MP3Format (AudioFormat): GStreamer produces ID3v2 tags, but not ID3v1. This seems unnecessarily unkind to stupid players. """ - tag = E3.Tag() - tag.link(path) - tag.setTextEncoding(E3.UTF_8_ENCODING) - try: - tag.update(E3.ID3_V1_1) - except (UnicodeEncodeError, E3.tag.GenreException): - pass + f = E3.load(path) + if f is None: return + t = f.tag + if t is None: return + for v in [E3.id3.ID3_V2_3, E3.id3.ID3_V1]: + try: f.tag.save(version = v) + except (UnicodeEncodeError, + E3.id3.GenreException, + E3.id3.TagException): + pass defformat('mp3', MP3Format) @@ -1498,8 +1543,9 @@ def grobble(master, targets, noact = False): ## the appropriate categories. Later, we'll apply policy to the ## files, by category, and work out what to do with them all. else: - gf = GIO.File(masterfile) - mime = gf.query_info('standard::content-type').get_content_type() + mime = GIO.file_new_for_path(masterfile) \ + .query_info('standard::content-type', 0) \ + .get_content_type() cats = [] for cat in pmap.iterkeys(): id = cat.identify(masterfile, mime)