From 13a6fdb89daec7adf1afa9361905b2c7ec6f925a Mon Sep 17 00:00:00 2001 From: Andrey Mazo Date: Tue, 24 Jun 2014 04:02:05 +0400 Subject: [PATCH 01/10] Make script executable --- shuffle.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 shuffle.py diff --git a/shuffle.py b/shuffle.py old mode 100644 new mode 100755 From 9f47cf5587fbc34d35feddcfa5856d50e5e6a5c1 Mon Sep 17 00:00:00 2001 From: Andrey Mazo Date: Tue, 24 Jun 2014 04:03:12 +0400 Subject: [PATCH 02/10] Fix exception handling --- shuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shuffle.py b/shuffle.py index e30852c..db2ede6 100755 --- a/shuffle.py +++ b/shuffle.py @@ -24,7 +24,7 @@ def raises_unicode_error(str): try: str.decode('utf-8').encode('latin-1') return False - except UnicodeEncodeError, UnicodeDecodeError: + except (UnicodeEncodeError, UnicodeDecodeError): return True def hash_error_unicode(item): From 9e4782e3c117b0ef9307f801e27b7c7909a25f80 Mon Sep 17 00:00:00 2001 From: Andrey Mazo Date: Tue, 24 Jun 2014 04:05:32 +0400 Subject: [PATCH 03/10] Remove stale voiceover files to save space --- shuffle.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shuffle.py b/shuffle.py index db2ede6..b72a3e6 100755 --- a/shuffle.py +++ b/shuffle.py @@ -408,6 +408,9 @@ class Shuffler(object): self.rename = rename def initialize(self): + # remove existing voiceover files (they are either useless or will be overwritten anyway) + for dirname in ('iPod_Control/Speakable/Playlists', 'iPod_Control/Speakable/Tracks'): + shutil.rmtree(os.path.join(self.path, dirname), ignore_errors=True) for dirname in ('iPod_Control/iTunes', 'iPod_Control/Music', 'iPod_Control/Speakable/Playlists', 'iPod_Control/Speakable/Tracks'): make_dir_if_absent(os.path.join(self.path, dirname)) From 920589e23a1666e02b784dc59e545c737e970e6c Mon Sep 17 00:00:00 2001 From: Andrey Mazo Date: Tue, 24 Jun 2014 04:16:39 +0400 Subject: [PATCH 04/10] Add installation steps for Gentoo --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 5e85a8d..186016c 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,12 @@ This script requires: From the **Extra** repository: `pacman -S python2 mutagen` and from the AUR: `svox-pico-git` ([link](https://aur.archlinux.org/packages/svox-pico-git/)) +##### Gentoo Linux + +`PYTHON_TARGETS="python2_7" emerge -av media-libs/mutagen` +`layman --add=ikelos` ([ikelos](http://git.overlays.gentoo.org/gitweb/?p=dev/ikelos.git;a=summary)) +`ACCEPT_KEYWORDS="~amd64" emerge -av app-accessibility/svox` + ##TODO * Last.fm Scrobbler * Qt frontend From f64a67403fa20deabf26178420291fd64d88ae8c Mon Sep 17 00:00:00 2001 From: Andrey Mazo Date: Tue, 24 Jun 2014 04:26:06 +0400 Subject: [PATCH 05/10] Support voiceover for Russian files Use RHVoice to generate voiceover for Russian files and pico2wave for all other files. Detect Russian file names using a simple regexp. Assume iPod is mounted using utf8 instead of latin-1 codepage to allow Russian file names. (long file names are UTF-16 on disk anyway) --- README.md | 4 +++- shuffle.py | 56 ++++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 186016c..f5646bc 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,9 @@ This script requires: * [Python 2.7](http://www.python.org/download/releases/2.7/) * [Mutagen](https://code.google.com/p/mutagen/) -* [PicoSpeaker](http://picospeaker.tk/readme.php) +* [PicoSpeaker](http://picospeaker.tk/readme.php) -- for non-Russian files +* [RHVoice (master branch, 3e31edced402a08771d2c48c73213982cbe9333e)](https://github.com/Olga-Yakovleva/RHVoice) -- for Russian files only +* [SoX](http://sox.sourceforge.net) -- for Russian files only ##### Ubuntu diff --git a/shuffle.py b/shuffle.py index b72a3e6..d0b272d 100755 --- a/shuffle.py +++ b/shuffle.py @@ -1,4 +1,5 @@ #!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- import sys import struct import urllib @@ -10,6 +11,9 @@ import subprocess import collections import errno import argparse +import shutil +import re +import tempfile audio_ext = (".mp3", ".m4a", ".m4b", ".m4p", ".aa", ".wav") list_ext = (".pls", ".m3u") @@ -43,6 +47,43 @@ def validate_unicode(path): extension = os.path.splitext(path)[1].lower() return "/".join(path_list) + (extension if last_raise and extension in audio_ext else '') +class Text2Speech(object): + + @staticmethod + def text2speech(out_wav_path, text): + # ensure we deal with unicode later + if not isinstance(text, unicode): + text = unicode(text, 'utf-8') + lang = Text2Speech.guess_lang(text) + if lang == "ru-RU": + Text2Speech.rhvoice(out_wav_path, text) + else: + Text2Speech.pico2wave(out_wav_path, text) + + # guess-language seems like an overkill for now + @staticmethod + def guess_lang(unicodetext): + lang = 'en-GB' + if re.search(u"[А-Яа-я]", unicodetext) is not None: + lang = 'ru-RU' + return lang + + @staticmethod + def pico2wave(out_wav_path, unicodetext): + subprocess.call(["pico2wave", "-l", "en-GB", "-w", out_wav_path, unicodetext]) + + @staticmethod + def rhvoice(out_wav_path, unicodetext): + tmp_file = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) + tmp_file.close() + + proc = subprocess.Popen(["RHVoice", "--voice=Elena", "--variant=Russian", "--volume=100", "-o", tmp_file.name], stdin=subprocess.PIPE) + proc.communicate(input=unicodetext.encode('utf-8')) + # make a little bit louder to be comparable with pico2wave + subprocess.call(["sox", tmp_file.name, out_wav_path, "norm"]) + + os.remove(tmp_file.name) + class Record(object): @@ -75,7 +116,7 @@ class Record(object): # Create the voiceover wav file fn = "".join(["{0:02X}".format(ord(x)) for x in reversed(dbid)]) path = os.path.join(self.base, "iPod_Control", "Speakable", "Tracks" if not playlist else "Playlists", fn + ".wav") - subprocess.call(["pico2wave", "-l", "en-GB", "-w", path, text]) + Text2Speech.text2speech(path, text) def path_to_ipod(self, filename): if os.path.commonprefix([os.path.abspath(filename), self.base]) != self.base: @@ -215,11 +256,6 @@ class Track(Record): def populate(self, filename): self["filename"] = self.path_to_ipod(filename) - # Make the assumption that the FAT filesystem codepage is Latin-1 - # We therefore need to convert any UTF-8 filenames reported by dirwalk - # into Latin-1 names - self["filename"] = self["filename"].decode('utf-8').encode('latin-1') - if os.path.splitext(filename)[1].lower() in (".m4a", ".m4b", ".m4p", ".aa"): self["filetype"] = 2 @@ -243,11 +279,11 @@ class Track(Record): self.albums.append(album) if audio.get("title", "") and audio.get("artist", ""): - text = " - ".join(audio.get("title", "") + audio.get("artist", "")) + text = u" - ".join(audio.get("title", u"") + audio.get("artist", u"")) # Handle the VoiceOverData - if type(text) != type(''): - text = text.encode('utf8', 'ignore') + if isinstance(text, unicode): + text = text.encode('utf-8', 'ignore') self["dbid"] = hashlib.md5(text).digest()[:8] #pylint: disable-msg=E1101 self.text_to_speech(text, self["dbid"]) @@ -452,7 +488,7 @@ class Shuffler(object): # Construct the appropriate iTunesDB file # Construct the appropriate iTunesSD file # http://shuffle3db.wikispaces.com/iTunesSD3gen -# Use festival to produce voiceover data +# Use SVOX pico2wave and RHVoice to produce voiceover data # def check_unicode(path): From 384c92b7c587178210a9d0c7515575303574e057 Mon Sep 17 00:00:00 2001 From: Andrey Mazo Date: Tue, 24 Jun 2014 15:10:57 +0400 Subject: [PATCH 06/10] Add argument to set volume gain for all tracks Partially revert 8b38b0fb90101a44a10573daa5c29f582dca0553 to set default volume gain to 0 instead of 60 (which is extremely loud for my ears with my headphones). --- shuffle.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/shuffle.py b/shuffle.py index d0b272d..810b2c1 100755 --- a/shuffle.py +++ b/shuffle.py @@ -93,6 +93,7 @@ class Record(object): self._fields = {} self.voiceover = parent.voiceover self.rename = parent.rename + self.trackgain = parent.trackgain def __getitem__(self, item): if item not in self._struct.keys(): @@ -230,7 +231,7 @@ class Track(Record): ("header_length", ("I", 0x174)), ("start_at_pos_ms", ("I", 0)), ("stop_at_pos_ms", ("I", 0)), - ("volume_gain", ("I", 60)), + ("volume_gain", ("I", int(self.trackgain))), ("filetype", ("I", 1)), ("filename", ("256s", "\x00" * 256)), ("bookmark", ("I", 0)), @@ -433,7 +434,7 @@ class Playlist(Record): return output + chunks class Shuffler(object): - def __init__(self, path, voiceover=True, rename=False): + def __init__(self, path, voiceover=True, rename=False, trackgain=0): self.path, self.base = self.determine_base(path) self.tracks = [] self.albums = [] @@ -442,6 +443,7 @@ class Shuffler(object): self.tunessd = None self.voiceover = voiceover self.rename = rename + self.trackgain = trackgain def initialize(self): # remove existing voiceover files (they are either useless or will be overwritten anyway) @@ -512,17 +514,28 @@ def check_unicode(path): os.rename(src, dest) return ret_flag +def nonnegative_int(string): + try: + intval = int(string) + except ValueError: + raise argparse.ArgumentTypeError("'%s' must be an integer" % string) + + if intval < 0: + raise argparse.ArgumentTypeError("'%s' is negative while it shouldn't be" % string) + return intval + if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--disable-voiceover', action='store_true', help='Disable Voiceover Feature') parser.add_argument('--rename-unicode', action='store_true', help='Rename Files Causing Unicode Errors, will do minimal required renaming') + parser.add_argument('--track-gain', type=nonnegative_int, default=0, help='Store this (nonnegative integer) volume gain for all tracks; 0 (default) means no gain and is usually fine; e.g. 60 is very loud even on minimal player volume') parser.add_argument('path') result = parser.parse_args() if result.rename_unicode: check_unicode(result.path) - shuffle = Shuffler(result.path, voiceover=not result.disable_voiceover, rename=result.rename_unicode) + shuffle = Shuffler(result.path, voiceover=not result.disable_voiceover, rename=result.rename_unicode, trackgain=result.track_gain) shuffle.initialize() shuffle.populate() shuffle.write_database() From 51c61c5438fdf331625526405a50ea2dfa8d7a48 Mon Sep 17 00:00:00 2001 From: Andrey Mazo Date: Wed, 25 Jun 2014 10:41:36 +0400 Subject: [PATCH 07/10] Update README with --track-gain argument --- README.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f5646bc..7a60583 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,22 @@ Forked from the [shuffle-db-ng project](https://code.google.com/p/shuffle-db-ng/ Just put your audio files into the mass storage of your IPod and shuffle.py will do the rest ```bash $ python shuffle.py -h -usage: shuffle.py [-h] [--disable-voiceover] [--rename-unicode] path +usage: shuffle.py [-h] [--disable-voiceover] [--rename-unicode] + [--track-gain TRACK_GAIN] + path positional arguments: path optional arguments: - -h, --help show this help message and exit - --disable-voiceover Disable Voiceover Feature - --rename-unicode Rename Files Causing Unicode Errors, will do minimal - required renaming + -h, --help show this help message and exit + --disable-voiceover Disable Voiceover Feature + --rename-unicode Rename Files Causing Unicode Errors, will do minimal + required renaming + --track-gain TRACK_GAIN + Store this (nonnegative integer) volume gain for all + tracks; 0 (default) means no gain and is usually fine; + e.g. 60 is very loud even on minimal player volume ``` #### Additions to the original From a25125106bbd031892b5ceb18c3bb83b028f72f6 Mon Sep 17 00:00:00 2001 From: Andrey Mazo Date: Thu, 26 Jun 2014 21:34:29 +0400 Subject: [PATCH 08/10] Update README with RHVoice installation for Gentoo --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7a60583..d03dbca 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,8 @@ From the **Extra** repository: `pacman -S python2 mutagen` and from the AUR: `sv `PYTHON_TARGETS="python2_7" emerge -av media-libs/mutagen` `layman --add=ikelos` ([ikelos](http://git.overlays.gentoo.org/gitweb/?p=dev/ikelos.git;a=summary)) -`ACCEPT_KEYWORDS="~amd64" emerge -av app-accessibility/svox` +`layman --overlays="https://raw.githubusercontent.com/ahippo/rhvoice-gentoo-overlay/master/repositories.xml" --fetch --add=ahippo-rhvoice-overlay` ([ahippo-rhvoice-overlay](https://github.com/ahippo/rhvoice-gentoo-overlay)) +`ACCEPT_KEYWORDS="~amd64" emerge -av app-accessibility/svox app-accessibility/rhvoice` ##TODO * Last.fm Scrobbler From d2ecc95276655a712d052972da1a0533d476c074 Mon Sep 17 00:00:00 2001 From: Andrey Mazo Date: Thu, 26 Jun 2014 21:41:34 +0400 Subject: [PATCH 09/10] Fix Gentoo installation formatting in README --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d03dbca..d75aae6 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,13 @@ From the **Extra** repository: `pacman -S python2 mutagen` and from the AUR: `sv ##### Gentoo Linux -`PYTHON_TARGETS="python2_7" emerge -av media-libs/mutagen` -`layman --add=ikelos` ([ikelos](http://git.overlays.gentoo.org/gitweb/?p=dev/ikelos.git;a=summary)) -`layman --overlays="https://raw.githubusercontent.com/ahippo/rhvoice-gentoo-overlay/master/repositories.xml" --fetch --add=ahippo-rhvoice-overlay` ([ahippo-rhvoice-overlay](https://github.com/ahippo/rhvoice-gentoo-overlay)) -`ACCEPT_KEYWORDS="~amd64" emerge -av app-accessibility/svox app-accessibility/rhvoice` +```bash +PYTHON_TARGETS="python2_7" emerge -av media-libs/mutagen +layman --add=ikelos +layman --overlays="https://raw.githubusercontent.com/ahippo/rhvoice-gentoo-overlay/master/repositories.xml" --fetch --add=ahippo-rhvoice-overlay +ACCEPT_KEYWORDS="~amd64" emerge -av app-accessibility/svox app-accessibility/rhvoice +``` +References to the overlays above: [ikelos](http://git.overlays.gentoo.org/gitweb/?p=dev/ikelos.git;a=summary), [ahippo-rhvoice-overlay](https://github.com/ahippo/rhvoice-gentoo-overlay) ##TODO * Last.fm Scrobbler From 6925159342ea10737b8715ae22bd2d21cf179f00 Mon Sep 17 00:00:00 2001 From: Nimesh Ghelani Date: Sat, 28 Jun 2014 19:38:01 +0530 Subject: [PATCH 10/10] track gain range max limit set to 99 --- README.md | 6 +++--- shuffle.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d75aae6..2026ff7 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ optional arguments: --rename-unicode Rename Files Causing Unicode Errors, will do minimal required renaming --track-gain TRACK_GAIN - Store this (nonnegative integer) volume gain for all - tracks; 0 (default) means no gain and is usually fine; - e.g. 60 is very loud even on minimal player volume + Store this volume gain (0-99) for all tracks; 0 + (default) means no gain and is usually fine; e.g. 60 + is very loud even on minimal player volume ``` #### Additions to the original diff --git a/shuffle.py b/shuffle.py index 810b2c1..12d230e 100755 --- a/shuffle.py +++ b/shuffle.py @@ -520,15 +520,15 @@ def nonnegative_int(string): except ValueError: raise argparse.ArgumentTypeError("'%s' must be an integer" % string) - if intval < 0: - raise argparse.ArgumentTypeError("'%s' is negative while it shouldn't be" % string) + if intval < 0 or intval > 99: + raise argparse.ArgumentTypeError("Track gain value should be in range 0-99") return intval if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--disable-voiceover', action='store_true', help='Disable Voiceover Feature') parser.add_argument('--rename-unicode', action='store_true', help='Rename Files Causing Unicode Errors, will do minimal required renaming') - parser.add_argument('--track-gain', type=nonnegative_int, default=0, help='Store this (nonnegative integer) volume gain for all tracks; 0 (default) means no gain and is usually fine; e.g. 60 is very loud even on minimal player volume') + parser.add_argument('--track-gain', type=nonnegative_int, default=0, help='Store this volume gain (0-99) for all tracks; 0 (default) means no gain and is usually fine; e.g. 60 is very loud even on minimal player volume') parser.add_argument('path') result = parser.parse_args()