8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LiveViewでDJコントローラーを使う 〜MIDIデバイスをWebでハックする〜

Last updated at Posted at 2025-12-22

僕のアドカレ恒例?DDJ-FLX4のネタです
DDJ-FLX4は入院中(2年前)に頂いた物です
大事にネタにさせてもらってます

今年はブラウザから直接MIDIデバイスにアクセスして、Elixirに送信します
去年と通信方法が違います

これこそWebだと思い込んでいます

今回の実行イメージ

前提知識

つまり、Elixirでの通信は成功してます
これを、ブラウザで直接通信します

WebMidi.jsを使えば、通信可能です

これができればhook化するだけです

これ1時間で完成しました

ネタバラシします

  • AI(Gemini)を使ってソース作りました
  • 過去のソースを食わせました
    • 前提として一度JS化してます

AIとのやりとりログ

僕がソースを見て気に入らないところは修正させています
結果、ある程度知識がないと作れません
AIに任せれば簡単でしょは、そんなに甘い世界ではないです
理解していれば、AIは強力な武器になります
前提知識があって成り立ちます

ソースコード

hook

assets/js/hooks/midi_hook.js
import { WebMidi } from "webmidi";

const MidiHook = {
  mounted() {
    this.initMidi();

    this.handleEvent("connect_device", ({ name }) => {
      this.connectToDevice(name);
    });
  },

  async initMidi() {
    try {
      await WebMidi.enable();

      const devices = WebMidi.inputs.map(i => ({ name: i.name }));
      this.pushEvent("update_devices", { devices });
    } catch (err) {
      console.error("WebMidi error:", err);
    }
  },

  connectToDevice(deviceName) {

    WebMidi.inputs.forEach(i => i.removeListener());
    const input = WebMidi.inputs.find(i => i.name === deviceName);

    if (input) {
      input.addListener("controlchange", e =>
        this.pushMidi("cc", e.message.channel, e.controller.number, e.rawValue));

      input.addListener("noteon", e =>
        this.pushMidi("note", e.message.channel, e.note.number, e.rawVelocity));

      this.pushEvent("connection_success", { name: input.name });
    }
  },

  pushMidi(type, channel, number, value) {
    this.pushEvent("midi_event", { type, channel, number, value });
  },

  destroyed() {
    WebMidi.disable();
  }
};

export default MidiHook;

Elixir側(操作画面)

