Chienomi

ポータブルプレイヤーをWalkmanからAndroidに

Live With Linux::dailyhack

最近ウォークマンの調子が悪い。充電すると電池はフルを示す4ゲージになるのだが、10分程度で1ゲージに減ってしまう。その状態でも5時間程度は再生できるのだが、とはいえ3年足らずでこれは、とてもがっかりだ。

だが、それはそれとしても、ウォークマンにはかなりがっかりポイントが色々あった。

  • サポートしているフォーマットにVorbis, Opusがない
  • アルバムアートがcover.jpgではなく、<アルバム名>.jpgである必要がある
  • 接続端子が独自である上に、常時ゴムのカバー(非常に外れやすい)をつけておく必要がある
  • Bluetooth接続が弱く、周囲にBluetoothイヤフォンを使っている人がいるとめちゃくちゃ音が飛ぶ
  • Bluetooth接続ではイコライザは効かない

特に接続端子は面倒で、モバイルバッテリーを使うようなケースでは専用ケーブルを持ち歩く必要があるし、「ゴムカバーが外れた状態での故障は保証しない」という強気さで、でそのカバーが持ち歩いてるといつの間にか外れている。 なのでしょっちゅうゴムカバーのスペアを購入することになるし、ものすごく邪魔だ。

良いところとしては

  • バッテリー持ちが良い
  • 日本語のアーティスト名を読みで並べ替えてくれる

というのがあるのだが、これでバッテリーもちが悪くなってしまうと、あまり使う理由がない。

本記事はこれをきっかけにしてオーディオライブラリの持ち歩きを見直した話なのだが、多岐に渡ってかなりテクニカルなことをしているので、参考になったり、楽しめたりするのではないだろうか。

AAC, Vorbis, Opus

ウォークマンはVorbisをサポートしない関係でAACでエンコードする必要があった。公式サイトによるとサポートされているのは

  • MP3 ( .mp3):32 - 320kbps (VBR対応) / 32, 44.1, 48kHz
  • WMA ( .wma):32 - 192kbps (VBR対応) / 44.1kHz
  • ATRAC ( .oma):48 - 352 kbps (66/105/132kbps は ATRAC3) / 44.1kHz
  • ATRAC Advanced Lossless ( .oma):64 - 352 kbps (132 kbps は ATRAC3 base layer) / 44.1 kHz
  • FLAC ( .flac):16, 24bit / 8-384kHz
  • WAV ( .wav):16, 24, 32bit(Float/Integer) / 8-384kHz
  • AAC ( .mp4, .m4a, .3gp):16-320kbps / 8-48kHz
  • HE-AAC ( .mp4, .m4a, .3gp):32-144kbps / 8-48kHz
  • Apple Lossless ( .mp4, .m4a):16, 24bit / 8-384kHz
  • AIFF ( .aif, .aiff, .afc, .aifc):16, 24, 32bit/ 8-384kHz
  • DSD ( .dsf, .dff):1bit / 2.8224, 5.6448, 11.2896 MHz
  • APE ( .ape):8, 16, 24bit / 8-192kHz (Fast, Normal, High)
  • MQA ( .mqa.flac):対応

とのことである。 SonyとしてはATRACを推したいのだろうけど、なかなか馬鹿げている。 そしてマイナーな形式をサポートしているにも関わらずVorbisをサポートしない。 この中で現実的に最も高音質なのはAACである。

だが、AACはMP3同様に、エンコーダによる音質の差異が大きい音声フォーマットである。 効率的な変換を考えると、音質と両立するにはfdkaacが有力だが、Manjaroの標準のffmpegはfdkaacを含まずにビルドされており、ffmpeg-libfdk_aacというAURパッケージが必要となる。 しかし、ffmpegは様々なものから依存されており、AURパッケージを使うとアップデート時にかなり困ることがある。

ffmpegを使わず、fdkaac(1)を使うという方法もあるが、メタデータ取り扱いにおいて満足できる結果にならなかったため、結局ffmpegを使う方法に戻っている。

AACの場合、.aac(ADTS)だとメタデータの扱いでものすごく困ることが多々あるが、.m4a(MPEG4)だと非常に安定している。だが、取り扱いという意味ではFLACほどじゃないけれどちょっとクセがあるので、歓迎するかというと、正直あんまり歓迎しない。

