DDJ-FLX4のPadでシーケンシャルウインカーを作ろう

PioneerDjのDDJ-FLX4でシーケンシャルウインカーを実装します

# 前提と基礎知識

https://qiita.com/t-yamanashi/items/90ab67186a68e85fc1bf

の続編です

# ElixirでPadのシーケンシャルウインカー
プログラム仕様
- Pad1〜Pad8を使って表現します
- 上段、下段のPadを使います
- デッキ1、デッキ2のPadは内側から光ます

表示例

```
□□□□　□□□□　
□□□□　□□□□
```

```
□□□■　■□□□　
□□□■　■□□□
```

```
□□■■　■■□□　
□□■■　■■□□
```

```
□■■■　■■■□　
□■■■　■■■□
```

```
■■■■　■■■■　
■■■■　■■■■
```

前回のコラムでDDJ-FLX4固有の部分をモジュール化しました
前回のコラムに通信仕様を書いた為説明は省略

```elixir:lib/ddj.ex
defmodule Ddj do
  @moduledoc """
  Documentation for `Ddj`.
  """

  def led_on, do: "7F"
  def led_off, do: "00"

  def pad1, do: "00"
  def pad2, do: "01"
  def pad3, do: "02"
  def pad4, do: "03"
  def pad5, do: "04"
  def pad6, do: "05"
  def pad7, do: "06"
  def pad8, do: "07"

  def deck1, do: "97"
  def deck2, do: "99"

  def pad_list, do: [pad1(), pad2(), pad3(), pad4(), pad5(), pad6(), pad7(), pad8()]

  def open do
    {_, output} = PortMidi.open(:output, "DDJ-FLX4 MIDI 1")
    output
  end

  def close(output) do
    PortMidi.close(:output, output)
  end

  def send_midi(v, output) do
    Enum.map(v, fn x -> hex_to_dec(x) end)
    |> List.to_tuple()
    |> then(&PortMidi.write(output, &1))
  end

  def hex_to_dec(hex), do: String.to_integer(hex, 16)

  def pad_led(output, deck, pad, led_sw) do
    [deck, pad, led_sw]
    |> send_midi(output)
  end
end
```

ウインカープログラムの本体です
```elixir:lib/turn_signal.ex
defmodule TurnSignal do
  @moduledoc """
  Documentation for `TurnSignal`.
  """
  import Ddj

  @doc """
  左側のウインカーの光るリスト
  """
  def deck1_signal do
    [
      {pad4(), pad8()},
      {pad3(), pad7()},
      {pad2(), pad6()},
      {pad1(), pad5()}
    ]
  end

  @doc """
  右側のウインカーの光るリスト
  """
  def deck2_signal do
    # 左側と逆になる
    deck1_signal()
    |> Enum.reverse()
  end

  def start() do
    output = open()

    # ウインカーは5回
    1..5
    |> Enum.each(fn _ -> task_deck(output) end)

    close(output)
  end

  @doc """
  左右ウインカー1回分
  """
  def task_deck(output) do
    # 非同期で左ウインカーを処理
    task_deck1 =
      Task.async(fn ->
        deck1_signal()
        |> signal_process(deck1(), output)
      end)

    # 非同期で右ウインカーを処理
    task_deck2 =
      Task.async(fn ->
        deck2_signal()
        |> signal_process(deck2(), output)
      end)

    # 左右のウインカーを処理が終わるまで待機
    Task.await(task_deck1)
    Task.await(task_deck2)
  end

  @doc """
  片方のウインカーを処理
  """
  def signal_process(pattern, deck, output) do
    pattern
    |> Enum.each(&signal_on(&1, deck, output))

    Process.sleep(300)

    pattern
    |> Enum.each(&signal_off(&1, deck, output))

    Process.sleep(300)
  end

  @doc """
  上段、下段のPadの表示/非表示
  """
  def signal({pad_up, pad_down}, deck, led_sw, output) do
    pad_led(output, deck, pad_up, led_sw)
    pad_led(output, deck, pad_down, led_sw)
  end

  @doc """
  上段、下段のPadの表示
  表示後100ミリ秒待つ
  """
  def signal_on({pad_up, pad_down}, deck, output) do
    signal({pad_up, pad_down}, deck, led_on(), output)
    Process.sleep(100)
  end

  @doc """
  上段、下段のPadの非表示
  """
  def signal_off({pad_up, pad_down}, deck, output),
    do: signal({pad_up, pad_down}, deck, led_off(), output)
end

```

補足
左右のウインカーは、非同期で動きます
但し、左右のウインカーが終わるまで次の処理を実行しません
データを作るのがめんどくさいので非同期処理しました（手抜き）

実行

```bash
$ mix run --eval "TurnSignal.start()"
```

# 動作結果

<iframe width="560" height="315" src="https://www.youtube.com/embed/6hGmikKVYnI?si=bOZmgbtCQPC546rb" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>