Elixirで🐈「チピチピチャパチャパ」🐈

猫がよ！「チピチピチャパチャパ」してんじゃねーよ

と言うことで「チピチピチャパチャパ」演奏するプログラムを作りたいと思います
ネタがわからない方は「チピチピチャパチャパ 　猫ミーム」で検索してください


# 前提条件
- OS Ubuntu 22.04

# 環境構築

soxのインストール
```sh
$ sudo apt install sox
```
# プロジェクト作成

```sh
$ mix new sox
```
# ソースを書く

mix playコマンドを追加
`mix play ファイル名`
時に指定したファイルの曲を流します

```elixir:lib/mix/tasks/play.ex
defmodule Mix.Tasks.Play do
  @moduledoc """
  曲を再生するタスクです
  """
  use Mix.Task

  @shortdoc "曲を再生する"
  def run(arg) do
    play(arg)
  end

  defp play([]), do: Sox.play("music1.txt")

  defp play(arg) do
    arg
    |> List.first()
    |> Sox.play()
  end
end
```

曲データ書く

フォーマット仕様
- partで小節を分けます
- mainでどの小節を再生するか決めます
- 例　a4 16．はa4の音階で付点16音符
- r0は休止

この曲データは
part1 → part1 →  part2 →  part3 →  part1 →  part1 →  part2 →  part4
で再生します

```music3.txt
part1
a4 16.
r0 16
a4 16.
r0 16
a4 16.
r0 16
a4 16.
r0 16
g4 16.
r0 16
a4 16.
r0 16
g4 16.
r0 16
a4 16.
r0 16
part2
a4 16.
r0 16
a4 16.
r0 16
a4 16.
r0 16
a4 16.
r0 16
c5 16.
r0 16
c5 16.
r0 16
b4 16.
r0 16
g4 16.
r0 16
part3
a4 4
r0 16
a4 4
r0 16
a4 4
r0 16
a4 4
r0 16
part4
a4 1
main 1 1 2 3 1 1 2 4
```

仕様
- 上記の曲を、解析して再生します
- soxコマンドで指定した周波数、単音ごとwavファイルを作成します
- soxコマンドでファイルを結合します
- aplayコマンドで結合したファイルを再生します

```elixir
defmodule Sox do
  @a4_frequency 440

  @c4_note_no 60
  @d4_note_no 62
  @e4_note_no 64
  @f4_note_no 65
  @g4_note_no 67
  @a4_note_no 69
  @b4_note_no 71
  
  @r_note_no 0

  @dotted_note 1.5

  @bpm 200

  @tmp_dir "/dev/shm/sox_ymn"

  @play_file_name "ymn.wav"

  @moduledoc """
  Documentation for `Sox`.
  """

  @doc """
  曲を再生します

  ## Examples

      iex> Sox.play("music1.txt")
      :ok
  """
  def play(file_name) do
    File.rm_rf(@tmp_dir)
    File.mkdir(@tmp_dir)

    File.read!(file_name)
    |> text_to_play()

    :ok
  end

  def text_to_play(text) do
    # partとメインを分割
    [parts | main] =
      text
      |> String.split("main ")

    part_map =
      get_list_part(parts)
      |> Enum.map(&create_part_keyword(&1))

    main
    |> List.first()
    |> String.split(" ")
    |> Enum.map(&Keyword.get(part_map, String.to_atom(&1)))
    |> List.flatten()
    |> Enum.map(&create_play_syntax(&1))
    |> Enum.with_index(1)
    |> Enum.map(&sox(&1))
    |> Enum.map(&Task.await(&1))

    sox_merge_cmd()
    aplay_cmd()
  end

  def get_list_part(parts) do
    parts
    |> String.split("part")
    |> Enum.reject(&(&1 == ""))
  end

  def create_part_keyword(part) do
    [part_name | part_data] =
      part
      |> String.split("\n")
      |> Enum.reject(&(&1 == ""))

    {String.to_atom(part_name), part_data}
  end

  def create_play_syntax(line) do
    [note, time] = line |> String.split(" ")
    {note, time}
  end

  def note_no_to_frequency(@a4_note_no), do: @a4_frequency

  def note_no_to_frequency(note) when note < @a4_note_no,
    do: @a4_frequency / :math.pow(2, 1 / 12 * (@a4_note_no - note))

  def note_no_to_frequency(note) when note > @a4_note_no,
    do: @a4_frequency * :math.pow(2, 1 / 12 * (note - @a4_note_no))

  def sox({{note, time}, index}) do
    file_name =
      index
      |> Integer.to_string()
      |> String.pad_leading(10, "0")
      |> then(&"#{@tmp_dir}/#{&1}.wav")

    [alphabet | number] = note |> String.split("") |> Enum.reject(&(&1 == ""))
    note_no = get_note_no(alphabet) + get_octaval_and_semitone(number)
    sox(note_no, time, file_name)
  end

  def sox(0, time, file_name) do
    sec = get_sec(time)
    Task.async(fn -> sox_cmd(0, sec, file_name) end)
  end

  def sox(note, time, file_name) do
    sec = get_sec(time)

    frequency = note_no_to_frequency(note)
    Task.async(fn -> sox_cmd(frequency, sec, file_name) end)
  end

  def sox_cmd(frequency, time, file_name),
    do: System.cmd("sox", ~w"-n #{file_name} synth #{time} sin #{frequency}")

  def sox_merge_cmd(), do: System.cmd("sox", ~w"#{@tmp_dir}/*.wav #{@tmp_dir}/#{@play_file_name}")
  def aplay_cmd(), do: System.cmd("aplay", ~w"#{@tmp_dir}/#{@play_file_name}")

  def get_sec("1"), do: get_sec("4") * 4
  def get_sec("1."), do: get_sec("1") * @dotted_note
  def get_sec("2"), do: get_sec("4") * 2
  def get_sec("2."), do: get_sec("2") * @dotted_note
  def get_sec("4"), do: 60 / @bpm
  def get_sec("4."), do: get_sec("4") * @dotted_note
  def get_sec("8"), do: get_sec("4") / 2
  def get_sec("8."), do: get_sec("8") * @dotted_note
  def get_sec("16"), do: get_sec("4") / 4
  def get_sec("16."), do: get_sec("16") * @dotted_note

  def get_note_no("c"), do: @c4_note_no
  def get_note_no("d"), do: @d4_note_no
  def get_note_no("e"), do: @e4_note_no
  def get_note_no("f"), do: @f4_note_no
  def get_note_no("g"), do: @g4_note_no
  def get_note_no("a"), do: @a4_note_no
  def get_note_no("b"), do: @b4_note_no
  def get_note_no("r"), do: @r_note_no

  def get_octaval_and_semitone(number) when length(number) == 2 do
    [octaval | semitone] = number
    semitone = List.first(semitone)
    get_octaval_and_semitone(octaval) + get_semitone(semitone)
  end

  def get_octaval_and_semitone(number) when length(number) == 1 do
    List.first(number)
    |> get_octaval_and_semitone()
  end

  def get_octaval_and_semitone(number) do
    String.to_integer(number)
    |> get_octaval()
  end

  def get_octaval(octaval), do: (octaval - 4) * 12
  def get_semitone("+"), do: 1
  def get_semitone("-"), do: -1
  def get_semitone("#"), do: 1
end
```

曲を再生
```bash
$ mix play music3.txt
```

<iframe width="560" height="315" src="https://www.youtube.com/embed/fb0E87_e7fM?si=pU-NKafgJ-lRk5MG" 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>