SwiftでiO甚のギタヌチュヌナヌを曞いたずき。 たた、DSP、定圚波、および±0.1Hzの粟床を達成する方法に぀いおも少し説明したす

この蚘事では、どのようにしおチュヌナヌを䜜成するアむデアを思い぀いたのか、それが䜕に぀ながったのかに぀いお説明したす。 たた、倧孊で取埗したDSPデゞタル信号凊理の分野での控えめな知識ず、それらがどのようにいく぀かの問題の解決に圹立ったかを共有したす。 そしおもちろん、このプロゞェクトの実装䞭に埗たSwiftでの゜ヌスコヌドずプログラミングの経隓を共有したす。



背景



ギタヌ甚に自分のチュヌナヌを曞くずいうアむデアは、私が倧孊にいた10幎ほど前のかなり前に思い぀きたした。 これにはいく぀かの理由がありたした。





䞀般的に、このアむデアは長い間頭にありたしたが、ようやく空き時間、iOsデバむス、モバむル開発の経隓があったので、今幎しか実珟できたせんでした。



理論のビット



ペヌロッパの匊楜噚に少し粟通しおいる人なら誰でも、各匊が特定の音に合わせられおおり、特定の呚波数がこの音に察応しおいるこずを知っおいたす。 音笊ず呚波数の違いは、各音笊に音楜的な機胜があるこずです。 そしお、この機胜は、他の音に察する音の盞察的な䜍眮によっお決たりたす。たずえば、 Cメゞャヌシヌケンスのノヌトtoは、 トニックの圹割を果たしたす。 メむンで最も安定したサりンド。 この音笊が少し動いた堎合、その機胜を倱いたす。 抜出された音が音笊に察応するためには、音笊の間隔ず呚波数の比率の間の厳密な察応関係を芳察する必芁がありたす。 抜出された音ず音笊に蚘茉された関数の察応は、匊によっお生成される呚波数の粟床ず安定性に䟝存したす。



匊によっお生成される音は䜕ですか アメリカの研究者であり䜜曲家のりラゞミヌル・りサチェフスキヌが最初のシンセサむザヌに提案したADSRモデルを䜿甚するず、匊の音は䜕らかの゚ンベロヌプで倉調された調和振動になりたす。 この゚ンベロヌプはADSRず呌ばれたす。 4぀の特城的なポむントがありたす攻撃 英語の攻撃 、衰退 英語の枛衰 、持続 英語の持続 、枛衰 英語のリリヌス 。











サステむン間隔は、呚波数を最も明確に䌝えたす。 振幅はほずんど倉化せずに倉動したす。 ギタヌが理想的な単調音を生成する堎合、ADSR゚ンベロヌプを考慮に入れるず、このような振動のスペクトルは狭いストリップの圢になりたす。 このストリップの圢状は、゚ンベロヌプスペクトルに察応したす。











しかし、実際の機噚は非線圢振動プロセスを生成し、その結果、倍音ず呌ばれる远加の高調波が珟れたす。











これらの倍音は、かなり朜行性のある仲間の旅行者です。 それらは基本音に勝り、呚波数の決定を劚げる可胜性がありたす。 しかし、それでも、通垞、メむントヌンは远加の操䜜なしで明確に定矩されたす。



したがっお、これらの衚珟に基づいお、プログラムが動䜜するパスを抂説できたす。



  1. 音波のフヌリ゚倉換を蚈算する
  2. スペクトルの基本音を芋぀ける
  3. ピッチ呚波数を蚈算する


定圚波に぀いお



通垞、グラフ䞊の音は時間掃匕ずしお衚されたす。 暪軞のれロ点では、倀は芳枬の最初の瞬間にあり、それぞれ1秒、2秒など埌に芳枬される倀になりたす。 この堎合の枬定プロセスは、巊から右に均䞀に移動する架空のフレヌムずしお想像できたす。 このフレヌムは別の名前で呌び出すこずができたす芳枬りィンドり 英語の芳枬りィンドり 、芳枬間隔 英語の芳枬間隔 、時間りィンドり 英語の時間りィンドり -これらの甚語はすべお同じこずを意味したす。











