ルギア君の戯言

雑多な記事。

というわけで

できた。

/* License is GPL. Treat as free but no support.
 * This program needs "oggenc" "ogginfo" "ffmpeg" "iconv" to run.
 * This program needs "taglib" to compile.
 *
 * ex: g++ all_to_ogg.cpp -o all_to_ogg -I{path-to-taglib-headers} -ltag \
 *     -DPATH_OGGENC={path-to-oggenc} -DPATH_FFMPEG={path-to-ffmpeg}
 */

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <dirent.h>
#include <sys/stat.h>
#include <string>
#include <locale>
#include <sstream>
#include <iconv.h>
#include <stack>

// taglib headers.
#include <tlist.h>
#include <fileref.h>
#include <tfile.h>
#include <tag.h>

using namespace std;

#ifndef PATH_OGGENC
#define PATH_OGGENC "/usr/bin/oggenc"
#endif

#ifndef PATH_FFMPEG
#define PATH_FFMPEG "/usr/bin/ffmpeg"
#endif

bool ifsjis(TagLib::String& input);
string ucs2utf(TagLib::String& input);
string iconv(iconv_t cd, TagLib::String& in);
TagLib::String utf2ucs(string& input);
string make_path(stack<dirent> list);

int main(int argc, char** argv) {
  setlocale(LC_CTYPE, "");
  iconv_t sjis_to_utf8;
  sjis_to_utf8 = iconv_open("UTF-8", "SHIFT_JIS");
  
  stack<DIR*>   list;
  stack<dirent> path;
  DIR* d = opendir(".");    // current directory.
  if(d == NULL) {
    exit(EXIT_FAILURE);
  }
  list.push(d);
  bool first = true;
  string target;
  while(list.size() != 0) {
    dirent* dp = readdir(list.top());
    if(dp == NULL) {
      target = target.substr(0, target.rfind('/'));
      cout << "Out from: " << target << endl;
      closedir(list.top());
      list.pop();
      path.pop();
      continue;
    }
    if((!path.empty()) && (!first)) {
      path.pop();
    }
    path.push(*dp);
    if(dp->d_name[0] == '.') {
      first = false;
      continue;
    }
    target = make_path(path);
    if(dp->d_type == DT_DIR) {
      cout << "Into: " << target << endl;
      DIR* d = opendir(target.c_str());
      list.push(d);
      first = true;
      continue;
    }
    first = false;
    if(dp->d_type != DT_REG && dp->d_type != DT_UNKNOWN) {    // pipe, link, etc.
      cout << "Skipping because not file: " << target << endl;
      continue;
    }
    string fname = dp->d_name;
    string::size_type dot = fname.rfind('.');
    string suffix;
    if(dot != 0 && dot != string::npos) {
      suffix = fname.substr(dot + 1, string::npos);
    } else {
      suffix = "";
    }
    if(suffix != "mp3" && suffix != "MP3") {
      cout << "Skipping because not mp3: " << target << endl;
      continue;
    }
    cout << "Converting: " << target << endl;
    string out_file = target.substr(0, target.length() - 3) + "ogg";
    string command = (string)(PATH_FFMPEG " -i \"") + target + 
                     ("\" -f flac -acodec flac -aq 100 -ac 2 -ar 48000 - 2> /dev/null | "
                      PATH_OGGENC " -q 10 -o \"") + 
                     out_file + "\" - 1>/dev/null 2>/dev/null";
    int ret;
    ret = system(command.c_str());
    if(ret != 0) {
      while(!list.empty()) {
        closedir(list.top());
        list.pop();
      }
      break;
    }
    TagLib::FileRef i_file(target.c_str());
    TagLib::FileRef o_file(out_file.c_str());
    TagLib::Tag* i_tag = i_file.tag();
    TagLib::Tag* o_tag = o_file.tag();
    TagLib::String i_str;
    TagLib::String o_str;
    string utf8;
    cout << "  Title : \"";
    i_str = i_tag->title();
    if(ifsjis(i_str)) {
      utf8 = iconv(sjis_to_utf8, i_str);
      cout << utf8;
      o_str = utf2ucs(utf8);
    } else {
      utf8 = ucs2utf(i_str);
      cout << utf8;
      o_str = i_str;
    }
    o_tag->setTitle(o_str);
    cout << "\"" << endl;
    cout << " Artist : \"";
    i_str = i_tag->artist();
    if(ifsjis(i_str)) {
      utf8 = iconv(sjis_to_utf8, i_str);
      cout << utf8;
      o_str = utf2ucs(utf8);
    } else {
      utf8 = ucs2utf(i_str);
      cout << utf8;
      o_str = i_str;
    }
    o_tag->setArtist(o_str);
    cout << "\"" << endl;
    cout << "  Album : \"";
    i_str = i_tag->album();
    if(ifsjis(i_str)) {
      utf8 = iconv(sjis_to_utf8, i_str);
      cout << utf8;
      o_str = utf2ucs(utf8);
    } else {
      utf8 = ucs2utf(i_str);
      cout << utf8;
      o_str = i_str;
    }
    o_tag->setAlbum(o_str);
    cout << "\"" << endl;
    cout << "Comment : \"";
    i_str = i_tag->comment();
    if(ifsjis(i_str)) {
      utf8 = iconv(sjis_to_utf8, i_str);
      cout << utf8;
      o_str = utf2ucs(utf8);
    } else {
      utf8 = ucs2utf(i_str);
      cout << utf8;
      o_str = i_str;
    }
    o_tag->setComment(o_str);
    cout << "\"" << endl;
    cout << "  Track : ";
    o_tag->setTrack(i_tag->track());
    cout << i_tag->track();
    cout << endl;
    cout << "  Genre : \"";
    i_str = i_tag->genre();
    if(ifsjis(i_str)) {
      utf8 = iconv(sjis_to_utf8, i_str);
      cout << utf8;
      o_str = utf2ucs(utf8);
    } else {
      utf8 = ucs2utf(i_str);
      cout << utf8;
      o_str = i_str;
    }
    o_tag->setGenre(o_str);
    cout << "\"" << endl;
    o_file.file()->save();
  }
  
  iconv_close(sjis_to_utf8);
  return EXIT_SUCCESS;
}

