/*
 * wav2mp3.js
 * まとめて WAV を MP3 に変換する.
 * - 入力ファイルやフォルダはコマンドラインで指定する. フォルダの場合はそのフォ
 *   ルダ内の *.wav を入力ファイルとして扱う.
 * - 出力ファイル名は入力ファイル名の拡張子を .mp3 に変えたものになる.
 *
 * 例: D:\sound1\*.wav と D:\sound2\*.wav を MP3 にする
 *   wav2mp3.js D:\sound1 D:\sound2
 *
 * 例: E:\sound3\*.wav を MP3 160 kbps (モノラルは 80 kbps) にする
 *   wav2mp3.js E:\sound3 /kbps:160
 *
 * 例: 開かれているファイル群を MP3 128 kbps (モノラルは 64 kbps) でフォルダ
 *     F:\sound4 に出力
 *   wav2mp3.js F:\sound4 /kbps:128 /export
 *
 * Options:
 * /kbps:<bitrate in kbps>
 * /fs:<sampling rate in kHz>
 * /export
 * /force
 * /suspend
 */

@set @EXIT_SUCCESS = 0
@set @EXIT_FAILURE = 1

@set @DEFAULT_CH_KBPS = 64      // チャンネル当たりのビットレート (kbps)

var InputExtRegExp = /^wav$/i;  // 入力ファイル拡張子 (正規表現オブジェクト)
var OutputExt = "mp3";          // 出力ファイル拡張子

var Shell= new ActiveXObject("WScript.Shell");
var Fso = new ActiveXObject("Scripting.FileSystemObject");
var Opt = new struct_options();

main();
exit();

// =======================================================================

// 文字端末にテキストを表示.

function echo(text)
{
    Script.Echo(text);
}

// スクリプトの終了関数. 引数は省略可.

function exit(exit_code)
{
    Script.Quit(exit_code);
}

// スクリプトの使用方法を表示.

function print_usage()
{
    echo(Script.ScriptName);
    echo("  [/kbps[:<ビットレート kbps>]]");
    echo("  [/fs:<サンプリング レート kHz>]");
    echo("  [/force] [/suspend]");
    echo("  <フォルダ名/ファイル名> [<フォルダ名/ファイル名 2> [...]]");
    echo();
    echo("  /kbps     ビットレートの指定 (kbps)。");
    echo("  /fs       優先するサンプリング レート (kHz)。");
    echo("  /export   開かれているファイル群を圧縮する。");
    echo("  /force    既存ファイルがあっても上書きする。");
    echo("  /suspend  終了後 OS を待機状態にする。");
    echo();
    echo("注: ビットレートの指定は 2 ch の場合の値 (kbps) を書く。");
    echo();
    echo("注: ビットレートの既定値は " + (@DEFAULT_CH_KBPS * 2) + " kbps。");
    echo();
    echo("注: フォルダ名やファイル名が一つも指定されていない場合は、指定コー");
    echo("    デックのフォーマットが列挙表示される。");
    echo();
}

// 使用可能フォーマットを表示. 下の print_formats() から呼ばれる.

function print_fmts(codec_info, fs, ch)
{
    var fmt_list, fmt, i;

    fmt_list = codec_info.GetCodecFormats(fs, ch);
    fmt_list = fmt_list.toArray();      // VBArray -> JScript array

    if (fmt_list.length) {
        echo("--- " +
            (fs / 1000) + " kHz / " +
            ch + " ch" +
            " ---");

        for (i in fmt_list)
            echo(fmt_list[i].Desc);

        echo();
    }
    return fmt_list.length;
}

// 使用可能フォーマットを表示

function print_formats(codec_info)
{
    var fs_list = new Array();
    var n;
    for (n = 1; n <= 4; n *= 2) {
        fs_list[fs_list.length] = 48000 / n;
        fs_list[fs_list.length] = 44100 / n;
        fs_list[fs_list.length] = 32000 / n;
    }

    echo("使用可能な圧縮フォーマット:");

    var num_fmts = 0;
    var fs, ch, i;
    for (i in fs_list) {
        for (ch = 2; ch; --ch) {
            fs = fs_list[i];
            num_fmts += print_fmts(codec_info, fs, ch);
        }
    }

    if (num_fmts == 0)      // たぶん 0 にはならないと思う.
        echo("(なし)");

    echo("※全てのフォーマットを列挙していない可能性があります。");
}

// コマンドラインオプションの既定値

