Марк Хит (Mark Heath)

Все были раздосадованы, когда в последнем эпизоде «American Idol»(*) появился безнадежно фальшивящий конкурсант. Ведь в беспощадном мире музыкальных телешоу достаточно взять ноту на полтона ниже, и ты вылетел. Случается, что некоторые участники демонстрируют абсолютный музыкальный слух на протяжении всего выступления, но на последней ноте провалят все дело.

В подобной ситуации может помочь автокоррекция (autotune) — эффект, который подстраивает высоту голоса, исправляя фальшь. Сегодня благодаря мощи современных микропроцессоров автоматическая настройка возможна в реальном времени, что нередко выручает певцов на концертах вживую.

В настоящее время широко известен эффект автокоррекции от компании Antares. Программа Antares Auto-Tune продается в розницу за $249, а упрощенная версия — всего за $100. Помимо простого повышения высоты голоса певца, с помощью автокоррекции можно создавать уникальные вокальные эффекты с роботизированным звучанием — этот прием стал популярен в последние годы благодаря его использованию такими артистами, как T-Pain, и группой авторов видеороликов «Auto-Tune the News» на YouTube. В 1998 году, когда этот эффект был применен в сингле Шер «Believe», продюсер использовал такую экстремальную настройку, что вместо небольшой подстройки высоты звука автокоррекция мгновенно «переключалась» к ближайшей «правильной» ноте.

Вот пример того, на что способна программа Autotune.

Как работает Autotune?

Эффект автокоррекции состоит из двух частей. Первая — определение высоты звука (pitch detection). На данном этапе вычисляется доминирующая частота входного сигнала. Следует сказать, что автокоррекция обычно используется с монофоническими аудиоисточниками (т. е. проигрывание осуществляется по одной ноте, а не целыми аккордами). Поэтому очень важно, чтобы гитара была правильно настроена. Также замечу, что, например, программный пакет Celemony's Melodyne отличается почти невероятными возможностями смещения высоты полифонического звука.

Вторая часть — смещение высоты звука (pitch shifting), или «коррекция» конкретной ноты. Правда, чем сильнее сдвиг высоты звука, тем искусственнее получается результат. Стоит отметить, что абсолютно совершенная высота ноты не всегда желательна. Иногда важной частью исполнения являются, например, смешанные ноты, получаемые как результат вибрато, и их исключение испортило бы общее впечатление.

Создание .NET-алгоритма Autotune

В этом проекте мы создадим эффект автокоррекции для .NET. Истинным энтузиастам звукозаписи следует просто отправиться за покупкой приличного пакета автокоррекции, но, чтобы немного развлечься, мы посмотрим, можно ли добиться такого же эффекта, который даст нам скромный вариант автокоррекции, использованной Шер (или T-Pain, если вы предпочитаете эту группу).

Для начала я предпринял поиск уже существующих реализаций автокоррекции с открытым исходным кодом. Это привело меня к awesomebox — проекту, созданному Рави Парикхом (Ravi Parikh) и Кигеном Поппеном (Keegan Poppen) еще в то время, когда они были студентами стэндфордского университета. Они любезно разрешили мне воспользоваться их кодом, в котором применяются автокоррелятор для определения высоты звука и алгоритм смещения высоты с открытым исходным кодом от эксперта в обработке звука с помощью DSP-процессоров Стефана М. Бернси (Stephan M. Bernsee).

Перенос кода из C++ в C#

У языков C/C++ и C# довольно схожий синтаксис, что делает возможным перенос кода без внесения чрезмерно большого количества изменений. Однако следует помнить, что long в C — это int в C# (т. е. размер переменной такого типа составляет 32 разряда, а не 64).

Кроме того, компилятор C# больше волнуется о всяких пустяках, чем C/C++, когда дело доходит до преобразований между типами float, double и int. Добавив суффикс «f» в числовые литералы, вы избавитесь от большей части ошибок компилятора.

Головной болью могут стать указатели. Я стараюсь заменять их целочисленными переменными, используемыми для указания индексов в массиве. Конечно, вы можете использовать небезопасный код, но это ограничит ваш выбор, если вы планируете последующий перенос в Silverlight или Windows Phone 7, которые не разрешают присутствия небезопасного кода или взаимодействия с неуправляемым кодом. Необходимые математические функции доступны в классе System.Math.

В качестве примера сравните этот файл исходного кода на C++ для смещения высоты звука с моим вариантом преобразования в код на C#.

Захват аудио с помощью NAudio

Захват аудио обеспечивают Interop-оболочки Windows-функций WaveIn. Ниже приведен код, позволяющий начать запись:

C#:

waveIn = new WaveIn();

waveIn.DeviceNumber = recordingDevice;

waveIn.DataAvailable += waveIn_DataAvailable;

waveIn.RecordingStopped += new EventHandler(waveIn_RecordingStopped);

waveIn.WaveFormat = recordingFormat;

waveIn.StartRecording();

VB.Net:

waveIn = New WaveIn

waveIn.DeviceNumber = recordingDevice

AddHandler waveIn.DataAvailable, AddressOf waveIn_DataAvailable

AddHandler waveIn.RecordingStopped, AddressOf waveIn_RecordingStopped

waveIn.WaveFormat = _recordingFormat

waveIn.StartRecording()

Последовательность действия следующая.

  1. Создайте новое устройство WaveIn.
  2. Укажите номер устройства (0 — для выбора устройства записи по умолчанию) (необязательный шаг).
  3. Добавьте обработчик для события DataAvailable — именно в нем вы будете принимать исходные аудиоданные.
  4. Добавьте обработчик для события RecordingStopped. Это позволит нам закрывать ранее созданный временный WAV-файл.
  5. Задайте формат записи. В этом проекте мы будем использовать монофоническую запись (т. е. один канал), 16 бит, 44,1 кГц — такие настройки по умолчанию задаются для большинства микрофонов.
  6. Вызовите метод StartRecording.

Всякий раз, когда звуковая карта сообщает о появлении нового буфера записанного аудиопотока, мы принимаем его содержимое в обработчике события DataAvailable.

C#:

void waveIn_DataAvailable(object sender, WaveInEventArgs e)
{
    byte[] buffer = e.Buffer;
    int bytesRecorded = e.BytesRecorded;
    WriteToFile(buffer, bytesRecorded);

    for (int index = 0; index < e.BytesRecorded; index += 2)
    {
        short sample = (short)((buffer[index + 1] << 8) |
                                buffer[index + 0]);
        float sample32 = sample / 32768f;
        sampleAggregator.Add(sample32);
    }
}

VB.Net

