家に 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)
}
Whisper.cpp の統合
アーキテクチャ
通話中、音声フィルター(ios_aufilt)から 16kHz の PCM データを受け取り、ローカル(自分)とリモート(相手)の 2 系統に分けて whisper に流す。これにより話者識別(speaker attribution)が可能になる。
whisper.cpp は iOS の Metal をサポートしており、推論速度が大幅に向上する。whisper.xcframework をビルドする際に Metal を有効にするだけでよい。
llama.cpp の統合
Q4 量子化モデルを使用。推論は Swift の actor で管理し、トークンのストリーミング出力は SwiftUI の過剰な再描画を防ぐため 250ms のスロットリングをかけている。
対応プロトコル・機能
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 の統合について質問があればコメントで気軽にどうぞ。
閩南語(ミンナン語)を話す開発者、林エミン でした。


