僕のアドカレ恒例?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をハックできます
通信仕様を見なくてもある程度は解析が可能です