混响与延迟类似,都为声音添加了回声。 不过,它的不同之处在于,延迟会添加一个回声(即使该回声会重复出现,每次都会更安静),而混响会在声音中添加多个回声。 基本上,混响使声音听起来像是在教堂、深洞或小浴室中演奏的。 延迟只会让事情听起来有点回声。
该技术非常简单。 您有一个样本缓冲区来保存最后 N 个样本,然后当您处理传入样本时,您添加来自延迟缓冲区的多个样本,每个样本乘以不同的音量(幅度)并将其添加到传入样本中以获得输出样本。 然后,您还将该传出样本放入循环缓冲区中当前索引处的混响缓冲区中。
这是一些关于如何实现它的伪代码:
reverbBuffer[reverbSamples] = 0;
reverbIndex= 0;
for (i = 0; i < numSamples; ++i)
{
outSample[i] = inSample[i];
for (j = 0; j < numTaps; ++j)
outSample[i] += reverbBuffer[reverbIndex - taps[j].tapTime] * taps[j].feedbackMultiplier;
reverbBuffer[reverbIndex] = outSample[i];
reverbIndex++;
if (reverbIndex>= reverbSamples)
reverbIndex= 0;
}
在下面的示例代码以及上面的混响处理示例中,以下是所使用的拍子的时间和幅度。幅度以 dB 和幅度的形式给出,因此您可以看到您更喜欢的那个。
Time (ms)
d
B
Amplitude
79
−
25
0.0562
130
−
23
0.0707
230
−
15
0.1778
340
−
23
0.0707
470
−
17
0.1412
532
−
21
0.0891
662
−
13
0.2238
Time (ms) dB Amplitude 79−250.0562130−230.0707230−150.1778340−230.0707470−170.1412532−210.0891662−130.2238
Time (ms) 79130230340470532662dB−25−23−15−23−17−21−13 Amplitude 0.05620.07070.17780.07070.14120.08910.2238
通过更多的努力,您可能会想出一些更好的抽头值来使混响听起来更好。下面的示例代码加载 in.wav,使用上面提到的 Tap 对其进行处理,并将其写出为 out.wav。 与往常一样,波形加载代码对于某些波形格式存在一些问题。
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
#include
#include
#include
#define _USE_MATH_DEFINES
#include
template
struct SNumeric
{
public:
explicit SNumeric(const T &value) : m_value(value) { }
SNumeric() : m_value() { }
inline T& Value() { return m_value; }
inline const T& Value() const { return m_value; }
typedef SNumeric TType;
typedef T TInnerType;
// Math Operations
TType operator+ (const TType &b) const
{
return TType(this->Value() + b.Value());
}
TType operator- (const TType &b) const
{
return TType(this->Value() - b.Value());
}
TType operator* (const TType &b) const
{
return TType(this->Value() * b.Value());
}
TType operator/ (const TType &b) const
{
return TType(this->Value() / b.Value());
}
TType& operator+= (const TType &b)
{
Value() += b.Value();
return *this;
}
TType& operator-= (const TType &b)
{
Value() -= b.Value();
return *this;
}
TType& operator*= (const TType &b)
{
Value() *= b.Value();
return *this;
}
TType& operator/= (const TType &b)
{
Value() /= b.Value();
return *this;
}
TType& operator++ ()
{
Value()++;
return *this;
}
TType& operator-- ()
{
Value()--;
return *this;
}
// Extended Math Operations
template
T Divide(const TType &b)
{
return ((T)this->Value()) / ((T)b.Value());
}
// Logic Operations
bool operator< (const TType &b) const {
return this->Value() < b.Value();
}
bool operator<= (const TType &b) const {
return this->Value() <= b.Value();
}
bool operator> (const TType &b) const {
return this->Value() > b.Value();
}
bool operator>= (const TType &b) const {
return this->Value() >= b.Value();
}
bool operator== (const TType &b) const {
return this->Value() == b.Value();
}
bool operator!= (const TType &b) const {
return this->Value() != b.Value();
}
private:
T m_value;
};
typedef uint8_t uint8;
typedef uint16_t uint16;
typedef uint32_t uint32;
typedef int16_t int16;
typedef int32_t int32;
typedef SNumeric TFrequency;
typedef SNumeric TTimeMs;
typedef SNumeric TSamples;
typedef SNumeric TFractionalSamples;
typedef SNumeric TDecibels;
typedef SNumeric TAmplitude;
typedef SNumeric TPhase;
static const float c_pi = (float)M_PI;
static const float c_twoPi = c_pi * 2.0f;
//=====================================================================================
// Structs
//=====================================================================================
struct SSoundSettings
{
TSamples m_sampleRate;
TTimeMs m_lengthMs;
TSamples m_currentSample;
};
struct SReverbTap
{
TSamples m_timeOffset;
TAmplitude m_feedback;
};
class CMultitapReverb
{
public:
CMultitapReverb(const std::vector& taps)
: m_sampleIndex(0)
{
m_taps = taps;
TSamples largestTimeOffset(0);
std::for_each(m_taps.begin(), m_taps.end(),
[&largestTimeOffset](const SReverbTap& tap)
{
if (tap.m_timeOffset > largestTimeOffset)
largestTimeOffset = tap.m_timeOffset;
}
);
if (largestTimeOffset.Value() == 0)
return;
m_samples.resize(largestTimeOffset.Value()+1);
std::fill(m_samples.begin(), m_samples.end(), TAmplitude(0.0f));
}
TAmplitude ProcessSample(TAmplitude sample)
{
if (m_samples.size() == 0)
return sample;
TAmplitude outSample = sample;
std::for_each(m_taps.begin(), m_taps.end(),
[&outSample, this](const SReverbTap& tap)
{
size_t tapSampleIndex;
if (tap.m_timeOffset.Value() > m_sampleIndex)
tapSampleIndex = m_samples.size() - 1 - (tap.m_timeOffset.Value() - m_sampleIndex);
else
tapSampleIndex = m_sampleIndex - tap.m_timeOffset.Value();
outSample += m_samples[tapSampleIndex] * tap.m_feedback;
}
);
m_samples[m_sampleIndex] = outSample;
m_sampleIndex++;
if (m_sampleIndex >= m_samples.size())
m_sampleIndex = 0;
return outSample;
}
private:
std::vector m_taps;
std::vector m_samples;
size_t m_sampleIndex;
};
inline TDecibels AmplitudeToDB(TAmplitude volume)
{
return TDecibels(log10(volume.Value()));
}
inline TAmplitude DBToAmplitude(TDecibels dB)
{
return TAmplitude(pow(10.0f, dB.Value() / 20.0f));
}
TSamples SecondsToSamples(const SSoundSettings &s, float seconds)
{
return TSamples((int)(seconds * (float)s.m_sampleRate.Value()));
}
TSamples MilliSecondsToSamples(const SSoundSettings &s, float milliseconds)
{
return SecondsToSamples(s, milliseconds / 1000.0f);
}
TTimeMs SecondsToMilliseconds(float seconds)
{
return TTimeMs((uint32)(seconds * 1000.0f));
}
TFrequency Frequency(float octave, float note)
{
return TFrequency((float)(440 * pow(2.0, ((double)((octave - 4) * 12 + note)) / 12.0)));
}
template
T AmplitudeToAudioSample(const TAmplitude& in)
{
const T c_min = std::numeric_limits::min();
const T c_max = std::numeric_limits::max();
const float c_minFloat = (float)c_min;
const float c_maxFloat = (float)c_max;
float ret = in.Value() * c_maxFloat;
if (ret < c_minFloat)
return c_min;
if (ret > c_maxFloat)
return c_max;
return (T)ret;
}
TAmplitude GetLerpedAudioSample(const std::vector& samples, TFractionalSamples& index)
{
uint32 a = (uint32)floor(index.Value());
uint32 b = a + 1;
float fract = index.Value() - floor(index.Value());
float ampA = 0.0f;
if (a >= 0 && a < samples.size())
ampA = samples[a].Value();
float ampB = 0.0f;
if (b >= 0 && b < samples.size())
ampB = samples[b].Value();
return TAmplitude(fract * ampB + (1.0f - fract) * ampA);
}
void NormalizeSamples(std::vector& samples, TAmplitude maxAmplitude)
{
if (samples.size() == 0)
return;
TAmplitude largestAbsVal = TAmplitude(abs(samples.front().Value()));
std::for_each(samples.begin() + 1, samples.end(), [&largestAbsVal](const TAmplitude &a)
{
TAmplitude absVal = TAmplitude(abs(a.Value()));
if (absVal > largestAbsVal)
largestAbsVal = absVal;
}
);
largestAbsVal /= maxAmplitude;
if (largestAbsVal <= TAmplitude(0.0f))
return;
std::for_each(samples.begin(), samples.end(), [&largestAbsVal](TAmplitude &a)
{
a /= largestAbsVal;
if (a >= TAmplitude(1.0f))
{
int ijkl = 0;
}
}
);
}
void ResampleData(std::vector& samples, int srcSampleRate, int destSampleRate)
{
if (srcSampleRate == destSampleRate)
return;
float fResampleRatio = (float)destSampleRate / (float)srcSampleRate;
int nNewDataNumSamples = (int)((float)samples.size() * fResampleRatio);
std::vector newSamples;
newSamples.resize(nNewDataNumSamples);
for (int nIndex = 0; nIndex < nNewDataNumSamples; ++nIndex)
newSamples[nIndex] = GetLerpedAudioSample(samples, TFractionalSamples((float)nIndex / fResampleRatio));
std::swap(samples, newSamples);
}
void ChangeNumChannels(std::vector& samples, int nSrcChannels, int nDestChannels)
{
if (nSrcChannels == nDestChannels ||
nSrcChannels < 1 || nSrcChannels > 2 ||
nDestChannels < 1 || nDestChannels > 2)
{
return;
}
if (nDestChannels == 2)
{
std::vector newSamples;
newSamples.resize(samples.size() * 2);
for (size_t index = 0; index < samples.size(); ++index)
{
newSamples[index * 2] = samples[index];
newSamples[index * 2 + 1] = samples[index];
}
std::swap(samples, newSamples);
}
else
{
std::vector newSamples;
newSamples.resize(samples.size() / 2);
for (size_t index = 0; index < samples.size() / 2; ++index)
newSamples[index] = samples[index * 2] + samples[index * 2 + 1];
std::swap(samples, newSamples);
}
}
float PCMToFloat(unsigned char *pPCMData, int nNumBytes)
{
switch (nNumBytes)
{
case 1:
{
uint8 data = pPCMData[0];
return (float)data / 255.0f;
}
case 2:
{
int16 data = pPCMData[1] << 8 | pPCMData[0];
return ((float)data) / ((float)0x00007fff);
}
case 3:
{
int32 data = pPCMData[2] << 16 | pPCMData[1] << 8 | pPCMData[0];
return ((float)data) / ((float)0x007fffff);
}
case 4:
{
int32 data = pPCMData[3] << 24 | pPCMData[2] << 16 | pPCMData[1] << 8 | pPCMData[0];
return ((float)data) / ((float)0x7fffffff);
}
default:
{
return 0.0f;
}
}
}
struct SMinimalWaveFileHeader
{
unsigned char m_szChunkID[4]; //0
uint32 m_nChunkSize; //4
unsigned char m_szFormat[4]; //8
unsigned char m_szSubChunk1ID[4]; //12
uint32 m_nSubChunk1Size; //16
uint16 m_nAudioFormat; //18
uint16 m_nNumChannels; //20
uint32 m_nSampleRate; //24
uint32 m_nByteRate; //28
uint16 m_nBlockAlign; //30
uint16 m_nBitsPerSample; //32
unsigned char m_szSubChunk2ID[4]; //36
uint32 m_nSubChunk2Size; //40
};
template
bool WriteWaveFile(const char *fileName, const std::vector &samples, const SSoundSettings &sound)
{
//open the file if we can
FILE *file = fopen(fileName, "w+b");
if (!file)
return false;
//calculate bits per sample and the data size
const int32 bitsPerSample = sizeof(T) * 8;
const int dataSize = samples.size() * sizeof(T);
SMinimalWaveFileHeader waveHeader;
//fill out the main chunk
memcpy(waveHeader.m_szChunkID, "RIFF", 4);
waveHeader.m_nChunkSize = dataSize + 36;
memcpy(waveHeader.m_szFormat, "WAVE", 4);
//fill out sub chunk 1 "fmt "
memcpy(waveHeader.m_szSubChunk1ID, "fmt ", 4);
waveHeader.m_nSubChunk1Size = 16;
waveHeader.m_nAudioFormat = 1;
waveHeader.m_nNumChannels = 1;
waveHeader.m_nSampleRate = sound.m_sampleRate.Value();
waveHeader.m_nByteRate = sound.m_sampleRate.Value() * 1 * bitsPerSample / 8;
waveHeader.m_nBlockAlign = 1 * bitsPerSample / 8;
waveHeader.m_nBitsPerSample = bitsPerSample;
//fill out sub chunk 2 "data"
memcpy(waveHeader.m_szSubChunk2ID, "data", 4);
waveHeader.m_nSubChunk2Size = dataSize;
//write the header
fwrite(&waveHeader, sizeof(SMinimalWaveFileHeader), 1, file);
//write the wave data itself, converting it from float to the type specified
std::vector outSamples;
outSamples.resize(samples.size());
for (size_t index = 0; index < samples.size(); ++index)
outSamples[index] = AmplitudeToAudioSample(samples[index]);
fwrite(&outSamples[0], dataSize, 1, file);
//close the file and return success
fclose(file);
return true;
}
bool ReadWaveFile(const char *fileName, std::vector& samples, int32 sampleRate)
{
//open the file if we can
FILE *File = fopen(fileName, "rb");
if (!File)
{
return false;
}
//read the main chunk ID and make sure it's "RIFF"
char buffer[5];
buffer[4] = 0;
if (fread(buffer, 4, 1, File) != 1 || strcmp(buffer, "RIFF"))
{
fclose(File);
return false;
}
//read the main chunk size
uint32 nChunkSize;
if (fread(&nChunkSize, 4, 1, File) != 1)
{
fclose(File);
return false;
}
//read the format and make sure it's "WAVE"
if (fread(buffer, 4, 1, File) != 1 || strcmp(buffer, "WAVE"))
{
fclose(File);
return false;
}
long chunkPosFmt = -1;
long chunkPosData = -1;
while (chunkPosFmt == -1 || chunkPosData == -1)
{
//read a sub chunk id and a chunk size if we can
if (fread(buffer, 4, 1, File) != 1 || fread(&nChunkSize, 4, 1, File) != 1)
{
fclose(File);
return false;
}
//if we hit a fmt
if (!strcmp(buffer, "fmt "))
{
chunkPosFmt = ftell(File) - 8;
}
//else if we hit a data
else if (!strcmp(buffer, "data"))
{
chunkPosData = ftell(File) - 8;
}
//skip to the next chunk
fseek(File, nChunkSize, SEEK_CUR);
}
//we'll use this handy struct to load in
SMinimalWaveFileHeader waveData;
//load the fmt part if we can
fseek(File, chunkPosFmt, SEEK_SET);
if (fread(&waveData.m_szSubChunk1ID, 24, 1, File) != 1)
{
fclose(File);
return false;
}
//load the data part if we can
fseek(File, chunkPosData, SEEK_SET);
if (fread(&waveData.m_szSubChunk2ID, 8, 1, File) != 1)
{
fclose(File);
return false;
}
//verify a couple things about the file data
if (waveData.m_nAudioFormat != 1 || //only pcm data
waveData.m_nNumChannels < 1 || //must have a channel
waveData.m_nNumChannels > 2 || //must not have more than 2
waveData.m_nBitsPerSample > 32 || //32 bits per sample max
waveData.m_nBitsPerSample % 8 != 0 || //must be a multiple of 8 bites
waveData.m_nBlockAlign > 8) //blocks must be 8 bytes or lower
{
fclose(File);
return false;
}
//figure out how many samples and blocks there are total in the source data
int nBytesPerBlock = waveData.m_nBlockAlign;
int nNumBlocks = waveData.m_nSubChunk2Size / nBytesPerBlock;
int nNumSourceSamples = nNumBlocks * waveData.m_nNumChannels;
//allocate space for the source samples
samples.resize(nNumSourceSamples);
//maximum size of a block is 8 bytes. 4 bytes per samples, 2 channels
unsigned char pBlockData[8];
memset(pBlockData, 0, 8);
//read in the source samples at whatever sample rate / number of channels it might be in
int nBytesPerSample = nBytesPerBlock / waveData.m_nNumChannels;
for (int nIndex = 0; nIndex < nNumSourceSamples; nIndex += waveData.m_nNumChannels)
{
//read in a block
if (fread(pBlockData, waveData.m_nBlockAlign, 1, File) != 1)
{
fclose(File);
return false;
}
//get the first sample
samples[nIndex].Value() = PCMToFloat(pBlockData, nBytesPerSample);
//get the second sample if there is one
if (waveData.m_nNumChannels == 2)
{
samples[nIndex + 1].Value() = PCMToFloat(&pBlockData[nBytesPerSample], nBytesPerSample);
}
}
//re-sample the sample rate up or down as needed
ResampleData(samples, waveData.m_nSampleRate, sampleRate);
//handle switching from mono to stereo or vice versa
ChangeNumChannels(samples, waveData.m_nNumChannels, 1);
return true;
}