function struct_options()
{
    this.ChKbps         = @DEFAULT_CH_KBPS;     // チャンネル当たりのビットレート (kbps)
    this.SampleRate     = 0;
    this.ExportMode     = false;
    this.ForceOverwrite = false;
    this.Suspend        = false;
    this.InputFiles     = new Array();
}

// 文字列→数値変換 (チャンネル当たりのビットレート, kbps).
// read_options() の部品.

function str2chkbps(str)
{
    var val = @DEFAULT_CH_KBPS;

    if ((typeof(str) == "string") && str.length) {
        val = parseInt(str);
        if (val >= 1000)        // たぶん bps
            val = Math.round(val / 1000);
        val /= 2;               // チャンネル当たりの kbps
    }
    return val;
}

// 文字列→数値変換 (サンプリングレート, Hz).
// read_options() の部品.

function str2fs(str)
{
    var val = 0;        // 既定の戻り値

    if ((typeof(str) == "string") && str.length) {
        val = parseFloat(str);
        if (val < 1000)     // たぶん kHz
            val *= 1000;

        if (val < 8000)
            val = 8000;
        else if (val > 48000*4)
            val = 48000*4;

        val = Math.round(val);      // 整数化
        switch (val) {
        case 44000*4:
        case 44000*2:
        case 44000:
        case 22000:
        case 11000:
            val += val / 440;       // 44000 なら 44100 になる.
            break;
        }
    }
    return val;
}

// コマンドラインのオプションからグローバル変数 Opt.XXX を設定する.

function read_options()
{
    var args = Script.Arguments;

    // ヘルプオプションがあれば使用方法を表示して終了.

    if (args.Named.Exists("?")) {
        print_usage();
        exit(@EXIT_FAILURE);
    }

    // 名前付きオプションを調べる.
    var e, key;

    for (e = new Enumerator(args.Named); !e.atEnd(); e.moveNext()) {
        key = e.item().toLowerCase();

        // ビットレート
        if (key == "kbps")
            Opt.ChKbps = str2chkbps(args.Named(key));

        // サンプリングレート
        else if (key == "fs")
            Opt.SampleRate = str2fs(args.Named(key));

        // 開かれているファイルの圧縮
        else if (key == "export")
            Opt.ExportMode = true;

        // ファイル上書きオプション
        else if (key == "force")
            Opt.ForceOverwrite = true;

        // 電源コントロール
        else if (key == "suspend")
            Opt.Suspend = true;

        else {
            echo("エラー: 不明なオプション " + e.item());
            exit(@EXIT_FAILURE);
        }
    }

    // フォルダ名/ファイル名
    var i;

    for (i = 0; i < args.Unnamed.length; ++i)
        Opt.InputFiles[i] = args.Unnamed(i);
}

// 圧縮フォーマットの選択.
// 下の select_format() から呼ばれる.

function select_fmt(codec_info, sample_rate, channels)
{
    var fmt_list, fmt;
    var fmt_cand = null;        // 戻り値候補
    var i, diff, diff2;

    fmt_list = codec_info.GetCodecFormats(sample_rate, channels);
    fmt_list = fmt_list.toArray();      // VBArray -> JScript array

    for (i in fmt_list) {
        fmt = fmt_list[i];

        if (fmt.IsQualityBased)     // 一応確認
            continue;

        diff = Math.round(fmt.Bitrate / 1000) - Opt.ChKbps * channels;
        if (diff == 0)
            return fmt;

        if (fmt_cand == null)
            fmt_cand = fmt;
        else {
            diff2 = Math.round(fmt_cand.Bitrate / 1000) - Opt.ChKbps * channels;
            if (diff2 * diff >= 0) {        // 同符号
                if (Math.abs(diff2) > Math.abs(diff))
                    fmt_cand = fmt;
            }
            else {  // 異符号の場合はユーザー指定 bitrate より低いほうを残す.
                if (diff <= 0)
                    fmt_cand = fmt;
            }
        }
    }

    return fmt_cand;        // null が返る場合あり.
}

function select_format(codec_info, sample_rate, channels)
{
    var fmt = select_fmt(codec_info, sample_rate, channels);

    if ((fmt == null) && (channels == 1))
        fmt = select_fmt(codec_info, sample_rate, 2);

    return fmt;
}

// ショートカット (*.lnk) なら, それの指すファイル名に置き換える (フルパス).
// ショートカットでなければそのまま戻り値にする.