音質の話をするならば、192kbps以上のAACは非常に良くて、特にAppleのエンコーダは非常に優れている。 Opusはかなり良いが、fdkaacと良い勝負といったところだ。

つまり、現代においてVorbisはわずかに音質面で見劣りする。 一方、Opusもサポートは難しい。Opusをサポートする機器が少なく、AndroidはOpusをサポートするものの、拡張子.opusは認識せず、.ogaまたは.oggである必要がある。さらに、.ogaでは認識しないアプリがいるほか、私が愛用するONKYO HF Music PlayerはOpusをサポートしない。

まとめると、次のようなデメリットを抱えている。

フォーマット 問題点
AAC ファイルそのものの扱いがやや難しく、エンコーダ事情がとても面倒
Vorbis 音質面で他の2フォーマットより見劣りするほか、エンコードが少し遅い
Opus 再生環境が限られる

なお、この話を前提として、「Vorbisで良いか、Opusにこだわるべきか」を判断するべくスペクトラム確認&聴き比べを行ったのだが、「Vorbisのほうが良い音に聴こえる」という問題に直面した。 スペクトラムで見てもVorbisのほうが再現度が高そうだったので非常に困惑したが、ちゃんと見てみるとOpusがだいたい320kbpsだった(311kbps)のに対し、Vorbisは400kbpsを越えていた(405kbps)ので、そういうことかと思ったのだけど、320kbps級のlossyなフォーマットで良し悪しを判断できる耳であることが判明してテンションが上がった。

さて、これを踏まえてなのだけれど、128kbpsあたりでやるつもりであれば、Opusにするのはかなりの利益が得られるからやる価値はある。 192kbpsだとOpusとAACはだいぶクオリティが近づくので、Opusの再生環境の厳しさを考えるとAACのほうが有力だ。 256kbps, 320kbpsであれば音質を取るならAACだけれど、そもそも劣化度合いは相当小さくなってくるため、Vorbisで音質を少し妥協することで扱いやすくするという選択肢もある。

MP3は音質面でも取り扱い面でもそんなに良くないので、MP3にするくらいなら、Ogg Vorbisを使うか、libaac使ってAAC/m4aにするほうがメリットはあると思う。

前提の現状

  • オーディオフルライブラリはカテゴリ分けされ、ネットワークストレージ上に置かれている。元データがPCMである場合、FLACになっている
  • オーディオフルライブラリはアルバムアートが埋め込みになっているものが多い
  • ポータブルライブラリはVorbisが2種類とAACが1種類あるが、Vorbisは古い端末用に変換されたものなので使わない
  • ポータブルライブラリはオーディオライブラリから実際にプレイヤーで再生できる形になっているものを規則的に配置してひとつのライブラリにしている
  • ポータブルライブラリは元ライブラリでWAV/FLACのものについては320kbps VBR AAC(m4a)にしている
  • ポータブルライブラリのAAC変換はfdkaacを使っている
  • プレイリストはポータブルライブラリ上で組まれており、元のライブラリはひとつにまとまっていないという点を含めて再生に適さない
  • ポータブルライブラリはWalkmanへの転送を前提としているため、カバーアートはcover.jpgの形になっているほか、<アルバム名>.jpgとしてもコピーされている
  • ポータブルライブラリは88.1GB, 同様に組んだ場合のオーディオライブラリは226.4GBである

FLACにした場合の44.1kHz/16bitオーディオはだいたい1000kbpsあたりになるため、320kbps AACに対しては3倍程度と考えられる。

FLACでいいのでは…?

割とギリギリではあるが、現状ならFLACでも256GBのmicroSDに収めることができる。 また、将来的に考えても今は512GB microSDが6000円くらいだから、「足りなくなったら512GB」というのもナシではないレベルだ。

microSDはほんとにすぐ壊れるので出費は安くないかもしれないけれど。

まず多くの端末はハイレゾFLACは全くメリットを持たない。 また、Bluetoothイヤフォンを使う場合もFLACであるメリットは全くない。 ここでFLACにする理由はシンプルに「変換めんどい」である。

手間がないことを重視するならVorbisにすればいい、320kbpsあればポータブルプレイヤーで気にすることなどない、ということではあるのだけど、なんとなく精神衛生上「ほかより音質が劣る」ということが気になってしまったためである。

