2014年から個人でアプリを作り続けてきて、壁紙や癒し・引き寄せ系のジャンルで累計5,000万ダウンロードを超えました。このジャンルで長く運営していると、「音」がいかにアプリ体験の核心にあるかを痛感します。BGMが途切れた瞬間、ユーザーは現実に引き戻されます。そのまま離脱してしまう人も少なくありません。
先日、新しい瞑想・癒しアプリの試作をRorkで進めていたとき、このアンビエントサウンドの実装でかなりの時間を使ってしまいました。Rorkは画面構成やAPIとの接続が得意なツールですが、音に関してはAIが生成するコードにいくつかのクセがあります。同じ問題にぶつかる方が少なくないはずなので、詰まったポイントと解決策を整理しておきます。
問題1:ループの継ぎ目でブツッと音が途切れる
最初にぶつかったのは、ループ再生時の「プツッ」という音切れです。RorkにExpoでアンビエントサウンドを実装するよう依頼すると、多くの場合以下のようなコードが生成されます。
// Rorkが生成しやすいパターン(ループ途切れが起きやすい)
const sound = await Audio.Sound.createAsync(
require('./assets/rain.mp3'),
{ isLooping: true }
);
await sound.playAsync();isLooping: true を設定すれば自動でループするはずですが、MP3ファイルの末尾に僅かなサイレンスがエンコードされていると、そこで必ずブツッと音が途切れます。これはReact Native固有の問題ではなく、MP3フォーマット自体の特性です。
解決策:WAVまたはm4aに変換 + PlaybackStatusで手動ループ制御
import { Audio } from 'expo-av';
import { useEffect, useRef } from 'react';
export function useAmbientSound(assetSource) {
const soundRef = useRef(null);
useEffect(() => {
let isMounted = true;
const loadAndPlay = async () => {
await Audio.setAudioModeAsync({
playsInSilentModeIOS: true, // 問題2の対策も兼ねる
staysActiveInBackground: true,
shouldDuckAndroid: false,
});
const { sound } = await Audio.Sound.createAsync(
assetSource,
{ shouldPlay: true, volume: 0.6 }
);
if (!isMounted) return;
soundRef.current = sound;
// ループ途切れ対策: onPlaybackStatusUpdateで手動制御
sound.setOnPlaybackStatusUpdate(async (status) => {
if (status.didJustFinish && isMounted) {
await sound.replayAsync(); // 末尾に達したら即座に再スタート
}
});
};
loadAndPlay();
return () => {
isMounted = false;
soundRef.current?.unloadAsync();
};
}, []);
return soundRef;
}isLooping に頼らず、didJustFinish を検知して replayAsync() を呼ぶほうが、実機でのループ精度が格段に上がります。また、音源ファイルはなるべくWAVかm4a(AAC)を使い、MP3は避けることをお勧めします。
問題2:iOSのサイレントモードで無音になる
癒し系アプリでよくあるシナリオです。ユーザーが「寝る前に使いたい」と思っているのに、着信音スイッチをオフにしていると音が一切聞こえありません。AppStoreのレビューにも「BGMが聞こえない」というコメントが来やすい問題です。
Rorkが生成するデフォルトのコードではこの設定が入っていないことがほとんどです。必ず明示的に playsInSilentModeIOS: true を設定する必要があります。
// アプリ起動時またはサウンド初期化前に必ず実行
await Audio.setAudioModeAsync({
playsInSilentModeIOS: true, // ← これがないと着信音オフで無音になる
allowsRecordingIOS: false,
staysActiveInBackground: true,
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX,
shouldDuckAndroid: false,
playThroughEarpieceAndroid: false,
});この設定はアプリ全体で一度実行すれば有効ですが、AppDelegate.m やInfo.plistへの追記も必要です。Rorkのプロンプトで「iOSのサイレントモードでも音を再生する設定を追加して」と明示的に伝えると、対応したコードを生成してくれることが多くなります。
問題3:長時間のバックグラウンド再生でバッテリーが異常消耗する
これは気づくのに時間がかかりました。実機テスト中、「1時間使っただけでバッテリーが20%減った」という事象が発生しました。
原因を調べると、staysActiveInBackground: true とバックグラウンドオーディオモードを設定したものの、Androidで shouldDuckAndroid: false + 高ビットレートMP3の組み合わせが常に最高優先度でデコードし続けていることがわかりました。
加えて、Rorkが生成したコードでは PlaybackStatus の購読コールバックが必要以上に高頻度で呼ばれ、UIスレッドに負荷をかけていたことも一因でした。
解決策:音源の最適化 + progressUpdateIntervalMillisの設定
const { sound } = await Audio.Sound.createAsync(
assetSource,
{
shouldPlay: true,
volume: 0.6,
progressUpdateIntervalMillis: 1000, // デフォルト500ms → 1000msに変更
// 低ビットレートで十分な音源はロード時点で品質を指定しない
}
);音源ファイル自体は、アンビエントサウンドであれば96kbps程度のAACで十分です。320kbpsや非圧縮WAVをストリーミングし続けるのは過剰です。私が運営してきたアプリでは、96kbps m4aに統一したところバッテリー消耗が約40%改善しました。
Rorkへの依頼でうまくいくプロンプトパターン
以上の3問題を踏まえて、Rorkに癒し系アプリのオーディオ実装を依頼するときは、以下のように具体的な条件を指定するとはるかに精度が上がります。
アンビエントサウンドを実装してください。要件:
- ループ再生(途切れなし。isLoopingではなくdidJustFinishで再スタート)
- iOSのサイレントモードでも再生(playsInSilentModeIOS: true)
- バックグラウンド再生対応(staysActiveInBackground: true)
- progressUpdateIntervalMillisは1000msに設定
- ファイル形式はm4a(WAV可)
「途切れなし」「サイレントモードでも再生」という言葉をプロンプトに入れるだけで、Rorkの生成コードの質が変わります。AIに何をしてほしいかではなく、何を避けてほしいかを伝える点が肝心です。
音で作るユーザー体験の厚み
音は視覚よりも先にユーザーの感情に届きます。12年間、癒しや引き寄せのジャンルでアプリを作ってきてつくづく感じることです。
両家の祖父が宮大工で、「見えないところまで丁寧に組む」という感覚を傍で見て育ちました。ユーザーには見えないオーディオ設定の一行にも、同じ姿勢で向き合うことが、長く使ってもらえるアプリにつながると考えています。
Rorkを使えばアプリの骨格は驚くほど速く作れますが、ユーザー体験の細部は、こうした「Rorkが気にしないポイント」を開発者側で丁寧に埋めていく必要があります。
瞑想・癒し系アプリを作ろうとしている方に、この記事が少しでも役立てば嬉しいです。