LoginSignup
3
3

More than 3 years have passed since last update.

レーザーハープをつくってみる

Posted at

レーザーハープとは?

レーザーを使った楽器で、レーザーを遮ることで音を出します。
"レーザーハープ"でググると色々な例を見れますし、自作しておられる方もいらっしゃいます。

レーザーハープをGoogle検索

興味があったので、ESP32とmruby/cでつくってみる内容を記録します。
開発環境はmacOSを使用しています。

ソースコード

開発環境の構築

参考資料
itocさんの ESP32×mruby/c IoTハンズオン

開発環境の構築に、上記の内容を利用させていただきました。
ビルド・実行は参考資料の環境で行います。

材料

材料名 数量 備考
ESP32-DevKitC 1
フォトトランジスタ 2 照度センサです。光を当てると大きな電流が流れます。
レーザー発光モジュール 2
ブレッドボード 1
抵抗10kΩ 2
洗濯バサミ 適量 レーザーを固定するのに使用しました。

構成

レーザーが遮られたらWi-Fi経由でメッセージをPCに送信します。PCはメッセージを受けとって音を鳴らします。音の再生にはSonic Piを使用します。
Sonic PiはRubyのコードで音楽を作成して演奏できるツールです。
Sonic Piでは、OSC(Open Sound Control 後述)という形式のメッセージを受け取って音を鳴らすことができます。

laser-harp-001.png

レーザー側(ESP32)の実装

レーザーを遮り、メッセージをPCに送信する処理を実装する以下のような回路を作成しました。
緑色のLEDの部分がフォトトランジスタです。

laser-harp-003.png
※電子工作初心者のため、おかしなところがあるかもしれません。

回路の説明

ESP32のADC(analog to digital converter)とフォトトランジスタを繋ぎます。
後述するプログラムで、GPI34,GPI35に入力される明るさの値を測定します。

1つ目のレーザーモジュール用のフォトトランジスタ

GPI34、GPIO32、GNDに繋ぎます。

2つ目のレーザーモジュール用のフォトトランジスタ

GPI35、GPIO33、GNDに繋ぎます。

注意点
* フォトトランジスタの短い方の足をGPI34,35に繋ぎます。
* ESP32のADCにはADC1とADC2があります。ADC2を使う場合、現時点ではWi-Fiと一緒に使えないようなので注意が必要です。図の回路ではADC1を使用しています。

回路については以上です。

プログラム

次にESP32に書き込むプログラムを実装します。
実装にはmruby/cを使用します。
※ mruby/cからESP32の開発環境esp-idfを利用します。

1. Wi-Fiとの接続 

Wi-Fiとの接続処理を実装します。
c_set_up_wifi関数を、mruby/cのsetup_wifiメソッドとして定義します。
SSIDとパスワード、PCのIPは環境によって書き換えが必要です。
メッセージ送信先のポート番号(Sonic Piの待受ポート)は4559固定です。

※ 実装にはesp-idfテンプレートを参考にしました。

esp32/main/main.c
#define WIFI_SSID "YOUR_WIFI_SSID"
#define WIFI_PASSWORD "YOUR_WIFI_PASSWORD"
#define HOST_IP_ADDRESS "192.168.0.Y"
#define HOST_OSC_PORT 4559

(中略)

static void c_setup_wifi(mrb_vm *vm, mrb_value *v, int argc)
{
  char addr_str[128];
  int addr_family;
  int ip_protocol;

  nvs_flash_init();
  tcpip_adapter_init();
  ESP_ERROR_CHECK(esp_event_loop_init(ESP_OK, NULL));
  wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
  ESP_ERROR_CHECK(esp_wifi_init(&cfg));
  ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
  ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));

  wifi_config_t sta_config = {
      .sta = {
          .ssid = WIFI_SSID,
          .password = WIFI_PASSWORD,
          .bssid_set = false}};
  ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config));
  ESP_ERROR_CHECK(esp_wifi_start());
  ESP_ERROR_CHECK(esp_wifi_connect());

  while (1)
  {
    dest_addr.sin_addr.s_addr = inet_addr(HOST_IP_ADDRESS);
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(HOST_OSC_PORT);
    addr_family = AF_INET;
    ip_protocol = IPPROTO_IP;
    inet_ntoa_r(dest_addr.sin_addr, addr_str, sizeof(addr_str) - 1);

    sock = socket(addr_family, SOCK_DGRAM, ip_protocol);
    if (sock < 0)
    {
      ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
    }
    else
    {
      ESP_LOGI(TAG, "Socket created, sending to %s:%d:%d", HOST_IP_ADDRESS, HOST_OSC_PORT, sock);
      break;
    }

    if (sock != -1)
    {
      ESP_LOGE(TAG, "Shutting down socket and restarting...");
      shutdown(sock, 0);
      close(sock);
    }
  }
}