また、FLACかAACにすれば、AndroidでもWalkmanでもどっちでも再生できるSDカードをつくる道が拓ける。 私はシンプルにAACが好きじゃないので、FLACのほうがいい。変換しないほうが圧倒的に問題も少ないし。

だがそもそも、現状ではライブラリとして完成しているのがAAC側だけなので、まずFLACをライブラリとして完成させる必要がある。 逆にいえば、それさえしてしまえば変換は難しい話ではない。

ライブラリ構築への道

ファイルを集める

オーディオライブラリはmasterストレージの様々なディレクトリにあり、これらを変換して集めたのが現在のポータブルライブラリである。 ポータブルライブラリで調整されたものもあり、単にこれらをひとつのディレクトリに集めただけでは足りないが、まずはひとつのディレクトリにする必要がある。

masterストレージはファイルサーバー上のHDDであるが、書き込みは断片化を避けるためまとまった形での転送のみが許可される。通常はSSHFSでroマウントしている。

これをメインPCのSSD上に載せると、更新を反映するのも楽になる。

ファイルサーバーへの反映はnasrsyncというコマンドを用いてやっている。このコマンドは

nasrsync <from...> <to>

の形となっており、基本的にはrsyncの引数だが、サーバー側ではrrsyncが使われるため<to>仮想ルートからのパスになる。

ライブラリ上ではファイルサーバー上ではディレクトリ名も変わっているため推測不可能だが、個々のディレクトリをファイルサーバー上のディレクトリにマップすることが可能である。

そこで、.upstreamというテキストファイルを置くことにした。 このファイルに

/global/media/sound/CD/

などと書いておき、

nasrsync ./ $(<.upstream)

とすれば同期できるわけだ。

プレイリスト変換

AAC側のプレイリストは、当然ながら拡張子.m4aになっているからファイルパスそのままでは機能しない。 また、AAC側はプレイヤーでの再生を考慮してDOS形式にファイル名が丸められている。 これは、LinuxよりもDOSファイルシステムのほうが使える文字種が限られており、Linuxではvalidなファイル名がDOSファイルシステムでは通らないことがあるためである。

そこで、これまで何度か登場しているあいまいマッチングのテクニックを使って存在するファイルに投射していく。 ここでAAC側には存在するがFLAC側にはないファイル、何らかの理由でファイル名が異なっているファイル、同一パスとみなされる重複したファイルを検出する。

この処理はconvert-playlist.rbとしてripcdに置いてある。

#!/bin/ruby
require 'find'

SOURCE_DIR = File.expand_path ARGV.shift
DEST_DIR = File.expand_path ARGV.shift

PLAYLISTS = {}

$music_db = {}

if !DEST_DIR || DEST_DIR.empty?
  abort "convert-playlist.rb"
end

Dir.chdir DEST_DIR

playlist_files = []

Find.find(".") do |fp|
  if not %w:.wav .flac .ogg .mp3 .aac .m4a .oga .opus .wma .ra:.include? File.extname fp
    next
  end
  nfp = fp.unicode_normalize().downcase.sub(/\.[^.]+$/, "").delete('!?"\\<>*|:_ -')
  if $music_db[nfp]
    abort "Normalized path name #{nfp} is not unique (assigning #{fp}, already #{$music_db[nfp]})"
  end
  $music_db[nfp] = fp
end

Dir.chdir SOURCE_DIR

playlist_files = Dir.glob("*.m3u")

playlist_files.each do |pfp|
  pfl = []
  File.foreach(pfp) do |line|
    if line =~ /^\s*#/
      pfl.push line
    else
      ptrp = pfp.sub(%r:^./:, "").include?("/") ? (pfp.sub(%r:/[^/]*$:, "") + "/" + line).strip.unicode_normalize().downcase.sub(/\.[^.]+$/, "").delete('!?"\\<>*|:_ -') : line.strip.unicode_normalize().downcase.sub(/\.[^.]+$/, "").delete('!?"\\<>*|:_ -')
      if ptrp !~ %r:^\./:
        ptrp = "./" + ptrp
      end
      ptr_dest = $music_db[ptrp]
      unless ptr_dest
        #pp $music_db
        abort "No match #{line.strip} in #{pfp}"
      end
      pfl.push(ptr_dest + "\n")
    end
  end
  PLAYLISTS[pfp] = pfl.join