Private Sub waveIn_DataAvailable(ByVal sender As Object, ByVal e As WaveInEventArgs)
    Dim buffer() = e.Buffer
    Dim bytesRecorded = e.BytesRecorded
    WriteToFile(buffer, bytesRecorded)

    For index = 0 To e.BytesRecorded - 1 Step 2
        Dim sample = CShort(buffer(index + 1)) << 8 Or CShort(buffer(index + 0))
        Dim sample32 = sample / 32768.0F
        _sampleAggregator.Add(sample32)
    Next index
End Sub

WaveInEventArgs содержит информацию о количестве записанных байтов (e.BytesRecorded) и указатель на буфер, хранящий эти байты (e.Buffer). Обработчик делает с записанными данными две вещи. Сначала он вызывает WriteToFile, который с помощью класса WaveFileWriter из NAudio пишет данные на диск:

C#:

// перед записью, настраиваем WaveFileWriter...
writer = new WaveFileWriter(waveFileName, recordingFormat);

// ... каждый получаемый блок передаем в  WaveFileWriter:
writer.WriteData(buffer, 0, bytesRecorded);

// ... и когда запись останавливается мы должны вызвать Dispose для 
// корректного завершения .WAV-файла
writer.Dispose()

VB.Net:

writer = New WaveFileWriter(waveFileName, _recordingFormat)
writer.WriteData(buffer, 0, bytesRecorded)
writer.Dispose()

Преобразование аудиоданных в значения с плавающей точкой и наоборот

По окончании записи мы получаем WAV-файл, к которому применяется наш эффект автокоррекции. Однако этот WAV-файл содержит 16-разрядные выборки (т. е. System.Int16, или short). Другими словами, мы имеем последовательность пар байтов, каждая из которых представляет число в диапазоне от -32768 до 32767. В цифровой обработке сигналов, которой мы будем заниматься, лучше всего использовать последовательность чисел с плавающей точкой (System.Single, или float) в диапазоне от -1.0f до 1.0f. Это общее требование, поэтому NAudio предоставляет вспомогательный класс Wave16ToFloatProvider для преобразования аудио из short в float. Ниже показан код, который принимает WAV-файл, применяя к нему алгоритм автокоррекции:

C#:

public static void ApplyAutoTune(string fileToProcess, string tempFile, AutoTuneSettings autotuneSettings)
{
    using (WaveFileReader reader = new WaveFileReader(fileToProcess))
    {
        IWaveProvider stream32 = new Wave16toFloatProvider(reader);
        IWaveProvider streamEffect = new AutoTuneWaveProvider(stream32, autotuneSettings);
        IWaveProvider stream16 = new WaveFloatTo16Provider(streamEffect);
        using (WaveFileWriter converted = new WaveFileWriter(tempFile, stream16.WaveFormat))
        {
    // для правильной работы алгоритма быстрого преобразования Фурье (FFT) необходимо,
    // чтобы длина буфера была степенью 2, однако если длина буфера окажется слишком большой
    // то  высота тона, не будет определяться достаточно быстро
    // подходящий размер буфера: 8192, 4096, 2048, 1024
    // (некоторым алгоритмам определения высоты звука требуется размер по крайней мере 2048)
            byte[] buffer = new byte[8192]; 
            int bytesRead;
            do
            {
                bytesRead = stream16.Read(buffer, 0, buffer.Length);
                converted.WriteData(buffer, 0, bytesRead);
            } while (bytesRead != 0 && converted.Length < reader.Length);
        }
    }
}

VB.Net

Public Shared Sub ApplyAutoTune(ByVal fileToProcess As String,
    ByVal tempFile As String,
    ByVal autotuneSettings As AutoTuneSettings)
    Using reader As New WaveFileReader(fileToProcess)
        Dim stream32 As IWaveProvider = New Wave16ToFloatProvider(reader)
        Dim streamEffect As IWaveProvider = New AutoTuneWaveProvider(stream32, autotuneSettings)
        Dim stream16 As IWaveProvider = New WaveFloatTo16Provider(streamEffect)
        Using converted As New WaveFileWriter(tempFile, stream16.WaveFormat)
    ' для правильной работы алгоритма быстрого преобразования Фурье (FFT) необходимо,
    ' чтобы длина буфера была степенью 2, однако если длина буфера окажется слишком большой
    ' то  высота тона, не будет определяться достаточно быстро
    ' подходящий размер буфера: 8192, 4096, 2048, 1024
    ' (некоторым алгоритмам определения высоты звука требуется размер по крайней мере 2048)
                        Dim buffer(8191) As Byte
            Dim bytesRead As Integer
            Do
                bytesRead = stream16.Read(buffer, 0, buffer.Length)
                converted.WriteData(buffer, 0, bytesRead)
            Loop While bytesRead <> 0 AndAlso converted.Length < reader.Length
        End Using
    End Using
End Sub

А вот как это работает:

  1. Сначала мы используем WaveFileReader, чтобы открыть только что созданный файл, содержащий 16-разрядные выборки.
  2. Затем с помощью Wave16ToFloatProvider преобразуем их в значения с плавающей точкой.
  3. Далее прогоняем их через наш эффект автокоррекции (AutotuneWaveProvider). Принцип его работы я поясню позже.
  4. С помощью WaveFloatTo16Provider выполняем обратное преобразование в 16-битные выборки, готовые к записи в WAV-файл (мы могли бы использовать 32-разрядный WAV, но это привело бы к пустой трате дискового пространства).
  5. Подготовив аудиоконвейер, мы можем читать из WaveFloatTo16Provider и извлекать звуковые данные прямо из WAV-файла. Нам нужно читать блоками, размер которых кратен значениям в степени 2, так как мы пропускаем данные через быстрое преобразование Фурье (Fast Fourier Transform, FFT). Если нам нужно считывать блоки произвольного размера, придется вводить в конвейер еще один элемент для накопления в буфере такого объема данных, которого будет достаточного для передачи через FFT.
  6. Наконец, используя WaveFileWriter, пишем считанные данные в WAV-файл.

AutoTuneWaveProvider

Как мы видели в последнем фрагменте кода, AutoTuneWaveProvider является частью нашего аудиоконвейера, которая и выполняет эффект автокоррекции. Он реализует NAudio-интерфейс IWaveProvider, который при необходимости можно использовать в конвейере для воспроизведения в реальном времени, хотя в нашем примере кода этого не делается (см. далее — раздел о производительности). Вот что представляет собой конструктор AutoTuneWaveProvider:

C#:

