Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- using UnityEngine;
- using TMPro;
- using UnityEngine.UI;
- using System;
- [RequireComponent(typeof(AudioSource))]
- public class TunerController : MonoBehaviour
- {
- [Header("UI")]
- public TextMeshProUGUI freqText;
- public TextMeshProUGUI noteText;
- public TextMeshProUGUI centsText;
- public Image tuningNeedle;
- [Header("Microphone settings")]
- public int sampleRate = 16000; // можно поставить 44100 для точности
- public int clipLengthSec = 1;
- public string microphoneDevice = "";
- [Header("Detection settings")]
- public float minFreq = 80f; // низкий предел
- public float maxFreq = 1200f; // верхний предел
- public float rmsThreshold = 0.0015f; // порог для наличия сигнала
- public float acfPeakThreshold = 0.25f; // минимальная нормализованная ACF для доверия (0..1)
- public int sampleWindow = 4096; // окно анализа - 4096 даёт лучшую устойчивость
- AudioSource audioSource;
- AudioClip micClip;
- string micDevice;
- float uiInterval = 0.09f; // обновлять UI ~11 раз в секунду
- float uiTimer = 0f;
- void Start()
- {
- audioSource = GetComponent<AudioSource>();
- if (Microphone.devices.Length == 0)
- {
- Debug.LogWarning("No microphone devices found.");
- return;
- }
- micDevice = string.IsNullOrEmpty(microphoneDevice) ? Microphone.devices[0] : microphoneDevice;
- micClip = Microphone.Start(micDevice, true, clipLengthSec, sampleRate);
- while (!(Microphone.GetPosition(micDevice) > 0)) { } // ждём старта
- audioSource.loop = true;
- audioSource.clip = micClip;
- // Не обязательно воспроизводить микрофонный поток на динамик:
- audioSource.mute = true;
- audioSource.Play();
- }
- void Update()
- {
- uiTimer += Time.unscaledDeltaTime;
- if (uiTimer < uiInterval) return;
- uiTimer = 0f;
- float freq = DetectPitch();
- if (freq > 0)
- {
- float midiFloat = FrequencyToMidi(freq);
- int octave;
- string note = MidiToNoteName(midiFloat, out octave);
- float cents = FrequencyToCents(freq, midiFloat);
- if (freqText) freqText.SetText("{0:F1} Hz", freq);
- if (noteText) noteText.SetText("{0}{1}", note, octave);
- if (centsText) centsText.SetText("{0:+0.0;-0.0}¢", cents);
- if (tuningNeedle)
- {
- float angle = Mathf.Clamp(cents, -50f, 50f) / 50f * 45f;
- tuningNeedle.rectTransform.localRotation = Quaternion.Euler(0, 0, -angle);
- }
- }
- else
- {
- if (freqText) freqText.SetText("-- Hz");
- if (noteText) noteText.SetText("-");
- if (centsText) centsText.SetText("");
- if (tuningNeedle) tuningNeedle.rectTransform.localRotation = Quaternion.Euler(0, 0, 0);
- }
- }
- void OnDestroy()
- {
- if (micClip != null && Microphone.IsRecording(micDevice))
- Microphone.End(micDevice);
- }
- float DetectPitch()
- {
- if (micClip == null) return -1f;
- // Получаем позицию микрофона и безопасно читаем окно samples (учитывая wrap)
- int micPos = Microphone.GetPosition(micDevice);
- if (micPos <= 0) return -1f;
- float[] samples = new float[sampleWindow];
- int startPos = micPos - sampleWindow;
- if (startPos < 0)
- {
- // читаем с конца и начала (wrap)
- int part1 = sampleWindow + startPos; // количество с конца
- micClip.GetData(samples, 0); // read whole clip then copy - simpler but may be heavy
- // более быстрый вариант: читать двумя кусками
- // но для простоты ниже - читаем полное окно через GetData с корректным offset
- // (Unity позволяет negative offset? нет) — безопасный способ — использовать временный буфер
- // реализация ниже читает с позиции 0..sampleWindow-1 shifted accordingly:
- // вместо сложного копирования используем workaround:
- }
- // Простая унифицированная реализация: читаем блок, начиная с (micPos - sampleWindow),
- // если startPos < 0, используем два чтения
- if (startPos >= 0)
- {
- micClip.GetData(samples, startPos);
- }
- else
- {
- int part1 = sampleWindow + startPos; // количество с конца
- float[] tail = new float[part1];
- float[] head = new float[sampleWindow - part1];
- micClip.GetData(tail, micClip.samples - part1);
- micClip.GetData(head, 0);
- Array.Copy(tail, 0, samples, 0, part1);
- Array.Copy(head, 0, samples, part1, head.Length);
- }
- // DC removal: вычитаем среднее
- float mean = 0f;
- for (int i = 0; i < samples.Length; i++) mean += samples[i];
- mean /= samples.Length;
- for (int i = 0; i < samples.Length; i++) samples[i] -= mean;
- // RMS
- float sumSq = 0f;
- for (int i = 0; i < samples.Length; i++) sumSq += samples[i] * samples[i];
- float rms = Mathf.Sqrt(sumSq / samples.Length);
- if (rms < rmsThreshold)
- {
- // слабый сигнал (тишина/шум)
- // Debug.Log($"RMS too low: {rms}");
- return -1f;
- }
- // Hamming window
- ApplyHammingWindow(samples);
- // Автокорреляция и нормализация по acf0
- int maxLag = Mathf.FloorToInt(sampleRate / minFreq);
- int minLag = Mathf.CeilToInt(sampleRate / maxFreq);
- if (maxLag >= samples.Length) maxLag = samples.Length - 1;
- if (minLag < 1) minLag = 1;
- float[] acf = new float[maxLag + 1];
- // вычисляем acf[0] (энергия)
- float acf0 = 0f;
- for (int i = 0; i < samples.Length; i++) acf0 += samples[i] * samples[i];
- if (acf0 <= 1e-9f) return -1f;
- for (int lag = minLag; lag <= maxLag; lag++)
- {
- float sum = 0f;
- for (int i = 0; i < samples.Length - lag; i++)
- sum += samples[i] * samples[i + lag];
- acf[lag] = sum / acf0; // нормализованная автокорреляция (в диапазоне ~0..1)
- }
- // Найдём лучший лаг (максимум нормализованной ACF) в диапазоне
- int bestLag = -1;
- float bestVal = 0f;
- for (int lag = minLag; lag <= maxLag; lag++)
- {
- if (acf[lag] > bestVal)
- {
- bestVal = acf[lag];
- bestLag = lag;
- }
- }
- if (bestLag <= 0) return -1f;
- // Проверка надежности: bestVal уже в [0..1] примерно
- if (bestVal < acfPeakThreshold) // например 0.25
- {
- // сигнал некорректный или доминируют гармоники
- // Debug.Log($"ACF peak too low: {bestVal}");
- return -1f;
- }
- // Парболическая интерполяция для уточнения пика
- float refinedLag = ParabolicInterpolation(acf, bestLag);
- // refinedLag — дробный лаг, частота:
- float frequency = (float)sampleRate / refinedLag;
- // Debug лог для диагностики (убери в релизе)
- Debug.Log($"micPos={micPos} rms={rms:F4} acf0={acf0:F4} bestLag={bestLag} bestVal={bestVal:F3} freq={frequency:F1}");
- return frequency;
- }
- static void ApplyHammingWindow(float[] data)
- {
- int N = data.Length;
- for (int i = 0; i < N; i++)
- {
- float w = 0.54f - 0.46f * Mathf.Cos(2f * Mathf.PI * i / (N - 1));
- data[i] *= w;
- }
- }
- static float ParabolicInterpolation(float[] acf, int lag)
- {
- if (lag <= 0 || lag >= acf.Length - 1) return lag;
- float y0 = acf[lag - 1];
- float y1 = acf[lag];
- float y2 = acf[lag + 1];
- float denom = (y0 - 2f * y1 + y2);
- if (Mathf.Abs(denom) < 1e-8f) return lag;
- float shift = 0.5f * (y0 - y2) / denom;
- return lag + shift;
- }
- // ---------- нота / перевод ----------
- static float FrequencyToMidi(float freq)
- {
- return 69f + 12f * Mathf.Log(freq / 440f, 2f);
- }
- static string MidiToNoteName(float midiFloat, out int octave)
- {
- int midi = Mathf.RoundToInt(midiFloat);
- string[] names = { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" };
- int noteIndex = midi % 12;
- octave = (midi / 12) - 1;
- return names[(noteIndex + 12) % 12];
- }
- static float FrequencyToCents(float freq, float midiFloat)
- {
- float nearestMidi = Mathf.Round(midiFloat);
- float cents = 1200f * Mathf.Log(freq / MidiToFrequency(nearestMidi), 2f);
- return cents;
- }
- static float MidiToFrequency(float midi)
- {
- return 440f * Mathf.Pow(2f, (midi - 69f) / 12f);
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment