суббота, 7 декабря 2013 г.

Аудио книги

В последнее время пристрастился я слушать аудио книги. Слушаю на сматрфоне LG Optimus G e975

Слушаю с помощью программы Ambling BookPlayer. Автор этой программы залег на дно, и уже не принимает советов и просьб по усовершенствованию программы (а у меня есть пара идеек). Зато GooglePlay уверенно принимает деньги за эту программу, чем я и воспользовался.

Аудио книги почему то принято разбивать на огромное количество файлов. Даже если один файл - одна глава, все равно это как то надумано. Мне почему то не нравится такая файло-помоечная структура. По крайней мере хранить этот бардак неудобно.

Я написал на bash небольшую программку, которая объединяет эти файлы в один, а заодно создает XML файл для экспорта в bookreader.
Работает она из под наутилуса Ubuntu.
#!/bin/bash
#----------------------------------------------------------------------
# Программа объеденения музыкальных файлов (книг)
# Для установки скопировать в папку 
#  ~/.local/share/nautilus/scripts

#----------------------------------------------------------------------
# --- Версионность ---
# Начальная версия
#VERSION=0.1
# улучшение алгоритмов кодирования
#VERSION=0.2
#VERSION=0.3
# Заполнение полей в соответствии с xml описанием для импорта в ambling book reader
#VERSION=0.4
# Добавлено переименование обложки в соответствии с названием книги.
#VERSION=0.41
# Баг. Обложка не конвертится. (fixed)
# Добавить полоску времени, а не пульсар (fixed)
#VERSION=0.42
# Добавлено управление битрейтом, генерацией Обложки, XML, результирующего файла (fixed)
VERSION=0.43

#----------------------------------------------------------------------
# Переменные и константы
ERROR=192
INPUT="concat:"
BITRATE="96k|32k|64k|128k|192k|256k"
MIN_BITRATE="96k|32k|64k|128k|192k|256k"
MAX_BITRATE="96k|32k|64k|128k|192k|256k"
CODEC="libvorbis"
CURENT_DIR=$(pwd)
IN_DURATION=0
CREATE_COVER=TRUE
CREATE_XML=TRUE
CREATE_ONEOGG=TRUE

#----------------------------------------------------------------------
# xml ambling bookplayer Tags
BOOK_NAME=""
AUTHOR_NAME=""
NARRATOR_NAME=""
PUBLISHER=""
COPYRIGHT=""
IMAGE_FILE_NAME=""
SERIES_NAME=""
SERIES_SEQUENCE=""

#----------------------------------------------------------------------
# audiofile Tags (атавизм, времени не было сделать ревизию)
TITLE=""
ARTIST=""
ALBUM_ARTIST=""
ALBUM=""
GENRE="AudioBook"
DATE=""
TRACK=""
PUBLISHER=""
ENCODED_BY=""
COPYRIGHT=""
COMPOSER=""
PERFORMER=""
DISC=""
ENCODER="ffmpeg"
COVER=""

check_installed()
{
  PKG_OK=$(dpkg-query -W --showformat='${Status}\n' $1 |grep "install ok installed")

  if [ "" == "$PKG_OK" ]; then
    zenity --no-wrap --error --text="$1 package not found!\n Install it"
    exit $ERROR
  fi
}

# Выход по ошибке
exit_on_error()
{
  yad --title "Error" --text "$1" 200 200
  exit 192
}

# Возвращает в секундах время звучания звукового файла
duration()
{
  RET=$(ffmpeg -i "$1" 2>&1 | grep Duration | awk '{ print $2 }' | tr -d ',')
  S=$(echo ${RET:0:2}*60*60+${RET:3:2}*60+${RET:6:2} | bc)
  echo $S
}

check_nautilus_env()
{
  #--------------------------------------------------------------------
  # Сохраняем для отладки
  echo "Аргументы командной строки запуска скрипта в Nautius" >> tst.txt
  echo "$NAUTILUS_SCRIPT_SELECTED_FILE_PATHS" >> tst.txt
  echo "" >> tst.txt

  if [ "$NAUTILUS_SCRIPT_SELECTED_FILE_PATHS" == "" ]; then
    echo "Этот скрипт запускается из под наутилуса"
    exit_on_error
  fi
}

#######################################################################
#                                Main code
#

#----------------------------------------------------------------------
check_nautilus_env
check_installed yad
check_installed python-mutagen

#----------------------------------------------------------------------
# Получаем имена тегов

AUTHOR_NAME=$(mid3iconv --encoding=Windows-1251 --dry-run --debug "$1" | grep TPE1 | sed 's/TPE1=//')
if [ "$AUTHOR_NAME" == "" ]; then
  AUTHOR_NAME="$1"
fi

BOOK_NAME=$(mid3iconv --encoding=Windows-1251 --dry-run --debug "$1" | grep TIT2 | sed 's/TIT2=//')
if [ "$BOOK_NAME" == "" ]; then
  BOOK_NAME="$1"
fi

SERIES_NAME="$BOOK_NAME"

TAGS=$(yad --geometry=400x200 --center\
    --title="Enter Track Tags" \
    --form --text="$CURENT_DIR" \
    --field="Название Книги" \
    --field="Автор Книги" \
    --field="Название серии" \
    --field="Номер книги в серии" \
    --field="Читает" \
    --field="Обложка:FL" \
    --field="Конвертировать обложку:CHK" \
    --field="Создавать XML:CHK" \
    --field="Создавать ogg файл:CHK" \
    --field="Средний битрейт:CB" \
    --field="Минимальный битрейт:CB" \
    --field="Максимальный битрейт:CB" \
    --item-separator='|' \
    "$BOOK_NAME" \
    "$AUTHOR_NAME" \
    "$SERIES_NAME" \
    "$SERIES_SEQUENCE" \
    "$NARRATOR_NAME" \
    "$IMAGE_FILE_NAME" \
    $CREATE_COVER \
    $CREATE_XML \
    $CREATE_ONEOGG \
    $BITRATE \
    $MIN_BITRATE \
    $MAX_BITRATE \
    --button="gtk-ok:0" --button="gtk-cancel:1")

if [[ $? -eq 1 ]]; then
  #  cancel button pressed
  exit 192
fi

BOOK_NAME=$(echo $TAGS | awk 'BEGIN {FS="|" } { print $1 }')
BOOK_NAME=$(echo "$BOOK_NAME" | tr '[ ]' '[_]')
AUTHOR_NAME=$(echo $TAGS | awk 'BEGIN {FS="|" } { print $2 }')
AUTHOR_NAME=$(echo "$AUTHOR_NAME" | tr '[ ]' '[_]')
SERIES_NAME=$(echo $TAGS | awk 'BEGIN {FS="|" } { print $3 }')
SERIES_NAME=$(echo "$SERIES_NAME" | tr '[ ]' '[_]')
SERIES_SEQUENCE=$(echo $TAGS | awk 'BEGIN {FS="|" } { print $4 }')
SERIES_SEQUENCE=$(echo "$SERIES_SEQUENCE" | tr '[ ]' '[_]')
NARRATOR_NAME=$(echo $TAGS | awk 'BEGIN {FS="|" } { print $5 }')
NARRATOR_NAME=$(echo "$NARRATOR_NAME" | tr '[ ]' '[_]')
IMAGE_FILE_NAME=$(echo $TAGS | awk 'BEGIN {FS="|" } { print $6 }')
IMAGE_FILE_NAME=$(basename $IMAGE_FILE_NAME)
CREATE_COVER=$(echo $TAGS | awk 'BEGIN {FS="|" } { print $7 }')
CREATE_XML=$(echo $TAGS | awk 'BEGIN {FS="|" } { print $8 }')
CREATE_ONEOGG=$(echo $TAGS | awk 'BEGIN {FS="|" } { print $9 }')
BITRATE=$(echo $TAGS | awk 'BEGIN {FS="|" } { print $10 }')
MIN_BITRATE=$(echo $TAGS | awk 'BEGIN {FS="|" } { print $11 }')
MAX_BITRATE=$(echo $TAGS | awk 'BEGIN {FS="|" } { print $12 }')
BUTTON_OK=$(echo $TAGS | awk 'BEGIN {FS="|" } { print $13 }')
BUTTON_CANSEL=$(echo $TAGS | awk 'BEGIN {FS="|" } { print $14 }')

#----------------------------------------------------------------------
# Сохраняем для отладки
echo "RAW значения формы----------------------------------" > tst.txt
echo $TAGS >> tst.txt
echo "
Разобраные значения формы----------------------------------
BOOK_NAME: $BOOK_NAME
AUTHOR_NAME: $AUTHOR_NAME
SERIES_NAME: $SERIES_NAME
SERIES_SEQUENCE: $SERIES_SEQUENCE
NARRATOR_NAME: $NARRATOR_NAME
IMAGE_FILE_NAME: $IMAGE_FILE_NAME
" >> tst.txt

for arg
do
  NF=$(echo "$arg" | tr '[ ]' '[_]')
  INPUT=$INPUT"|$NF"
  if [ ! "$arg" == "$NF" ]; 
    then 
      if [ -e "$NF" ]; then
        exit_on_error "$NF: File exist already.\n\nExiting."
      fi
      mv "$arg" "$NF"; 
  fi
  NF_DURATION=$(duration $NF)
  IN_DURATION=$(( $IN_DURATION+$NF_DURATION ))
done

INPUT=$(echo "$INPUT" | sed 's/:|/:/')

