Compare commits

..
5 Commits
Author SHA1 Message Date
gryf 7a671a5a6d Make opus default encoder 2026-09-25 09:52:06 +02:00
gryf 5a45d56784 Added opus encoder and decoder 2026-09-25 09:49:37 +02:00
gryf 38dc4234e0 Use fstring intsead of string interpolation.
Also, removed python2 mentioning in readme.
2026-09-25 09:19:38 +02:00
gryf 8fe2c53ce9 Drop Python 2 support 2026-08-19 15:22:17 +02:00
gryf 2b81619bfe Added support for musepack decoding 2026-08-19 15:05:47 +02:00
2 changed files with 83 additions and 40 deletions

No files matched your search

+14 -8
View File
@@ -15,9 +15,10 @@ conversion between different type of audio formats.
Requirements Requirements
============ ============
- python in version 2.7 or 3 - python 3
- `mutagen`_ python library for tag read/write - `mutagen`_ python library for tag read/write
- `vorbis tools`_ for ``ogg`` files encode/decode - `vorbis tools`_ for ``ogg`` files encode/decode
- `opus tools`_ for ``opus`` files encode/decode
- `lame`_ for ``mp3`` files encode/decode - `lame`_ for ``mp3`` files encode/decode
- `flac`_ for ``flac`` files decode - `flac`_ for ``flac`` files decode
- `mac`_ for ``Monkey audio codecs``/``ape`` files decode - `mac`_ for ``Monkey audio codecs``/``ape`` files decode
@@ -37,16 +38,19 @@ Conversion can be performed from the following formats:
- WavePack - WavePack
- Ape - Ape
- Ogg Vorbis - Ogg Vorbis
- Ogg Opus
Currently supported encoders: Currently supported encoders:
- Ogg Vorbis - Ogg Vorbis
- Ogg Opus
- MP3 (lame) - MP3 (lame)
There is an option to set the quality for the encoders - for *Ogg* files there There is an option to set the quality for the encoders - for *Vorbis* files
would be used an ``-q`` option for ``oggenc`` command, and for the *mp3* there would be used an ``-q`` option for ``oggenc`` command, for the *mp3*
format, ``-V`` option would be used for ``lame`` command. Consult corresponding format ``-V`` option would be used for ``lame`` command and ``--bitrate`` for
man pages for details. ``opusenc`` for the *Opus* files. Consult corresponding encoder man pages for
details.
Usage Usage
===== =====
@@ -57,9 +61,10 @@ Given that there are couple of music files, simplest usage is as follows:
$ flac2ogg.py directory_of_music_files/* $ flac2ogg.py directory_of_music_files/*
All files from that directory will be encoded to *ogg* by defaults. If there All files from that directory will be encoded to *opus* by default. If there
are already some *ogg* files, new files would have ``_encoded_`` added into the are already some *opus* files, new files would have ``_encoded_`` added into
filename. Note, that output files will be placed next to the original files. the filename. Note, that output files will be placed next to the original
files.
License License
======= =======
@@ -69,6 +74,7 @@ This work is licensed on 3-clause BSD license. See LICENSE file for details.
.. _mutagen: https://mutagen.readthedocs.io/en/latest/ .. _mutagen: https://mutagen.readthedocs.io/en/latest/
.. _vorbis tools: http://www.vorbis.com/ .. _vorbis tools: http://www.vorbis.com/
.. _opus tools: https://opus-codec.org
.. _flac: http://www.vorbis.com/ .. _flac: http://www.vorbis.com/
.. _lame: http://lame.sourceforge.net/ .. _lame: http://lame.sourceforge.net/
.. _mac: http://www.deb-multimedia.org/dists/testing/main/binary-amd64/package/monkeys-audio.php .. _mac: http://www.deb-multimedia.org/dists/testing/main/binary-amd64/package/monkeys-audio.php
+69 -32
View File
@@ -14,18 +14,15 @@ from mutagen.mp3 import MP3
from mutagen.easyid3 import EasyID3, EasyID3KeyError from mutagen.easyid3 import EasyID3, EasyID3KeyError
VERSION = '1.0' VERSION = '1.3'
def match_file(path): def match_file(path):
"""Get all wav files from provided location""" """Get all wav files from provided location"""
matched_files = []
re_ = re.compile(r"^.*_\d+.wav$") re_ = re.compile(r"^.*_\d+.wav$")
matched_files = [os.path.join(path, filename)
for filename in os.listdir(os.path.abspath(path)): for filename in os.listdir(os.path.abspath(path))
if re_.match(filename): if re_.match(filename)]
matched_files.append(os.path.join(path, filename))
return sorted(matched_files) return sorted(matched_files)
@@ -52,14 +49,14 @@ def encode(obj):
obj.encode() obj.encode()
class CueTrack(object): class CueTrack:
def __init__(self): def __init__(self):
"""Init""" """Init"""
self.performer = None self.performer = None
self.title = None self.title = None
class CueObjectParser(object): class CueObjectParser:
def __init__(self, cuefile): def __init__(self, cuefile):
"""Init""" """Init"""
self.cuefile = cuefile self.cuefile = cuefile
@@ -108,7 +105,7 @@ class CueObjectParser(object):
continue continue
class Encoder(object): class Encoder:
"""Encoder base class""" """Encoder base class"""
EXT = ".undefined" EXT = ".undefined"
@@ -118,7 +115,7 @@ class Encoder(object):
def encode(self, input_fname, output_fname): def encode(self, input_fname, output_fname):
"""Encode file""" """Encode file"""
raise NotImplementedError() raise NotImplementedError
class OggEncoder(Encoder): class OggEncoder(Encoder):
@@ -127,13 +124,13 @@ class OggEncoder(Encoder):
def __init__(self, quality=None): def __init__(self, quality=None):
"""Init""" """Init"""
super(OggEncoder, self).__init__(quality) super().__init__(quality)
if self.quality is None: if self.quality is None:
self.quality = 8 self.quality = 8
def encode(self, input_fname, output_fname): def encode(self, input_fname, output_fname):
sp.check_call(["oggenc", "-q%s" % self.quality, input_fname, sp.check_call(["oggenc", f"-q{self.quality}", input_fname, '-o',
'-o', output_fname]) output_fname])
class Mp3Encoder(Encoder): class Mp3Encoder(Encoder):
@@ -142,16 +139,30 @@ class Mp3Encoder(Encoder):
def __init__(self, quality=None): def __init__(self, quality=None):
"""Init""" """Init"""
super(Mp3Encoder, self).__init__(quality) super().__init__(quality)
if self.quality is None: if self.quality is None:
self.quality = 6 self.quality = 6
def encode(self, input_fname, output_fname): def encode(self, input_fname, output_fname):
sp.check_call(["lame", "-V%s" % self.quality, input_fname, sp.check_call(["lame", f"-V{self.quality}", input_fname, output_fname])
class OpusEncoder(Encoder):
"""Opus encoder"""
EXT = ".opus"
def __init__(self, quality=None):
"""Init"""
super().__init__(quality)
if self.quality is None:
self.quality = 128
def encode(self, input_fname, output_fname):
sp.check_call(["opusenc", "--bitrate", self.quality, input_fname,
output_fname]) output_fname])
class FileType(object): class FileType:
"""Base class for file objects""" """Base class for file objects"""
extensions = {'ogg': '.ogg', extensions = {'ogg': '.ogg',
'mp3': '.mp3'} 'mp3': '.mp3'}
@@ -175,7 +186,7 @@ class FileType(object):
def extract_wav(self): def extract_wav(self):
"""Dummy function for wav files""" """Dummy function for wav files"""
raise NotImplementedError() raise NotImplementedError
def encode(self): def encode(self):
"""Encode and tag file""" """Encode and tag file"""
@@ -283,7 +294,7 @@ class M4aType(FileType):
"""M4a filetype""" """M4a filetype"""
def __init__(self, filename, encoder): def __init__(self, filename, encoder):
super(M4aType, self).__init__(filename, encoder) super().__init__(filename, encoder)
def extract_wav(self): def extract_wav(self):
"""Extract m4a file to wav""" """Extract m4a file to wav"""
@@ -291,14 +302,14 @@ class M4aType(FileType):
if "," in wav: if "," in wav:
wav = wav.replace(",", "\\,") wav = wav.replace(",", "\\,")
sp.check_call(["mplayer", "-vo", "none", self.filename, "-ao", sp.check_call(["mplayer", "-vo", "none", self.filename, "-ao",
"pcm:file=%s" % wav]) f"pcm:file={wav}"])
class WavType(FileType): class WavType(FileType):
"""Uncompressed wav filetype""" """Uncompressed wav filetype"""
def __init__(self, filename, encoder): def __init__(self, filename, encoder):
super(WavType, self).__init__(filename, encoder) super().__init__(filename, encoder)
self.wav = filename self.wav = filename
self.tmp_wav_remove = False self.tmp_wav_remove = False
@@ -315,21 +326,45 @@ class Mp3Type(FileType):
sp.check_call(["lame", "--decode", self.filename, self.wav]) sp.check_call(["lame", "--decode", self.filename, self.wav])
class MusepackType(FileType):
"""Musepack filetype"""
def __init__(self, filename, encoder):
super().__init__(filename, encoder)
def extract_wav(self):
"""Extract m4a file to wav"""
wav = self.wav
if "," in wav:
wav = wav.replace(",", "\\,")
sp.check_call(["mpcdec", self.filename, self.wav])
class OggType(FileType): class OggType(FileType):
"""Ogg Vorbis filetype""" """Ogg Vorbis filetype"""
def extract_wav(self): def extract_wav(self):
"""Extract mp3 file to wav""" """Extract ogg vorbis file to wav"""
sp.check_call(["oggdec", self.filename]) sp.check_call(["oggdec", self.filename])
class Converter(object): class OpusType(FileType):
"""Opus filetype"""
def extract_wav(self):
"""Extract opus file to wav"""
sp.check_call(["opusdec", self.filename])
class Converter:
"""Main class for converting files""" """Main class for converting files"""
extract_map = {'.ape': ApeType, extract_map = {'.ape': ApeType,
'.flac': FlacType, '.flac': FlacType,
'.m4a': M4aType, '.m4a': M4aType,
'.mp3': Mp3Type, '.mp3': Mp3Type,
'.mpc': MusepackType,
'.ogg': OggType, '.ogg': OggType,
'.opus': OpusType,
'.wav': WavType, '.wav': WavType,
'.wv': WvType} '.wv': WvType}
@@ -369,11 +404,11 @@ class Converter(object):
base + ".wv.cue", base + ".ape.cue"): base + ".wv.cue", base + ".ape.cue"):
if os.path.exists(tmp): if os.path.exists(tmp):
cuefile = tmp cuefile = tmp
print("*** cuefile: %s" % cuefile) print(f"*** cuefile: {cuefile}")
break break
if cuefile is None: if cuefile is None:
print("*** No cuefile found for `%s'" % filename) print(f"*** No cuefile found for `{filename}'")
return return
cue = CueObjectParser(cuefile) cue = CueObjectParser(cuefile)
@@ -382,7 +417,7 @@ class Converter(object):
# list of wavs to encode # list of wavs to encode
klass = Converter.extract_map.get(ext.lower()) klass = Converter.extract_map.get(ext.lower())
if not klass: if not klass:
print("*** Cannot find right converter for `%s'" % ext) print(f"*** Cannot find right converter for `{ext}'")
return return
fobj = klass(filename, encoder) fobj = klass(filename, encoder)
@@ -413,7 +448,8 @@ class Converter(object):
ENCODERS = {'ogg': OggEncoder, ENCODERS = {'ogg': OggEncoder,
'mp3': Mp3Encoder} 'mp3': Mp3Encoder,
'opus': OpusEncoder}
def main(): def main():
@@ -424,12 +460,13 @@ def main():
help='Split output file with provided *.cue file') help='Split output file with provided *.cue file')
arg.add_argument('-r', '--recursive', action='store_true', arg.add_argument('-r', '--recursive', action='store_true',
help='Do the files searching recursive') help='Do the files searching recursive')
arg.add_argument('-e', '--encoder', default='ogg', type=str, arg.add_argument('-e', '--encoder', default='opus', type=str,
choices=('ogg', 'mp3'), help='Encoder to use. Defaults ' choices=('ogg', 'mp3', 'opus'),
'to "ogg"') help='Encoder to use. Defaults to "opus"')
arg.add_argument('-q', '--quality', help='Quality of the encoded file. ' arg.add_argument('-q', '--quality', help='Quality of the encoded file. '
'Consult "lame" and "oggenc" for details. Defaults are ' 'Consult "lame", "oggenc" and "opusenc" manuals for '
'6 for lame and 8 for oggenc.') 'details. Defaults are -V6 for lame, -q8 for oggenc and '
'--bitrate 128 for opusenc')
arg.add_argument('-v', '--version', action='version', arg.add_argument('-v', '--version', action='version',
version='%(prog)s v' + VERSION, version='%(prog)s v' + VERSION,
help='Display version and exit.') help='Display version and exit.')