幼稚園のクリスマス会の出し物のためにアプリを作ってみた

## ストーリー

私は子どもが通う幼稚園の父母の会の役員をやっています。毎年幼稚園のクリスマス会では父母の会の役員による出し物があり、今年はクラッピングという、打楽器(カスタネット)四重奏をやることになりました（[参考動画](https://www.youtube.com/watch?v=27bYlM8T8kg)）。そのために戯れに[練習用のウェブアプリ](https://applepedlar.github.io/clapping)なんかを作ったりしていました。そして先日の練習会では譜面台を4パートぶん用意し、本番と同じような形でなんとか止まらずに演奏できる状態になりました。そしてその場で会長より「音に合わせて電球が光るといいよねー」というアイディアをいただき、実現方法を検討しました。

## 要件の考察

- 光源
  - ON/OFFが容易
  - 光量がある程度大きい
  - 簡単に用意できる!

- ON/OFFスイッチ
  - カスタネットの打音を阻害しない
  - カスタネットの打音との同期ができる
  - 簡単に用意できる
- 光源とスイッチを繋ぐ方法
  - 無線が望ましい
- 以上が4組必要

数時間ほど考えた結果、以下のようなシステムによって実現可能であるという結論に至りました。

## システム構成

- 光源: iPhoneのLEDライト
  - LEDを観客に向けて譜面台に固定
  - まぶしすぎるので白布か何かで拡散させることを検討中。
- ON/OFFスイッチ: AB Shutter 3
  - これをカスタネットと手の間に置き、叩いた衝撃でボタンを押す。
  - <img src="https://qiita-image-store.s3.amazonaws.com/0/21577/83d534c2-fad1-dc07-0916-31372fd7c096.jpeg" width=30%>
  - 各パートで自信がある人が使う。
- 光源とスイッチを繋ぐ方法: Bluetooth

AB Shutter 3 については、以前[この記事](https://qiita.com/vimyum/items/8b7548ca8cf45383c5b0)が話題になった時にAmazonで5個ほど衝動買いしたものが手元にあるのでそれを使用します。

AB Shutter 3 の大きいほうのボタンはボリュームアップであり、ボリュームアップキーが押されたときにLEDを点灯させることができればシステムは完成です。

さっそくAppStoreでボリュームキーとLEDライトが連動するアプリを探してみましたが、見つかりませんでした。仕方ないので自作することにしました。自宅にはMacが無いのでWindowsタブレットにVisualStudioをインストールし、Xamarinで開発。iPhoneにインストールされたXamarin Live Player上で実行できるようにしました。

## コード

見た目はタイトルが表示されるだけの簡素すぎるアプリです。
システムボリュームの変化を検出し、LEDが点灯します。ボリュームが上がり切るとそれ以上ボリュームが変化せずイベントが来なくなるので、LED点灯時にボリュームを一定レベルに戻します。

```
using System;
using UIKit;
using Foundation;
using AVFoundation;
using AudioToolbox;
using MediaPlayer;
using System.Threading.Tasks;

namespace LEDControl {

    public class ViewController : UIViewController {
        AVAudioSession audioSession = AVAudioSession.SharedInstance();
        private NSObject _notificationHandle;

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
            Title = "ボリュームボタンでLEDライト発光！";
            View.BackgroundColor = UIColor.White;
            
            initVolumeKeyEvent();
            _notificationHandle = NSNotificationCenter.DefaultCenter.AddObserver(UIApplication.WillEnterForegroundNotification, HandleAppWillEnterForeground);
        }

        public override void ViewDidDisappear(bool animated)
        {
            base.ViewDidDisappear(animated);
            NSNotificationCenter.DefaultCenter.RemoveObserver(_notificationHandle);
        }

        public override void ObserveValue(NSString keyPath, NSObject ofObject, NSDictionary change, IntPtr context)
        {
            base.ObserveValue(keyPath, ofObject, change, context);
        }

        private void HandleAppWillEnterForeground(NSNotification notification)
        {
            initVolumeKeyEvent();
        }

        private void initVolumeKeyEvent()
        {
            if (audioSession == null)
            {
                audioSession = AVAudioSession.SharedInstance();
            }
            audioSession.SetActive(true);
            AudioSession.CurrentHardwareOutputVolumeChanged += (float obj) => {
                BeginInvokeOnMainThread(() => {
                    if (MPMusicPlayerController.ApplicationMusicPlayer.Volume != 0.3F)
                    {
                        lightAsync(true);
                    }
                    MPMusicPlayerController.ApplicationMusicPlayer.Volume = 0.3F;
                });
            };
        }

        async void lightAsync(bool onOff)
        {
            var device = AVCaptureDevice.GetDefaultDevice(AVMediaType.Video);
            if (device == null)
            {
                return;
            }

            NSError error = null;
            device.LockForConfiguration(out error);
            if (error != null)
            {
                device.UnlockForConfiguration();
                return;
            }
            else
            {
                device.SetTorchModeLevel(1F, out error);
                
                if (onOff)
                {
                    device.TorchMode = AVCaptureTorchMode.On;
                }
                else
                {
                    device.TorchMode = AVCaptureTorchMode.Off;
                }
                device.UnlockForConfiguration();
            }
            if (onOff)
            {
                await Task.Delay(50);
                lightAsync(false);
            }
        }
    }
}
```

## DEMO
<img src="https://qiita-image-store.s3.amazonaws.com/0/21577/1ba652d7-2c8f-dce3-590c-f527e1b510e2.gif" width=30%> <img src="https://qiita-image-store.s3.amazonaws.com/0/21577/cebe9462-54a3-9154-861d-56370bd838b7.gif" width=30%>

## 問題点

### 配布がめんどくさい

4パート分のiPhoneを用意する必要があり、手元の3台のiPhoneだけでは足りません。他の役員さんのiPhoneにもインストールする必要があります。配布するには開発マシンとiPhoneを同一ネットワークに接続し、デバイスの登録とインストールを行う必要があります。当日まで練習日は無いため、幼稚園に開発マシンを持ち込み、iPhoneのインターネット共有を使ってなんとかするしかないです。

### アプリが消える？

Xamarin Live Playerにインストールしたアプリは、しばらくするといつの間にか実行できなくなっています。たぶん24時間ほど経つと消えてしまうのではないかと思っていますが、何かの拍子に本番直前に消えてしまうと困ります。

### 故障が心配で練習できない

カスタネットの下にAB Shutter 3を置いて叩くわけですが、叩いた衝撃が強すぎるとAB Shutter 3の押しボタンスイッチを破損してしまう可能性があります。練習時に壊れるのが怖いのでたぶんほぼぶっつけ本番になりそうです。5つあるので練習で1つ犠牲にしてもいいんですが・・

## あとがき

衝動買いして半ば放置していたAB Shutter 3 が役に立ちそうで本当に良かったです。

実は今回(たぶん)初めてiOSアプリを作ってみたわけですが、今はWindowsでも無料でiOSアプリが作れて、実機で実行までできてしまう時代なんですね。素晴らしいですね。

あとは当日まで頑張って練習するのみです。どうなることやら。