function get_link_target(file_path)
{
    if (Fso.GetExtensionName(file_path).toLowerCase() == "lnk")
        return Shell.CreateShortcut(file_path).TargetPath;
    else
        return file_path;
}

// 引数のファイルを writer で変換して圧縮ファイルを作る.
// 出力ファイル名は入力ファイル名の拡張子を置き換えたものになる.

function process_file(writer, input_file)
{
    var codec_info = writer.CodecInfo;

    var input_ext = Fso.GetExtensionName(input_file);       // '.' は含まない.
    var output_file = input_file.slice(0, -input_ext.length) + OutputExt;

    echo(output_file);

    // ファイルが存在して作成時刻も元ファイルより新しければ, そのままにする.
    // ただし /force オプション指定時は上書きする.

    if (!Opt.ForceOverwrite)
    if (Fso.FileExists(output_file))
    if (Fso.GetFile(output_file).DateLastModified > Fso.GetFile(input_file).DateLastModified) {
        echo(" ファイルが既に存在します。");
        return;
    }

    // 元ファイルを開く.

    var clip = Application.CreateSoundClip(input_file);

    // 圧縮フォーマット選択.
    // サンプリングレートはユーザーの指定, 元ファイルのそれ, 44 kHz, 22 kHz の順で使えるものを選ぶ.
    // チャンネル数は元ファイルのそれ, 2 ch の順で使えるものを選ぶ.

    var fmt = null;
    if (Opt.SampleRate)
        fmt = select_format(codec_info, Opt.SampleRate, clip.Channels, clip.BitsPerSample);

    if ((fmt == null) && (Opt.SampleRate != clip.SampleRate))
        fmt = select_format(codec_info, clip.SampleRate, clip.Channels, clip.BitsPerSample);

    if ((fmt == null) && (Opt.SampleRate != 44100) && (clip.SampleRate != 44100))
        fmt = select_format(codec_info, 44100, clip.Channels, clip.BitsPerSample);

    if ((fmt == null) && (Opt.SampleRate != 22050) && (clip.SampleRate != 22050))
        fmt = select_format(codec_info, 22050, clip.Channels, clip.BitsPerSample);

    if (fmt == null) {
        echo(" 圧縮フォーマットが見つかりません。");
        echo("(途中終了)");
        exit(@EXIT_FAILURE);
    }

    echo(" 圧縮フォーマット: " + fmt.Desc);

    // 実行
    var time;       // in milli seconds

    writer.CodecFormat = fmt;
    time = Application.System.GetTickCount();
    writer.Process(clip, output_file);
    time = Application.System.GetTickCount() - time;

    // かかった時間を表示
    var sec, min;

    sec = Math.round(time / 1000);
    min = Math.floor(sec / 60);     // 端数切捨て
    sec -= min * 60;
    echo(" 経過時間: " + min + " 分 " + sec + " 秒");

    // ファイルサイズの比率を表示

    var fi = Fso.GetFile(get_link_target(input_file));
    var fo = Fso.GetFile(output_file);
    echo(" ファイル サイズ比: " + Math.round(fo.Size / fi.Size * 100) + "%");
}

// フォルダ folder_path (文字列) 内を調べ, 拡張子が InputExtRegExp にマッチする
// ファイルがあれば圧縮ファイルを作る.

function process_folder(writer, folder_path)
{
    var folder = Fso.GetFolder(folder_path);
    var fc, input_file, input_ext;

    for (fc = new Enumerator(folder.Files); !fc.atEnd(); fc.moveNext()) {
        input_file = fc.item().Path;
        input_ext = Fso.GetExtensionName(get_link_target(input_file));
        if (InputExtRegExp.test(input_ext))
            process_file(writer, input_file);
    }
}

// 開かれているファイル群のうち引数で渡されたものを圧縮する.