bool ifsjis(TagLib::String& input) {
  bool only_ASCII = true;
  for(unsigned int i = 0; i < input.length(); i++) {
    unsigned short int tmp[2];
    tmp[0] = input[i];
    if(tmp[0] < 0x0080) {
      continue;    // no problem.
    } else if(tmp[0] > 0x00A0 && tmp[0] < 0x00E0) {
      only_ASCII = false;
      continue;
    } else {
      only_ASCII = false;
      i++;
      tmp[1] = input[i];
      if(((tmp[0] > 0x0080 && tmp[0] < 0x00A0) || (tmp[0] >= 0x00E0 && tmp[0] < 0x00FD)) &&
         ((tmp[1] > 0x003F && tmp[1] < 0x007F) || (tmp[1] >= 0x0080 && tmp[1] < 0x00FD))) {
        continue;    // noproblem.
      } else {
        return false;
      }
    }
  }
  if(only_ASCII)
    return false;
  else
    return true;
}

string ucs2utf(TagLib::String& input) {
  string out;
  out = "";
  for(unsigned int i = 0; i < input.length(); i++) {
    unsigned short int tmp = input[i];
    if(tmp < 0x0080) {
      out += (unsigned char) tmp & 0x7F;
    } else if(tmp < 0x0800) {
      out += (unsigned char) ((tmp >> 6) | 0xC0);
      out += (unsigned char) ((tmp & 0x003F) | 0x80);
    } else {
      out += (unsigned char) ((tmp >> 12) | 0xE0);
      out += (unsigned char) (((tmp >> 6) & 0x003F) | 0x80);
      out += (unsigned char) ((tmp & 0x003F) | 0x80);
    }
  }
  return out;
}