public AutoTuneWaveProvider(IWaveProvider source, AutoTuneSettings autoTuneSettings)
{
    this.autoTuneSettings = autoTuneSettings;
    if (source.WaveFormat.SampleRate != 44100)
        throw new ArgumentException("AutoTune only works at 44.1kHz");
    if (source.WaveFormat.Encoding != WaveFormatEncoding.IeeeFloat)
        throw new ArgumentException("AutoTune only works on IEEE floating point audio data");
    if (source.WaveFormat.Channels != 1)
        throw new ArgumentException("AutoTune only works on mono input sources");

    this.source = source;
    this.pitchDetector = new AutoCorrelator(source.WaveFormat.SampleRate);
    this.pitchShifter = new SmbPitchShifter(Settings);
    this.waveBuffer = new WaveBuffer(8192);
}

VB.Net

Public Sub New(ByVal source As IWaveProvider, ByVal autoTuneSettings As AutoTuneSettings)
    Me.autoTuneSettings = autoTuneSettings
    If source.WaveFormat.SampleRate <> 44100 Then
        Throw New ArgumentException("AutoTune only works at 44.1kHz")
    End If
    If source.WaveFormat.Encoding <> WaveFormatEncoding.IeeeFloat Then
        Throw New ArgumentException("AutoTune only works on IEEE floating point audio data")
    End If
    If source.WaveFormat.Channels <> 1 Then
        Throw New ArgumentException("AutoTune only works on mono input sources")
    End If

    Me.source = source
    Me.pitchDetector = New AutoCorrelator(source.WaveFormat.SampleRate)
    ' альтернативный определитель высоты тона:
    ' Me.pitchDetector = New FftPitchDetector(source.WaveFormat.SampleRate)
    Me.pitchShifter = New SmbPitchShifter(Settings, source.WaveFormat.SampleRate)
    Me.waveBuffer = New WaveBuffer(8192)
End Sub

А вот несколько моментов, на которые стоит обратить особое внимание:

  1. Мы передаем IWaveProvider источника — из него будут поступать данные.
  2. Мы проверяем правильность формата источника — монофонический вход с использованием значений с плавающей точкой.
  3. Мы также передаем объект AutoTuneSettings. Он не только инкапсулирует настройки автокоррекции, но и важен, если вы захотите подстраивать параметры в реальном времени, пока эффект действует.
  4. Затем мы создаем два ключевых компонента нашего эффекта автокоррекции: детектор высоты звука (который использует автокоррелятор) и блок смещения высоты звука.
  5. Наконец, мы создаем буфер, используемый при обработке аудио. Это может быть массив byte[], но мы применяем WaveBuffer из NAudio, так как он использует хитрый трюк, позволяющий нам приводить byte[] к float[] без участия небезопасного кода или необходимости копировать все данные.

Главное в любой реализации IWaveProvider — ее метод Read. Именно в нем потребитель аудиоданных (обычно это звуковая карта или WaveFileWriter) запрашивают данные. Эти данные должны передаваться в виде байтового массива, и по возможности вы должны возвращать ровно столько байтов, сколько запросили (если это невозможно, то, как правило, требуется дополнительный уровень буферизации, а иначе воспроизводимый звук будет прерывистым).

Ниже представлена наша реализация метода Read:

C#:

public int Read(byte[] buffer, int offset, int count)
{
    if (waveBuffer == null || waveBuffer.MaxSize < count)
    {
        waveBuffer = new WaveBuffer(count);
    }

    int bytesRead = source.Read(waveBuffer, 0, count);

    // иногда последнюю порцию данных необходимо округлить:
    if (bytesRead > 0) bytesRead = count;

    int frames = bytesRead / sizeof(float); 
    float pitch = pitchDetector.DetectPitch(waveBuffer.FloatBuffer, frames);
        
    // попытка увеличить значимость, удерживая высоту тона
    // на протяжении, по крайней мере, еще одного буфера
    if (pitch == 0f && release < maxHold)
    {
        pitch = previousPitch;
        release++;
    }
    else
    {
        this.previousPitch = pitch;
        release = 0;
    }
    
    WaveBuffer outBuffer = new WaveBuffer(buffer);

    pitchShifter.ShiftPitch(waveBuffer.FloatBuffer, pitch, 0.0f, outBuffer.FloatBuffer, frames);

    return frames * 4;
}

VB.Net:

Public Function Read(ByVal buffer() As Byte, ByVal offset As Integer,
                        ByVal count As Integer) As Integer Implements NAudio.Wave.IWaveProvider.Read
    If waveBuffer Is Nothing OrElse waveBuffer.MaxSize < count Then
        waveBuffer = New WaveBuffer(count)
    End If

    Dim bytesRead = source.Read(waveBuffer, 0, count)
    'Debug.Assert(bytesRead = count)

    ' иногда последнюю порцию данных необходимо округлить:
    If bytesRead > 0 Then
        bytesRead = count
    End If

    'pitchsource->getPitches()
    Dim frames = bytesRead \ Len(New Single) ' MRH: was count
    Dim pitch = pitchDetector.DetectPitch(waveBuffer.FloatBuffer, frames)

   '  попытка увеличить значимость, удерживая высоту тона
  ‘ на протяжении, по крайней мере, еще одного буфера

    If pitch = 0.0F AndAlso release < maxHold Then
        pitch = previousPitch
        release += 1
    Else
        Me.previousPitch = pitch
        release = 0
    End If

    Dim midiNoteNumber = 40
    Dim targetPitch = CSng(8.175 * Math.Pow(1.05946309, midiNoteNumber))

    Dim outBuffer As New WaveBuffer(buffer)

    pitchShifter.ShiftPitch(waveBuffer.FloatBuffer, pitch, targetPitch, outBuffer.FloatBuffer, frames)

    Return frames * 4
End Function

При этом происходит следующее:

  1. Сначала нужно считать из источника данные (в нашем случае это WAV-файл, преобразованный в выборки со значениями с плавающей точкой).
  2. Если мы получаем меньше данных, чем ожидали, значит, мы находимся в концевой части файла, поэтому притворяемся, будто получили целый буфер.
  3. Определяем, сколько у нас «кадров» аудиоданных (это количество совпадает с числом выборок, поскольку звук монофонический). Так как аудиоданные имеют формат с плавающей точкой, количество кадров равно количеству байтов, деленному на четыре.
  4. Пропускаем данные через алгоритм определения высоты звука (см. ниже).
  5. Используем кое-какой экспериментальный код для стабилизации определения высоты звука, сообщая предыдущую частоту, когда выборка высоты не выполняется.
  6. Наконец, передаем данные в наш блок смещения высоты звука, включая детали, относящиеся к определенной высоте звука.

Теперь, когда мы посмотрели на общую картину AutotuneWaveProvider, давайте глубже покопаемся в двух основных компонентах: детекторе высоты звука и блоке ее смещения.

Определение высоты звука с автокорреляцией

