🎧

DenoでDJ用の便利コマンドを自作する話(CDJ350と仲良くしよう)

2024/06/06に公開

似たようなことがarisane氏の記事に書いてあります。
やり方は違うけどゴールはだいたい同じです。

これを読んで得られる知識

  • Denoで簡単なコマンドが作れるようになる
  • 日本語→ローマ字変換のライブラリについて軽く知ることができる
  • DJがなにやってるかなんとなくわかるようになる
  • CDJ350と戦えるで遊べるようになる

前文(コマンドを作るに至る経緯など)

DJ(ディージェー、ディスクジョッキー?)ってご存じでしょうか?

世の中にはクラブとかクラブイベントとかDJバーとか、なんかこう音楽が途切れることなくいい感じにかかっている空間や催しがあって、黙って音楽を聴いたり酒を飲んだり人と喋ったり騒いだりできるわけです。

そこで音楽を「途切れることなくいい感じに」再生しているのがいわゆるDJです。
なんか他にもラジオで曲かけたり他にも多様な生態があるっぽいですが、ここではそういうことにしておきます。

DJが「途切れることなくいい感じに音楽をかける」ために使われるDJ機材というものがあります。

(DJ機材の例)これはPioneer DJ(現AlphaTheta)から2020年9月に発売されたプレイヤーCDJ-3000とミキサーDJM-900NXS2の組み合わせです。プレイヤーは2024年現在の最新機種。総額80万くらい?

プレイヤーは音楽を再生できる機械、ミキサーは個々のプレイヤーから受け取った音をプレイヤーごとにどのくらいの大きさで鳴らすか決めて音を混ぜて出力できる機械です。

これを使ってどうやって曲を途切れることなくかけるのかをフローチャートで説明します。
こんなかんじ。

曲をいい感じに繋ぐテクニックは無限にあるのですが、ミニマムはこうです。
オカダダさんという有名なDJから直接聞いたので間違いない(たぶん)

単純に言うと「左右のプレイヤーで交互に選曲&再生、真ん中のミキサーで前の曲はフェードアウト・次の曲はフェードイン」していくわけです。

詳しくはこういうDJの手元の動画がたくさんあるのでYoutubeとかで検索してみてください。

今回作ったソフトウェア

https://github.com/kuwa72/m3u8romajinizer

こちらです。
ざっくり機能を説明すると
M3Uファイルに記載されたMP3,AACなど音楽ファイルを読み込み、曲情報として埋め込まれた曲名とアーティスト名の日本語部分をローマ字化して別ディレクトリにコピーする
というものになります。

これがなぜ必要かというと、Pioneer DJから20年ほど前に発売されていたCDJ-350という機材だと日本語を表示できず選曲に支障が出るからです。

CDJ350のディスプレイはこんな感じ。

CDJ3000はこんな感じで曲を選べる。楽。神。

ここ10年くらいで発売された機材(CDJ-2000以降)は日本語が表示されるので必要ありません。
ほとんどの場所でCDJ-2000以降が使われているので、これからDJを始める方でこのツールが必要になることはあまりないでしょう。

ソフトウェアの中身の説明

なんとなく1バイナリにしたかったのと、便利なライブラリが揃っていることを期待して、処理系としてDeno、言語としてTypeScriptを使っています。

処理手順でいうとこんな感じです。

  1. m3uファイルを読み込む(m3u8、utf8前提です)
  2. 音楽ファイルのタイトルとアーティスト名を抽出
  3. 日本語→ローマ字化
  4. 情報をローマ字化した情報にしつつ別ディレクトリにコピー(ffmpegを使用)

個々の処理は素晴らしいライブラリがあったので、順番に呼び出しているだけです。

  • m3uファイルのパース: npm:m3u8-parser
  • 楽曲情報の抽出: npm:music-metadata
  • ローマ字化: npm:kuroshiro
  • 楽曲情報の更新とコピー: ffmpegを直接呼び出し

引数処理にオプションパーサを使おうかと思ったのですが、ファイル1個と出力先を指定しているだけなのでargsから直接取得してます。

const args = Deno.args;
console.log(args);
const [m3uPath, outPath] = args;
if (!m3uPath) {
  console.error("No m3u path provided");
  Deno.exit(1);
}

ファイルの読み込みはDenoのAPIを直接使っています。JavaScriptの都合上、ファイル読み込みAPIの標準がなく処理系依存になっているのでこんな感じになるかと。詳細はAPIドキュメントを参照してください。

const m3u = await Deno.readTextFile(m3uPath);

書き込み先の権限チェックはこんな感じらしい。

Deno.permissions.query({ name: "write", path: outPath }).catch(() => {
  console.error("No write permission to out path");
  Deno.exit(1);
});

コマンドの存在確認はバージョンチェックで起動できるかで行っています。

// check ffmpeg command from path, if not found, use current dir
let ffmpegPath = "ffmpeg";
const ffmpegCommand = new Deno.Command("ffmpeg", { args: ["-version"] });
try {
  await ffmpegCommand.output();
} catch {
  const ffmpegCommand = new Deno.Command("./ffmpeg", { args: ["-version"] });
  try {
    await ffmpegCommand.output();
  } catch {
    console.error("No ffmpeg command found");
    Deno.exit(1);
  }
  ffmpegPath = "./ffmpeg";
}

今回使わせていただいている日本語→ローマ字変換のライブラリ Kuroshiro はこんな感じでフィルタ関数を定義して使っています。
ローマ字のルール選択はお好みで選択できます。

変換しきれない文字を_に置き換えたり、小さいディスプレイで見やすいように先頭大文字にしたり、ディスプレイに表示されなそうな長すぎる文字列を切り捨てたりしています。

