毎回1つのテーマを定めて行う、Radiotalk主催のテックイベント「Radiotalk Tech Talk」。4月28日開催の記念すべき第1回目では、「Radiotalk」をテーマに、Radiotalkが描くビジョン、そして実際にRadiotalkのサービスの基幹を形成するインフラ設計について、Radiotalk社員による発表を行いました。
(1)Radiotalkが実現するビジョン(Radiotalk 代表取締役 井上佳央里 @JD_KAORI)
最初のセッションは、Radiotalk株式会社 代表取締役社長の井上佳央里より、「Radiotalkが実現するビジョン」。Radiotalkがサービスとして目指すビジョンをお話しました。(以降、各章ではスピーカーたちの発言要旨をまとめます)
トークをエンタメに 〜音声の力で「個の熱狂経済圏」を作る
Radiotalkが掲げるビジョンは「トークをエンタメにする」。人生を送るうえで欠かされることのない「会話」がエンタメになったら毎日がエンタメの宝庫になるのでは、という思いからサービスを運営しています。
メルカリやBASEなどを使って個人が自由に商品を売る「個人経済圏」が注目されているいま、Radiotalkは音声の強みである「熱狂度の高さ」に注目し、「熱狂経済圏」を作り上げようとしています。この「熱狂経済圏」を作り上げるビジネスモデルとして、Radiotalkが取り入れているのが、「ギフティング」。ライブ配信や収録配信においてリスナーから贈られる有料ギフトを通じ、「ラジオトーカー」と呼ばれる配信者たちが自らのトークを収益化しています。
個人のストーリーや思想にファンが生まれ、経済が生まれていく
音声配信の魅力は「見た目とか年齢とか全然関係なく人気になれる」という点。これまで動画系のライブ配信では本筋とはされてこなった層の人々にも、脚光が当たってきているのです。
その人が語るストーリーや言葉、思想にリスナーがどんどん引きつけられてファンになり、それに対して課金が発生していく──。こうした「人間的なボキャブラリーやパーソナリティが共感を呼び、応援される経済圏」をRadiotalkで作っていきたいと考えています。
(2)「インフラ環境が変更される可能性ってなんだかんだないよね?」「あるよ。」APIにおけるインフラ層の抽象化と実例(Radiotalk CTO 斉藤裕気 @gamu1012)
続いては、Radiotalk株式会社 CTOの斉藤裕気による「APIにおけるインフラ層の抽象化と実例」。Radiotalkの基盤を支えるサーバサイドの設計における「抽象化」の取り組みについてお話しました。
サービスが変化し続ける時代に「抽象化」の概念は重要
サービス開発におけるエンジニアリングは、「完成品をリリースをして終わり」という時代から、ユーザーの反応や市場の反応を見ながら改善を継続し、ソフトウェアを進化させ続けていく、いわば「リリースがスタート」の時代へと突入しました。
いまやサービスはつねに変化し、それに応じて機能や実現方法も変化することが大前提となっています。そんななか、実装したサービスやソフトウェアを変化、改善し続けていくために強力な役割を果たす要素のひとつとして、今回は「抽象化」にフォーカスしてお話します。
相次ぐインフラの変更に対応するための「抽象化」
Radiotalkのようなサービスにおいては、機能の変化に応じてユースケースや、使用するインフラの構造もめまぐるしく変化していきます。今回はこのインフラ部分に焦点を当ててお話をしたいと思います。
サービス変化の過程では、必要に応じて裏側のインフラもまた変化していくことを想定しなければなりません。データベースがMySQLからPostgreSQLになる場合もありえるでしょうし、キャッシュがファイルベースからRedisになることもありえるでしょう。ファイルストレージがS3から別のクラウドストレージになることも今後十分にありえます。前職時代こそ「インフラの変更は本当に発生するのか?」と思いながら取り組んでいた抽象化ですが、Radiotalkに入社してからは、よりその必要性を実感するようになりました。
Azure→OpenStack→CGP... 1年半で2度も基盤インフラを変えたRadiotalk
ここで、サービスとしてのRadiotalkがインフラをどのように変化させていったか、振り返ってみたいと思います。
Radiotalkはエキサイト株式会社の社内ベンチャーとしてスタートし、2017年8月にサービスを開始しました。当初はオフショアで開発を行っていたため、インフラとして先方の開発チームからも利用しやすいMicrosoft Azureを使っていましたが、その後社内開発チームの立ち上がりにあわせ、当時エキサイトがメインで利用していたOpenStackへと移行しました。
その後、エキサイトではプライベートクラウドからパブリッククラウドへの移行が検討されていたのですが、RadiotalkとしてはSpeech-To-Textなどをいち早く利用したいという思いから、いちはやくパブリッククラウドへと舵を切り、2019年2月にGCPへとインフラを移行しました。
わずか1年半のあいだに2回も基盤インフラを変えるというのは、さすがに異例のことでした。しかし、どう成長していくかわからないスタートアップという舞台においては、必要なところを抜き出しながらインフラを変化させていくというアプローチが非常に有効だったのです。
「抽象化ありき」で考えてもいけない
抽象化にあたって大事なことは要求にフォーカスすることです。今回の要求は「データの永続化」です。ここで間違えてはいけないポイントは、たんに「MySQLを抽象化する」のではなく、「データの永続化のためにデータストアを抽象化させ、MySQLで実現する」ということです。
次に大事にしたのが「YAGNI(You Ain't Gonna Need It:実際に必要になるまで機能を追加しない)」という考え方です。具体的に存在する要求や課題をきっちり抽象化するのは大事であると同時に、それが難しければ抽象化をしないという方法もあります。「なんでもかんでもとにかく抽象化すればよい」という考えになってしまうと、Redisのようにもともと多様な使い方が出来る仕組みの前では、とくに痛い目を見ることになってしまいます。
もっとも、こうした考え方はあくまで原則論なので、そのまま現場に持ってこれるかというと、難しいでしょう。エンジニアやプロダクトマネージャーらと議論を重ね、「本当にユーザーが求めているものはどういうことか」というところにきちんと向き合わなければならないのは言うまでもありません。
(3)Radiotalk Androidアプリにおけるモジュール分割の課題とこれから(Radiotalk Androidエンジニア 牧山瞭 @_rmakiyama)
続いてのLTは、Radiotalk Androidエンジニア・牧山瞭による「Radiotalk Androidアプリにおけるモジュール分割の課題とこれから」。RadiotalkのAndroid向けアプリの開発現場で実践しているモジュール分割の方法についてお話しました。
RadiotalkのAndorid版アプリは2018年10月にリリースしましたが、当時の設計からマルチモジュールを採用していたので、今回は「ひとつのAppモジュールをどう分割するか」といった話は出てきません。
また、開発体制についても、現在は私ひとりがフルタイムで行っており、これから複数人体制での開発へと取り組んでいこうとしている状況です。
サービスのフェーズや開発組織の大きさによっても事情は異なってくるので、すべてのプロジェクトでこれからお話する方法がオススメ、というわけではないことは、あらかじめ念頭においておいていただければと思います。
現在のRadiotalk Android版のモジュール構成
現在のRadiotalk Android版アプリにおけるモジュール構成は、図の通りとなっています。
「features」と表している部分は、検索やライブ配信など、機能(feature)単位で分割したモジュールがいくつもある形となっています。いわゆる垂直方向の分割です。
「clients」と表している部分は、TalkClientや、LiveClientなど、Radiotalk内でメインとなるロジックをまとめたモジュールがいくつかある形です。これによって、開発の当初から大きく変わらない主要な処理を共通化し、実装の高速化を図っています。
「screen_navigation」モジュールでは画面遷移を抽象化させています。マルチモジュール環境では画面遷移で問題が起きやすい部分があるのですが、ここでは循環依存を回避しつつ、機能間での画面遷移を実現させています。
さらにドメインモデル的なアプローチとして、収録やライブ配信といった領域ごとにそれぞれモジュールを切り出し、ライブラリ的な扱いができるように実装しています。これらのモジュールを使う際は基本的にインターフェイス経由で処理を呼び出すようにしており、使う側からは具体的な技術詳細を隠すよう実装しています。すなわち、機能を呼び出す側は、その裏側の仕組みを一切意識する必要がありません。
機能ごとに独立して開発し、主要ロジックも共通化しているため、個々の部分で改善点はあるにせよ、全体としては特にストレスなく実装が行えています。
もっとも、これは現在私がひとりで開発を行っているという前提によるものなので、今後のプロダクトの成長や、開発チームの拡張も視野に入れていかなければいけません。
現状の課題は「制約設計」「機能ごとのインフラの取り回し」「設計思想の明文化」
先ほどCTOの斉藤のLTにもあったとおり、いまやプロダクトを作って終わりの時代ではありません。機能追加や開発組織の変化も含め、プロダクトの成長に合わせて常に改善を続ける必要があります。これを踏まえ、現在の課題について考えてみたいと思います。
Featureモジュールの中身はよくあるMVVMパターンを用いており、1モジュールの中でパッケージごとにレイヤー分けしています。
AndroidではKotlinで開発しているのですが、この言語の制約を駆使しても、プレゼンテーション層からインフラ層を呼び出せてしまうなど、制約の設計が弱いかなと感じています。またFeatureモジュールごとにインフラ層を持っているので、別のFeatureで定義しているAPIを使いたい、メモリキャッシュにアクセスしたいというときなど、インフラの取り回しが効きづらい部分も出てきています。
また、Feature間のモジュール依存についてもあまり整理できておらず、複数人のチームで開発する上ではあまり統一した設計思想が作れていないという点が目下の課題となっています。ひとりでだけで開発している状態では自身が気をつければよいですが、チームが大きくなるとコードレビューで設計の崩壊を防ぐのことはどうしても困難です。
そんななか私が注目したのは、最近読んだ『Design It! プログラマーのためのアーキテクティング入門』(Michael Keeling著/オライリー・ジャパン)にあった次の一文です。
改善にあたり、設計によって「何を」促進させたいのかを明確にし、そしてその設計思想が見えるよう意識することで、チームでの開発体験を向上させたいと考えました。
今後の改善策:「モジュール間依存の強制」「開発メンバーの判断コストを下げるモジュール設計」「巨大なCommonモジュールの分割」
これらの話を踏まえ、これからのモジュール構成について現時点で考えていることをお話ししたいと思います。
まず、これからの開発チームが達成したいことについて考えてみました。
これを踏まえ、中期的には、次の図のようなモジュール分割に変えようと考えています。
具体的には、Featureモジュールとして1モジュールに実装されていたレイヤー表現をモジュールとして分割すること、ひとつの大きいCommonモジュールとして共通化した処理を持っていた部分を分割したいというところ。また細かい部分では、次の改善への準備を目指す作り方を進めていきたいと思っています。
それぞれについて、もう少しだけ説明していきます。
まず、今までパッケージレベルで分けていたレイヤーをモジュール分割します。これにより、依存していないモジュールは参照できないため、たとえばプレゼンテーション層からインフラ層を呼び出せないといった制約を強めることができます。また、Kotlinの場合は、internal修飾子を使うことによって依存していないモジュール内でのみクラスを呼べるよう制約も設けられるため、インタフェースのみを公開し技術的関心事を隠蔽することができるようになります。これにより、ユースケースには「何をするのか」ということを記述し、インフラ層には「どうやって実現するのか」によりフォーカスして実装を進められるメリットもあります。
また今の段階では、ユースケースとインフラを担うモジュールをコンテキストごとに切らず、1モジュールにまとめることで、開発時の判断コストを下げることも考えています。ここには、これから新たな開発メンバーが入ってくることを考えたときに、コンテキストを簡単に理解してもらうことは難しいためであることと、変化の激しいスタートアップだとそのコンテキスト自体も今後変わっていくことが十分に考えられるためという意図があります。
また、これまではCommonモジュールとして「便利モジュール」を設けていたのですが、すべてのモジュールから依存されているがゆえ、このモジュールのクラスを変更すると、不必要な個所までまとめて再ビルドが起きるという問題を抱えていました。そのため、こちらは関心の分離をするため、レイヤーごとに分割していきます。
長期的にはしっかりのドメイン層を設け、ドメインを中心としたアーキテクチャに寄せていきたいなという思いがあります。まずは依存の逆転を見据え、インターフェースをしっかり切っていく実装をしていきたいと考えています。
成長し続けるプロダクトのため、設計も改善を続けていく
「ソフトウェアは作って終わりではない」というのが、Radiotalkの開発組織全体としての哲学です。プロダクトや開発組織の成長とともに、常に設計も改善を続けていくという気持ちを大事にしています。また、設計判断にはバズワードに飛びつくだけではなく、その導入に意味をもたせることを意識しています。
このような話を一緒にワイワイしながら成長させてくれるメンバーも募集しています!
>>急成長中VoiceTechベンチャー"Radiotalk"のエンジニア募集【サーバサイドエンジニア】
>>音声エンターテインメントを創るVoiceTechベンチャーのテックリード
(4)Radiotalk iOSの音声配信技術について(Radiotalk iOSエンジニア 竹内彰吾)
最後のLTは、Radiotalk iOSエンジニア 竹内彰吾による「Radiotalk iOSの音声配信技術について」。Radiotalkの根幹をなす音声配信技術についてお話しました。
Radiotalkの「再生まわり」
iOSアプリにおけるオーディオの再生・録音・ミックス・生成まわりをサポートするのが「Core Audio」。以下のように複数のフレームワークで構成されています。
Radiotalkでもっとも多く使っているのが、「AVFoundation」と呼ばれるフレームワークです。ストリーミング再生の場合、サーバーから音声のURLを取得し、AVFoundationに含まれる「AVPlayer」モジュールに渡すことで機能を実現しています。
ダウンロード再生、すなわちiPhoneの端末上にある音声ファイルを再生する場合には、同じくAVFoundationの中にある機能で「AVAudioPlayer」モジュールを使っています。この仕組みではストリーミング再生はできませんが、ローカルファイルの再生に適しているため、採用しています。
iPhoneのコントロールセンターやロック画面で、現在再生しているメディアの情報が表示されるのをよく目にしますが、こうした挙動もその都度実装しなければいけません。Radiotalkでは「MediaPlayer」クラスをimportし、その中の「MPNowPlayingInfoCenter」「MPRemoteCommandCenter」モジュールを組み合わせて実現しています。
「MPNowPlayingInfoCenter」にはメディアの再生時間や再生中のタイトル、配信ユーザー名を渡しています。コントロールセンターで再生ボタンや一時停止ボタン押されたときには「MPRemoteCommandCenter」でイベントをキャッチし、アプリの挙動と連携するようにしています。
Radiotalkの「録音まわり」(単体収録の場合)
続いては、音声の録音まわりを説明します。
ユーザーが1人でトークを録音し、配信する場合には、先程と同じく「AVFoundation」をimportし、「AVAudioRecorder」モジュールを使用しています。
「AVAudioRecorder」にまずローカルの音声データを渡し、さらに録音の形式を設定ファイルで渡しています。その後AVAudioRecorderのrecordメソッドを叩くと、録音がスタートします。
Radiotalkの「録音まわり」(リモート収録の場合)
Radiotalkには、リモート上でゲストを呼んで一緒にトークをし、それを一つの音声データにまとめて配信する機能があります。この機能は「Agora」という配信プラットフォームを利用しています。
この「Agora」は、複数の音声インプットをミックスし、一つの音声データとして出力してくれます。具体的な使い方としてはローカルに音声ファイルのパスを作成し、AgoraSDKに渡すことで、Agora上でトークを録音し、ローカル上に音声ファイルを完成させるというものです。Radiotalk側ではとくに実装はしておらず、Agoraのサービスを純粋に利用しているという形です。
Radiotalkの「音声編集まわり」
Radiotalkでは、収録した音声をあとから編集・エフェクトをほどこしたり、スピードや音量を変えることができます。この機能はAVFoundationをimportし、その中の「AVAudioEngine」を使用して実現しています。
「AVAudioEngine」は音声の生成、処理、入出力をトータルに扱うモジュールです。使い方としては、このAVAudioEngine に、音量を変更する「AVAudioPlayerNode」、音の高さや速度を変更する「AVAudioUnitTimePitch」を装着することでエフェクト機能を実装しています。
続いて、波形編集の部分です。
Radiotalkでは収録した音声ファイルの一部分を切り抜いたりできる編集機能を提供していますが、この実装もAVFoundationを使用しています。具体的には「AVAudioRecorder」で波形を取得し、編集した後の波形を用いて「AVConposition」で音声ファイルを再生成しています。
Radiotalkの「ライブ配信まわり」(単独ライブ配信)
Radiotalkにはライブ配信の機能が備わっており、配信者が配信をしている音声を大勢の人がリスナーとして聞くことができます。このライブ配信のアーキテクチャについて説明していきます。
まずは「単独ライブ配信」。これは、1人のホストと複数のリスナーの場合で構成される場合を指します。配信者がiPhoneに対して喋っている音声データを取得し、それを音声サーバーのRTMP(Real Time Messaging Protocol:リアルタイム通信プロトコル)として配信することで、リスナーがそれを生配信として聞くことができます。
加えてRadiotalk側のサーバーでは、リスナーからギフトやコメントが送信された際にAPIリクエストを行います。リスナー側は、これをWebSocket(双方向通信プロトコル)経由で受け取ることで、自分を含めリスナーからのギフトやコメントがリアルタイムに表示される仕組みとなっています。
Radiotalkの「ライブ配信まわり」(コラボライブ配信)
もうひとつが「コラボライブ配信」。これは、配信者がリスナーの中から何人かをピックアップし、一緒に配信できる機能です。ピックアップしたリスナーのことを「ゲスト」と呼んでいます。
配信者とゲストとの会話には、先程のリモート配信でも出てきた「Agora」を使用しています。
配信者側のアプリでは、まずAgora上にルームを作成し、ゲストに対してRadiotalkのサーバー経由で招待を送ります。ゲストはWebSocketでこれを受け取ってAgoraのルームに参加し、複数人が同時に会話できる状況ができあがります。さらに、このAgoraからミックスされた音声をRadiotalkのサーバーで取得し、リスナーにHLSで送信するという仕組みになっています。
Apple製品間の「サンプリングレート違い」を埋めるための工夫
iPhoneから音声データを取得し、音声サーバーに流すまでのあいだに、低レベル(ハードウェア制御に近い)APIを用いた実装が必要になります。実はこの部分こそが、結構頭を悩ませるポイントでした。iPhoneやiPad、AirPodsなどのApple製品は、それぞれ製品ごとに機器の音声サンプリングレートが異なっています。Radiotalkでは、これらのサンプリングレートをひとつに統一するため、アプリ側で変換を行っています。
まず、サンプリングレートを取得する方法ですが、AVFoundationをimportし、「AVAudioEngine」と「AVAudioNode」を組み合わせて実装しています。具体的にはマイクから受け取る音声と、あらかじめ設定したサンプリングレートに基づくアウトプット定義を比較し、44.1KHzで入力されたものはそのまま変換しないで通しています。
一方、16KHzで入力された場合、過去にうまくいかなかった事情から、いちど48KHzに変換したうえで44.1KHzに再度変換する、というトリッキーな操作をしています。アップサンプリング(サンプリング周波数を上げる)操作はよい感じに進むのですが、48KHzを44.1KHzに下げるといったダウンサンプリング(サンプリング周波数を下げる)はなかなか難しく、(バッドノウハウ的に)このような処理を行っているのが現状です。
最後に「音声開発あるある」
音声周りの開発をしているうえでの「あるある」なのですが、コワーキングスペースなど、周りに人がいる中で開発をしていてハウリングを起こしてしまったり、音楽を聞きながら作業をしていて、ホストとゲスト側それぞれの疎通を確認するために端末のイヤホンを差し替えていたら、その拍子に爆音の音楽が流れてしまったり…… ということがよくあります。
あとは、Android側をホストにして、iOSをクライアント側にしていた際、環境が異なるAndroid側から音声が送られたのを聞いて、うっかりiOS側の問題が解決したと思い込んでしまったり……。このように恥ずかしい気持ちになることも多いですが、概ね楽しく開発を行っています。