Xperia Ear Duoとバイノーラル録音とXamarinで、秋葉原をデートする聴覚MR(AR)アプリを作ってみる

はじめに

作るもの

Xperia Ear Duo と バイノーラル録音 を使って、
秋葉原を女性に案内してもらう(デートする)アプリを作って遊んでみようと思います。
なお、アプリは、Xamarin.Fomrs でサクッと作ります。

アプリを起動したまま秋葉原を歩き、
特定の場所に近づくと音声が流れる。という仕組みです。

非常に残念ながら、女性の声を録音できなかったので、30過ぎのおっさんの声で実装しましたが、
あまりに気持ち悪いので、音声は非公開とさせていただきます(´・ω・`)
せめて、ダンディなオジサマボイスが出したかった(´・ω・`)

周囲の音と機器の音の両方が聴こえるデバイス

一般的なイヤホンやヘッドホンは没入型のデバイスでした。
音楽の世界に浸る。という意味ではスピーカーも没入型寄りだと思います。

最近、周囲の音が普通に聞こえるイヤホンがいくつか発売されています。
ソニーモバイルコミュニケーションズ の Xperia Ear Duo(XEA20)が、代表例かと思います。

こういった製品は、現実の音(周囲の音)とイヤホン(機器)からの音を同時に聴くことができます。
つまり、音の複合現実、聴覚のMixed Realityがこれで実現できます。
(∩´∀`)∩ わーい

Xperia Ear Duo(XEA20
Xperia Ear Duo(XEA20) | Xperia(TM) Smart Products | ソニー

バイノーラル録音

あたかもその場に居合わせたかのような臨場感を再現できる。
といわれているバイノーラル録音ですが、従来は高価な機材が必要でした。

ところが近年、簡易的ですが、安い商品が複数現れ、簡単にこれを試せるようになりました。
(∩´∀`)∩ わーい

立体的に音を録音できるため「右耳の耳元で囁く」といった使い方はもちろん、
右斜め後ろ方向から駆け寄ってくる」とか
斜め下から話しかけるようにして、背の小さい人を再現
などといったことができます。

IMG_079.JPG
※こういったものがAmazonで買えます。

作る!

録音する