const kuroshiro = new Kuroshiro.default();
await kuroshiro.init(new Analyzer());

// ascii safe func, compress unknowns to one _
const asciiSafe = (str: string) => {
  return str.replace(/[^a-zA-Z0-9\.\-]+/g, "_");
};
const toRome = async (str: string) => {
  str = await kuroshiro.convert(str, {
    to: "romaji",
    romajiSystem: "passport",
    mode: "spaced",
  });
  // Upper case all first letter
  str = str.replace(/\b\w/g, (c) => c.toUpperCase());
  // compress spaces
  str = str.replace(/\s+/g, " ");
  // first 32 chars
  return str.substring(0, 32);
};

ファイルごとの処理のループはこんな感じです。
何してるかはコメントに書いたので察してください 🙏

targets.forEach(async (target: string) => {
  // DenoのAPIで音楽ファイル読み込み
  const buf = await Deno.readFile(target);

  // 音楽ファイルをパーサーに渡して情報取得
  const meta = await mm.parseBuffer(buf);
  let ext = target.split(".").pop()?.toLowerCase();
  let force = false;
  const copyOpts = ["-c:a", "copy"]; // 曲データが再エンコードされないようにコピーを指定

  // 未対応のフォーマットは処理しない
  if (!(ext === "mp3" || ext === "aac" || ext === "m4a")) {
    console.log("Unsupported format: " + ext);
    force = true;
    ext = "m4a"; // force aac
  }

  // convert title and artist to romaji
  let rtitle = await toRome(meta.common?.title ?? "");
  const rartist = await toRome(
    meta.common?.artist ?? meta.common?.albumartist ?? ""
  );
  const rfilename = await toRome(
    target.replaceAll("\\", "/").split("/").pop() ?? "unknown"
  );
  if (rtitle === "") {
    rtitle = rfilename;
  }

  // いちおうファイル名もローマ字化しておく
  // これでRekordoxを通さずただUSBメモリにファイルコピーしたときもDJできると思う
  const filename = asciiSafe(rtitle) + " - " + asciiSafe(rartist) + "." + ext;
  const fullpath = outPath + "/" + filename;

  // ffmpegというか外部コマンドを実行するにはこういう感じでDenoのAPIを使うようです。
  const ffmpegCommand = new Deno.Command(ffmpegPath, {
    args: [
      "-y",
      "-i",
      target,
      "-c:v",
      "copy",
      ...(force ? [] : copyOpts),
      "-metadata",
      `title=${rtitle}`,
      "-metadata",
      `artist=${rartist}`,
      fullpath,
    ],
  });
  const { success, stdout, stderr } = await ffmpegCommand.output();
  const d = new TextDecoder(); // コマンド出力はバイナリ列なので文字列化するにはこれを使うっぽい
  if (!success) {
    console.log(success, d.decode(stdout), d.decode(stderr));
    Deno.exit(1);
  }
});

denoで実行ファイルを作成する

こんなタスクを定義しました。

deno compile --allow-write --allow-read --allow-env --allow-run --unstable --target x86_64-pc-windows-msvc main.ts

実行ファイルを出力するには compile サブコマンドを使用します。

また、Denoがファイル入出力やコマンド実行などを行うには適切な権限付与が必要です。
今回はファイルの入出力、コマンド実行、環境変数取得の権限が必要なので色々allowしています。

--targetでどのプラットフォーム向けのバイナリを出力するか決めれるようです。

package.jsonにタスク定義するようにdeno.jsonにタスク定義すると楽なようでした。

あとがき

日本語出ない問題の解決方法は他にも何個かありますが、どれもトレードオフがあります。
どう機材を使ってDJをするかは、どれも一長一短あるので結構悩みどころですね。

  • バイナル
    • メリット:環境さえあればまず音は出る。音がいい(らしい)
    • デメリット:針は自前(高い)。重い。割れる。音が飛ぶ。
  • CD
    • メリット:曲情報が出ないがラベルとかに書くはずなのでなんとかなる、機材トラブルはたぶん少なめ。
    • デメリット:CDJ3000とかCD使えない環境が時々ある。データからCD−Rにするのが面倒。荷物増える。曲数持ち歩けない。
  • PCDJまたは外部コントローラー
    • メリット:接続できさえすれば楽。選曲で困ることはない
    • デメリット:機材持ち込み可能かは店とかイベント次第?荷物は増える。接続が難しいときがある。接続できても出力小さいときがある
  • 曲情報がアルファベットの曲だけでDJする
    • メリット:日本語面で困ることはない。そもそも邦楽かけなければ大丈夫。
    • デメリット:手数が限られるけど一番楽ではある

日本語問題の他にもCDJ-350は新しい機材と比較して次のような不便がありますが、がんばりましょう。

  • USBメモリの読み書きは遅め
  • USBメモリが大きすぎると読み込めない?512GBのSSDとかダメでした。
  • 新しい機材ではUSBメモリ1本をプレイヤーに刺すと他のプレイヤーでもそのUSBメモリから曲が読み込めるが、そんな機能はない
  • Rekordboxで設定したBPMなど諸々の情報が読み込まれない
  • 曲の波形が見えない
  • CUE系の機能がない
  • 強制的にBPMを同期させるSYNC機能がない

そんなわけで、↓のステッカーでおなじみのDJバーでとりあえずDJできるようになりました。
店長の人脈が広くて面白いイベントが多いし酒が安くてタバコも吸えるので、その辺OKな人はぜひ遊びに行きましょう。僕も時々います。

Discussion