したがっお、フレヌムにより、枬定プロセスがどのように行われ、どの瞬間がフレヌムの最初に衚瀺され、どの瞬間が最埌に衚瀺されるかを理解できたす。 これに基づいお、座暙系ずフレヌムを比范するずどうなるか想像できたす。音波はグラフの右偎に衚瀺され、巊偎に消えたように感じたす。 そのような波は進行ず呌ばれたす











しかし、音波のそのような衚珟は有益ではありたせん。なぜなら、 波は非垞に速く動くこずができたす。 私たちの仕事は、どういうわけか波を止めるこずです。 波が止たるためには、その速床が0に等しいこずが必芁です。 波には2぀の速床がありたす。フェヌズずグルヌプです。2皮類の定圚波を取埗できたす。











グルヌプ速床がれロに等しい定圚波は、その゚ンベロヌプが垞に1぀の堎所に残るずいう事実によっお特城付けられたす。 しかし同時に、振動は止たりたせん-れロずハンプは暪座暙に沿っお動き続けたす。 明らかに、そのような波は私たちには適しおいない、なぜなら 興味深いのは、ADSR゚ンベロヌプの内郚、぀たり、サステむンモヌドで振動が芳枬される瞬間です。



これを行うには、䜍盞速床がれロの別のタむプの定圚波がありたす。











䜍盞速床がれロの堎合、ノヌドずハンプは垞に1か所に留たるため、高調波振動の圢状を簡単に確認しお、理想的な正匊波圢状にどれだけ近いかを評䟡できたす。 そのような波を埗るためのアルゎリズムは明らかです

  1. ピッチ段階を芋぀ける
  2. 衚瀺された波を䜍盞ごずにシフトしたす


実装



マむク録音



実際、AppleはObjective-C / Swiftから倚くの高レベルのマルチメディア機胜を提䟛しおいたす。 しかし、本質的に、サりンドの操䜜はAudio Queue Servicesを䞭心に展開されたす。



Audio Queue Servicesは、以前にSound ManagerおよびOS Xで提䟛されおいた機胜ず同様の機胜を提䟛したす。同期などの远加機胜を远加したす。 Sound ManagerはOS X v10.5で非掚奚になり、64ビットアプリケヌションでは機胜したせん。 Audio Queue Servicesは、すべおの新芏開発および既存のMacアプリのSound Managerの代替ずしお掚奚されたす。

゜ヌス



しかし、かなり高レベルの゜リュヌションであったSoundManagerずは異なり、Audio Queue Servicesは、Swift Cコヌドを単玔に繰り返す䞍噚甚なラッパヌです。



func AudioQueueNewInput(_ inFormat: UnsafePointer<AudioStreamBasicDescription>, _ inCallbackProc: AudioQueueInputCallback, _ inUserData: UnsafeMutablePointer<Void>, _ inCallbackRunLoop: CFRunLoop!, _ inCallbackRunLoopMode: CFString!, _ inFlags: UInt32, _ outAQ: UnsafeMutablePointer<AudioQueueRef>) -> OSStatus
      
      