「待ったー?」って言いながら後ろから駆け寄ってきて、耳元で「遅くなってごめんね」って囁きます。
秋葉原が舞台なので、神田明神の説明もしてみます。UDXの説明もしてみました。
立体的に録音されます。おっさんの声が(´・ω・`)

やってみた感じですが、できる限り広くて静かなところでないとうまく録音されません。
部屋の中で音が反響して、会議室感のある音になったり、
真上からエアコンの音が聴こえちゃったりします。

アプリを作る

せっかくなので、iOSでもAndroidでも動くものを、サクッと簡単にXamarin.Formsで作ります。
素敵なコードを書いたり、イケてるアプリを作ることが目的ではないので、
すぐにできる超簡単なものを作りました

ソースコード

一式GitHubにおいておきました。GitHubはこちら
なお、下記のライブラリをNuGet経由で入れて使用しています
Xam.Plugin.Geolocator」 簡単にGPSが使えます。
Plugin.MediaManager」 簡単に音声ファイルなどを再生できます。

MainPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:AkihabaraDate"
             x:Class="AkihabaraDate.MainPage">

    <StackLayout VerticalOptions="Center" HorizontalOptions="Center">
        <Button Text="デート開始" Clicked="DatesStartButton_OnClicked" />
        <Label Text="" />
        <Button Text="デート終了" Clicked="DatesStopButton_OnClicked" />
        <Label Text="" />
        <Label Text="" />
        <Button Text="再生" Clicked="VoiceStartButton_OnClicked" />
        <Label Text="" />
        <Button Text="再生停止" Clicked="VoiceStopButton_OnClicked" />
    </StackLayout>

</ContentPage>

MainPage.xaml.cs

using System;
using Xamarin.Forms;

namespace AkihabaraDate
{
    public partial class MainPage : ContentPage
    {
        private readonly Model _model = new Model();

        public MainPage()
        {
            InitializeComponent();
            _model.Initialize();
        }

        private void DatesStartButton_OnClicked(object sender, EventArgs e)
        {
            _model.RunDates();
        }

        private void DatesStopButton_OnClicked(object sender, EventArgs e)
        {
            _model.StopDates();
        }

        private void VoiceStartButton_OnClicked(object sender, EventArgs e)
        {
            _model.ToggleTalk();
        }

        private void VoiceStopButton_OnClicked(object sender, EventArgs e)
        {
            _model.StopVoice();
        }
    }
}

Model.cs

using System;
using System.Timers;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Plugin.Geolocator;
using Plugin.Geolocator.Abstractions;
using Plugin.MediaManager;

namespace AkihabaraDate
{
    public class Model
    {
        private readonly List<SpotList> _spotList = new List<SpotList>
        {
            new SpotList{SpotName = "ダミーセリフ",Latitude = 0,Longitude = 0,VoiceName = "音声ファイル名.mp3"},
            new SpotList{SpotName = "秋葉原駅電気街口を出たところ",Latitude = 35.698466,Longitude = 139.773114,VoiceName = "音声ファイル名.mp3"},
            new SpotList{SpotName = "AKIHABARA_UDX",Latitude = 35.700525,Longitude = 139.772508,VoiceName = "音声ファイル名.mp3"},
            new SpotList{SpotName = "神田明神",Latitude = 35.701922,Longitude = 139.767846,VoiceName = "音声ファイル名.mp3"},
        };

        private double Latitude { get; set; }
        private double Longitude { get; set; }
        private int NearestPlaceNo { get; set; }
        private readonly Timer _timer = new Timer(2000);

        /// <summary>
        /// デートを開始する。
        /// </summary>
        public void RunDates()
        {
            _timer.Stop();

            var lastVoiceNo = 0;
            _timer.Elapsed += (sender, e) =>
            {
                Initialize();

                if (NearestPlaceNo == 0 || lastVoiceNo == NearestPlaceNo) return;
                PlayVoice(_spotList[NearestPlaceNo].VoiceName);
                lastVoiceNo = NearestPlaceNo;
            };

            _timer.Start();
        }

        /// <summary>
        /// デートを止める。
        /// </summary>
        public void StopDates()
        {
            _timer.Stop();
        }

        /// <summary>
        /// ユーザの現在地に合わせた音声を再生する
        /// </summary>
        public void ToggleTalk()
        {
            Initialize();
            PlayVoice(_spotList[NearestPlaceNo].VoiceName);
        }

        /// <summary>
        /// ユーザの現在地を取得し、一番近い音声再生スポットの場所を返す。
        /// </summary>
        public void Initialize()
        {
            GetPosition();
            NearestPlaceNo = GetNearestPlaceNo();
        }

        /// <summary>
        /// ユーザから一番近い場所までの距離を計算する。
        /// </summary>
        /// <returns>ユーザから一番近い場所のIndex</returns>
        private int GetNearestPlaceNo()
        {
            var distance = new List<int>();

            foreach (var spot in _spotList)
            {
                distance.Add(CalcDistance(Latitude, Longitude, spot.Latitude, spot.Longitude));
            }

            //一番近い場所に対して100m以内だったら、その場所のIndexを返す。そうでなければとりあえず0を返す。
            return distance.Min() <= 100 ? distance.IndexOf(distance.Min()) : 0;
        }


        /// <summary>
        /// 声を再生する。
        /// </summary>
        private async void PlayVoice(string voiceName)
        {
            CrossMediaManager.Current.MediaQueue.Clear();
            await CrossMediaManager.Current.Play("https://音声ファイルを置いたURL/" + voiceName);
        }

        /// <summary>
        /// 声を止める
        /// </summary>
        public void StopVoice()
        {
            CrossMediaManager.Current.Stop();
        }

        /// <summary>
        /// 緯度経度を2つ渡すと、その間の距離を返す。
        /// </summary>
        /// <param name="lat1">緯度1</param>
        /// <param name="long1">経度1</param>
        /// <param name="lat2">緯度2</param>
        /// <param name="long2">経度2</param>
        /// <returns>間の距離</returns>
        private static int CalcDistance(double lat1, double long1, double lat2, double long2)
        {
            var latAvg = Deg2Rad(lat1 + ((lat2 - lat1) / 2));
            var latDifference = Deg2Rad(lat1 - lat2);
            var lonDifference = Deg2Rad(long1 - long2);
            var curRadiusTemp = 1 - 0.00669438 * Math.Pow(Math.Sin(latAvg), 2);
            var meridianCurvatureRadius = 6335439.327 / Math.Sqrt(Math.Pow(curRadiusTemp, 3));
            var primeVerticalCircleCurvatureRadius = 6378137 / Math.Sqrt(curRadiusTemp);

            var distance = Math.Pow(meridianCurvatureRadius * latDifference, 2)
                           + Math.Pow(primeVerticalCircleCurvatureRadius * Math.Cos(latAvg) * lonDifference, 2);
            distance = Math.Sqrt(distance);

            return (int)Math.Round(distance);
        }

        /// <summary>
        /// ラジアン変換。
        /// </summary>
        /// <param name="deg">角度(度数法)</param>
        /// <returns>ラジアン</returns>
        private static double Deg2Rad(double deg)
        {
            return (deg / 180) * Math.PI;
        }

        /// <summary>
        /// ユーザの現在位置を取得し、緯度経度を格納する。
        /// </summary>
        private async void GetPosition()
        {
            var position = await GetGeolocate();
            Latitude = position.Latitude;
            Longitude = position.Longitude;
        }

        /// <summary>
        /// ユーザの現在位置を取得する。
        /// </summary>
        /// <returns>ユーザの現在位置</returns>
        private static async Task<Position> GetGeolocate()
        {
            var locator = CrossGeolocator.Current;

            //1. 50m
            locator.DesiredAccuracy = 50;
            return await locator.GetPositionAsync(TimeSpan.FromSeconds(0.5));
        }
    }
}

アプリ

超簡単なアプリができました。(`・ω・´)
デート開始 を押すとスタートです。
(再生は同じ音声を何回も再生します)

アプリ

遊ぶ

早速、アプリを入れたiPhoneを持って、秋葉原駅の電気街口に行きます。
そして、デート開始を押します!

おっさんが駆け寄ってきます!
耳元で囁きます!

周囲の音が普通に聞こえるので、自然におっさんの声が溶け込み、
リアリティがあります。まさにMixed Realityです。(´・ω・`)

なんだこれ(´・ω・`)
そのままUDXに行きます。

UDXについて勝手に話し始めました!
おっさんが。(´・ω・`)

おわりに

今回作ったような、疑似デートアプリだけでなく、
例えば、
「一緒に走ってくれて、ペースが落ちたら少し前方から励ます声が聴こえる」
といったものも簡単に作れそうですね。

HoloLensに代表される視覚のMixed Realityは、
まだ、街中で気軽に身に着けるようなサイズや見た目ではないと思います。

その点、聴覚のMR(AR)は機器も小型化され、
現時点でも、そんなに違和感なく使用することができます。
一般の人に対しては、視覚のMRより聴覚のMRの方が先に流行るかもしれませんね。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.