bollu
learns digital audio synthesis from the blog at the bottom of the sea.
the name is a pun on Kaveri, A wav
/river goddess,
and Covariance
Sources are:
- BasicSynth
- textbook
- demofox / blog under the ocean audio synth tutorials
- Making granular audio synthesis super long using PaulStretch
- Synth secrets
- NES APU
- Remake ableton music making tutorial? https://learningmusic.ableton.com/make-beats/make-beats.html
- use boss dr110 drum sound synth
- sound: ?
- Frequency (what musicians call pitch)
- time interval between samples: sample time (note, this is a DELTA quantity!)
- samples per second : frequency / sample rate.
- sample rate determines what frequencies we can produce!
- To represent a perodic signal, we need at min, one value above,
one value below zero. So we need at least two samples per cycle.
Thus, if I can produce samples at a rate of
fs
, I can produce a periodic sinusoidal waveform at a rate offmax = fs/2
, to have values above and below zero. This is the Nyquist shannon theorem. - Upper limit for sound: 20,000 Hz. So we use sampling frequency > 40,000 Hz.
- Standard: 44,100 Hz.
- Number of samples generated for a given sound: duration x sample rate.
- Sample number: time in seconds x number of samples per second;
S = T fs
. Intuitively, discretize time in sample space. - For sample number bit-size, we estimate that at a rate of 44,100 Hz, a 32 bit
number can represent
(1 << 31)/44100
sample numbers ~= 13.5 hours. Plenty for any song making. - For amplitude bit-size, we want to take into account dynamic range of hearing.
We can't detect much difference across the range of hearing.
So we use decibels, which are logarithmic. 1db = smallest change in amplitude
humans can hear.
AdB = 20 log_10 (A1/A0)
. Each new bit we use will give us20 log_10(2/1) ~= 6 dB
. 16 bit value can hold16x6 = 96 dB
. Music has a dynamic range of around100 dB
, so 16-bit works well for us.
The range of hearing is quite large, a ratio of about 10^12 to 1. [Sid: I don't understand what this means].
- PhsAccum: phase
- FrqValue: frequency
- AmpValue: amplitude
- SampleRate:
44100
. - SampleScale: convert AmpValue to output range:
(1<<15) - 1 = 32767
.
WAVE
file is aRIFF
file (resource interchange file format).- nested chunks of data.
- chunk header:
8
byte [ charID:4
byte / 4 chars; chunksize:4
byte / integer-little-endian] - We consider
WAVE
to be a fixed length header, followed by variable length data. (1) Write the header. (2) Write the samples. Each sample is one 16 bit word per channel. - For stereo output, two samples are written interleaved: [left channel; right channel]
-
To generate a signal we need to know (1) number of samples in one period and (2) phase increment for one sample time / phase velocity
φ
. -
Solving for (1): Recall that Period/
T
: time taken for waveform to repeat. Reciprocal of frequency;T=1/f
. Multiplying sample ratefs
with time of one period1/f
gives us number of samples per periodfs/f
. -
Solving for (2): The total phase is
2pi
. So the phase increment per sample time / phase velocity isφ = 2pi/(fs/f)
. -
The value of the nth sample
s[n]
is equal to the peak volume at samplen
An
(ie, the radius of the circle at that sample), times the sin of the phase angle at sample n,θ[n]
.s[n] = A[n] sin(θ[n])
. The nth phase angleθ[n]
is the phase increment per sample / phase velocity times the number of samples:θ[n] = φ * n
-
In total, this gives us
s[n] = A[n] θ[n] = A[n] sin(φ * n) = A[n] sin (2π/fs * f * n)
. Implemented as:
frqRad = twoPI / sampleRate;
totalSamples = duration * sampleRate;
for (n = 0; n < totalSamples; n++) {
sample[n] = volume * sin(frqRad*frequency*n);
}
This can be optimised into:
frqRad = twoPI / sampleRate;
phaseIncr = frqRad * frequency;
phase = 0; volume = 1;
totalSamples = duration * sampleRate;
for (n = 0; n < totalSamples; n++) {
sample[n] = volume * sin(phase);
phase += phaseIncr; // keep track of phase
if (phase >= twoPI) { phase -= twoPI; }
}
- The duration value is specified in seconds and must be multiplied by the sample rate to determine the number of samples.
- Musicians express frequency (Hz) as a pitch or possibly as a number representing
a key on a keyboard. Frequency can be calculated directly from the pitch or
key number. Each half-step between notes in the musical scale represents a
multiplication by the frequency ratio of
2^(1/12) (~1.059)
(a) 12 because there are 12 notes:A...G
, (b) base2
because logarithm?. Given a known pitch of frequencyf0
and the number of half-steps between pitchesh
, the frequency of any pitch is (f0·2^(h/12)
). - Pitch is thus an index into a frequency table.
Include/SynthDefs.h
Include/GenWave.h
Src/Common/Global.cpp
- ASR: attack sustain release. "release" is from analog synths where the envelope generator was triggered when a key on the keeb was pressed.
phaseIncr = frqRad * frequency;
phase = 0;
totalSamples = duration * sampleRate;
attackTime = attackRate * sampleRate;
decayTime = decayRate * sampleRate;
decayStart = totalSamples – decayTime;
if (attackTime > 0) {
envInc = peakAmp / attackTime; // derivative
}
volume = 0;
for (n = 0; n < totalSamples; n++) {
if (n < attackTime || n > decayStart) {
volume += envInc;
} else if (n == attackTime) {
// get rid of roundoff err
volume = peakAmp;
} else if (n == decayStart) {
envInc = –volume / decayTime;
}
sample[n] = volume * sin(phase);
if ((phase += phaseIncr) >= twoPI)
phase -= twoPI;
}
-
A linear change in amplitude does NOT produce a linear change in perceived loudness.
-
In order to double the loudness of a sound, we need to double the power of the sound, and that requires an exponential increase in amplitude(!)
-
ADSR envelope: attack, decay, sustain, release.
- In the case of a square wave there are little “horns” where the transition from peak to peak overshoots a little and then settles back. These effects are the result of summing a finite series of partials rather than the infinite series of partials specified by the Fourier transform. The ripple and overshoot is called the Gibbs Phenomenon after the engineer who identified the cause.
- Eliminate the ripple by (1) using a large number of partials, and (2)
multiply the amplitude of all partials above the fundamental with a small
value called the Lanczos sigma:
σ = sin(x)/x
, wherex = nπ/M
, whereM
is the total number of partials andn
is the partial number. - FM paper:
The Synthesis of Complex Audio Spectra by Means of Frequency Modulation
. - in FM, one oscialltor varies another oscillator:
f(t) = Ac sin(ωc t + (Am sin(ωm t ))
wherec
is for carrier,m
is for modulator. - Modulator frequency is usually specified as a multiple of the carrier and identified by the term c:m ratio. In general, integer multiples produce a harmonic spectrum while non-integer multiples produce an inharmonic spectrum.
- Fixed modulator amplitude affects different carriers differently. Eg, if
modulator amplitude is
50Hz
, and carrier is100Hz
, then we get a band of50-150Hz
, ie,50%
delta from the "center". On the other hand, if carrier is a1000Hz
, then we get a band of950-1050Hz
, ie,5%
delta from the "center". - So, good way to talk about influence of modulator on carrier is in terms
of the index of modulation:
I = Δf/fm
whereΔf
is the maximum frequency deviation, andfm
is the modulator frequency. - Amplitude modulation: change amplitude by a sine wave.
- White noise: noise of all frequencies. Just generate samples using a PRNG.
- Low rumbly noise: generate noise, but hold the same sample for multiple sample times. This makes it seem rumbly (!). [bollu: alternatively, run a low-pass filter].
- Store table, linearly intepolate values that are not keyed (fractional indexes).
- Use fixed-point for table.
- Mixer: literally keep an out variable for channels (one for mono, two for stereo) and add in all contributions.
- Panning: creating an apparent angle of displacement from a position directly in front
of the listener. We can pan by setting volume of left and right. This will create
a sense of spatiality of the sound. Usually, we do this by having a "pan value"
from the left, and scaling the left volume by
panLeft
and the right volume by(1 - panLeft)
. - The linear calculation of the pan amplitude is simple and efficient but produces the well-known “hole in the middle effect" (!) (TODO), where the change in intensity causes us to perceive that the sound has moved away/closer instead of left/right.
- One way to perform a better calculation is to take a
sin
of apparent angle. The pan angle can change from-45
to+45
degrees, with0
being centered. We normalize the angle to[-1, +1]
. The amplitudes are calculated as:
leftPan = sin((1 - panSet)/2 * PI/2);
rightPan = sin((1 + panSet)/2 * PI/2);
-
Delay is represented by
z^(-1)
. -
Adding up delayed sinusoids creates a phase shift in the output!
sin(a) + sin(b) = 2sin((a+b)/2)sin((a-b)/2)
. -
Phase shift depends on sample rate and frequency of signal. If sample rate is
10kHz
, a one sample delay is0.0001
seconds of delay. For a signal of 100Hz, the time period is1/100 = 0.01
seconds. So, a delay of0.0001
seconds in a total time period of0.01
seconds represents2pi * 0.01/0.0001 = 3.6 degree
of phase shift. -
A signal at
1000Hz
has a time period of1/1000 = 0.001
seconds. So a delay of0.0001
seconds in a total time period of0.001
seconds represents a2pi * 0.001 / 0.0001 = 36 degree
phase shift. This phase shift causes a lower amplitude, and causes destructive interference at higher frequencies. So this implements a low-pass filter. For a high pass filter, we subtract the signal with its previous timestep. -
FIR: finite impulse response filter:
y[n] = ax[n] + bx[n-1]
. -
IIR: infinite impulse response:
y[n] = bx[n] + ay[n-1]
. -
Bi-quad filter:
y[n] = b0(x[n] - a1 y[n-1] - a2 y[n-2]) + b1 x[n-1] + b2 x[n-1]
. That is, it has two degrees of "feedback" and two degrees of "history". -
For low-pass FIR, calculate impulse response by convolving with windowed
sinc
. -
Window a
sinc
by multiplying with a "hamming window"/"hanning window" -
Bi-quad filter can be adapted for many tasks. For example, butterworth filter.
-
All-pass filter: filter that allows all frequencies, but shifts phase in a frequency-dependent fashion.
v[n] = x[n] - g v[n-1]
andy[n] = g v[n] + v[n-1]
. -
Dynamic filter:
-
Delay ~ FIFO.
-
Accoustics: delay line ~ echo.
-
attenuator after delay ~ loss of signal energy in the echo.
-
total time delay = length of delay line * sample time.
-
can tap from the middle of a delay line as well.
-
use ring buffer.
-
Resonator: allow signal in delay line to recirculate
out = delayBuf[delayIndex] * decay;
delayBuf[delayIndex] = in + out;
if (++delayIndex >= delayLen)
delayIndex = 0;
- Allpass delay: TODO (???)
- Variable delay: vary delay time while adding and removing samples. We keep the length of the delay buffer constant, but we move the read tap around.
delayRange = (delayMax - delayMin) / 2;
delayMid = delayMin + delayRange;
delayBuf[delayWrite] = in; // write data in
if (++delayWrite >= delayLen) { delayWrite -= delayLen; } // go back
delayOffset = delayMid + (delayRange * sin(phase));
if ((delayRead = delayWrite – delayOffset) < 0) { delayRead += delayLen; }
out = delayBuf[delayRead];
- Problem: index will move by fractional amout. This creates an artefact called zipper noise which is caused by quantization. So we need to LERP.
delayOffset = delayMid + (delayRange * sin(phase));
delayRead = delayWrite – delayOffset;
if (delayRead < 0)
delayRead += delayLen;
readInt = floor(delayRead);
readFract = delayRead – readInt;
if (delayOffset < 1)
delayBuf[delayWrite] = in;
out = delayBuf[readInt] * (1 – readFract);
if (--readInt < 0)
readInt += delayLen;
out += delayBuf[readInt] * readFract;
if (delayOffset >= 1)
delayBuf[delayWrite] = in;
if (++delayWrite >= delayLen)
delayWrite -= delayLen;
out *= decay;
-
Furthermore, room reverberation requires a buildup of reflected sounds over time. For that we need to combine multiple resonators at different time delays. When the delay lengths differ, their combination produces an increasing density of sounds and will also tend to mask the ringing effect. When the delay times are relative primes, the resonant peak of one delay line is different from the others and will attenuate the frequencies that the other resonators amplify.
-
Schroeder reverb: This reverb uses a set of parallel comb filters plus two all- pass filters in series.
-
as the amount of reverb and the reverb time increase, the metallic ringing sound becomes more noticeable. Another noticeable effect of this design is a fluttering sound that shows up when very short sounds with sharp attack transients are used.
Flanging is a technique developed in analog recording that adds a delayed signal to the original signal by running two tape recorders in parallel. The speed of each tape recorder is alternately varied by touching the flange of the tape reel, momentarily slowing down the recorder.
flange: A flange is a protruded ridge, lip or rim, either external or internal, that serves to increase strength
This frequency modulation technique, although not a physical model for natural sound, is shown to be a very powerful perceptual model for at least some.
energy is stolen from the carrier distributed amongst an increasing number of side frequencies.
amplitude is determined by
$J_n(I)$ , bessel functions of the first kind, where the input$I = d/m$ is the modulation index, ratio of peak deviation of frequency to total base frequency of the carrier. The total bandwidth is approximatiely2(d+m)
, whered
is peak deviation andm
is frequency of the modulating wave.
The special richnessof this FM technique lies in the fact that there are ratios of carrier, modulating frequencies and values of the index that produces sideband components which fall in the negative frequency domain of the spectrum (?) (what does this mean?) TODO: take a course on this stuff! The negative components reflect around 0Hz and "mix" with the components in the +ve domain.
Eg. let
c =100Hz, m = 100Hz, I = 4
.
Since we have
c/m = 1
, or more generally a rational number, we havec/m = N1/N2
whereN1
andN2
are integers, which results in harmonic spectra.
/*===================================================
Written by Alan Wolfe 5/2012
http://demofox.org
some useful links about the wave file format:
http://www.piclist.com/techref/io/serial/midi/wave.html
https://ccrma.stanford.edu/courses/422/projects/WaveFormat/
Note: This source code assumes that you are on a little endian machine.
===================================================*/
#include
#include
#include
#define _USE_MATH_DEFINES
#include
//define our types. If your environment varies, you can change these types to be what they should be
typedef int int32;
typedef unsigned int uint32;
typedef short int16;
typedef unsigned short uint16;
typedef signed char int8;
typedef unsigned char uint8;
//some macros
#define CLAMP(value,min,max) {if(value max) { value = max; }}
//this struct is the minimal required header data for a wav file
struct SMinimalWaveFileHeader
{
//the main chunk
unsigned char m_szChunkID[4];
uint32 m_nChunkSize;
unsigned char m_szFormat[4];
//sub chunk 1 "fmt "
unsigned char m_szSubChunk1ID[4];
uint32 m_nSubChunk1Size;
uint16 m_nAudioFormat;
uint16 m_nNumChannels;
uint32 m_nSampleRate;
uint32 m_nByteRate;
uint16 m_nBlockAlign;
uint16 m_nBitsPerSample;
//sub chunk 2 "data"
unsigned char m_szSubChunk2ID[4];
uint32 m_nSubChunk2Size;
//then comes the data!
};
//0 to 255
void ConvertFloatToAudioSample(float fFloat, uint8 &nOut)
{
fFloat = (fFloat + 1.0f) * 127.5f;
CLAMP(fFloat,0.0f,255.0f);
nOut = (uint8)fFloat;
}
//–32,768 to 32,767
void ConvertFloatToAudioSample(float fFloat, int16 &nOut)
{
fFloat *= 32767.0f;
CLAMP(fFloat,-32768.0f,32767.0f);
nOut = (int16)fFloat;
}
//–2,147,483,648 to 2,147,483,647
void ConvertFloatToAudioSample(float fFloat, int32 &nOut)
{
double dDouble = (double)fFloat;
dDouble *= 2147483647.0;
CLAMP(dDouble,-2147483648.0,2147483647.0);
nOut = (int32)dDouble;
}
//calculate the frequency of the specified note.
//fractional notes allowed!
float CalcFrequency(float fOctave,float fNote)
/*
Calculate the frequency of any note!
frequency = 440×(2^(n/12))
N=0 is A4
N=1 is A#4
etc...
notes go like so...
0 = A
1 = A#
2 = B
3 = C
4 = C#
5 = D
6 = D#
7 = E
8 = F
9 = F#
10 = G
11 = G#
*/
{
return (float)(440*pow(2.0,((double)((fOctave-4)*12+fNote))/12.0));
}
//this writes a wave file
//specify sample bit count as the template parameter
//can be uint8, int16 or int32
template
bool WriteWaveFile(const char *szFileName, float *pRawData, int32 nNumSamples, int16 nNumChannels, int32 nSampleRate)
{
//open the file if we can
FILE *File = fopen(szFileName,"w+b");
if(!File)
{
return false;
}
//calculate bits per sample and the data size
int32 nBitsPerSample = sizeof(T) * 8;
int nDataSize = nNumSamples * sizeof(T);
SMinimalWaveFileHeader waveHeader;
//fill out the main chunk
memcpy(waveHeader.m_szChunkID,"RIFF",4);
waveHeader.m_nChunkSize = nDataSize + 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 = nNumChannels;
waveHeader.m_nSampleRate = nSampleRate;
waveHeader.m_nByteRate = nSampleRate * nNumChannels * nBitsPerSample / 8;
waveHeader.m_nBlockAlign = nNumChannels * nBitsPerSample / 8;
waveHeader.m_nBitsPerSample = nBitsPerSample;
//fill out sub chunk 2 "data"
memcpy(waveHeader.m_szSubChunk2ID,"data",4);
waveHeader.m_nSubChunk2Size = nDataSize;
//write the header
fwrite(&waveHeader,sizeof(SMinimalWaveFileHeader),1,File);
//write the wave data itself, converting it from float to the type specified
T *pData = new T[nNumSamples];
for(int nIndex = 0; nIndex = 2 * (float)M_PI)
fPhase -= 2 * (float)M_PI;
while(fPhase 1.0f)
fPhase -= 1.0f;
while(fPhase 1.0f)
fPhase -= 1.0f;
while(fPhase < 0.0f)
fPhase += 1.0f;
if(fPhase 1.0f)
fPhase -= 1.0f;
while(fPhase < 0.0f)
fPhase += 1.0f;
float fRet;
if(fPhase 2.0f)
fPhase -= 1.0f;
if(nSeed != nLastSeed)
{
float fValue = ((float)rand()) / ((float)RAND_MAX);
fValue = (fValue * 2.0f) - 1.0f;
//uncomment the below to make it slightly more intense
/*
if(fValue < 0)
fValue = -1.0f;
else
fValue = 1.0f;
*/
return fValue;
}
else
{
return fLastValue;
}
}
//the entry point of our application
int main(int argc, char **argv)
{
//our parameters that all the wave forms use
int nSampleRate = 44100;
int nNumSeconds = 4;
int nNumChannels = 1;
float fFrequency = CalcFrequency(3,3); // middle C
//make our buffer to hold the samples
int nNumSamples = nSampleRate * nNumChannels * nNumSeconds;
float *pData = new float[nNumSamples];
//the phase of our oscilator, we don't really need to reset it between wave files
//it just needs to stay continuous within a wave file
float fPhase = 0;
//make a naive sine wave
for(int nIndex = 0; nIndex < nNumSamples; ++nIndex)
{
pData[nIndex] = sin((float)nIndex * 2 * (float)M_PI * fFrequency / (float)nSampleRate);
}
WriteWaveFile("sinenaive.wav",pData,nNumSamples,nNumChannels,nSampleRate);
//make a discontinuitous (popping) sine wave
for(int nIndex = 0; nIndex < nNumSamples; ++nIndex)
{
if(nIndex < nNumSamples / 2)
{
float fCurrentFrequency = CalcFrequency(3,3);
pData[nIndex] = sin((float)nIndex * 2 * (float)M_PI * fCurrentFrequency / (float)nSampleRate);
}
else
{
float fCurrentFrequency = CalcFrequency(3,4);
pData[nIndex] = sin((float)nIndex * 2 * (float)M_PI * fCurrentFrequency / (float)nSampleRate);
}
}
WriteWaveFile("sinediscon.wav",pData,nNumSamples,nNumChannels,nSampleRate);
//make a continuous sine wave that changes frequencies
for(int nIndex = 0; nIndex < nNumSamples; ++nIndex)
{
if(nIndex = 2 * (float)M_PI)
fPhase -= 2 * (float)M_PI;
while(fPhase = 2 * (float)M_PI)
fPhase -= 2 * (float)M_PI;
while(fPhase < 0)
fPhase += 2 * (float)M_PI;
pData[nIndex] = sin(fPhase);
}
}
WriteWaveFile("sinecon.wav",pData,nNumSamples,nNumChannels,nSampleRate);
//make a sine wave
for(int nIndex = 0; nIndex < nNumSamples; ++nIndex)
{
pData[nIndex] = AdvanceOscilator_Sine(fPhase,fFrequency,(float)nSampleRate);
}
WriteWaveFile("sine.wav",pData,nNumSamples,nNumChannels,nSampleRate);
//make a quieter sine wave
for(int nIndex = 0; nIndex < nNumSamples; ++nIndex)
{
pData[nIndex] = AdvanceOscilator_Sine(fPhase,fFrequency,(float)nSampleRate) * 0.4f;
}
WriteWaveFile("sinequiet.wav",pData,nNumSamples,nNumChannels,nSampleRate);
//make a clipping sine wave
for(int nIndex = 0; nIndex < nNumSamples; ++nIndex)
{
pData[nIndex] = AdvanceOscilator_Sine(fPhase,fFrequency,(float)nSampleRate) * 1.4f;
}
WriteWaveFile("sineclip.wav",pData,nNumSamples,nNumChannels,nSampleRate);
//make a saw wave
for(int nIndex = 0; nIndex < nNumSamples; ++nIndex)
{
pData[nIndex] = AdvanceOscilator_Saw(fPhase,fFrequency,(float)nSampleRate);
}
WriteWaveFile("saw.wav",pData,nNumSamples,nNumChannels,nSampleRate);
//make a square wave
for(int nIndex = 0; nIndex < nNumSamples; ++nIndex)
{
pData[nIndex] = AdvanceOscilator_Square(fPhase,fFrequency,(float)nSampleRate);
}
WriteWaveFile("square.wav",pData,nNumSamples,nNumChannels,nSampleRate);
//make a triangle wave
for(int nIndex = 0; nIndex < nNumSamples; ++nIndex)
{
pData[nIndex] = AdvanceOscilator_Triangle(fPhase,fFrequency,(float)nSampleRate);
}
WriteWaveFile("triangle.wav",pData,nNumSamples,nNumChannels,nSampleRate);
//make some noise or... make... some... NOISE!!!
for(int nIndex = 0; nIndex 0 ? pData[nIndex-1] : 0.0f);
}
WriteWaveFile("noise.wav",pData,nNumSamples,nNumChannels,nSampleRate);
//make a dumb little song
for(int nIndex = 0; nIndex < nNumSamples; ++nIndex)
{
//calculate which quarter note we are on
int nQuarterNote = nIndex * 4 / nSampleRate;
float fQuarterNotePercent = (float)((nIndex * 4) % nSampleRate) / (float)nSampleRate;
//intentionally add a "pop" noise mid way through the 3rd quarter note
if(nIndex == nSampleRate * 3 / 4 + nSampleRate / 8)
{
pData[nIndex] = -1.0f;
continue;
}
//do different logic based on which quarter note we are on
switch(nQuarterNote)
{
case 0:
{
pData[nIndex] = AdvanceOscilator_Sine(fPhase,CalcFrequency(4,0),(float)nSampleRate);
break;
}
case 1:
{
pData[nIndex] = AdvanceOscilator_Sine(fPhase,CalcFrequency(4,2),(float)nSampleRate);
break;
}
case 2:
case 3:
{
pData[nIndex] = AdvanceOscilator_Sine(fPhase,CalcFrequency(4,5),(float)nSampleRate);
break;
}
case 4:
{
pData[nIndex] = AdvanceOscilator_Sine(fPhase,CalcFrequency(4,5 - fQuarterNotePercent * 2.0f),(float)nSampleRate);
break;
}
case 5:
{
pData[nIndex] = AdvanceOscilator_Sine(fPhase,CalcFrequency(4,3 + fQuarterNotePercent * 2.0f),(float)nSampleRate);
break;
}
case 6:
{
pData[nIndex] = AdvanceOscilator_Sine(fPhase,CalcFrequency(4,5 - fQuarterNotePercent * 2.0f),(float)nSampleRate) * (1.0f - fQuarterNotePercent);
break;
}
case 8:
{
pData[nIndex] = AdvanceOscilator_Saw(fPhase,CalcFrequency(4,0),(float)nSampleRate);
break;
}
case 9:
{
pData[nIndex] = AdvanceOscilator_Saw(fPhase,CalcFrequency(4,2),(float)nSampleRate);
break;
}
case 10:
case 11:
{
pData[nIndex] = AdvanceOscilator_Saw(fPhase,CalcFrequency(4,5),(float)nSampleRate);
break;
}
case 12:
{
pData[nIndex] = AdvanceOscilator_Saw(fPhase,CalcFrequency(4,5 - fQuarterNotePercent * 2.0f),(float)nSampleRate);
break;
}
case 13:
{
pData[nIndex] = AdvanceOscilator_Saw(fPhase,CalcFrequency(4,3 + fQuarterNotePercent * 2.0f),(float)nSampleRate);
break;
}
case 14:
{
pData[nIndex] = AdvanceOscilator_Saw(fPhase,CalcFrequency(4,5 - fQuarterNotePercent * 2.0f),(float)nSampleRate) * (1.0f - fQuarterNotePercent);
break;
}
default:
{
pData[nIndex] = 0;
break;
}
}
}
WriteWaveFile("song.wav",pData,nNumSamples,nNumChannels,nSampleRate);
//free our data buffer
delete[] pData;
}
#include <stdio.h>
#include <memory.h>
#include <inttypes.h>
#include <vector>
// constants
const float c_pi = 3.14159265359f;
const float c_twoPi = 2.0f * c_pi;
// typedefs
typedef uint16_t uint16;
typedef uint32_t uint32;
typedef int16_t int16;
typedef int32_t int32;
//this struct is the minimal required header data for a wav file
struct SMinimalWaveFileHeader
{
//the main chunk
unsigned char m_chunkID[4];
uint32 m_chunkSize;
unsigned char m_format[4];
//sub chunk 1 "fmt "
unsigned char m_subChunk1ID[4];
uint32 m_subChunk1Size;
uint16 m_audioFormat;
uint16 m_numChannels;
uint32 m_sampleRate;
uint32 m_byteRate;
uint16 m_blockAlign;
uint16 m_bitsPerSample;
//sub chunk 2 "data"
unsigned char m_subChunk2ID[4];
uint32 m_subChunk2Size;
//then comes the data!
};
//this writes
template <typename T>
bool WriteWaveFile(const char *fileName, std::vector<T> data, int16 numChannels, int32 sampleRate)
{
int32 dataSize = data.size() * sizeof(T);
int32 bitsPerSample = sizeof(T) * 8;
//open the file if we can
FILE *File = nullptr;
fopen_s(&File, fileName, "w+b");
if (!File)
return false;
SMinimalWaveFileHeader waveHeader;
//fill out the main chunk
memcpy(waveHeader.m_chunkID, "RIFF", 4);
waveHeader.m_chunkSize = dataSize + 36;
memcpy(waveHeader.m_format, "WAVE", 4);
//fill out sub chunk 1 "fmt "
memcpy(waveHeader.m_subChunk1ID, "fmt ", 4);
waveHeader.m_subChunk1Size = 16;
waveHeader.m_audioFormat = 1;
waveHeader.m_numChannels = numChannels;
waveHeader.m_sampleRate = sampleRate;
waveHeader.m_byteRate = sampleRate * numChannels * bitsPerSample / 8;
waveHeader.m_blockAlign = numChannels * bitsPerSample / 8;
waveHeader.m_bitsPerSample = bitsPerSample;
//fill out sub chunk 2 "data"
memcpy(waveHeader.m_subChunk2ID, "data", 4);
waveHeader.m_subChunk2Size = dataSize;
//write the header
fwrite(&waveHeader, sizeof(SMinimalWaveFileHeader), 1, File);
//write the wave data itself
fwrite(&data[0], dataSize, 1, File);
//close the file and return success
fclose(File);
return true;
}
template <typename T>
void ConvertFloatSamples (const std::vector<float>& in, std::vector<T>& out)
{
// make our out samples the right size
out.resize(in.size());
// convert in format to out format !
for (size_t i = 0, c = in.size(); i < c; ++i)
{
float v = in[i];
if (v < 0.0f)
v *= -float(std::numeric_limits<T>::lowest());
else
v *= float(std::numeric_limits<T>::max());
out[i] = T(v);
}
}
//calculate the frequency of the specified note.
//fractional notes allowed!
float CalcFrequency(float octave, float note)
/*
Calculate the frequency of any note!
frequency = 440×(2^(n/12))
N=0 is A4
N=1 is A#4
etc...
notes go like so...
0 = A
1 = A#
2 = B
3 = C
4 = C#
5 = D
6 = D#
7 = E
8 = F
9 = F#
10 = G
11 = G#
*/
{
return (float)(440 * pow(2.0, ((double)((octave - 4) * 12 + note)) / 12.0));
}
class CKarplusStrongStringPluck
{
public:
CKarplusStrongStringPluck (float frequency, float sampleRate, float feedback)
{
m_buffer.resize(uint32(float(sampleRate) / frequency));
for (size_t i = 0, c = m_buffer.size(); i < c; ++i) {
m_buffer[i] = ((float)rand()) / ((float)RAND_MAX) * 2.0f - 1.0f; // noise
//m_buffer[i] = float(i) / float(c); // saw wave
}
m_index = 0;
m_feedback = feedback;
}
float GenerateSample ()
{
// get our sample to return
float ret = m_buffer[m_index];
// low pass filter (average) some samples
float value = (m_buffer[m_index] + m_buffer[(m_index + 1) % m_buffer.size()]) * 0.5f * m_feedback;
m_buffer[m_index] = value;
// move to the next sample
m_index = (m_index + 1) % m_buffer.size();
// return the sample from the buffer
return ret;
}
private:
std::vector<float> m_buffer;
size_t m_index;
float m_feedback;
};
void GenerateSamples (std::vector<float>& samples, int sampleRate)
{
std::vector<CKarplusStrongStringPluck> notes;
enum ESongMode {
e_twinkleTwinkle,
e_strum
};
int timeBegin = 0;
ESongMode mode = e_twinkleTwinkle;
for (int index = 0, numSamples = samples.size(); index < numSamples; ++index)
{
switch (mode) {
case e_twinkleTwinkle: {
const int c_noteTime = sampleRate / 2;
int time = index - timeBegin;
// if we should start a new note
if (time % c_noteTime == 0) {
int note = time / c_noteTime;
switch (note) {
case 0:
case 1: {
notes.push_back(CKarplusStrongStringPluck(CalcFrequency(3, 0), float(sampleRate), 0.996f));
break;
}
case 2:
case 3: {
notes.push_back(CKarplusStrongStringPluck(CalcFrequency(3, 7), float(sampleRate), 0.996f));
break;
}
case 4:
case 5: {
notes.push_back(CKarplusStrongStringPluck(CalcFrequency(3, 9), float(sampleRate), 0.996f));
break;
}
case 6: {
notes.push_back(CKarplusStrongStringPluck(CalcFrequency(3, 7), float(sampleRate), 0.996f));
break;
}
case 7: {
mode = e_strum;
timeBegin = index+1;
break;
}
}
}
break;
}
case e_strum: {
const int c_noteTime = sampleRate / 32;
int time = index - timeBegin - sampleRate;
// if we should start a new note
if (time % c_noteTime == 0) {
int note = time / c_noteTime;
switch (note) {
case 0: notes.push_back(CKarplusStrongStringPluck(55.0f, float(sampleRate), 0.996f)); break;
case 1: notes.push_back(CKarplusStrongStringPluck(55.0f + 110.0f, float(sampleRate), 0.996f)); break;
case 2: notes.push_back(CKarplusStrongStringPluck(55.0f + 220.0f, float(sampleRate), 0.996f)); break;
case 3: notes.push_back(CKarplusStrongStringPluck(55.0f + 330.0f, float(sampleRate), 0.996f)); break;
case 4: mode = e_strum; timeBegin = index + 1; break;
}
}
break;
}
}
// generate and mix our samples from our notes
samples[index] = 0;
for (CKarplusStrongStringPluck& note : notes)
samples[index] += note.GenerateSample();
// to keep from clipping
samples[index] *= 0.5f;
}
}
//the entry point of our application
int main(int argc, char **argv)
{
// sound format parameters
const int c_sampleRate = 44100;
const int c_numSeconds = 9;
const int c_numChannels = 1;
const int c_numSamples = c_sampleRate * c_numChannels * c_numSeconds;
// make space for our samples
std::vector<float> samples;
samples.resize(c_numSamples);
// generate samples
GenerateSamples(samples, c_sampleRate);
// convert from float to the final format
std::vector<int32> samplesInt;
ConvertFloatSamples(samples, samplesInt);
// write our samples to a wave file
WriteWaveFile("out.wav", samplesInt, c_numChannels, c_sampleRate);
}
- rainmood sounds! So, lightning crack, rumbling.
- fireplace.
- coffee shop clinking?