TagLib::String utf2ucs(string& input){
  TagLib::String out;
  out = L"";
  for(unsigned int i = 0, r = 1; i < input.length(); i += r) {
    unsigned char tmp[3];
    tmp[0] = input[i];
    tmp[1] = input[i + 1];
    tmp[2] = input[i + 2];
    TagLib::wchar ucs = 0x0000;
    if(tmp[0] < 0x80) {
      ucs = tmp[0];
      r = 1;
    } else if(tmp[0] > 0xC0 && tmp[0] < 0xE0) {
      ucs  = (TagLib::wchar)(tmp[0] & 0x3F) << 6;
      ucs |= (TagLib::wchar)(tmp[1] & 0x3F);
      r = 2;
    } else if(tmp[0] > 0xE0) {
      ucs  = (TagLib::wchar)(tmp[0] & 0x0F) << 12;
      ucs |= (TagLib::wchar)(tmp[1] & 0x3F) << 6;
      ucs |= (TagLib::wchar)(tmp[2] & 0x3F);
      r = 3;
    }
    out += ucs;
  }
  return out;
}

#define S_SIZE 1024

string iconv(iconv_t cd, TagLib::String& in) {
  string s;
  for(unsigned int i = 0; i < in.length(); i++) {
    s += (unsigned char) in[i] & 0xFF;
  }
  istringstream si;
  ostringstream so;
  si.str(s);
  char* s_src = new char[S_SIZE];
  char* s_dst = new char[S_SIZE];
  char* p_src;
  char* p_dst;
  size_t n_src;
  size_t n_dst;
  while(!si.eof()) {
    si.getline(s_src, S_SIZE);
    p_src = s_src;
    p_dst = s_dst;
    n_src = strlen(s_src);
    n_dst = S_SIZE-1;
    while(0 < n_src){
      iconv(cd, &p_src, &n_src, &p_dst, &n_dst);
    }
    *p_dst = '\0';
    so << s_dst;
  }
  delete [] s_src;
  delete [] s_dst;
  return so.str();
}

string make_path(stack<dirent> list) {
  string ret = "";
  while(!list.empty()) {
    ret = (string)list.top().d_name + ((ret != "") ? ("/" + ret) : (""));
    list.pop();
  }
  return "./" + ret;
}

エラー処理は全くして無いので、自己責任でお願いします。
ようするに、自分で使う用だったからべつにいらないと思って。
どうせいずれ Segmentation Fault するし(おい)。
文字コード変換に関しても wikipedia にあったのそのままだし*1
変換先を変えたい場合は、

    string command = (string)(PATH_FFMPEG " -i \"") + target + 
                     ("\" -f flac -acodec flac -aq 100 -ac 2 -ar 48000 - 2> /dev/null | "
                      PATH_OGGENC " -q 10 -o \"") + 
                     out_file + "\" - 1>/dev/null 2>/dev/null";

を変えてください。
変換元を変えたい場合は、

    if(suffix != "mp3" && suffix != "MP3") { // ← 変換元の拡張子
      cout << "Skipping because not mp3: " << target << endl;
      continue;
    }
    cout << "Converting: " << target << endl;
    string out_file = target.substr(0, target.length() - 3) + "ogg"; // ← 変換先の拡張子


実行すると CPU使用率 100% になるのであしからず。

・・・
Out from: ./東方/Touhou_Ripped_Soundtracks/Touhou_PC-98_Ripped/01_Touhou_Reiiden_Highly_Responsive_to_Prayers                                                   
Out from: ./東方/Touhou_Ripped_Soundtracks/Touhou_PC-98_Ripped                  
Into: ./東方/Touhou_Ripped_Soundtracks/th08_東方永夜抄_Imperishable_Night
Into: ./東方/Touhou_Ripped_Soundtracks/th08_東方永夜抄_Imperishable_Night/WAV
Converting: ./東方/Touhou_Ripped_Soundtracks/th08_東方永夜抄_Imperishable_Night/WAV/th08_18_月まで届け、不死の煙.mp3
  Title : "月まで届け、不死の煙"
 Artist : "上海アリス幻樂団"
  Album : "東方永夜抄 〜 Imperishable Night"
Comment : ""
  Track : 18
  Genre : ""
Converting: ./東方/Touhou_Ripped_Soundtracks/th08_東方永夜抄_Imperishable_Night/WAV/th08_06_懐かしき東方の血_Old_World.mp3
・・・

のように make 風に表示されます。


変換されたファイルは元もとあったディレクトリと同じディレクトリに保存されます。
このプログラムは元のファイルは削除しないので空き容量に注意。


まあ、そんなところかな。

*1:使うなって書いてあった。詳しくは [wikipedia:Iconv]