minghaiの日記 このページをアンテナに追加 RSSフィード

2008-12-06 [Android] SJISのMP3タグを正しく表示する

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のDBSJISの文字列を入れてくれているので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より

00000yyyyyxxxxxx

110yyyyy10xxxxxx

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

P
82B582DC82B582DC50

C282C2B5C282C39CC282C2B5C282C39C50

一文字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である

otot 2009/04/09 15:13 SJISのID3タグがそもそも規格違反ですよね?v2.4でutf8に変換したのを利用すべきなのでは?

minghaiminghai 2009/04/16 01:03 誰もがWinamp3以前を使っていた昔から日本ではSJISを用いるのが普通でした。
これはMacast等を使っていたMacの世界でも同じです。
変換を強いることは一つの選択肢ですが、上のような簡単な方法でサポートできるのであればサポートするほうが商売上有益であると私は思います。

t 2016/04/09 05:31 こちらのコードを試させてもらったのですが、UTF8で書かれているはずのタグが文字化けするなど動作がおかしかったので、UTF8の文字を直接getbyteで送り込んでみたのですが、UTFで変換されないといけないはずがSJISで文字化けして出力されてしまいます。 Androidもかなりバージョン上がってるので何か動作が変わったりしたのでしょうか…

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証