end

Dir.chdir DEST_DIR

PLAYLISTS.each do |k, v|
  File.open(k, "w") do |f|
    f.puts v
  end
end

これが通るようになるまでやると、かなりライブラリとしては整ってくる。

(中にはプレイリスト自体が古かったりして苦労した。)

まいてつ……

まいてつというエロゲーのコンプリートパック(DLsiteで安売りしてることが多いので結構有名)に入っているサントラはすごく面倒な構造をしているので、これを他と同じ形式になるように整える。 ちなみに、これは整ったソースライブラリが存在しないので、元データから新規に起こしている。

まずはartist/album/song形式になるのが正しいため、Loseディレクトリ以下にフラットになるように配置していき、

for i in *
do
  (
    cd $i
    flac --best *.wav
    rm *.wav
  )
done

としてFLACにする。

あとはkid3を使ってメタデータを書いていけばいいのだけど、なかなかめんどくさい。 とりあえずtrack, artist, titleは埋めたいところだが、これまた一貫性がない。

ファイル名にartist名等がないものはとりあえず無視して、ファイル名=titleとみなしてkid3で処理する。

<track>_「<title>」<artist>.<ext>形式になっているものは次の方法で拾う。

#!/usr/bin/ruby
# -*- mode: ruby; coding: UTF-8 -*-
require 'taglib'

Dir.glob("*.flac").each do |i|
  abort "FILE NAME INVALID" unless i =~ /^(\d+)_「(.*?)」(.*)\.flac/
  track = $1
  title = $2
  artist = $3
  TagLib::FileRef.open(i) do |flac|
    tag = flac.tag
    tag.track = track.to_i
    tag.title = title
    tag.artist = artist
    flac.save
  end
end

これでAlbum Artist, Album, Disc Numberを入れればOKである。 ただし、11_「ロオド・ラスト」ハチロクver.wavのような例外が紛れ込んでおり、これは手で修正が必要。

続いて<track>_「<title>」(<scene>) <artist>.<ext>形式になっているもの。 こちらは

#!/usr/bin/ruby
# -*- mode: ruby; coding: UTF-8 -*-
require 'taglib'

Dir.glob("*.flac").each do |i|
 p i
  abort "FILE NAME INVALID" unless i =~ /^(\d+)_「(.*?)」\((.*?)\) (.*)\.flac/
  track = $1
  title = $2 + $3
  artist = $4
  album = File.basename Dir.pwd.sub(/-\d+$/, "")
  TagLib::FileRef.open(i) do |flac|
    tag = flac.tag
    tag.track = track.to_i
    tag.title = title
    tag.artist = artist
    tag.album = album
    flac.save
  end
end