FFMPEG_CMD="ffmpeg -i \"$INPUT\" \\
-maxrate $MAX_BITRATE -minrate $MIN_BITRATE -ab $BITRATE -acodec $CODEC \\
-metadata title=\"$BOOK_NAME\" \\
-metadata artist=\"$AUTHOR_NAME\" \\
-metadata genre=\"Audiobook\" \\
-metadata album_artist=\"$NARRATOR_NAME\" \\
-metadata album=\"$SERIES_NAME\" \\
-metadata date=\"$DATE\" \\
-metadata track=\"$SERIES_SEQUENCE\" \\
-metadata publisher=\"$PUBLISHER\" \\
-metadata encoded_by=\"$ENCODED_BY\" \\
-metadata copyright=\"$COPYRIGHT\" \\
-metadata composer=\"$COMPOSER\" \\
-metadata performer=\"$PERFORMER\" \\
-metadata disc=\"$DISC\" \\
-metadata encoder=\"$ENCODER\" \\
\"$AUTHOR_NAME-$BOOK_NAME.ogg\""

#----------------------------------------------------------------------
# Сохраняем для отладки
echo "--------------------------------"
echo "Время зввучания: $IN_DURATION сек." >> tst.txt

#----------------------------------------------------------------------
# Для отладки удобно эти команды скинуть в отдельные командные файлы
echo "#!/bin/bash" > ffmpeg_cmd.sh
echo "" >> ffmpeg_cmd.sh
echo "$FFMPEG_CMD" >> ffmpeg_cmd.sh
$(chmod +x ffmpeg_cmd.sh)

echo "#!/bin/bash" > cover_convert_cmd.sh
echo "" >> cover_convert_cmd.sh
echo "convert $IMAGE_FILE_NAME -resize 400x400 $AUTHOR_NAME-$BOOK_NAME.jpg">> cover_convert_cmd.sh
$(chmod +x cover_convert_cmd.sh)

#----------------------------------------------------------------------
# Сконвертируем обложку книги
if [ $CREATE_COVER == "TRUE" ]; then
  RET=$(./cover_convert_cmd.sh)
fi

#----------------------------------------------------------------------
# Сгенерим xml описание книги
if [ $CREATE_XML == "TRUE" ]; then
  echo "
    $BOOK_NAME
    $AUTHOR_NAME
    /sdcard/audiobooks/$AUTHOR_NAME-$BOOK_NAME.jpg
    $SERIES_NAME
    $SERIES_SEQUENCE
    $NARRATOR_NAME
    
        $BOOK_NAME 
        
    
    
        /sdcard/audiobooks/$AUTHOR_NAME-$BOOK_NAME.ogg
    
" > $AUTHOR_NAME-$BOOK_NAME.xml
fi

#----------------------------------------------------------------------
# Собственно, запуск конвертирования
if [ $CREATE_ONEOGG == "TRUE" ]; then
  touch /tmp/$$
  ( ( echo 1 
      while [ -f /tmp/$$ ] 
        do 
          sleep 10
   # Вычисления процента сконвертированного
          OUT_DURATION=$(duration "$AUTHOR_NAME-$BOOK_NAME.ogg")
          PERCENTAGE=$(( OUT_DURATION*100/IN_DURATION))
          echo $PERCENTAGE
        done
      echo 100 ) | yad --title "$CURENT_DIR" \
                     --progress \
                     --progress-text="$AUTHOR_NAME-$BOOK_NAME.ogg" \
                     --percent=0 --auto-close --no-buttons --width=300) &
  RET=$(./ffmpeg_cmd.sh)
  rm /tmp/$$
  wait
fi

#----------------------------------------------------------------------
# Конец
yad --no-wrap \
   --title "Конвертирование завершено" \
   --text "$CURENT_DIR/$AUTHOR_NAME-$BOOK_NAME.ogg"

rm ./cover_convert_cmd.sh
rm ./ffmpeg_cmd.sh
rm ./tst.txt


Если захотите ей воспользоваться, то сохрание ее в ~/.local/share/nautilus/scripts/merge_mp3.sh. После чего дайт права на исполнение следующей командой.
chmod +x ~/.local/share/nautilus/scripts/merge_mp3.sh
Кроме того необходимо установить пакеты yad и python-mutagen

Пользоваться ей очень просто. Сначала в Nautilus выделяю файлы книги, которые хочу объединить.




Затем нажимаю правую кнопку и выбираю скрипт:




В появившемся окне заполняю поля, которые будут отображаться bookplayer




Наблюдаем за ходом конвертирования



Дизастер! Оказалось, что в Android плохая реализация проигрывания ogg файлов с переменным битрейтом. Плеер не может точно позиционировать на выбранное место. Выглядит это так: вы выставляете проигрывание с позиции 1 час 28 мин 33 сек. Плеер передвигает вперед минут на 20 и начинает воспроизведение. За это я господ их Гугле материл. Бороться с этим можно только установив при кодировании постоянный битрейт.

Для книги вполне достаточно битрейта 96 к.


Полезные ссылки:
1. Кодирование аудио с потерями. Что к чему?
2. mutagen