Как зациклить аудиофайл с одинарной высотой звука без перерыва или щелчка
У меня есть несколько (моно) записей флейты разных высот, чтобы сделать синтезатор флейты. Каждая из записей имеет длительность 1 секунда, в то время как синтезатор должен воспроизводить высоту звука любой заданной длины. Так что в основном мне нужно зациклить запись, чтобы воспроизвести высоту звука в течение более длительного периода. Я пытался реализовать это с помощью NAudio, но при перемотке всегда слышен щелчок.
Я всегда сохраняю образцы в файл WAV, поэтому это не должно быть проблемой задержки. Я также обрезал аудио, чтобы зацикленные сэмплы не имели щелчка. При выполнении обрезки я также старался, чтобы итоговое число сэмплов было кратным числу сэмплов за цикл (рассчитано с использованием стандартной частоты шага). Казалось, немного помогло, но щелчок все еще существует.
Я также пробовал исчезать. Когда я просто потушил последние сэмплы, а затем постепенно начал сэмплировать, звук щелчка исчез, однако во время перехода вы могли услышать разрыв (короткое молчание). Затем я позволил перекрытию постепенного исчезновения и постепенного появления, и щелчок вернулся снова.
Я искал это некоторое время, и в основном люди говорят о кроссфейдинге. Есть ли что-то не так с моим подходом перекрытия постепенного исчезновения и постепенного появления?
Вот код:
internal class NeverEndingPitchSampleProvider : ISampleProvider
{
// The amount of samples to discard at beginning and end
private const double TrimSamples = 0.1;
// The amount of samples to fade out and fade in
private const double FadeSamples = 0.05;
private List<float> samples;
private int currentPosition;
public WaveFormat WaveFormat { get; }
public NeverEndingPitchSampleProvider(ISampleProvider sourceProvider, double frequency)
{
this.WaveFormat = sourceProvider.WaveFormat;
this.samples = SampleProviderHelper.GetAllSamples(sourceProvider);
this.Trim(frequency);
this.CrossFade();
this.currentPosition = 0;
}
public int Read(float[] buffer, int offset, int count)
{
int samplesToRead = count;
while (samplesToRead > 0)
{
int newPosition = this.currentPosition + samplesToRead;
if (newPosition > this.samples.Count)
{
newPosition = this.samples.Count;
}
for (int i = this.currentPosition; i < newPosition; i++)
{
buffer[offset++] = this.samples[i];
}
samplesToRead -= newPosition - this.currentPosition;
this.currentPosition = newPosition >= this.samples.Count ? 0 : newPosition;
}
return count;
}
private void Trim(double frequency)
{
// Basic trimming: discard some of the starting and ending samples
int samplesToTrim = (int)(NeverEndingPitchSampleProvider.TrimSamples * this.samples.Count);
int samplesToKeep = this.samples.Count - samplesToTrim - samplesToTrim;
// Align with the pitch frequency
int samplesPerCycle = (int)(this.WaveFormat.SampleRate / frequency);
samplesToKeep -= samplesToKeep % samplesPerCycle;
this.samples = this.samples.GetRange(samplesToTrim, samplesToKeep);
}
private void CrossFade()
{
int samplesToFade = (int)(this.samples.Count * NeverEndingPitchSampleProvider.FadeSamples);
int fadeOutStart = this.samples.Count - samplesToFade;
// Fade-in fade-out separately
this.FadeIn(0, samplesToFade);
this.FadeOut(fadeOutStart, samplesToFade);
// Overlap at the end
for (int i = 0; i < samplesToFade; i++)
{
this.samples[fadeOutStart + i] += this.samples[i];
}
// Remove the beginning samples
this.samples = this.samples.GetRange(samplesToFade, fadeOutStart);
}
private void FadeIn(int begin, int count)
{
for (int i = 0; i < count; i++)
{
this.samples[begin + i] *= (float)(i + 1) / count;
}
}
private void FadeOut(int begin, int count)
{
for (int i = 0; i < count; i++)
{
this.samples[begin + i] *= (float)(count - i) / count;
}
}
}