Swiftには䜎レベルのコヌドからの利益はないため、オヌディオキャプチャは補助Cコヌドに任せたした。 バッファ管理のマむナヌコヌドを省略した堎合、レコヌドのセットアップは、AudioQueueNewInput関数を䜿甚しおAQRecorderState構造䜓を初期化するこずです。



  void AQRecorderState_init(struct AQRecorderState* aq, double sampleRate, size_t count){ aq->mDataFormat.mFormatID = kAudioFormatLinearPCM; aq->mDataFormat.mFormatFlags = kLinearPCMFormatFlagIsPacked | kLinearPCMFormatFlagIsSignedInteger; aq->mDataFormat.mSampleRate = sampleRate; aq->mDataFormat.mChannelsPerFrame = 1; aq->mDataFormat.mBitsPerChannel = 16; aq->mDataFormat.mFramesPerPacket = 1; aq->mDataFormat.mBytesPerPacket = 2;// for linear pcm aq->mDataFormat.mBytesPerFrame = 2; AudioQueueNewInput(&aq->mDataFormat, HandleInputBuffer, aq, NULL, kCFRunLoopCommonModes, 0, &aq->mQueue); DeriveBufferSize(aq->mQueue, &aq->mDataFormat, (double)count / sampleRate, // seconds &aq->bufferByteSize); for (int i = 0; i < kNumberBuffers; ++i) { AudioQueueAllocateBuffer(aq->mQueue, aq->bufferByteSize, &aq->mBuffers[i]); AudioQueueEnqueueBuffer(aq->mQueue, aq->mBuffers[i], 0, NULL); } aq->mCurrentPacket = 0; aq->mIsRunning = true; aq->buffer = Buffer_new(32768); aq->preview_buffer = Buffer_new(5000); AudioQueueStart(aq->mQueue, NULL); }
      
      





デヌタの蚘録は、HandleInputBuffer関数を介しお行われたす。 Buffer_write_intsを呌び出すず、デヌタがintからfloatに倉換され、さらに凊理するためにバッファヌに保存されたす。



  static void HandleInputBuffer ( void *aqData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, const AudioTimeStamp *inStartTime, UInt32 inNumPackets, const AudioStreamPacketDescription *inPacketDesc ) { struct AQRecorderState* pAqData = (struct AQRecorderState*)aqData; if(inNumPackets == 0 && pAqData->mDataFormat.mBytesPerPacket != 0) inNumPackets = inBuffer->mAudioDataByteSize / pAqData->mDataFormat.mBytesPerPacket; const SInt16* data = inBuffer->mAudioData; Buffer_write_ints(pAqData->buffer, data, inNumPackets); Buffer_write_ints(pAqData->preview_buffer, data, inNumPackets); if (pAqData->mIsRunning == 0) return; AudioQueueEnqueueBuffer(pAqData->mQueue, inBuffer, 0, NULL); }
      
      





パフォヌマンスの問題ずSwift



圓初、アむデアはSwift蚀語を100䜿甚するこずでした。 䞀般的に、私はそうし、Accelerateラむブラリの実装が䜿甚されたFFTを陀くSwiftのすべおのコヌドを曞き盎したした。 しかし、奇劙なこずに、Swiftバヌゞョンはプロセッサ時間の95の領域で倧きな負荷を発生させ、信号凊理の遅延によりレンダリングが非垞に遅くなりたした。



もちろん、この圢匏では、アプリケヌションは䜿甚に適しおいないため、すべおの信号凊理をAccelerateラむブラリヌに完党に転送する必芁がありたした。 しかし、この埌でも、負荷は䟝然ずしお高いたたでした。 1぀のパスのみを必芁ずする配列を䜿甚しお、これらの操䜜をCに転送する必芁がありたした。 線圢ランタむム。 説明のために、SwiftずCで同じコヌドを瀺したす。



  class Processing{ ... func getFrequency() -> Double { var peak: Double = 0 var peakFrequency: Double = 0 for i in 1..<spectrum.count/2 { var spectrumValue: Double = p->spectrum[i] var f: Double = fd * i / spectrum.count if (spectrumValue > peak) { peak = spectrum[i] peakFrequency = f } } return peakFrequency } }
      
      





  double get_frequency(p* processing){ double peak = 0; double peakFrequency = 0; for(size_t i = 1; i < p.spectrumLength / 2; i ++){ double spectrumValue = p->spectrum[i]; double f = p->fd * i / p->spectrumLength; if (spectrumValue > peak) { peak = spectrum; peakFrequency = f; } } return peakFrequency; }
      
      





䞀般に、1秒間に20コヌルの頻床のサむクルでアレむを簡単に通過しおも、デバむスにかなりの負荷がかかる可胜性がありたす。



おそらくこれはSwiftの最初のバヌゞョンの問題でしたが、最終的にはビット単䜍の操䜜を実行するすべおをSwiftから完党に陀倖する必芁がありたした。 そのため、Swiftでは、配列の䜜成、Cで蚘述された補助ラむブラリぞの匕き枡し、およびOpenGL ES 2でのコヌドのレンダリングを担圓する担圓者は1人だけでした。



しかし、Swiftは圹に立ちたしたか もちろん、高レベルのタスクに関しおは、Swiftはこの眰金に察凊したす。 コヌドの蚘述ははるかに快適であり、最新の構文では、セミコロンが䞀定でなく、倚くの盎感的で䟿利な構文糖が必芁です。 したがっお、Swiftを䜿甚するずいく぀かの問題が発生するずいう事実にもかかわらず、䞀般的に、この蚀語は非垞に優れおいるように芋えたした。



マむク感床の問題



そのため、Cでコヌドの䞀郚を曞き換えた埌、ギタヌをチュヌニングできる瞬間が近づいおいるように芋えたした。 しかし、その埌、私はたったく考えなかった別のトラブルが発生したした。 iPhoneのマむクは、スペクトルの䜎呚波郚分をひどくカットしたす。 もちろん、スマヌトフォンのマむクは理想ずはほど遠いものず想定したしたが、すべおがはるかに悪いこずが刀明したした。 閉塞はすでに200 Hzで始たりたした。











ギタヌのチュヌニングに関しおは、このような呚波数応答の劚害により、6匊のチュヌニングが䞍可胜になりたす。 呚波数は玄80 Hzです。 この匱䜓化により、基本音は呚波数160 Hz、240 Hzなどの高調波で沈み始めたす。











この問題に察する2぀の可胜な解決策をすぐに抂説したした。



最初のオプションはもっず面癜そうだ。なぜなら、 ナヌザヌの远加の努力は必芁ありたせんでした。 それにもかかわらず、圌は完党に裕犏ではなかった。 倚くの悪い状況に぀ながりたした。 たずえば、䞻呚波数がマむクによっお非垞に匷くカットされ、その最初の高調波の1〜2のレベルになるこずがありたした。 さらに、 ギタヌのレゟネヌタヌは非垞に非線圢なデバむスであるため、2番目、3番目、さらには4番目の高調波が振幅で競合し始めるず、状況がしばしば発生したした。 これにより、トヌンはメむンのトヌンよりも4倍高くキャプチャされるずいう事実に至りたした。



原則ずしお、これらの問題はプログラムで解決できたす。 倱望の䞻な原因は、ギタヌの倍音が非垞に倧きく動くため、それらを調敎しおも±0.1 Hzの粟床が埗られないこずです。 これは明らかに、メむントヌンがかなり安定した呚波数の匊によっお蚭定されるずいう事実によるものであり、逆に、高調波は䞻にギタヌボディの振動のためにサポヌトされ、匊の音䞭に数ヘルツずれるこずがありたす。



したがっお、最初の決定は、より予枬可胜で利䟿性の䜎い決定のために攟棄されなければなりたせんでした。 そのため、ロヌパスフィルタヌの呚波数応答はほが次のようになりたす。







カットオフ呚波数の右偎の障害物は倍音を抑制し、ピッチが再び広くなるようにしたす。 信号察雑音比のこの党䜓的な䜎䞋の代䟡は支払われ、その結果、粟床はわずかに䜎䞋したすが、蚱容範囲内です。







粟床ずパフォヌマンス



デゞタル信号凊理では、芳枬りィンドりのサむズを遞択するタスクが垞にポップアップしたす。 倧きな芳枬りィンドりを䜿甚するず、より倚くの情報を収集しお、信号パラメヌタヌの正確で安定した掚定を行うこずができたす。 䞀方、これは、䞀床に倚数のサンプルを保存および凊理する必芁があるずいう事実に加えお、信号凊理の比䟋遅延に起因する倚くの問題を匕き起こしたす。



同様に、Accelerateでは、32,768サンプルを超えないシヌケンスのスペクトルを蚈算できたす。 しかし、このような数のカりントは、スペクトル内の呚波数グリッドのステップが玄1.35 Hzであるこずを意味したす。 䞀方で、これは、たずえば、 440 Hzの呚波数を持぀最初のオクタヌブ 、぀たり 開いおいる最初の文字列最も现いで取埗されるノヌト。 しかし、6番目の文字列では、そのような間違いは臎呜的です。 たずえば、 倧きなオクタヌブのmiず、倧きなオクタヌブの間にあるのは 3 Hzだけです。 ぀たり 1.35 Hzの゚ラヌは、トヌンの半分の゚ラヌです。







それにもかかわらず、この問題の解決策は非垞に簡単ですが、時間呚波数分析のフルパワヌも瀺しおいたす。 なぜなら 数秒の信号を蓄積するこずは䞍可胜であるため、フヌリ゚倉換を繰り返しお基本呚波数のスペクトルの倀を蓄積できたす。 数孊的には、結果は基本呚波数で1.35 Hzのフィルタヌを凊理するのず同等です。 耇雑な読み取り倀が16個しかないため、結果の粟床を16倍にできたす。 最倧玄0.08 Hzで、わずかに正確な±0.1 Hzです。







぀たり、基本音の倀に関する情報がない堎合、時間枠を5秒に増やしお、±0.1 Hzの粟床を取埗し、163840サンプルを凊理する必芁がありたす。 しかし、以来 0.743秒の時間枠では、1.35 Hzの粟床で呚波数掚定倀を䜜成できたす。より正確な掚定倀を埗るには、2.7 Hzのサンプリングレヌトで非垞に狭い垯域からサンプルを蓄積するだけで十分です。 このためには、2.7 Hz * 5 s = 13.75の耇雑な読み取り倀たたは䞞めおマヌゞンを取っお取埗する堎合は16で十分です。



音笊ず呚波数の䞀臎



このタスクは、Swiftでは非垞に簡単に解決できたす。 サポヌトされおいるツヌルずマッチングルヌルに関するすべおの情報を入力する特別なTunerクラスを䜜成したした。 これらの蚈算はすべお、2぀の匏「baseFrequency * pow2.0、n-b/ 12.0」および「12.0 * logf / baseFrequency/ log2+ b」に基づいおいたす。

ここで、baseFrequencyは440 Hzたたは256 Hzの基本呚波数であり、bはからsubcontractたでの敎数のノヌト番号です。



コヌドは非垞に䞭囜語です



 class Tuner { ... init(){ addInstrument("guitar", [ ("Standard", "e2 a2 d3 g3 b3 e4"), ("New Standard", "c2 g2 d3 a3 e4 g4"), ("Russian", "d2 g2 b2 d3 g3 b3 d4"), ("Drop D", "d2 a2 d3 g3 b3 e4"), ("Drop C", "c2 g2 c3 f3 a3 d4"), ("Drop G", "g2 d2 g3 c4 e4 a4"), ("Open D", "d2 a2 d3 f#3 a3 d4"), ("Open C", "c2 g2 c3 g3 c4 e4"), ("Open G", "g2 g3 d3 g3 b3 d4"), ("Lute", "e2 a2 d3 f#3 b3 e4"), ("Irish", "d2 a2 d3 g3 a3 d4") ]) ... } ... func noteNumber(noteString: String) -> Int { var note = noteString.lowercaseString var number = 0 var octave = 0 if note.hasPrefix("c") { number = 0; } if note.hasPrefix("c#") { number = 1; } ... if note.hasPrefix("b") { number = 11; } if note.hasSuffix("0") { octave = 0; } if note.hasSuffix("1") { octave = 1; } ... if note.hasPrefix("8") { octave = 8; } return 12 * octave + number } func noteString(num: Double) -> String { var noteOctave: Int = Int(num / 12) var noteShift: Int = Int(num % 12) var result = "" switch noteShift { case 0: result += "c" case 1: result += "c#" ... default: result += "" } return result + String(noteOctave) } func noteFrequency(noteString: String) -> Double { var n = noteNumber(noteString) var b = noteNumber(baseNote) return baseFrequency * pow(2.0, Double(n - b) / 12.0); } func frequencyNumber(f: Double) -> Double { var b = noteNumber(baseNote); return 12.0 * log(f / baseFrequency) / log(2) + Double(b); } func frequencyDistanceNumber(f0: Double, _ f1: Double) -> Double { var n0 = frequencyNumber(f0) var n1 = frequencyNumber(f1) return n1 - n0; } func targetFrequency() -> Double { return noteFrequency(string) * fretScale() } func actualFrequency() -> Double { return frequency * fretScale() } func frequencyDeviation() -> Double { return 100.0 * frequencyDistanceNumber(noteFrequency(string), frequency) } }
      
      





定圚波の可芖化



楜噚の音波の圢状を芋るこずができる定圚波に぀いおは、すでに曞いたように、アルゎリズムはたったく簡単です-波長は芋぀かった基本呚波数から蚈算され、䜍盞が掚定され、その埌、芋぀かった倀からシフトが行われたす デヌタは補助プレビュヌバッファから取埗されたすが、メむンバッファずは異なり、蓄積されたせん。 ぀たり 「 タンブリングりィンドり 」アルゎリズムに埓っお動䜜したす。



  double waveLength = p->fd / f; size_t index = p->previewLength - waveLength * 2; double* src = &p->preview[index]; //            double re = 0; double im = 0; for (size_t i = 0; i < waveLength*2; i++) { double t = (double)2.0 * M_PI * i / waveLength; re += src[i] * cos(t); im += src[i] * sin(t); } double phase = get_phase(re, im); //     double shift = waveLength * phase / (2.0 * M_PI); //   //     double* shiftedSrc = &p->preview[index - (size_t)(waveLength - shift) - (size_t)waveLength];
      
      





倖芳



内蔵プレヌダヌに基づいお䜜成した倖芳ずナビゲヌションは、トラックを切り替える代わりに、ストリングの切り替えが発生したす











おわりに



Swift / Cアプリケヌションの開発パス党䜓には玄2か月かかりたした。 アプリケヌションの実装は非垞に困難であるこずが刀明したした。 第1に、スマヌトフォンのパフォヌマンスには䟝然ずしお倚くの課題が残されおおり、高玚蚀語での「盎接的な」゜リュヌションは、ナヌザヌによる日垞的な䜿甚にはたったく䞍適切であるこずが刀明しおいたす。 第二に、サりンド凊理のトピックは、iOの開発者の間で非垞に人気がないため、情報を少しず぀収集する必芁がありたす。 これはおそらく、モバむルアプリケヌション向けに開発する際のUIに関連しないトピックに関するものです。 第䞉に、SwiftはCデヌタず十分に通信できたすが、ずにかくこの開発方法はひどく䞍䟿であり、ひどく劎働を消費したす。



蚘事が非垞に重芁であるこずが刀明したずいう事実にもかかわらず、倚くの埮劙さず埮劙なニュアンスは未解決のたたでした。 アプリケヌションの゜ヌスコヌドが理解できない点を明確にするのに圹立぀こずを願っおいたす。



github.com/kreshikhin/scituner



゜ヌスコヌドには、MITラむセンスが付属しおいたす。 したがっお、目的のコヌドセクションたたはプロゞェクトコヌド党䜓を安党に䜿甚できたす。



All Articles