Hasli4

Untitled

Mar 25th, 2026
55
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 9.71 KB | None | 0 0
  1. using UnityEngine;
  2. using TMPro;
  3. using UnityEngine.UI;
  4. using System;
  5.  
  6. [RequireComponent(typeof(AudioSource))]
  7. public class TunerController : MonoBehaviour
  8. {
  9. [Header("UI")]
  10. public TextMeshProUGUI freqText;
  11. public TextMeshProUGUI noteText;
  12. public TextMeshProUGUI centsText;
  13. public Image tuningNeedle;
  14.  
  15. [Header("Microphone settings")]
  16. public int sampleRate = 16000; // можно поставить 44100 для точности
  17. public int clipLengthSec = 1;
  18. public string microphoneDevice = "";
  19.  
  20. [Header("Detection settings")]
  21. public float minFreq = 80f; // низкий предел
  22. public float maxFreq = 1200f; // верхний предел
  23. public float rmsThreshold = 0.0015f; // порог для наличия сигнала
  24. public float acfPeakThreshold = 0.25f; // минимальная нормализованная ACF для доверия (0..1)
  25. public int sampleWindow = 4096; // окно анализа - 4096 даёт лучшую устойчивость
  26.  
  27. AudioSource audioSource;
  28. AudioClip micClip;
  29. string micDevice;
  30. float uiInterval = 0.09f; // обновлять UI ~11 раз в секунду
  31. float uiTimer = 0f;
  32.  
  33. void Start()
  34. {
  35. audioSource = GetComponent<AudioSource>();
  36.  
  37. if (Microphone.devices.Length == 0)
  38. {
  39. Debug.LogWarning("No microphone devices found.");
  40. return;
  41. }
  42.  
  43. micDevice = string.IsNullOrEmpty(microphoneDevice) ? Microphone.devices[0] : microphoneDevice;
  44. micClip = Microphone.Start(micDevice, true, clipLengthSec, sampleRate);
  45.  
  46. while (!(Microphone.GetPosition(micDevice) > 0)) { } // ждём старта
  47. audioSource.loop = true;
  48. audioSource.clip = micClip;
  49. // Не обязательно воспроизводить микрофонный поток на динамик:
  50. audioSource.mute = true;
  51. audioSource.Play();
  52. }
  53.  
  54. void Update()
  55. {
  56. uiTimer += Time.unscaledDeltaTime;
  57. if (uiTimer < uiInterval) return;
  58. uiTimer = 0f;
  59.  
  60. float freq = DetectPitch();
  61. if (freq > 0)
  62. {
  63. float midiFloat = FrequencyToMidi(freq);
  64. int octave;
  65. string note = MidiToNoteName(midiFloat, out octave);
  66. float cents = FrequencyToCents(freq, midiFloat);
  67.  
  68. if (freqText) freqText.SetText("{0:F1} Hz", freq);
  69. if (noteText) noteText.SetText("{0}{1}", note, octave);
  70. if (centsText) centsText.SetText("{0:+0.0;-0.0}¢", cents);
  71.  
  72. if (tuningNeedle)
  73. {
  74. float angle = Mathf.Clamp(cents, -50f, 50f) / 50f * 45f;
  75. tuningNeedle.rectTransform.localRotation = Quaternion.Euler(0, 0, -angle);
  76. }
  77. }
  78. else
  79. {
  80. if (freqText) freqText.SetText("-- Hz");
  81. if (noteText) noteText.SetText("-");
  82. if (centsText) centsText.SetText("");
  83. if (tuningNeedle) tuningNeedle.rectTransform.localRotation = Quaternion.Euler(0, 0, 0);
  84. }
  85. }
  86.  
  87. void OnDestroy()
  88. {
  89. if (micClip != null && Microphone.IsRecording(micDevice))
  90. Microphone.End(micDevice);
  91. }
  92.  
  93. float DetectPitch()
  94. {
  95. if (micClip == null) return -1f;
  96.  
  97. // Получаем позицию микрофона и безопасно читаем окно samples (учитывая wrap)
  98. int micPos = Microphone.GetPosition(micDevice);
  99. if (micPos <= 0) return -1f;
  100.  
  101. float[] samples = new float[sampleWindow];
  102. int startPos = micPos - sampleWindow;
  103. if (startPos < 0)
  104. {
  105. // читаем с конца и начала (wrap)
  106. int part1 = sampleWindow + startPos; // количество с конца
  107. micClip.GetData(samples, 0); // read whole clip then copy - simpler but may be heavy
  108. // более быстрый вариант: читать двумя кусками
  109. // но для простоты ниже - читаем полное окно через GetData с корректным offset
  110. // (Unity позволяет negative offset? нет) — безопасный способ — использовать временный буфер
  111. // реализация ниже читает с позиции 0..sampleWindow-1 shifted accordingly:
  112. // вместо сложного копирования используем workaround:
  113. }
  114.  
  115. // Простая унифицированная реализация: читаем блок, начиная с (micPos - sampleWindow),
  116. // если startPos < 0, используем два чтения
  117. if (startPos >= 0)
  118. {
  119. micClip.GetData(samples, startPos);
  120. }
  121. else
  122. {
  123. int part1 = sampleWindow + startPos; // количество с конца
  124. float[] tail = new float[part1];
  125. float[] head = new float[sampleWindow - part1];
  126. micClip.GetData(tail, micClip.samples - part1);
  127. micClip.GetData(head, 0);
  128. Array.Copy(tail, 0, samples, 0, part1);
  129. Array.Copy(head, 0, samples, part1, head.Length);
  130. }
  131.  
  132. // DC removal: вычитаем среднее
  133. float mean = 0f;
  134. for (int i = 0; i < samples.Length; i++) mean += samples[i];
  135. mean /= samples.Length;
  136. for (int i = 0; i < samples.Length; i++) samples[i] -= mean;
  137.  
  138. // RMS
  139. float sumSq = 0f;
  140. for (int i = 0; i < samples.Length; i++) sumSq += samples[i] * samples[i];
  141. float rms = Mathf.Sqrt(sumSq / samples.Length);
  142. if (rms < rmsThreshold)
  143. {
  144. // слабый сигнал (тишина/шум)
  145. // Debug.Log($"RMS too low: {rms}");
  146. return -1f;
  147. }
  148.  
  149. // Hamming window
  150. ApplyHammingWindow(samples);
  151.  
  152. // Автокорреляция и нормализация по acf0
  153. int maxLag = Mathf.FloorToInt(sampleRate / minFreq);
  154. int minLag = Mathf.CeilToInt(sampleRate / maxFreq);
  155. if (maxLag >= samples.Length) maxLag = samples.Length - 1;
  156. if (minLag < 1) minLag = 1;
  157.  
  158. float[] acf = new float[maxLag + 1];
  159. // вычисляем acf[0] (энергия)
  160. float acf0 = 0f;
  161. for (int i = 0; i < samples.Length; i++) acf0 += samples[i] * samples[i];
  162. if (acf0 <= 1e-9f) return -1f;
  163.  
  164. for (int lag = minLag; lag <= maxLag; lag++)
  165. {
  166. float sum = 0f;
  167. for (int i = 0; i < samples.Length - lag; i++)
  168. sum += samples[i] * samples[i + lag];
  169. acf[lag] = sum / acf0; // нормализованная автокорреляция (в диапазоне ~0..1)
  170. }
  171.  
  172. // Найдём лучший лаг (максимум нормализованной ACF) в диапазоне
  173. int bestLag = -1;
  174. float bestVal = 0f;
  175. for (int lag = minLag; lag <= maxLag; lag++)
  176. {
  177. if (acf[lag] > bestVal)
  178. {
  179. bestVal = acf[lag];
  180. bestLag = lag;
  181. }
  182. }
  183.  
  184. if (bestLag <= 0) return -1f;
  185.  
  186. // Проверка надежности: bestVal уже в [0..1] примерно
  187. if (bestVal < acfPeakThreshold) // например 0.25
  188. {
  189. // сигнал некорректный или доминируют гармоники
  190. // Debug.Log($"ACF peak too low: {bestVal}");
  191. return -1f;
  192. }
  193.  
  194. // Парболическая интерполяция для уточнения пика
  195. float refinedLag = ParabolicInterpolation(acf, bestLag);
  196.  
  197. // refinedLag — дробный лаг, частота:
  198. float frequency = (float)sampleRate / refinedLag;
  199.  
  200. // Debug лог для диагностики (убери в релизе)
  201. Debug.Log($"micPos={micPos} rms={rms:F4} acf0={acf0:F4} bestLag={bestLag} bestVal={bestVal:F3} freq={frequency:F1}");
  202.  
  203. return frequency;
  204. }
  205.  
  206. static void ApplyHammingWindow(float[] data)
  207. {
  208. int N = data.Length;
  209. for (int i = 0; i < N; i++)
  210. {
  211. float w = 0.54f - 0.46f * Mathf.Cos(2f * Mathf.PI * i / (N - 1));
  212. data[i] *= w;
  213. }
  214. }
  215.  
  216. static float ParabolicInterpolation(float[] acf, int lag)
  217. {
  218. if (lag <= 0 || lag >= acf.Length - 1) return lag;
  219. float y0 = acf[lag - 1];
  220. float y1 = acf[lag];
  221. float y2 = acf[lag + 1];
  222. float denom = (y0 - 2f * y1 + y2);
  223. if (Mathf.Abs(denom) < 1e-8f) return lag;
  224. float shift = 0.5f * (y0 - y2) / denom;
  225. return lag + shift;
  226. }
  227.  
  228. // ---------- нота / перевод ----------
  229. static float FrequencyToMidi(float freq)
  230. {
  231. return 69f + 12f * Mathf.Log(freq / 440f, 2f);
  232. }
  233.  
  234. static string MidiToNoteName(float midiFloat, out int octave)
  235. {
  236. int midi = Mathf.RoundToInt(midiFloat);
  237. string[] names = { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" };
  238. int noteIndex = midi % 12;
  239. octave = (midi / 12) - 1;
  240. return names[(noteIndex + 12) % 12];
  241. }
  242.  
  243. static float FrequencyToCents(float freq, float midiFloat)
  244. {
  245. float nearestMidi = Mathf.Round(midiFloat);
  246. float cents = 1200f * Mathf.Log(freq / MidiToFrequency(nearestMidi), 2f);
  247. return cents;
  248. }
  249.  
  250. static float MidiToFrequency(float midi)
  251. {
  252. return 440f * Mathf.Pow(2f, (midi - 69f) / 12f);
  253. }
  254. }
Advertisement
Add Comment
Please, Sign In to add comment