Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- using UnityEngine;
- using UnityEngine.UI;
- using System;
- using System.Linq;
- [RequireComponent(typeof(AudioSource))]
- public class TunerController : MonoBehaviour
- {
- [Header("UI")]
- public Text freqText;
- public Text noteText;
- public Text centsText;
- public Image tuningNeedle; // (опционально) стрелка для визуального индикатора
- [Header("Microphone settings")]
- public int sampleRate = 16000; // рекомендовано: 16000 или 44100 (меньше - легче вычисления)
- public int clipLengthSec = 1; // сколько сек в цикле записи (1..2)
- public string microphoneDevice = ""; // если пусто — берётся первый
- [Header("Detection settings")]
- public float minFreq = 80f; // минимальная детектируемая частота (Hz) — низкая E ~82Hz
- public float maxFreq = 1000f; // максимальная частота для поиска (Hz)
- public float confidenceThreshold = 0.8f; // порог надежности автокорреляции (0..1)
- AudioSource audioSource;
- AudioClip micClip;
- string micDevice;
- int sampleWindow = 2048; // окно выборки для анализа (стремиться к степеням 2)
- void Start()
- {
- audioSource = GetComponent<AudioSource>();
- // найдем устройство
- if (Microphone.devices.Length == 0)
- {
- Debug.LogWarning("Microphone not found.");
- return;
- }
- micDevice = string.IsNullOrEmpty(microphoneDevice) ? Microphone.devices[0] : microphoneDevice;
- micClip = Microphone.Start(micDevice, true, clipLengthSec, sampleRate);
- // опционально подключаем микрофон к audioclip, чтобы слышать (обычно не нужно)
- audioSource.loop = true;
- while (!(Microphone.GetPosition(micDevice) > 0)) { } // ждём, пока начнётся запись
- audioSource.clip = micClip;
- audioSource.Play();
- }
- void Update()
- {
- if (micClip == null) return;
- float freq = DetectPitch();
- if (freq > 0)
- {
- float midi = FrequencyToMidi(freq);
- string note = MidiToNoteName(midi, out int octave);
- float cents = FrequencyToCents(freq, midi);
- if (freqText) freqText.text = $"{freq:F1} Hz";
- if (noteText) noteText.text = $"{note}{octave}";
- if (centsText) centsText.text = $"{cents:+0.0;-0.0}¢";
- if (tuningNeedle)
- {
- // визуальный индикатор: -50..+50 центов -> -45..+45 градусов
- float angle = Mathf.Clamp(cents, -50f, 50f) / 50f * 45f;
- tuningNeedle.rectTransform.localRotation = Quaternion.Euler(0, 0, -angle);
- }
- }
- else
- {
- if (freqText) freqText.text = $"-- Hz";
- if (noteText) noteText.text = $"-";
- if (centsText) centsText.text = $"";
- if (tuningNeedle) tuningNeedle.rectTransform.localRotation = Quaternion.Euler(0, 0, 0);
- }
- }
- void OnDestroy()
- {
- if (micClip != null && Microphone.IsRecording(micDevice))
- Microphone.End(micDevice);
- }
- float DetectPitch()
- {
- // Берём окно samples
- float[] samples = new float[sampleWindow];
- int micPos = Microphone.GetPosition(micDevice);
- if (micPos < sampleWindow) return -1f;
- micClip.GetData(samples, micPos - sampleWindow);
- // Применим оконную функцию (Hamming)
- ApplyHammingWindow(samples);
- // Автокорреляция
- 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[lag]
- 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;
- }
- // Найдём максимум автокорреляции (за исключением lag=0)
- 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;
- // Оценка надежности: сравнить максимум с значениями на соседних лагах
- float normalizedConfidence = bestVal / acf[minLag]; // грубая оценка (можно улучшить)
- if (normalizedConfidence < (1f - confidenceThreshold)) // если слишком слабый сигнал
- {
- // всё ещё можно вернуть частоту, но пометим как ненадёжную
- // здесь — возвращаем -1 для простоты
- // можно убрать это условие, если нужно всегда показывать результат
- // return -1f;
- }
- // Параболическая интерполяция для уточнения пика (sub-sample)
- float refinedLag = ParabolicInterpolation(acf, bestLag);
- float frequency = sampleRate / refinedLag;
- 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-6f) return lag;
- float shift = 0.5f * (y0 - y2) / denom;
- return lag + shift;
- }
- // Перевод частоты в MIDI номер (float), где A4 = 440Hz -> MIDI 69
- static float FrequencyToMidi(float freq)
- {
- return 69f + 12f * Mathf.Log(freq / 440f, 2f);
- }
- // Возвращает название ноты (C,C#,D...) и octave
- 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];
- }
- // Разница в центах между частотой и целым MIDI номером
- 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