ゲームに音声はとても重要な位置を占めますので、リソースの足りない個人開発と言えど
ゲーム上限定の効果音の違和感
ゲームでハイスコアや準ずる高得点を取得した際に、コングラッチュレーション的に画面に飛び散る紙吹雪と共に鳴らすのは吾ながら中々宜しい思い付きではないかと、五つ程、クラッカーの効果音を何とかかんとか捻り出して、WindowsやMacなどのPCで試聴しつつ、割りと好い感じの出来の手応えにほくそ笑み、早速Xcodeの当該プロジェクト内に放り込んでは、効果音に於いて何時もの様に利用するクラスで鳴らすのを聞いた時です、明らかな違和感が感じられたのは。違和感と言うよりは、明らかにPCで聴くのとは異なった音がゲームプログラムを通すと聴こえて来たのです。
放っておいても問題無いと言えば問題無いのですが、如何しても気になれば、心情的に対応せざるを得ない心持ちになったのです。
奇妙なスプリングリバーブ効果
確かにPCでの視聴に於いては、パン!と言った、クラッカーらしい破裂音に感じられる音が、テストプレイで高スコアを叩き出した時に放たれる其の音では、折角の破裂音が無駄に明るく軽々しくなって軽快は良いのですが、減衰しつつ繰り返される、所謂リバーブが掛かった様に聞こえるのです。残響が響くのは時と場合に仍っては効果的ですが、短く派手な音のクラッカーには丸で合いません。腑抜けて膝も抜けた感じになって折角のお祝い感が薄れてしまいます。パン!では無く、プヮン!プヮン、プゥヮン、プゥヮン……と原音を後方に重ね再生しつつ跳ねる如き音色はちょうどギター演奏に於けるスプリングリバーブの様な効果が掛かった様に聞こえて来るのです。
スプリングリバーブとはどんな印象を与える音かを聞くにちょうど良い動画がYouTubeに有りましたので、下に埋め込み置きます。興味有る向きはご視聴下さい。
開発機:MacBook Air M1 2020
MacOSバージョン:macOS Tahoe 26.0.1
Xcodeバージョン:26.0.1
言語:Swift
主関連アプリ:をちこち食堂
JKAudioPlayer
では上に記した「効果音に於いて何時もの様に利用するクラス」とは何かと問われれば、Apple謹製のスキームに於いては音声発生に於いては中々厄介が多く、以前抱えたトラブルを解決すべくネットを繰って検出したブログの情報[※1]に紹介されていた「JKAudioPlayer」が其れでした。此のクラスは「stack overflow」の「After installation on iPhone ios10, my ready to ship appstore app no longer play sounds! HELP! With ios9 everything worked perfectly」[※2]なる質問に「I was REALLY annoyed by this problem, so I created my own custom class called JKAudioPlayer to temporarily avoid this problem and handle all my audio for me. It that has 2 AVAudioPlayer objects. 1 to play music, and 1 to play sounds through it. Here's the class.」と回答されるに添えられていたものです。コピペで其の儘使える有用なコードですので以下に引用します。
import Foundation import SpriteKit import AVFoundation /**Manages a shared instance of JKAudioPlayer.*/ private let JKAudioInstance = JKAudioPlayer() /**Provides an easy way to play sounds and music. Use sharedInstance method to access a single object for the entire game to manage the sound and music.*/ open class JKAudioPlayer { /**Used to access music.*/ var musicPlayer: AVAudioPlayer! var soundPlayer: AVAudioPlayer! /** Allows the audio to be shared with other music (such as music being played from your music app). If this setting is false, music you play from your music player will stop when this app's music starts. Default set by Apple is false. */ static var canShareAudio = false { didSet { canShareAudio ? try! AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryAmbient) : try! AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategorySoloAmbient) } } /**Creates an instance of the JAAudio class so the user doesn't have to make their own instance and allows use of the functions. */ open class func sharedInstance() -> JKAudioPlayer { return JKAudioInstance } /**Plays music. You can ignore the "type" property if you include the full name with extension in the "filename" property. Set "canShareAudio" to true if you want other music to be able to play at the same time (default by Apple is false).*/ open func playMusic(_ fileName: String, withExtension type: String = "") { if let url = Bundle.main.url(forResource: fileName, withExtension: type) { musicPlayer = try? AVAudioPlayer(contentsOf: url) musicPlayer.numberOfLoops = -1 musicPlayer.prepareToPlay() musicPlayer.play() } } /**Stops the music. Use the "resumeMusic" method to turn it back on. */ open func stopMusic() { if musicPlayer != nil && musicPlayer!.isPlaying { musicPlayer.currentTime = 0 musicPlayer.stop() } } /**Pauses the music. Use the "resumeMusic" method to turn it back on. */ open func pauseMusic() { if musicPlayer != nil && musicPlayer!.isPlaying { musicPlayer.pause() } } /**Resumes the music after being stopped or paused. */ open func resumeMusic() { if musicPlayer != nil && !musicPlayer!.isPlaying { musicPlayer.play() } } /**Plays a single sound.*/ open func playSoundEffect(named fileName: String) { if let url = Bundle.main.url(forResource: fileName, withExtension: "") { soundPlayer = try? AVAudioPlayer(contentsOf: url) soundPlayer.stop() soundPlayer.numberOfLoops = 1 soundPlayer.prepareToPlay() soundPlayer.play() } } }
恐らくは、JK とは回答者の Senior iOS Software Engineer の Jozey氏のイニシャルでしょう、有り難く拝借していた此のコードにて、音声トラブルは解決され気持ち良く使わせて貰っており、絶大な信頼を置いていたのでした。従って此のクラスが原因とは思えませんでしたが、其れでも音声発生は此のクラスに頼っていましたから何某か見直すしか有りませんでした。
AVFoundationライブラリ
JKAudioPlayer クラスにはコードを見て明らかな様に AVFoundation フレームワークをインポートしています。中にも AVAudioPlayer APIを発声に利用しており、Apple謹製の音声ライブラリの厄介さが身に染みている当方に取り、此れが問題である様に思えてなりません。其処で当該クラスのドキュメント[※3]を参照しても、如何にも何とかなりそうな項目が見つけられません。
更にAVFoundation フレームワークについて調べみると、音声の振る舞いや再生モード、割り込み、バックグラウンドなどの環境設定に関するクラスである AVAudioSession クラス[※4]も AVAudioPlayer クラスと並列に用意されており、ドキュメントには ambient[※5]の文字列が見えます。如何やら此の AVAudioSession クラスは音声ファイルを再生するだけで、内部的に暗黙裏に使われるクラスである様で、余計な残響音が耳障りとなれば此の ambient を停止したくなるのが人情でしょう。
しかし、JKAudioPlayer クラスのコードを見てみると、AVAudioSessionのカテゴリを .ambient から .soloAmbient に切り替えることで、「アンビエント再生を止める」処理が見え、思い付きに目論んだ処は既に書かれていました。加えて「アンビエント」は通常思い浮かべる環境音楽的な、空間や雰囲気を演出する様な環境音として残響音に影響を与えているかに思い込んでいたのですが、確り調べてみると如何やら AVAudioSession.Categoryの ambient は、他の音と混ぜるか否かの設定であるに仍り環境音を名乗らせている Apple の仕様です。従って「アンビエント」に関わり無く、クラッカーには残響音効果が乘っている勘定になります。
空間オーディオフォーマット
如何も事此処に至るとコードで何とかなる様な状況で無い様に思えて来ます。更には関連するであろうアップルの仕様には頃日発表の有った記憶が残りますので調べてみると、WWDC 2025の公式セッション[※6]で発表したASAF(Apple Spatial Audio Format)が有りました。セッションでの詳細は分かり兼ねるので、関連情報[※7・8]も繰って見ましたが遠からずと雖も当たらず、と言う感じです。其の物ズバリが今回の事案にピタリと嵌まるのでは無さそうですが、“空間音響の新フォーマット”は如何にも残響音効果を連想させ、“既存の制作環境のまま”で“空間音響コンテンツを作成”可能であれば、又アップル社らしい余計なお世話で自動適用している感が否めず、今回の困った事態を招きそうではあります。
ただ、ASAFに関連する情報を閲していると、過去の記憶が脳裏の底から彷彿と蘇ります。確か、アップル社謹製ゲームエンジン SpriteKit に於いては音像の定位的効果を齎らす仕様が以前公開されており、過去の開発作業に於いて何某かの影響を受けた記憶の断片が浮き上がります。此処を鑑みてネットを繰ると以下の情報が検出されました。以下に各項目を仕様名と導入時期と主な利用目的順に並べ、時系列に早い順から三つ列挙します。
此方も遠からずと雖も当たらず、と言う感じで尚2015年以前と比較的古くもあり、“SceneKit Positional Audio”などは既に非推奨扱いにて、今回の困った現象を確実に排除してくれるものでは無さそうですが、此の使い方に関するドキュメント[※12]が有って SpriteKitの範疇に含まれる仕様となっています。様々関連情報を閲しては如何も、アップル社が開発者の為を慮って音響に余計なお世話を施してくれているのが原因では無いかと考えたのです。
音声のモノラル化
然れば、唯に音を鳴らすだけでお仕着せ仕様が介入するのでは無いかと判断し、其処には音像定位に与するステレオ音源が影響しているでは無いかと考えました。何となれば用意した残響音の介入せずに聞こえる「.caf」ファイルをターミナルで以て「.wav」ファイルから変換する際には自動的にステレオ音源化される様であるからです。此れを排除するにはモノラル音源なら如何だろう、と推し進めるのは自然の流れかに思います。
ターミナルでモノラル「.caf」ファイル変換するのは簡単でオプションに「-c 1」を追加するだけです。恐らくは「-c」はチャンネル数のオプションで「1」はモノラルを意味するのでしょう。
$ afconvert cracker.wav cracker_mono.caf -f caff -d LEI16@44100 -c 1
しかし此の浅はかな短絡思考は上手く運びませんでした。相変わらずクラッカー音はスプリングリバーブ効果を伴いクラッカーでは無い音へと強制変換させられて耳に届きます。扨はXcodeにモノラル使用が届いておらぬのでは無いかと、モノラル使用をコード内に明示的に JKAudioPlayer クラスに仕込み足掻いてもみました。
override init() {
super.init()
configureAudioSession()
}
private func configureAudioSession() {
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playback, mode: .moviePlayback, options: [])
try session.setPreferredOutputNumberOfChannels(1) // モノラル明示
try session.setActive(true)
} catch {
print("Audio session setup failed: \(error)")
}
}
結果は此処迄目を通してくれている向きにはご想像通りでしょう、無駄な足掻きに過ぎませんでした。
AVAudioEngine
音への拘りは自らには其の様に聞こえないと言うだけであって、本来不要と強いて言い切ってしまえばしまえる部分では無くも無いので、余りにも思う通り運ばず時間だけが過ぎて行けば、気に入らぬ状況を放置する方針も脳裏を過ぎりましたが、もう少し手掛かりを探ってみます。様々ネットの海を小舟で渡るも着岸の
AVAudioEngine[※13]を調べてみると如何やら AVAudioPlayer や AVAudioSession クラスと並立するクラスである様で、AVAudioPlayer が利用しさえすれば良しなに自動調整して発音してくれるのに対し、AVAudioEngin では細かな調整が必要となる様です。謂わば AVAudioPlayer が発音の為の簡易APIを提供し、対して発音を完全制御下に置くのを旨とする AVAudioEngin では当然ながら音声データに就いての深い知識を以てのAPIとの対峙を必要とするので、引っ繰り返せば、AVAudioPlayer は音の経路を制御出来ないのに対し、AVAudioEngin では技術さえ有すれば、自由自在な発音が可能になると言えます。扨では、AVAudioEngin に於いては制御の為の様々なパラメータを正確に与えねばなりませんが、何のパラメータも与えなければ、例え無知の故としても結果、原音に忠実な発声が得られるものと解釈されます。斯様に考え作成したクラスが以下になります。
import AVFoundation
class DryAudioPlayer {
private let engine = AVAudioEngine()
private let playerNode = AVAudioPlayerNode()
private var audioFile: AVAudioFile?
init(fileName: String, fileExtension: String = "caf", volume: Float = 1.0) {
setupAudio(fileName: fileName, fileExtension: fileExtension, volume: volume)
}
private func setupAudio(fileName: String, fileExtension: String, volume: Float = 1.0) {
guard let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension) else {
print("ファイルが見つかりません")
return
}
do {
audioFile = try AVAudioFile(forReading: url)
} catch {
print("音声ファイルの読み込みに失敗しました: \(error)")
return
}
engine.attach(playerNode)
engine.connect(playerNode, to: engine.mainMixerNode, format: audioFile?.processingFormat)
playerNode.volume = volume
do {
try engine.start()
} catch {
print("エンジンの起動に失敗しました: \(error)")
return
}
if let file = audioFile {
playerNode.scheduleFile(file, at: nil, completionHandler: nil)
playerNode.play()
}
}
}
音声ファイルと音量を引数に受け取って鳴らすだけのシンプルなクラスにて、Dry をクラス名に冠したのは自らのイニシャルにはあらずして、リバーブやディレイなどのエフェクトを掛けた音は
此のクラスを用いて実際に発声させるにはインスタンスを生成するだけです。何となれば上のコードに記される如く、DryAudioPlayer は初期化時に再生を開始するのであって、従って以下のように呼び出すだけで音が鳴ります。
var dryPlayer: DryAudioPlayer?
player = DryAudioPlayer(fileName: "cracker", fileExtension: "caf", volume: 1.0)
クラスのプロパティで変数 dryPlayer を宣言し、必要な箇所にて player インスタンスを生成するだけです。引数としては音声ファイルの名称と拡張子、音量を0から1の間で渡します。他にも様々機能の拡張は図れはしますが、今回は手元の開発案件においては此れにて充分です。
結言
以上の実装で以て実際に発声させてみた所、効果は劇的と言えば言い過ぎかも知れませんが、紆余曲折の末でしたので自身には然う言わしめる程のインパクトが有りました。他人様から見れば何の音響効果も無くなったファイル音声元々の生の発声であるだけですが、其れこそが今回の試行錯誤の目的であったのです。ゲームのハイスコアを祝福するクラッカー音はクラッカー音らしく破裂してくれる様になりました。
若しかしたら本記事を此処迄読んだ向きには、同じく衝撃音が衝撃を失って鳴る状況に難儀を感じているのかも知れません。似た様な状況を抱えている読者各位には是非とも本記事の実装の試行をお薦めします。
参考URL(※)
- SpriteKit で効果音を再生する方法(hawksnowlog:2017年11月10日)
- Sprite Kit no longer play sounds with iOS10(stack overflow:2016年11月16日)
- AVAudioPlayer(Apple Dev Documentation AVFAudio)
- AVAudioSession.Category(Apple Dev Documentation AVFAudio)
- ambient(Apple Dev Documentation AVFAudio)
- Apple Immersive Videoテクノロジーについて(Apple WWDC2025公式セッション)
- アップルの空間オーディオフォーマット「ASAF」とは? 独自音声コーデックも開発(PHILE WEB:2025年7月15日)
- Apple、空間音響フォーマット「ASAF」とコーデック「APAC」を発表(AUDIO MARKETING INSIGHTS:2025年7月28日)
- AVAudioEnvironmentNode(Apple Dev Documentation AVFAudio)
- PHASE(Apple Dev Documentation Framework)
- SCNAudioSource(Apple Dev Documentation SceneKit)
- Using Audio Nodes with the Scene’s Listener(Apple Dev Documentation SpriteKit)
- audioEngine(Apple Dev Documentation SpriteKit)