SJISのMP3タグを正しく表示する

MP3のID3にはエンコーディングの情報がない。
AndroidではMP3のタグはUTF-8でないと表示できないという仕様がある。
日本ではiTunesが流行る以前にはほとんどのツールがMP3タグにSJISを用いていた。
このため、AndroidにはせっかくMP3タグを自動でDBにて管理する機能がついているのに日本人には非常に使いにくいものとなっている。


Javaには漢字コードの変換がとても簡単に行える機能がある。
AndroidXPathがなかったりするけれど、運が良ければ簡単にSJISのID3タグが表示できるだろうと考えてやってみた。
それがまた、はまり道の始まりだった。

余計なことをするMediaScanner

最初はReaderを使うものだとばかりに思っていた。
しかし考えてみると手元に文字列はあるのだからWriterを使うのが正しい。
Javaには歴史的にReader/WriterとInputStream/OutputStreamという二つのIOのAPIがあり、さらにnioという拡張パッケージもありややこしい。
漢字コードの指定ができるのはInputStreamReaderというInputStreamを引数に取るReaderだ。
これがなぜかReaderは引数に取れない。
このため組み合わせを考えるのにいつも余計な時間を取られてうっとおしい。
今回MediaScannerがsqlite3のDBにSJISの文字列を入れてくれているのでSJISのStringを読みこんで、UTF-8を書き出せば良いと考えた。
良く考えるとAndroidのCursorにはgetBlobというのがあり、Stringでなく、byteで取りだせる。
さらにStringのコンストラクタにはbyte
に対する漢字コードが指定できたりする。
散々コードを書いてから単純にString(byte, String)だけで良いことに気付いた。


そこで散々に考えた上で、単純にgetBlobを行いStringのコンストラクタにてUTF-8に変換する関数を作成し、TextViewのsetTextに対しそれをかませた。
ところがこれを行ってもちっとも文字化けが直らない。
そこで色々とチェックをした上でbyteをLogで出力してみたところ実に変な仕様がわかったのである。


なんとMediaScannerはSJISの文字列を読むと、それをUTF-8の2byteコードに置き換えているようだ。
SJISは漢字が2byteで表現され、必ず1byte目が0x80以上になる。
UTF-8ではこれを以下のように表現する。

ここのTable3.1より

0 0 0 0 0 y y y y y x x x x x x

1 1 0 y y y y y 1 0 x x x x x x


しかしAndroidではSJISでしましまPが以下のように変換されてしまう。

P
82B5 82DC 82B5 82DC 50

C282 C2B5 C282 C39C C282 C2B5 C282 C39C 50


一文字2バイトが実に4バイトに変換される。
あくまでも推定だがどうも頭に0の1バイトを足して勝手に変換を行なっているようだ。

この変換後の文字列はもちろん文字化けして読めない文字列になるが、UTF-8の文字列として正しい。
このためContentProviderからgetBlobしたバイト列は既にSJISではなくUTF-8である。
MediaScannerがDBに入れた文字列をsqlite3で取ろうが、ContentProviderにて取ろうが既に文字化けを起こしている。


これを正しい文字列に直すには一旦UTF-8からSJISのバイト列に直してあげる必要がある。
さらに元々がSJISであったのか、それともUTF-8であったのかを判断するにはSJISに変更した上で、それがSJISとして正しい文字列であるのかどうかチェックする必要がある。
その判断に従い、本当にSJISであるのならばCharSetNameにSJISの指定をしてUTF-16に変換を行うのだ。*1
SJISの値として取れる値域は実はせまいので運が良ければこれでうまくいくはずだ。

http://qpon.quu.cc/pc/sjis.htm

実はこれでもうまくいかないケースがあるような気がして仕方がないが今のところうまく行っているので問題がでるまで忘れることにした。
以下はそのためのコードだ。

		private String utf8toSJIS(byte[] b) {
			int len = b.length;
			byte[] nb = new byte[len];
			int i = 0;
			int j = 0;
			while (i < len) {
				byte first = b[i++];
				if (first == 0) break;
				
				if ((first & 0x80) == 0) {
					nb[j++] = first;
				} else {
					byte second = b[i++];
					nb[j++] = (byte)((((first & 0x03) << 6) | (second & 0x3f)) & 0xff);
				}
			}
			
			byte[] last = new byte[j];
			System.arraycopy(nb, 0, last, 0, j);
			
			try {
				if (isValidSJIS(last))
					return new String(last, "SJIS");
				else
					return new String(b);
			} catch (UnsupportedEncodingException e) {
				throw new RuntimeException(e);
			}
		}

		private boolean isValidSJIS(byte[] last) {
			int len = last.length;
			int i = 0;
			while (i < len) {
				int b = last[i++] & 0xff;
				if (b < 0x80) continue;
				
				if (i >= len) return false;
				
				int c = last[i++] & 0xff;
				
				if ((0x81 <= b && 0x9f >= b) || (0xe0 <= b && 0xef >= b)) {
					if (0x40 <= c && 0xfc >= c && 0x7f != c) {
						continue;
					}
				}

				return false;			
			}
			return true;
		}

とりあえずこのコードを足すことによりAndroidにてSJIS及びUTF-8のID3タグを両方とも問題なく表示することに成功した。
といってもテストが足りなさすぎるのでバグ報告を歓迎します。
よろしくお願いいたします。

*1:ややこしいがJVM上ではStringはUTF-16である