(中略)

void app_main(void)
{
  .....
  mrbc_define_method(0, mrbc_class_object, "setup_wifi", c_setup_wifi);
  .....
}
esp32/mrblib/models/wifi.rb
class Wifi
  def self.setup
    setup_wifi
  end
end

2. フォトトランジスタの明るさを測る

フォトトランジスタで明るさを測定する処理を実装します。

実装にはesp-idfのADC1 Exampleを参考にしました。

esp32/main/main.c

static esp_adc_cal_characteristics_t *adc_chars;
static const adc_channel_t channel_1 = ADC_CHANNEL_6; //GPIO34
static const adc_channel_t channel_2 = ADC_CHANNEL_7; //GPIO35
static const adc_atten_t atten = ADC_ATTEN_DB_11;
static const adc_unit_t unit = ADC_UNIT_1;

(中略)

static void c_gpio_init_output(mrb_vm *vm, mrb_value *v, int argc)
{
  int pin = GET_INT_ARG(1);
  console_printf("init pin %d\n", pin);
  gpio_set_direction(pin, GPIO_MODE_OUTPUT);
}

static void c_gpio_set_level(mrb_vm *vm, mrb_value *v, int argc)
{
  int pin = GET_INT_ARG(1);
  int level = GET_INT_ARG(2);
  gpio_set_level(pin, level);
}

static void c_init_adc(mrb_vm *vm, mrb_value *v, int argc)
{
  int pin = GET_INT_ARG(1);
  adc1_config_width(ADC_WIDTH_BIT_12);
  if (pin == 34)
  {
    adc1_config_channel_atten(channel_1, atten);
  }
  else if (pin == 35)
  {
    adc1_config_channel_atten(channel_2, atten);
  };
  adc_chars = calloc(1, sizeof(esp_adc_cal_characteristics_t));
  esp_adc_cal_value_t val_type = esp_adc_cal_characterize(unit, atten, ADC_WIDTH_BIT_12, DEFAULT_VREF, adc_chars);
}

static void c_read_adc(mrb_vm *vm, mrb_value *v, int argc)
{
  int pin = GET_INT_ARG(1);
  uint32_t adc_reading = 0;
  for (int i = 0; i < NO_OF_SAMPLES; i++)
  {
    if (pin == 34)
    {
      adc_reading += adc1_get_raw((adc1_channel_t)channel_1);
    }
    else if (pin == 35)
    {
      adc_reading += adc1_get_raw((adc1_channel_t)channel_2);
    }
  }
  adc_reading /= NO_OF_SAMPLES;
  uint32_t millivolts = esp_adc_cal_raw_to_voltage(adc_reading, adc_chars);
  SET_INT_RETURN(millivolts);
}

void app_main(void)
{
  .....

  mrbc_define_method(0, mrbc_class_object, "gpio_init_output", c_gpio_init_output);
  mrbc_define_method(0, mrbc_class_object, "gpio_set_level", c_gpio_set_level);
  mrbc_define_method(0, mrbc_class_object, "init_adc", c_init_adc);
  mrbc_define_method(0, mrbc_class_object, "read_adc", c_read_adc);

  .....
}
esp32/mrblib/models/phototransistor.rb
class Phototransistor
  def initialize(gpio_pin, adc_pin)
    gpio_init_output(gpio_pin)
    gpio_set_level(gpio_pin, 1)
    init_adc(adc_pin)
    @adc_pin = adc_pin
  end

  # valueメソッドを実行することで、adcの値を読みます。
  def value
    read_adc(@adc_pin)
  end
end

3. レーザーが遮られた時にOSCメッセージ送信

次にOSCのメッセージを送信する処理を実装します。

OSCについては、wikipediaの記述のとおりです。

OpenSound Control(OSC)とは、電子楽器(特にシンセサイザー)やコンピュータなどの機器において音楽演奏データをネットワーク経由でリアルタイムに共有するための通信プロトコルである。

  wikipediaのOpenSound_Controlのページ

今回は、このプロトコルのメッセージをESP32で送信します。

OSCのメッセージは、URLに似た形式です。詳しい仕様はこちら
今回は、以下のメッセージを送信します。(※データをつけて送ることもできます)


# 送信するメッセージ

# 1つ目のレーザーが遮られた時に送信するメッセージ
/trigger/laser_1