function process_doc(writer, output_file, doc)
{
    var codec_info = writer.CodecInfo;

    echo(output_file);

    // ファイルが存在して作成時刻も元ファイルより新しければ, そのままにする.
    // ただし /force オプション指定時は上書きする.

    if (!Opt.ForceOverwrite)
    if (Fso.FileExists(output_file)) {
        echo(" ファイルが既に存在します。");
        return;
    }

    // 元ファイルを開く.

    doc.SelectionFrom = 0;
    doc.SelectionTo = doc.Length;

    var clip = doc.CreateSoundClip();

    // 圧縮フォーマット選択.
    // サンプリングレートはユーザーの指定, 元ファイルのそれ, 44 kHz, 22 kHz の順で使えるものを選ぶ.
    // チャンネル数は元ファイルのそれ, 2 ch の順で使えるものを選ぶ.

    var fmt = null;
    if (Opt.SampleRate)
        fmt = select_format(codec_info, Opt.SampleRate, clip.Channels, clip.BitsPerSample);

    if ((fmt == null) && (Opt.SampleRate != clip.SampleRate))
        fmt = select_format(codec_info, clip.SampleRate, clip.Channels, clip.BitsPerSample);

    if ((fmt == null) && (Opt.SampleRate != 44100) && (clip.SampleRate != 44100))
        fmt = select_format(codec_info, 44100, clip.Channels, clip.BitsPerSample);

    if ((fmt == null) && (Opt.SampleRate != 22050) && (clip.SampleRate != 22050))
        fmt = select_format(codec_info, 22050, clip.Channels, clip.BitsPerSample);

    if (fmt == null) {
        echo(" 圧縮フォーマットが見つかりません。");
        echo("(途中終了)");
        exit(@EXIT_FAILURE);
    }

    echo(" 圧縮フォーマット: " + fmt.Desc);

    // 実行
    var time;       // in milli seconds

    writer.CodecFormat = fmt;
    time = Application.System.GetTickCount();
    writer.Process(clip, output_file);
    time = Application.System.GetTickCount() - time;

    // かかった時間を表示
    var sec, min;

    sec = Math.round(time / 1000);
    min = Math.floor(sec / 60);     // 端数切捨て
    sec -= min * 60;
    echo(" 経過時間: " + min + " 分 " + sec + " 秒");

    // ファイルサイズの比率を表示

    var fi_size = (clip.Length * clip.SampleRate) * (clip.BitsPerSample / 8) * clip.Channels;
    var fo = Fso.GetFile(output_file);
    echo(" ファイル サイズ比: " + Math.round(fo.Size / fi_size * 100) + "%");
}

// 開かれているファイル群を圧縮する.

function process_documents(writer, output_dir)
{
    var names = new Array();
    var i, j, s, t, n;

    // 出力ファイル名

    for (i = 0; i < Application.Documents.Count; ++i)
        names[i] = Fso.GetBaseName(Application.Documents(i).Title);

    // 出力ファイル名の重複回避

    for (i in names) {
        s = names[i];
        for (j = 0; j < i; ++j) {
            if (s.toUpperCase() == names[j].toUpperCase())
                break;
        }
        if (j < i) {
            for (n = 2; ; ++n) {
                t = s + " (" + n + ")";
                for (j = 0; j < names.length; ++j) {
                    if (t.toUpperCase() == names[j].toUpperCase())
                        break;
                }
                if (j == names.length)
                    break;
            }
            names[i] = t;
        }
    }

    // 圧縮ファイル作成

    for (i in names) {
        s = output_dir + "\\" + names[i] + "." + OutputExt;
        process_doc(writer, s, Application.Documents(i));
    }
}

function main()
{
    // コマンドラインオプション読み込み.
    read_options();

    var writer = Application.CreateFileWriter("mp3");

    // 処理フォルダ/ファイルの指定がなかったら圧縮フォーマットを列挙表示して終了.
    if (Opt.InputFiles.length == 0) {
        print_formats(writer.CodecInfo);
        exit(@EXIT_FAILURE);
    }

    // 電源コントロール
    if (Opt.Suspend)
        Application.System.SetSuspendState(false, true, false);     // hibernate, force, immediate

    // 圧縮ファイル作成
    var i, str;

    if (Opt.ExportMode) {
        if (Opt.InputFiles.length != 1) {
            echo(" 出力先フォルダを一つだけ指定してください。");
            exit(@EXIT_FAILURE);
        }
        str = Opt.InputFiles[0];
        if ((str.length > 0) && (str.charAt(str.length - 1) == "\\"))
            str = str.substr(0, str.length - 1);
        if (Fso.FileExists(str)) {
            echo(str);
            echo(" パスが無効です。");
            exit(@EXIT_FAILURE);
        }
        process_documents(writer, str);
    }
    else {
        for (i in Opt.InputFiles) {
            str = Opt.InputFiles[i];
            if (Fso.FolderExists(str))
                process_folder(writer, str);
            else if (Fso.FileExists(str))
                process_file(writer, str);
            else {
                echo(str);
                echo(" パスが無効です。");
                echo("(途中終了)");
                exit(@EXIT_FAILURE);
            }
        }
    }

    exit(@EXIT_SUCCESS);
}