Распознавание высоты звука при автокоррекции крайне важно для получения приличных результатов. Если ваш код не в состоянии точно определить входную высоту звука, он неправильно вычислит и то, насколько нужно подстроить итоговую высоту. Однако реализация высококачественного определения высоты звука весьма трудна. Во-первых, микрофон может чудно передавать фоновые звуки. Во-вторых, когда вы поете в микрофон, сигнал состоит вовсе не из единственной частоты — он также включает «гармоники» на других частотах.

Хорошая новость в том, что нам нужно распознавать только основную высоту звука.

Алгоритм awesomebox использует при определении высоты звука автокорреляцию, но я внес несколько небольших изменений в реализацию этого алгоритма, пытаясь повысить его точность. У автокорреляции есть преимущество — это сравнительно быстрый процесс. Его основной принцип заключается в том, что, если сигнал периодический, то он будет отлично «коррелировать» сам с собой при смещении вперед (или назад) на один цикл.

Допустим, мы проверяем, что в данный момент исполнитель взял среднюю ноту «до». Ее частота — около 262 Гц. Если выборка осуществляется с частотой дискретизации 44,1 кГц (стандарт для аудио компакт-дисков), то сигнал будет повторяться примерно каждые 168 выборок (44100/262). Соответственно для каждой выборки в буфере мы подсчитываем сумму квадратов этой выборки и анализируем предыдущие 168 выборок. Так делается для каждого возможного смещения доминирующей частоты в диапазоне, который мы хотим определить (я использую диапазон от 85 до 300 Гц, который адекватен для определения высоты звука вокала). Смещение с самым большим значением, скорее всего, и является искомой частотой.

Давайте рассмотрим код алгоритма автокорреляции. Начнем с конструктора класса AutoCorrelator:

C#:

public AutoCorrelator(int sampleRate)
{
    this.sampleRate = (float)sampleRate;
    int minFreq = 85;
    int maxFreq = 255;

    this.maxOffset = sampleRate / minFreq;
    this.minOffset = sampleRate / maxFreq;
}

VB.Net

Public Sub New(ByVal sampleRate As Integer)
    Me.sampleRate = CSng(sampleRate)
    Dim minFreq = 85
    Dim maxFreq = 255

    Me.maxOffset = sampleRate \ minFreq
    Me.minOffset = sampleRate \ maxFreq
End Sub

Прежде всего, мы заранее вычисляем некоторые значения, основываясь на минимальной и максимальной частотах, которые мы ищем. Помните, что более низкие частоты определять труднее, чем более высокие, поэтому на присваивайте minFreq слишком малое значение. MaxOffset и MinOffset — максимальное и минимальное расстояния в обратном направлении, на которые мы будем смещаться в поисках совпадения.

C#:

public float DetectPitch(float[] buffer, int frames)
{
    if (prevBuffer == null)
    {
        prevBuffer = new float[frames];
    }

    float maxCorr = 0;
    int maxLag = 0;
     // начинаем с низких частот и заканчиваем высокими
        for (int lag = maxOffset; lag >= minOffset; lag--)
    {
        float corr = 0; //  сумма квадратов
        for (int i = 0; i < frames; i++)
        {
            int oldIndex = i - lag;
            float sample = ((oldIndex < 0) ? prevBuffer[frames + 
            corr += (sample * buffer[i]);
        }
        if (corr > maxCorr)
        {
            maxCorr = corr;
            maxLag = lag;
        }

    }
    for (int n = 0; n < frames; n++)
    { 
        prevBuffer[n] = buffer[n]; 
    }
    float noiseThreshold = frames / 1000f;

    if (maxCorr < noiseThreshold || maxLag == 0) return 0.0f;
    return this.sampleRate / maxLag;
}

VB.Net

Public Function DetectPitch(ByVal buffer() As Single, ByVal frames As Integer) As Single Implements IPitchDetector.DetectPitch
    If prevBuffer Is Nothing Then
        prevBuffer = New Single(frames - 1){}
    End If
    Dim secCor As Single = 0
    Dim secLag = 0

    Dim maxCorr As Single = 0
    Dim maxLag = 0

    '  начинаем с низких частот и заканчиваем высокими
    For lag = maxOffset To minOffset Step -1
        Dim corr As Single = 0 ' вычисление суммы квадратов
        For i = 0 To frames - 1
            Dim oldIndex = i - lag
            Dim sample = (If(oldIndex < 0, prevBuffer(frames + oldIndex), buffer(oldIndex)))
            corr += (sample * buffer(i))
        Next i
        If corr > maxCorr Then
            maxCorr = corr
            maxLag = lag
        End If
        If corr >= 0.9 * maxCorr Then
            secCor = corr
            secLag = lag
        End If
    Next lag
    For n = 0 To frames - 1
        prevBuffer(n) = buffer(n)
    Next n
    Dim noiseThreshold = frames / 1000.0F
    'Debug.WriteLine(String.Format("Max Corr: {0} ({1}), Sec Corr: {2} ({3})", Me.sampleRate / maxLag, maxCorr, Me.sampleRate / secLag, secCor))
    If maxCorr < noiseThreshold OrElse maxLag = 0 Then
        Return 0.0F
    End If
    'Return 44100.0f / secLag '—лучше работает для пения
    Return Me.sampleRate / maxLag
End Function

Обратите внимание на несколько моментов:

  1. Аудиоданные поступают в в��де массива значений с плавающей точкой. NAudio выполняет это преобразование 16-битного аудио за нас, используя Wave16ToFloatProvider.
  2. Предыдущий буфер сохраняется. Это позволяет обращаться к нему при поиске корреляции.
  3. Далее мы анализируем все возможные целочисленные смещения в нашем диапазоне и вычисляем коррелирующее значение.
  4. Корреляция вычисляется как сумма квадратов.
  5. Если это значение на данный момент самое большое, мы сохраняем значение «запаздывания» («lag») (т. е. количество выборок в обратном направлении, с которым осуществляется корреляция).
  6. Заметьте, что мы возвращаем 0 (т. е. частота не распознана), если не находим выделяющуюся частоту. Порог шума можно подстраивать в зависимости от входного аудиопотока.
  7. Наконец, мы преобразуем в частоту по формуле sampleRate / maxLag.

Я написал несколько модульных тестов для анализа точности распознавания на примере синусоидальных волн (которые, безусловно, распознавать легче всего). Вот результаты для аудиоданных при частоте дискретизации 44,1 кГц:

Тестовая частота, Гц

Распознанная высота звука, Гц

109,99

108,35

116,53

118,23

123,46

123,18

130,80

129,71

138,58

140,00

146,82

148,48

155,55

154,74

164,80

163,33

174,60

172,94

184,98

183,75

195,98

194,27

207,63

206,07

219,98

219,40

233,06

234,57

246,92

247,75

261,60

256,40

277,16

139,56

293,64

146,03

Заметьте, что распознанные частоты в последних двух тестах составляют всего половину реальных частот. Для наших целей это, в общем-то, не имеет значения, так как просто означает, что частота была определена на октаву ниже правильной ноты.

Чтобы повысить точность результатов автокоррелятора, можно внести пару изменений:

  • предварительно запускать полосовой фильтр для удаления любых частот за пределами нужного диапазона;
  • комбинировать результаты, полученные разными методами, например подсчетом частоты пересечения нулевых отметок (zero-crossings).

Определение высоты звука с помощью быстрого преобразования Фурье

Я решил реализовать альтернативный алгоритм определения высоты звука, чтобы проверить, смогу ли я добиться результатов получше. Этот подход заключается в использовании быстрого преобразования Фурье (FFT) — алгоритма, который преобразует сигналы из «временного домена» в «частотный домен».

Базовый подход состоит в следующем. Берем блок выборок (размер которого должен быть кратным значению в степени 2, например 1024) и применяем к нему FFT. Алгоритм FFT принимает на входе комплексные числа, которые для аудиосигналов являются вещественными. Используемая мной реализация ожидает во входном буфере чередующиеся вещественные и комплексные части. Вот наш код, помещающий в fftBuffer чередующиеся выборки:

C#:

private float[] fftBuffer;
private float[] prevBuffer;

public float DetectPitch(float[] buffer, int inFrames)
{
    Func<int, int, float> window = HammingWindow;
    if (prevBuffer == null)
    {
        prevBuffer = new float[inFrames];
    }
 
    // удваиваем количество кадров, т.к. мы объединяем текущий и предыдущий буферы
    int frames = inFrames * 2;
    if (fftBuffer == null)
    {
        fftBuffer = new float[frames * 2]; // умножаем на 2, так как ввод комплексный
    }
 
    for (int n = 0; n < frames; n++)
    {
        if (n < inFrames)
        {
            fftBuffer[n * 2] = prevBuffer[n] * window(n, frames);
            fftBuffer[n * 2 + 1] = 0; // необходимо очистить, поскольку fft модифицировал буфер
        }
        else
        {
            fftBuffer[n * 2] = buffer[n-inFrames] * window(n, frames);
            fftBuffer[n * 2 + 1] = 0; // // необходимо очистить, поскольку fft модифицировал буфер
        }
    }

VB.Net

Private fftBuffer() As Single
Private prevBuffer() As Single

Public Function DetectPitch(ByVal buffer() As Single,
                            ByVal inFrames As Integer) As Single Implements IPitchDetector.DetectPitch
    Dim window As Func(Of Integer, Integer, Single) = AddressOf HammingWindow
    If prevBuffer Is Nothing Then
        prevBuffer = New Single(inFrames - 1) {}
    End If

    ' удваиваем количество кадров, т.к. мы объединяем текущий и предыдущий буферы
    Dim frames = inFrames * 2
    If fftBuffer Is Nothing Then
        fftBuffer = New Single(frames * 2 - 1) {} ' умножаем на 2, так как ввод комплексный
    End If

    For n = 0 To frames - 1
        If n < inFrames Then
            fftBuffer(n * 2) = prevBuffer(n) * window(n, frames)
            fftBuffer(n * 2 + 1) = 0 ' необходимо очистить, поскольку fft модифицировал буфер
        Else
            fftBuffer(n * 2) = buffer(n - inFrames) * window(n, frames)
            fftBuffer(n * 2 + 1) = 0 ' необходимо очистить, поскольку fft модифицировал буфер
        End If
    Next n

Заметьте, что мы добавляем к началу предыдущий переданный буфер. Это распространенный способ увеличения точности и разрешения FFT за счет использования перекрывающихся окон, и его можно усовершенствовать сохранением трех предыдущих буферов, что дает перекрытие окон на 75% вместо 50%, как в этом примере.

Для более качественного определения пиковой частоты сигнал, передаваемый в FFT, лучше всего предварительно обработать весовой функцией «окно» («windowing» function). Таких функций несколько, и у каждой из них есть свои сильные и слабые стороны. Я использовал окно Хэмминга (Hamming window) как самое распространенное:

C#:

private float HammingWindow(int n, int N) 
{
    return 0.54f - 0.46f * (float)Math.Cos((2 * Math.PI * n) / (N - 1));
}

VB.Net

Private Function HammingWindow(ByVal n As Integer, ByVal _N As Integer) As Single
    Return 0.54F - 0.46F * CSng(Math.Cos((2 * Math.PI * n) / (_N - 1)))
End Function

Следующий шаг — передача нашего чередующегося буфера в алгоритм FFT. Здесь я использую реализацию Стефана Бернси (Stephan Bernsee), хотя в NAudio есть альтернативная реализация, которую тоже можно было бы задействовать. Для обратного (инверсного) FFT можно применять ту же функцию, и параметр со значением -1 (вопреки всякой логике) включает не обратное, а прямое преобразование по алгоритму FFT. Обработка данных происходит по месту их хранения, что очень удобно, поскольку нам не нужно сохранять содержимое входного буфера:

C#:

// предполагая, что число фреймов является степенью 2
SmbPitchShift.smbFft(fftBuffer, frames, -1);

VB.Net

' предпола��ая, что число фреймов является степенью 2
SmbPitchShift.smbFft(fftBuffer, frames, -1)

Выполнив FFT, мы готовы к интерпретации его вывода. Вывод FFT состоит из комплексных чисел (и вновь вещественные значения в буфере сменяются мнимыми), которые представляют элементы разрешения по частоте (frequency bins).

Мы начинаем с вычисления размера этих элементов и определения того, какие из них соответствуют диапазону интересующих нас частот:

C#:

float binSize = sampleRate / frames;
int minBin = (int)(85 / binSize);
int maxBin = (int)(300 / binSize);

VB.Net

Dim binSize = sampleRate / frames
Dim minBin = CInt(Fix(85 / binSize))
Dim maxBin = CInt(Fix(300 / binSize))

Например, если частота дискретизации равна 44,1 кГц и мы анализируем блок из 1024 значений, тогда каждый элемент представляет 43 Гц, что вряд ли отвечает нужной нам гранулярности. Чтобы повысить разрешение, можно выбрать либо дискретизацию с более высокой частотой, либо анализировать большие порции. Наш подход заключается в использовании перекрывающихся блоков размером 8192 выборки, так как мы каждый раз считываем 4096 значений. То есть мы добиваемся разрешения примерно в 5 Гц, что гораздо лучше.

Теперь мы можем вычислить амплитуду (или «интенсивность») каждой частоты нахождением суммы квадратов (строго говоря, после этого мы должны были бы вычислять квадратный корень, но нам этого не требуется, потому что мы просто ищем наибольшее значение):

C#:

float maxIntensity = 0f;
int maxBinIndex = 0;

for (int bin = minBin; bin <= maxBin; bin++)
{
    float real = fftBuffer[bin * 2];
    float imaginary = fftBuffer[bin * 2 + 1];
    float intensity = real * real + imaginary * imaginary;
    if (intensity > maxIntensity)
    {
        maxIntensity = intensity;
        maxBinIndex = bin;
    }
}

VB.Net

Dim maxIntensity = 0.0F
Dim maxBinIndex = 0
For bin = minBin To maxBin
    Dim real = fftBuffer(bin * 2)
    Dim imaginary = fftBuffer(bin * 2 + 1)
    Dim intensity = real * real + imaginary * imaginary
    If intensity > maxIntensity Then
        maxIntensity = intensity
        maxBinIndex = bin
    End If
Next bin

Поскольку мы идентифицировали элемент с максимальной интенсивностью, можно вычислить распознанную частоту:

C#:

return binSize * maxBinIndex;

VB.Net

Return binSize * maxBinIndex

Сейчас я не указываю минимальное пороговое значение для maxIntensity, но, возможно, если бы оно было очень мало, FFT-детектор высоты звука возвращал бы вместо не слишком точного ответа ноль, сообщая тем самым, что не распознал высоту звука.

Взгляните, какие результаты дает FFT-детектор высоты звука.

Тестовая частота, Гц

Распознанная высота звука, Гц

109,99

107,67

116,53

118,43

123,46

123,82

130,80

129,20

138,58

139,97

146,82

145,35

155,55

156,12

164,80

166,88

174,60

172,27

184,98

183,03

195,98

193,80

207,63

209,95

219,98

220,72

233,06

231,48

246,92

247,63

261,60

263,78

277,16

274,55

293,64

296,08

Как видите, он правильно выбирает основные частоты более высоких нот и в целом работает немного лучше автокоррелятора, поэтому я оставил его в качестве алгоритма по умолчанию. Однако вы можете заменить FFT-детектор на автокоррелятор или другой алгоритм, если он эффективнее работает с вашим звуковым материалом.

Существуют способы использования информации о фазах в выводе FFT для еще большего увеличения точности распознавания высоты звука, но это занятие я оставляю читателям в качестве упражнения!

Смещение высоты звука

Следующий шаг — определить, насколько нужно сдвинуть высоту звука. Самый простой способ сделать это — найти ближайшую к распознанной высоте звука музыкальную ноту. В таком случае величина смещения является просто отношением этих двух нот.

Однако нужно учесть некоторые дополнительные соображения. Во-первых, нам может понадобиться лишь приемлемое подмножество музыкальных нот. Например, только ноты в тональности «до-диез» или, может быть, «фа-диез» минор. Это, скорее всего, потребует несколько более радикальной подстройки.

Во-вторых, в зависимости от нужного эффекта для нас, возможно, будет нежелателен мгновенный переход к новой частоте. В используемом мной коде применяется довольно элементарный параметр временной атаки, что позволяет постепенно смещаться к новой частоте.

Реальная цифровая обработка сигналов для эффекта смещения высоты звука взята в более-менее нетронутом виде из кода Стефана Бернси, потому что она работает по-настоящему хорошо. В коде Бернси используется быстрое преобразование Фурье плюс уйма заумной математики, которую я почти понимаю, но не в такой мере, чтобы пытаться объяснять ее здесь! Вам лучше прочесть статью, в которой он сам поясняет, как это работает.

Класс, управляющий алгоритмом сдвига высоты звука, называется SmbPitchShifer и наследует от базового класса PitchShifter. Основную часть работы он выполняет в функции ShiftPitch:

C#:

public void ShiftPitch(float[] inputBuff, float inputPitch,
                       float targetPitch, float[] outputBuff, int nFrames)
{
     UpdateSettings();
     detectedPitch = inputPitch;

VB.Net

Public Sub ShiftPitch(ByVal inputBuff() As Single, ByVal inputPitch As Single,
                    ByVal targetPitch As Single, ByVal outputBuff() As Single, ByVal nFrames As Integer)
    UpdateSettings()
    detectedPitch = inputPitch

Параметру inputPitch присваивается частота, определенная PitchDetector. Параметр targetPitch сейчас не задействован, но будет использоваться для задания целевой высоты звука в реальном времени при приеме ввода, скажем, от MIDI-клавиатуры. В любом случае мы вызываем UpdateSettings, чтобы выяснить, изменялись ли с прошлого раза какие-нибудь настройки алгоритма автокоррекции.

Затем мы вычисляем величину сдвига для смещения высоты звука. Коэффициент сдвига, равный 1, означает отсутствие изменений. Мы не даем этому коэффициенту выйти за пределы диапазона значений от 0,5 до 2,0, так как за этими цифрами стоит изменение на целую октаву:

C#:

float shiftFactor = 1.0f;

if (inputPitch > 0)
{
    shiftFactor = snapFactor(inputPitch);
}

if (shiftFactor > 2.0) shiftFactor = 2.0f;
if (shiftFactor < 0.5) shiftFactor = 0.5f;

VB.Net

Dim shiftFactor = 1.0F

If inputPitch > 0 Then
   shiftFactor = snapFactor(inputPitch)
   shiftFactor += addVibrato(nFrames)
End If

If shiftFactor > 2.0 Then
   shiftFactor = 2.0F
End If
If shiftFactor < 0.5 Then
   shiftFactor = 0.5F
End If

Решение по выбору целевой ноты принимается в функции snapFactor:

C#:

protected float snapFactor(float freq)
{
    float previousFrequency = 0.0f;
    float correctedFrequency = 0.0f;
    int previousNote = 0;
    int correctedNote = 0;
    for (int i = 1; i < 120; i++)
    {
        bool endLoop = false;
        foreach (int note in this.settings.AutoPitches)
        {
            if (i % 12 == note)
            {
                previousFrequency = correctedFrequency;
                previousNote = correctedNote;
                correctedFrequency = (float)(8.175 * Math.Pow(1.05946309, (float)i));
                correctedNote = i;
                if (correctedFrequency > freq) { endLoop = true; }
                break;
            }
        }
        if (endLoop)
        {
            break;
        }
    }
    if (correctedFrequency == 0.0) { return 1.0f; }
    int destinationNote = 0;
    double destinationFrequency = 0.0;
    // решаем, куда мы сдвигаемся – вверх или вниз
    if (correctedFrequency - freq > freq - previousFrequency)
    {
        destinationNote = previousNote;
        destinationFrequency = previousFrequency;
    }
    else
    {
        destinationNote = correctedNote;
        destinationFrequency = correctedFrequency;
    }
    if (destinationNote != currPitch)
    {
        numElapsed = 0;
        currPitch = destinationNote;
    }
    if (attack > numElapsed)
    {
        double n = (destinationFrequency - freq) / attack * numElapsed;
        destinationFrequency = freq + n;
    }
    numElapsed++;
    return (float)(destinationFrequency / freq);
}

VB.Net:

Protected Function snapFactor(ByVal freq As Single) As Single
    Dim previousFrequency = 0.0F
    Dim correctedFrequency = 0.0F
    Dim previousNote = 0
    Dim correctedNote = 0
    For i = 1 To 119
        Dim endLoop = False
        For Each note As Integer In Me.settings.AutoPitches
            If i Mod 12 = note Then
                previousFrequency = correctedFrequency
                previousNote = correctedNote
                correctedFrequency = CSng(8.175 * Math.Pow(1.05946309, CSng(i)))
                correctedNote = i
                If correctedFrequency > freq Then
                    endLoop = True
                End If
                Exit For
            End If
        Next note
        If endLoop Then
            Exit For
        End If
    Next i
    If correctedFrequency = 0.0 Then
        Return 1.0f
    End If
    Dim destinationNote = 0
    Dim destinationFrequency = 0.0
    ' решаем, куда мы сдвигаемся – вверх или вниз
    If correctedFrequency - freq > freq - previousFrequency Then
        destinationNote = previousNote
        destinationFrequency = previousFrequency
    Else
        destinationNote = correctedNote
        destinationFrequency = correctedFrequency
    End If
    If destinationNote <> currPitch Then
        numElapsed = 0
        currPitch = destinationNote
    End If
    If attack > numElapsed Then
        Dim n = (destinationFrequency - freq) / attack * numElapsed
        destinationFrequency = freq + n
    End If
    numElapsed += 1
    Return CSng(destinationFrequency / freq)
End Function

Эта функция работает так: перебирает MIDI-ноты 0–120, и, если какая-то из них выбрана как одна из допустимых и поддерживаемых высот звука, мы запоминаем «скорректированную частоту», которую можно вычислить на основе номера MIDI-ноты по следующей формуле:

C#:

correctedFrequency = (float)(8.175 * Math.Pow(1.05946309, (float)midiNoteNumber));

VB.Net

correctedFrequency = CSng(8.175 * Math.Pow(1.05946309, CSng(i)))

Очевидно, высота звука попадет куда-то между двумя допустимыми нотами, поэтому находится ближайшая нота, и частота сдвигается на соответствующую величину.

Функция snapFactor также отвечает за реализацию параметра временной атаки (attack time parameter). Это позволяет плавно смещать destinationFrequency к целевой ноте в течение периода атаки. Вычислив коэффициент сдвига, мы теперь готовы передать свои данные реальному алгоритму смещения высоты звука:

C#:

int fftFrameSize = 2048;
int osamp = 8; // лучшее качество соответствует 32
SmbPitchShift.smbPitchShift(shiftFactor, nFrames, fftFrameSize, osamp, this.sampleRate, inputBuff, outputBuff);

VB.Net

Dim fftFrameSize = 2048
Dim osamp = 8 ' лучшее качество соответствует 32
SmbPitchShift.smbPitchShift(shiftFactor, nFrames, fftFrameSize, osamp, Me.sampleRate, inputBuff, outputBuff)

Последнее, что делается в функции ShiftPitch, — регистрируются выполненные смещения высоты звука. Записи об этих операциях помещаются в очередь (максимум 5000 элементов). Они очень полезны при диагностике, когда вы получаете от алгоритма не те результаты, которые ожидали:

С#:

shiftedPitch = inputPitch * shiftFactor;
updateShifts(detectedPitch, shiftedPitch, this.currPitch);

VB.Net

shiftedPitch = inputPitch * shiftFactor
updateShifts(detectedPitch, shiftedPitch, Me.currPitch)

Производительность

Производительность, как и следовало ожидать в управляемом приложении без обширных оптимизаций, была не особенно хороша. На своем лэптопе я смог выполнить автокоррекцию одной минуты аудиозаписи примерно за 90 секунд. Очевидно, что такая производительность исключает возможность автокоррекции в реальном времени. Я решил выполнить профилирование приложения, чтобы понять, можно ли по-быстрому что-то улучшить.

Средства профилирования в Visual Studio позволили выяснить, что 20% времени занимает определение высоты звука и 80% — ее смещение. Увы, выбор вариантов оптимизации был не слишком велик, так как дальнейший анализ указал на вызовы Math.Sin, которые и отнимали большую часть времени. Возможно, что создание таблиц поиска (lookup tables) поможет сэкономить какое-то время.

К счастью, есть другой вариант ускорения работы приложения. Алгоритм сдвига высоты звука принимает параметр передискретизации (oversampling), который по умолчанию равен 32 — максимальному значению. Однако мы можем пожертвовать качеством ради скорости. Задав этот параметр, равным 16, я смог выполнить автокоррекцию одной минуты аудиозаписи за 55 секунд (на своем лэптопе с процессором Core 2 Duo с тактовой частотой 2,4 ГГц) – реальное время, но только-только. Уменьшив это значение до 8, я сократил длительность процесса до 36 секунд. Результаты получались вполне приличными, поэтому я оставил в коде значение 8 для этого параметра.

Альтернативный способ ускорения работы — переход на другой алгоритм сдвига высоты звука. Вы могли бы начать с проверки того алгоритма, который я создал как часть проекта Skype Voice Changer, уже публиковавшийся в блоге Coding4Fun; этот алгоритм также способен работать в реальном времени (хотя никаких сравнений качества я не делал).

Создание GUI для тестов

Вместо того чтобы начинать с нуля, я решил взять за основу .NET Voice Recorder, WPF-приложение, созданное мной для предыдущей статьи в Coding4Fun. Оно использует преимущества .NET-библиотеки NAudio для записи и воспроизведения звука. GUI состоит из трех окон. В первом показывается устройство ввода, используемое для записи. Второе окно позволяет сделать короткую запись с микрофона, а третье — редактировать небольшую порцию записанных аудиоданных.

Вот снимок второго окна, в котором показывается прогресс записи:

clip_image002

А это окно, позволяющее обрезать запись, осуществлять предварительное прослушивание и записывать в WAV:

clip_image004

Как видите, я добавил новую кнопку для доступа к настройкам эффекта автокорреляции. В этом окне вы можете выбрать, какие ноты являются допустимыми, а также настроить время атаки, если вы не хотите получить роботизированный эффект. Я включил раскрывающееся меню, которое автоматически выбирает соответствующие ноты из различных октав.

clip_image006

Когда вы щелкаете кнопку Apply, применяется эффект автокоррекции (пока вы ждете в фоновом потоке), а затем вы возвращаетесь в это ��кно, что позволяет вам проиграть свою запись и послушать, как она звучит теперь. При желании вы можете вернуться назад и изменить параметры автокоррекции (или вообще выключить ее).

Библиотека MVVM Light

Исходное приложение VoiceRecorder использовало архитектуру MVVM (Model-View-ViewModel) для связывания данных с каждым представлением (view). Я обновил его, переведя на использование великолепной библиотеки MVVM Light, созданной Лорентом Буньоном (Laurent Bugnion). Это исключило необходимость в моих классах RelayCommand и ViewModelBase, а также позволило мне заменить свой ViewManager более расширяемой инфраструктурой, использующей агрегатор событий (Messenger), который включен в MVVM Light. Благодаря этому я смог быстро переходить из одного представления в другое, посылая соответствующее сообщение агрегатору событий:

С#:

private void NavigateToSaveView()
{
   Messenger.Default.Send(new NavigateMessage(SaveViewModel.ViewName, this.voiceRecorderState));
}

VB.Net

Private Sub NavigateToSaveView()
    Messenger.Default.Send(New NavigateMessage(SaveViewModel.ViewName, Me.voiceRecorderState))
End Sub

Выжимая максимум возможного из Autotune

К сожалению, Autotune — эффект, который не всегда дает желаемый результат. Очевидно, если вы хотите добиться впечатляющих результатов, вам лучше купить коммерческую реализацию программы, но я все же дам несколько советов насчет того, как выжать максимум возможного из алгоритма автокоррекции.

  • Добейтесь хорошего качества записи. Избегайте фонового шума, гудения, а также слишком тихих или чересчур громких (искаженных) записей. Если вам нужно петь под записанную звуковую дорожку, воспроизводите ее в наушниках, чтобы микрофон не подхватывал ее.
  • Узнайте, в какой октаве вы поете. Здесь тестовое приложение ничем вам не поможет: если вы не знаете, в какой октаве вы поете, то вряд ли сумеете выбрать подходящую октаву из списка. Вы ведь можете петь, даже переходя из одной октавы в другую! Если у вас есть под рукой настроенный музыкальный инструмент, сыграйте на нем ноту, чтобы получить для себя отправную высоту звука.
  • Выберите свою шкалу. Самый простой вариант — хроматическая шкала, где допустимы все 12 нот. Однако вы можете попытаться сделать так, чтобы автокоррекция превращала ваш вокал в монотонный. Пентатонные шкалы удобны, если вы хотите незамедлительно получить подходящий результат. В них содержится по пять нот, и, пока вы придерживаетесь этих нот, почти все, что вы поете, будет соответствовать записанной звуковой дорожке в выбранной октаве.
  • Настройте время атаки. Нулевое время атаки хорошо подходит для достижения роботизированного эффекта. Более длительные времена атаки будут увеличивать плавность переходов.
  • Почему возникает эффект «трели»? В этом алгоритме автокоррекции получение эффекта трели — обычное дело. Он связан либо с тем, что алгоритм меняет свое решение по выбору ноты, к которой осуществляется смещение высоты звука (детектор высоты звука не обеспечивает стабильного распознавания высоты), либо с тем, что детектор вообще не может определить высоту звука, из-за чего ваш голос не подвергается смещению высоты. Если вы хотите избавиться от трелей, попробуйте поэкспериментировать со слайдером разрешения сигнала (release slider) или даже изменить алгоритм этого разрешения.

Возможные усовершенствования

.NET Voice Recorder — проект с открытым исходным кодом. Он размещен на сайте CodePlex в хранилище Mercury(**), осуществляющем хостинг открытых проектов. Так чего же вы ждете? Выбирайте подходящий вариант и совершенствуйте его.

  • Улучшение алгоритма детектора высоты звука. Я уже высказывал некоторые предложения насчет того, как можно это сделать. Одна из идей, с которой, вероятно, стоит поэкспериментировать, — хранение распознанной частоты в течение короткого периода до тех пор, пока не будет обнаружена новая доминирующая частота.
  • Отображение обнаруженных высот звука. Эффект автокоррекции хранит данные о распознанных исходных высотах звука, а также о высотах, в которые он пытался преобразовать исходные. Вы могли бы отображать эту информацию для пользователя, например под волновым графиком, чтобы он видел, что именно распознается. (Эта информация сейчас выводится с помощью Debug.WriteLine, что полезно для отладочных целей.)
  • Как насчет того, чтобы предлагать подходящую шкалу? Вместо того чтобы взваливать на пользователя выбор октавы, к которой относятся ноты, может быть, стоит автоматически выбирать распознанные ноты?
  • Поддержка прямого ввода нужной высоты звука. Вместо того чтобы предоставить алгоритмам автокоррекции возможность самим определять, на какую ноту следует ориентироваться при смещении высоты звука в каждый конкретный момент, гораздо эффективнее разрешить пользователю вводить эти ноты. Для этого можно использовать MIDI-клавиатуру в реальном времени или рисовать ноты в элементе управления с интерфейсом в стиле фортепианной клавиатуры. Либо пользователи могли бы реализовать простой язык, специфичный для предметной области, чтобы указывать нужные ноты, например:
    • 0:00.0 C#
    • 0:01.5 E
    • 0:02.7 G#
  • Перенос на платформу Windows Phone 7. Это был бы весьма впечатляющий эффект на вашем смартфоне. Конечно, понадобилось бы добиться еще большей оптимизации производительности для продления работы аккумулятора. И вам пришлось бы стать дизайнером, чтобы придать программе более красивый облик, чем тот, который есть сейчас.

Об авторе

Марк Хит (Mark Heath) — автор нескольких .NET-приложений и библиотек с открытым исходным кодом, в том числе NAudio и Skype Voice Changer. Он работает в компании NICE Systems, где разрабатывает приложения для поиска, отображения и воспроизведения огромных объемов мультимедийных данных. Ведет собственный блог, Sound Code; кроме того, вы можете следить за его заметками на Twitter.

Примечания переводчика.

* American Idol — телешоу на телеканале FOX, основанное на популярном британском шоу Pop Idol. Цель участников — определить лучшего начинающего исполнителя в США.

** Хранилище Mercury (Mercury repository) – хранилище исходного кода, организующего децентрализованную модель разработки.