# 2つ目のレーザーが遮られた時に送信するメッセージ
/trigger/laser_2

次に、OSCの仕様に従ってメッセージをエンコードします。
エンコードは以下のRubyのgemで行いました。


# osc-rubyをインストール
$ gem install osc-ruby
encode.rb
require 'osc-ruby'
message = OSC::Message.new('/trigger/laser_1')
message.encode
# エンコードされたメッセージ
# => "/trigger/laser_1\x00\x00\x00\x00,\x00\x00\x00"

次にエンコードしたメッセージを送信する処理を実装します。
c_send_osc_message関数をmruby/cのsend_osc_messageとして定義します。
メッセージの送信にはesp-idfのudp_clientのサンプルを参考にしました。

esp32/main/main.c

static const char *TAG = "laser_harp";
static struct sockaddr_in dest_addr;
int sock;

(中略)

static void c_send_osc_message(mrb_vm *vm, mrb_value *v, int argc)
{
  unsigned char *payload = GET_STRING_ARG(1);
  size_t size = GET_INT_ARG(2);

  while (1)
  {
    int err = sendto(sock, payload, size, 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
    if (err < 0)
    {
      ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
    }
    else
    {
      ESP_LOGI(TAG, "Message sent");
      break;
    }
  }

void app_main(void)
{
  .....

  mrbc_define_method(0, mrbc_class_object, "send_osc_message", c_send_osc_message);

  .....
}

}
esp32/mrblib/models/osc.rb
class OSCClient
  def send(message)
    send_osc_message(message, message.size)
  end
end

4. ここまでの処理をまとめる

レーザーを表すレーザークラスを実装します。

esp32/mrblib/models/laser.rb
class Laser
  def initialize(gpio_pin, adc_pin, name)
    @pt = Phototransistor.new(gpio_pin, adc_pin)
    @name = name
  end

  # レーザーを遮られた時にtrueを返す。
  def play?
    # レーザーが遮られた時は、3000より小さい値
    value = @pt.value
    puts "#{@name}: #{value}"
    value < 3000
  end
end

作成した処理を実行するループを作成します。

esp32/mrblib/loops/master.rb
Wifi.setup
laser_1 = Laser.new(32, 34, 'laser_1')
laser_2 = Laser.new(33, 35, 'laser_2')
osc_client = OSCClient.new

loop do
  if laser_1.play?
    osc_client.send("/trigger/laser_1\x00\x00\x00\x00,\x00\x00\x00")
  end

  if laser_2.play?
    osc_client.send("/trigger/laser_2\x00\x00\x00\x00,\x00\x00\x00")
  end

  sleep 0.001
end

ESP32のプログラムは以上です。

ここまで出来たら、レーザーの位置を調整して、レーザーの光をフォトトランジスタに当てます。
以下のコマンドでESP32にプログラムを書き込みます。


$ cd esp32
$ make flash monitor

この状態でレーザーを遮ってみると、以下のような結果になるはずです。


.....
laser_1: 3074
laser_2: 3082
laser_1: 142  # ここでレーザー1を遮りました
I (153771) laser_harp: Message sent  #メッセージ送信
laser_2: 3079
laser_1: 3073
.....

Sonic Pi側(PC)の実装

送信されたメッセージを受け取って、音を鳴らす処理を実装します。

Sonic Piのインストール

公式サイトからダウンロードしてインストールします。

Sonic Piでのメッセージ受信設定

Sonic Piでリモート(ESP32)からのメッセージを受信には、
「Prefs > 入出力 > Networked OSC」の「OSCサーバーを有効にする」と「OSCメッセージをリモートから受信する」をチェックする必要があります。

laser-harp-004.png

メッセージを受信

Sonic Piで演奏するRubyプログラムを作成します。
以下のように記述することで、OSCのメッセージを受け取ることができます。

music.rb
# レーザー1を遮られた時に送信されるメッセージを受信して音を鳴らす。
live_loop :laser_1 do
  use_real_time
  sync "/osc/trigger/laser_1"
  play 60 # Cの音を鳴らす
end

# レーザー2を遮られた時に送信されるメッセージを受信して音を鳴らす。
live_loop :laser_2 do
  use_real_time
  sync "/osc/trigger/laser_2"
  play 64 # Eの音を鳴らす
end

music.rbをsonic_piで実行します。


# sonic_piを起動した上で以下のコマンドを実行。
$ cat music.rb | sonic_pi

この状態でレーザーを遮ると、Sonic Piから音が鳴ることを確認できます。😃♪

3
3
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
3
3