0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

で iOS SIP ソフトフォンを作った話 Baresip + Whisper + LLama.cpp

0
Posted at

家に 20 年以上動き続けている固定電話がある。ふとそれを見ていて気づいた——SIP はオープンスタンダードなのに、なぜ iOS にまともなクライアントがないのか。

既存のアプリはどれも UI が古いか、通話データを外部サーバーに送っている。そこで自分で作ることにした。それが YakPhone だ。

技術スタック概要

役割 採用技術
SIP プロトコル層 baresip 4.70 + libre
ブリッジ層 Objective-C++ (~2400行)
UI SwiftUI + UIKit (iOS 15+)
通話システム統合 CallKit + PushKit
音声転写 whisper.cpp (Metal GPU 加速)
LLM llama.cpp (Q4 量子化)
永続化 SQLite + iCloud バックアップ

なぜ baresip を選んだか

iOS 向けの主要な OSS SIP ライブラリは 3 つある。

PJSIP は歴史が長くドキュメントも豊富だが、iOS 向けのビルド設定が複雑で、商用ライセンスにも制限がある。

Linphone SDK はバイナリが 30MB を超え、独自の UI レイヤーを強制的に持ち込んでくる。ゼロから UI を書きたい場合には向かない。

baresip はモジュール式で BSD ライセンス。必要なモジュール(g711、audiounit、avcodec、srtp)だけをロードできる。C API が低レベルなので、イベントループとスレッドを精密にコントロールできる——これは CallKit の同期コールバック要件に対応する上で非常に重要だった。

Objective-C++ ブリッジの設計

baresip の C API を Swift から使うために、BaresipManager.mm という Objective-C++ ファイルを書いた。約 2400 行になった。

基本的な構造はこうなっている:

// C のコールバックから Swift/ObjC の世界に橋渡し
static void bevent_handler(enum bevent_ev ev, struct bevent *event, void *arg) {
BaresipManager *mgr = (__bridge BaresipManager *)arg;
   
switch (ev) {
 case BEVENT_REGISTER_OK: {
     NSString *accountId = [sharedInstance accountIdForUA:ua];
     dispatch_async(dispatch_get_main_queue(), ^{
     [mgr handleRegisterSuccess:YES message:@"OK" accountId:accountId];
   });
   break;
}
// ...

C のコールバックは baresip の re_main イベントループスレッドで呼ばれるため、UI 操作はすべて dispatch_async(dispatch_get_main_queue(), ...) で main thread に渡している。

CallKit + PushKit の統合

PushKit の罠
iOS 13 以降、VoIP push を受信したら必ず reportNewIncomingCall を呼ばなければならない。呼ばなかった場合、システムがプロセスを強制終了する。

しかし「SIP の INVITE が届いてから上報する」という実装は危険だ。ネットワーク状況によっては INVITE が来ない可能性があり、その場合プロセスが kill される。


解決策:

VoIP push 受信と同時に CallKit へプレースホルダー通話を上報
SIP INVITE が届いたら通話情報を update
一定時間内に INVITE が来なければ endCall(with: .failed) で終了

var onIncomingPush: (([AnyHashable: Any], @escaping () -> Void) -> Void)?

// PushKit delegate
func pushRegistry(_ registry: PKPushRegistry,
                  didReceiveIncomingPushWith payload: PKPushPayload,
                  for type: PKPushType,
                  completion: @escaping () -> Void) {
    onIncomingPush?(payload.dictionaryPayload, completion)
}

image.png

Whisper.cpp の統合

アーキテクチャ
通話中、音声フィルター(ios_aufilt)から 16kHz の PCM データを受け取り、ローカル(自分)とリモート(相手)の 2 系統に分けて whisper に流す。これにより話者識別(speaker attribution)が可能になる。

whisper.cpp は iOS の Metal をサポートしており、推論速度が大幅に向上する。whisper.xcframework をビルドする際に Metal を有効にするだけでよい。

llama.cpp の統合

Q4 量子化モデルを使用。推論は Swift の actor で管理し、トークンのストリーミング出力は SwiftUI の過剰な再描画を防ぐため 250ms のスロットリングをかけている。

image.png

対応プロトコル・機能

SIP: UDP / TCP / TLS、複数アカウント
メディア暗号化: DTLS-SRTP / SDES / ZRTP
NAT 越え: STUN / TURN
映像: H.264(VideoToolbox ハードウェアエンコード)、PiP、前後カメラ切り替え、DTMF
AI: Whisper リアルタイム転写、llama.cpp 通話要約(全てデバイス上、クラウド送信なし)
その他: iCloud バックアップ、Apple Watch 連携、ホーム画面ウィジェット、8 言語対応

おわりに
baresip は C の世界、SwiftUI は Swift の世界。この 2 つを Objective-C++ で繋ぎ、さらに whisper.cpp と llama.cpp も加えるのは、思ったより複雑だったが面白かった。

SIP や CallKit の統合について質問があればコメントで気軽にどうぞ。

image.png

閩南語(ミンナン語)を話す開発者、林エミン でした。

0
0
1

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?