defmodule VerificationMidiWeb.MidiLive do
  use VerificationMidiWeb, :live_view

  @doc """
  初期状態のセットアップ
  """
  @impl true
  def mount(_params, _session, socket) do
    {:ok,
     assign(socket,
       devices: [],
       current_device: nil,
       last_event: nil
     )}
  end

  # --- JavaScript Hook から届くイベント ---

  @doc """
  JS側で WebMidi.enable() が完了した際に、認識されているデバイス一覧を受け取ります。
  """
  @impl true
  def handle_event("update_devices", %{"devices" => devices}, socket) do
    {:noreply, assign(socket, devices: devices)}
  end

  @doc """
  セレクトボックスが変更された時、JS側へ特定のデバイスへの接続を命じます。
  """
  @impl true
  def handle_event("select_device", %{"device_name" => name}, socket) do
    {:noreply, push_event(socket, "connect_device", %{name: name})}
  end

  @doc """
  切断ボタンが押された時、JS側へリスナーの解除を命じます。
  """
  @impl true
  def handle_event("request_disconnect", _params, socket) do
    {:noreply, push_event(socket, "disconnect_device", %{})}
  end

  @doc """
  JS側での接続(または切断)が完了した際に、UIの状態を更新します。
  """
  @impl true
  def handle_event("connection_success", %{"name" => name}, socket) do
    {:noreply, assign(socket, current_device: name, last_event: nil)}
  end

  @impl true
  def handle_event("disconnected_status", _params, socket) do
    {:noreply, assign(socket, current_device: nil, last_event: nil)}
  end

  @doc """
  MIDI信号(CC/Note)を受信した際の処理。
  payload には type, channel, number, value が含まれます。
  """
  @impl true
  def handle_event("midi_event", payload, socket) do
    {:noreply, assign(socket, last_event: payload)}
  end

  # --- ビューの描画 ---

  @impl true
  def render(assigns) do
    ~H"""
    <div id="midi-scope" phx-hook="MidiHook" class="p-4 sm:p-10 font-sans text-gray-800 max-w-4xl mx-auto">
      <header class="mb-8 border-b pb-4">
        <h1 class="text-3xl font-black text-gray-900 tracking-tight">MIDI Verification</h1>
        <p class="text-sm text-gray-500 mt-1">LiveView & WebMidi.js Integration</p>
      </header>

      <section class="grid grid-cols-1 md:grid-cols-3 gap-8">
        <div class="md:col-span-1">
          <form phx-change="select_device" class="space-y-4">
            <div>
              <label class="block text-xs font-bold uppercase tracking-wider text-gray-400 mb-2">
                Input Device
              </label>
              <select name="device_name" class="w-full border-2 border-gray-200 p-3 rounded-xl bg-white focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10 outline-none transition-all cursor-pointer shadow-sm">
                <option value="">-- Select Device --</option>
                <%= for dev <- @devices do %>
                  <option value={dev["name"]} selected={dev["name"] == @current_device}>
                    <%= dev["name"] %>
                  </option>
                <% end %>
              </select>
            </div>
          </form>

          <div class="mt-6">
            <%= if @current_device do %>
              <button
                phx-click="request_disconnect"
                class="w-full py-2 px-4 rounded-lg text-sm font-semibold text-red-500 border border-red-200 hover:bg-red-50 hover:border-red-300 transition-colors"
              >
                Disconnect Device
              </button>
            <% end %>
          </div>
        </div>

        <div class="md:col-span-2">
          <div class={"relative p-8 border-2 rounded-3xl min-h-[300px] flex flex-col items-center justify-center transition-all duration-300 #{if @current_device, do: "bg-white border-blue-100 shadow-xl shadow-blue-500/5", else: "bg-gray-50 border-dashed border-gray-200"}"}>

            <%= if @current_device do %>
              <div class="absolute top-6 left-6 flex items-center gap-2">
                <span class="relative flex h-3 w-3">
                  <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
                  <span class="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span>
                </span>
                <span class="text-xs font-bold text-green-600 uppercase tracking-widest"><%= @current_device %></span>
              </div>

              <%= if @last_event do %>
                <div class="flex flex-col items-center">
                  <div class="flex gap-2 mb-6">
                    <span class="px-3 py-1 bg-blue-600 text-white rounded text-xs font-mono font-bold shadow-lg shadow-blue-500/20">
                      CH: <%= @last_event["channel"] %>
                    </span>
                    <span class="px-3 py-1 bg-gray-800 text-white rounded text-xs font-mono font-bold">
                      <%= String.upcase(@last_event["type"]) %> #<%= @last_event["number"] %>
                    </span>
                  </div>

                  <div class="text-9xl font-black text-blue-600 tabular-nums tracking-tighter animate-in zoom-in duration-200">
                    <%= @last_event["value"] %>
                  </div>

                  <div class="mt-4 w-full max-w-[200px] h-2 bg-gray-100 rounded-full overflow-hidden">
                    <div class="h-full bg-blue-500 transition-all duration-75" style={"width: #{@last_event["value"] / 127 * 100}%"}></div>
                  </div>
                </div>
              <% else %>
                <div class="text-center">
                  <div class="text-4xl mb-4 opacity-20">🎹</div>
                  <p class="text-gray-400 font-medium italic tracking-wide">Waiting for MIDI signal...</p>
                </div>
              <% end %>

            <% else %>
              <div class="text-center max-w-[200px]">
                <div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl">🔌</div>
                <p class="text-sm text-gray-400 leading-relaxed font-medium">Please select a device from the list to start monitoring.</p>
              </div>
            <% end %>
          </div>
        </div>
      </section>

      <footer class="mt-12 text-center text-xs text-gray-300 font-mono">
        VerificationMidi v0.1.0 | No Database Mode
      </footer>
    </div>
    """
  end
end

ざっくり説明

  • mount
    • hookでMIDIのデバイス一覧を取得します
  • セレクトボックスを選択時にhookのconnect_deviceを実行します
  • MIDIデバイスを動かすとhook側のpushMidiが呼ばれて
     Elixir側のhandle_event("midi_event", payload, socket)に通知されます

ソース

これ使えばMIDIデバイスのOUTをハックできます
通信仕様を見なくてもある程度は解析が可能です

8
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?