として拾う。 だが、こちらは02_「GRAND EXPRESS」(グランドOP さくらみこ.wavというファイル(カッコが閉じてない)が紛れこんでおり、こちらはファイル名のほうを手で修正した。

DOSファイル名への変換

DOSファイル名の変換はいままで圧縮ライブラリ生成時に行っていたのだが、面倒なので元ライブラリ側を修正してしまうことにした。 これはファイルサーバー上にも影響が出るため、ファイルサーバーにも同じ処理が必要。

#!/bin/zsh
find . -name '*[?"\<>*|:]*' | while read; do mv -v "$REPLY" "${REPLY//[\?\"\\<>\*|:]/_}"; done

ファイルサーバーではコマンドとして実行するため、1行になっている。

この方法はパスの中に複数回禁止文字が含まれる場合、親ディレクトリのリネームが発生して失敗する。 回避するようにも書けるが、それよりは単純に複数回実行して全部置き換えるまでやるほうが簡単。

プレイリストにも影響が出るので、同じことをプレイリストにもやる。 いつも言っているけれど、sedのアドレス指定の仕方は覚えておくべき。

#!/bin/zsh
for i in *.m3u
do
  sed -i '/^[^#]/ s/[?"<>*|:]/_/g' $i
done

アルバムアートの転送

アルバムアートをcover.jpgにする処理は変換時に行っている上に、AAC側で追加したアルバムアートもあるため、FLAC側が不完全だ。 そこで、また曖昧マッチングしてカバーアートをコピーする。

#!/bin/ruby
require 'find'

SOURCE_DIR = File.expand_path ARGV.shift
DEST_DIR = File.expand_path ARGV.shift

$cover_db = {}

if !DEST_DIR || DEST_DIR.empty?
  abort "convert-playlist.rb"
end

Dir.chdir DEST_DIR

playlist_files = []

Find.find(".") do |fp|
  next unless File.directory? fp
  nfp = fp.sub(%r:^\./:, "").unicode_normalize().downcase.delete('!?"\\<>*|:_ -')
  $cover_db[nfp] = fp
end

Dir.chdir SOURCE_DIR

Find.find(".") do |fp|
  if File.basename(fp) =~ /^cover\./
    nfp = File.dirname(fp).sub(%r:^\./:, "").unicode_normalize().downcase.delete('!?"\\<>*|:_ -')
    dir = $cover_db[nfp]
    unless dir
      $stderr.puts "Not found: #{nfp}"
      File.open("/home/haruka/cover_not_found", "a") {|f| f.puts fp}
      next
    end
    target_dir = [DEST_DIR, dir].join("/")
    next if File.exist?("#{target_dir}/cover.jpg") || File.exist?("#{target_dir}/cover.png")
    system("cp", "-v", fp, (target_dir + "/"))
  end
end

アルバムアートの複製

cover.jpgをアルバムネーム(ディレクトリ名)に合わせてコピーする。

#!/bin/zsh
for i in **/cover.(jpg|png)
do
  ext=${${i:t}:e}
  if [[ ! -e "${i:h}/${${i:h}:t}.${ext}" ]]
  then
    cp -v "$i" "${i:h}/${${i:h}:t}.${ext}"
  fi
done

ライブラリの転送

SDカードへの転送を念頭に置いている。

各種スクリプトファイルや.upstreamファイルなどが置かれているが、これらはオリジナルにのみあるべきものなので転送には含めない。

また、DOS形式のファイルに合わせるため、rsyncの-yオプションを使う。

ripitを使ってリッピングしているため、CDのアルバムディレクトリには.m3uファイルが含まれているが、これは機能せず邪魔なので、これも除外して後から手動でやる。

このスクリプトはAACライブラリにあったものをベースにしている。

#!/bin/zsh

if (( $# != 1 ))
then
  print "transfar.zsh <dest>" >&2
  exit 1
fi

rsync -rvy --exclude "*.zsh" --exclude "*.txt" --exclude "*.rb" --exclude ".*" --exclude "*.m3u" --ignore-existing ./ "${1%/}"/

圧縮ライブラリの作成

寝室PCのほうは持っているデータ量がやや多いことと、メインPCが7TBのSSDディスクを持つのに対して寝室は4TBであるため、大きいライブラリを持つとやや圧迫感がある。 そこで、従来同様に圧縮ライブラリを持つようにした。

これは、ポータブルプレイヤーと共有するためではなく、今後のためのテストを兼ねたものである。

#!/bin/zsh
setopt EXTENDED_GLOB

DEST_DIR="$1"
shift

rsync -rv --ignore-existing --exclude "*.flac" --exclude ".*" --exclude "*.zsh" --exclude "*.rb" --exclude "*.txt" --exclude "*.m3u" ./ "$DEST_DIR"/

for i in *~HiResMusics(/)
do
  (
    cd $i
    for j in **/*.flac
    do
      if [[ ! -e "$DEST_DIR/$i/${j:h}" ]]
      then
        mkdir -pv "$DEST_DIR/$i/${j:h}"
      fi
      
      ffmpeg -n -i "$j" -vn -c:v libopus -ac 2 -b:a 320k "$DEST_DIR/$i/${j:r}".opus
    done
  ) &
done

(
  cd HiResMusics
  for j in **/*.flac
  do
    if [[ ! -e "$DEST_DIR/HiResMusics/${j:h}" ]]
    then
      mkdir -pv "$DEST_DIR/HiResMusics/${j:h}"
    fi

    ffmpeg -n -i "$j" -vn -c:v libopus -b:a 320k -ar 48000 -ac 2 "$DEST_DIR/HiResMusics/${j:r}".opus
  done
)

wait

機能はするが、やや微妙である。 保有するライブラリのうち、FLACは以下のように配分されている。

Amazon : 444
CD : 6443
DLM : 497
HAL-Remastered : 3
HAL-Remix : 3
Harukamy : 21
HiResMusics : 166
SXG_Render : 30

変換が必要なファイルは圧倒的にCDに多い。

そのため、マルチスレッドで処理するようになっているにも関わらず、最終的には1スレッドだけが残ってしまう。 また、重複処理の排除をファイル単位でやっているため、無駄に重いループだ。

途中で間違ってPCを落としてしまったため、CDだけ16並列で別途処理するようにした。

#!/usr/bin/ruby
# -*- mode: ruby; coding: UTF-8 -*-

Dir.chdir("CD")
DEST_DIR = File.expand_path ARGV.shift

abort "No dest dir" unless DEST_DIR

list = []

16.times do |i|
  list.push []
end

Dir.glob("*/*").each_with_index do |x, index|
  list[index % 16].push x
end

list.each do |albums|
  fork do
    albums.each do |album|
      unless File.exist? [DEST_DIR, "CD", album].join("/")
        system "mkdir", "-pv", [DEST_DIR, "CD", album].join("/")
      end

      Dir.children(album).each do |i|
        next unless File.extname(i) == ".flac"
        system "ffmpeg", "-n", "-i", [album, i].join("/"), "-vn", "-c:v", "libopus", "-ac", "2", "-b:a", "320k", [DEST_DIR, "CD", album, i.sub(/\.flac$/, ".opus")].join("/")
      end
    end
  end
end

Process.waitall

並列にするとだいぶややこしいので、直列にして差分で送りやすいようにしたほうが良さそう。

なお、Opusにするときは-ac 2は必須で、今回の場合はハイレゾのままにはしたくないので-ar 48000も入れている。

プレイヤーの問題

ウォークマン

充電しておけば1日は使えるため、「使えない」というのはちょっと気が早い。 とりあえず共存できるフォーマットにはしたので、納得のいく形になるまでつなぐことは可能だ。

以前使っていたCOWON M2ほど特殊なフォーマットにはなっていないため、共存しやすいのは大きい。 ただし、もう少しライブラリサイズが大きくなると256GBをこえてしまうが、この場合はOgg VorbisであってもOgg Opusであっても対応できない。

F-02H

Fujitsu Arrowsの2016年のフラッグシップモデルで、Snapdragon808を搭載するタフネススマホ。 ハンドソープで洗ったが、特に問題はなかった。

もともと今の会社でGoogle Authenticatorの利用が必須になったときに会社用にするために買ったのだけど、本モデルではそもそも会社のアカウントでログインして使うことができなかったため、眠っていた。

しかしこやつ、Dolby持ちで、ハイレゾも対応したDACを載せているというプレイヤーとしては悪くないもの。 大きいのと、実用性に乏しい虹彩認証しか持ってないのが難点だったりするけれど。

ただ、実際使ってみると音飛びする。 そういえばあったなぁ、そんなの。SDカードから再生すると音飛びして使い物にならないっていうの。 Axon7で完璧に快適に聴けてたから忘れてた。 Zenfone 2 Selfieとか、その前のK4000 Proとか使ってたときは結構苦しめられてた。

本体だと発生しないのでおそらくは転送速度とバッファリングなんだけど、そんなことあるだろうか。 使ってるのSanDisk Ultraだというのに。

HF Playerは大惨事、Foobar2000も飛び飛びだった。 Powerampは問題なさそう。 飛びにくいプレイヤーというとPulserが思いつくけど、今はプレイリスト中心の使い方になっているので無理。 mpvもバックグラウンド再生できないので無理。

なお、Foobar2000は普通にUTF-8になっている.m3uのエンコーディングを正しく認識できないため、Foobar2000に合わせると複数環境での共存が難しいので使わないほうが良さそう。

やっぱりAndroidスマートフォンで音楽を楽しむのはかなり難しい感じがする。

また、古いスマートフォンだと音楽を再生してるとバッテリーがごりごり減る。 再生可能時間が結構短い。

補足 2024-03-12

Android 13で.opusが認識されるようになった模様。