CS 312 - Week 7 Class 2
CS 312 Audio Programming Winter 2020
Table of Contents
ANNOUNCEMENTS
I spent Thursday morning in CMC lab with Mike Tie to resolve the libsndfile link issues with hw72. It's fixed and now working in CMC. I've greatly simplified the steps to install libsndfile and revised the web page.
A new version of Qt has been pushed to WCC along with the same libsndfile changes. WCC is in use until 5:00pm today and I have not been able to test yet. I am pretty confident that the hw72 stuff in WCC will work as described below.
Important for WCC
You must login once, then logout, then login again to have the new changes pushed to your computer.
Follow the libsndfile steps described below. Once the new common/libsndfile folder is in place the changes should only affect the CMakeLists.txt file and not any code you've already written. You will have to replace the hw721 CMakeLists.txt file. All other hw72 CMakeLists.txt files should only require a set(PROJECT_NAME hw721_gensin) change.
7.2 Class
Our coverage of digital audio and digital synthesis begins today. All hw72 homework will be done in VScode terminal. We'll get back to using Qt in hw73.
We'll begin by installing the open source libsndfile library that enables reading and writing digital audio in many formats. We'll use the .wav file format. Why .wav and not .mp3? In short, wav is a lossless format while mp3 is a lossy format. MP3 files have become the common method of delivery over the internet because of their small size. MP3 files can be as much as one tenth the size of the WAV file. However that comes at a loss of quality.
https://www.dawsons.co.uk/blog/how-do-mp3-and-wav-files-differ
libsndfile
Click to download this file. It contains two files: sndfile.h and sndfile.hh. The C version uses .h, the C++ version uses .hh.
libsndfile_files_for_common_folder.zip
Important
If you already have a common/libsndfile folder delete it.
Then execute these commands.
# DOWNLOAD libsndfile above first cd $HOME312/common mkdir libsndfile
Unzip the file you downloaded and copy these two files into your common/libsndfile folder.
- sndfile.h
- sndfile.hh
Audacity sound editor
Audacity is a free open source audio editor with many tools and effects for modifying/analyzing sound files.
Its installed in both labs. If you want to use it on your personal computer the link is:
https://www.audacityteam.org/download/
Open Audacity
We're going to create two sine wave tones at a frequency of 440Hz, the standard orchestra reference pitch.
Choose Tone from the Generate menu.
Make these settings. The duration is 250 milliseconds.
The waveform is displayed.
Choose Export/Export as WAV from the File menu.
Save it in your $HOME312/cs312 folder using the name shown below.
Close that track by clicking the X in the upper right corner of the track.
Create a new sine tone at amplitude 0.5.
Export and save it in your $HOME312/cs312 folder using the name shown below.
You can see the amplitude scale is at the 0.5 level.
Close this track.
Open hw721_a440sine_amp1.wav from the Audacity File menu.
Audacity Waveform Tools
- 1 Zoom in
- 2 Zoom out
- 3 Zoom in to selection
- 4 Fit entire waveform in window
- 5 Fit entire waveform in left quarter of window
Audacity Play/Edit Tools
Use the Zoom In tool to view a couple periods of the sine wave.
Zoom in further until you can see the samples for one complete period.
Use the Select too to select one period of samples.
You should see 100/101 samples depending on the start and end points of your selection.
Sample rate and bit depth
You can see on the left in the above picture some information about the file.
- It's mono
- The sample rate was 44,100 Hz
- The bit depth was 32 bit floating point.
That means every second of sound contains 44,100 samples. Each sample has an amplitude range from -1.0 to +1.0 in 32 bit floating point values. The 44100 Hz sample rate was the used as the audio CD standard. For stereo recording you need 88,200 samples per second. Today's recording equipment can use sample rates of 48000 Hz, 96000 Hz, up to 192000 Hz.
It's an amazing process when you think of electronics behind the recording process where air pressure acts on the diaphragm of a microphone creating minute variations in movement that are amplified and turned into one second of digital audio.
Read this for more information.
DigitalAudioFundamentals.pdf
Period and Frequency
The period T (time) is the duration of one complete cycle of the waveform measured in seconds. The frequency F (cycles per second) is measured in Hertz (Hz). 1Hz is one cycle per second. The formula relating the two is shown below.
- nSamp is the number of samples.
- FS is the sample rate or Frequency of Samples, 44100 Hz
\[T{\rm{ = }}nSamp{\rm{*}}\frac{1}{{FS}}\]
One period of this sine wave is 100 samples.
\[T = \frac{{100}}{{FS}} = {\rm{0.0022676 sec}}\]
Calculate the frequency
The frequency of for one period of N samples at a sample rate of FS can be calculated using this formula.
\[f = \frac{{FS}}{N} = \frac{{44100}}{{100}} = 441Hz\]
Using an integer number of samples at a fixed sample rate we can only estimate frequency. 100 samples is too high for 440Hz and 101 samples is too low.
\[f = \frac{{44100}}{{100}} = 441Hz\]
\[f = \frac{{44100}}{{101}} = 436.63Hz\]
The frequency of this waveform actually is 440Hz which means its period would have to be 100.23 samples.
\[\frac{{44100}}{{400}}{\rm{ = 100.23}}\]
A common method of computing frequency requires fractional samples and uses a technique known as phase increment. It's based on the idea that a sine wave is equivalent to a phasor rotating counterclockwise around the origin of the unit circle. That's the subject of hw722.
Read this for more information.
Phasors.pdf
Time domain and frequency domain
A time series of waveform samples can be be viewed in two equivalent domains. The mathematical operation that is used to translate between the time and frequency domains is known as the Discrete Fourier Transform. The time domain view is commonly known as a time series. The frequency domain view is known as a spectrum.
- Time domain
- Amplitude vs. time
- Frequency domain
- Magnitude vs. frequency
Nyquist theorem
A fundamental theorem of Digital Signal Processing (DSP) known as the Nyquist Theorem or Sampling Theorem states that no information is lost between the time and frequency domains as long as the sample rate is at least twice the frequency of the highest frequency found in the signal. The sampling rate of 44,100Hz was chosen in part because the upper range of human hearing is around 20,000Hz and decreases with age.
Nyquist Rate
The Nyquist rate or Nyquist limit is defined as half the sample rate. All digital recording hardware pre-filters the signal to ensure no frequecies higher than the Nyquist Rate are passed to the analog to digital converter (ADC).
Frequency spectrum
Audacity uses the Fast Fourier Transform algorithm (FFT) to plot the spectrum of a sampled waveform. The FFT is a computationally fast implementation of the mathematical Discrete Fourier Transform (DFT).
View the hw721_a440sine_amp1.wav spectrum
Assuming the the hw721_a440sine_amp1.wav window is open, select the entire waveform using Command-click in the left panel shown by the red rectangle.
Choose Plot Spectrum from the Analyze menu.
Position the cursor on the highest peak and read the Peak frequency and amplitude. You may have to modify your settings to those shown in the picture below.
The frequency is 440 Hz and the amplitude is 0.1dB (decibels).
Open the hw721_a440sine_amp1half.wav spectrum
Follow the same steps as above.
- Select the entire waveform using Command-click
- Choose Plot Spectrum from the Analyze menu.
- Position the cursor on the highest peak and read the Peak frequency 440 Hz (A4).
The frequency is 440Hz as before but the amplitude is -6.1dB.
Decibels and amplitude
A decibel is a base 10 logarithmic ratio between two numbers. Because the two numbers in the ratio have the same units the ratio is dimensionless. In digital audio 0dB is the loudest sound that avoids clipping. All other audio dB values are negative.
The decibel is defined as
\[dB = 20\log \left( {{V \over {{V_{ref}}}}} \right)\]
In digital audio the number 1.0 is chosen as Vref. It's the maximum floating point sample value for amplitude. I used V in the formula because decibels originally referred to the ratio of voltages. In our two example wave files one is at full amplitude and the other at half amplitude. Their decibel ratio of one half is -6.02dB. The B in dB is capitalized for Alexander Graham Bell similar to the H in Hz for Heinrich Hertz.
\[20{\log _{10}}(0.5) = - 6.02\]
A 6dB drop is equivalent to halving the amplitude. That's why hw712_a440sine_half_amp is -6.1dB lower than hw712_a440sine_amp1.
You can use this popup menu in Audacity to view the waveforms on a decibel scale.
Start on homework.
Reading/Viewing
After class
Read and watch these items.
wav or mp3
https://www.dawsons.co.uk/blog/how-do-mp3-and-wav-files-differ
PDF
SixPropertiesOfSound.pdf
Digital Audio Fundamentals
Phasors
Video
You can end at 38:30. Rest of the video is optional.
https://www.youtube.com/watch?v=jNSiZqSQis4
Additional References
This is not required reading but there's more in depth information here if you want to pursue FM synthesis on your own.
Chowning: FM Theory & Applications By Musicians for Musicians.pdf
Also google "fm synthesis algorithm".
Homework 7.2
All homework for 7.2 is done in vsCode.
hw721_gensin
Generate a sine wave with user parameters for frequency, amplitude, and duration.
Setup
cd $HOME312/cs312 mkdir hw72 mkdir hw72/ cd hw72/hw721_gensin mkdir build touch CMakeLists.txt touch hw721_gensin.cpp code .
Copy this code into CMakeLists.txt
cmake_minimum_required(VERSION 3.5) set(PROJECT_NAME hw721_gensin) project(${PROJECT_NAME}) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -Wall") # CHANGE THE PATH TO YOUR COMMON FOLDER set(COMMON "<your_full_pathname_to_course_common_folder>") set(LIBSF "${COMMON}/libsndfile") set(ULL "/usr/local/lib") add_executable( ${PROJECT_NAME} ${LIBSF}/sndfile.hh ${PROJECT_NAME}.cpp ) target_include_directories(${PROJECT_NAME} PRIVATE ${COMMON} ${LIBSF}) target_link_libraries(${PROJECT_NAME} ${ULL}/libsndfile.1.dylib)
Copy this code into hw721_gensin.cpp
#include "sndfile.hh" #include <iostream> #include <vector> #include <cmath> // for M_PI // either_or typedef float MY_TYPE; #define LIB_SF_FORMAT SF_FORMAT_FLOAT; // SF_FORMAT_MY_TYPE defiend in "sndfile.h" // typedef double MY_TYPE; // #define LIB_SF_FORMAT SF_FORMAT_DOUBLE; // SF_FORMAT_MY_TYPE defiend in "sndfile.h" // END either_or const int FS = 44100; // CD sample rate const MY_TYPE T = 1.0 / FS; // sample period const MY_TYPE k2PI = 2 * M_PI; const MY_TYPE k2PIT = k2PI * T; std::vector<MY_TYPE> gen_sin(MY_TYPE freq, MY_TYPE ampl, MY_TYPE secs) { std::vector<MY_TYPE> v; std::cout << "You need to implement gen_sin(...)\n"; /*---------------------------------------------------- // FORMULA: samp(n) = A * sin( 2 * pi * freq * n * T); // calculute 2*pi*T outside of the for loop for efficiency // FORMULA: samp(n) = A * sin( k2PIT* freq * n); ----------------------------------------------------*/ return v; } bool samples2wavfile(const char *fname, const std::vector<MY_TYPE> &v) { // code borrowed and modified from libsndfile example sndfilehandle.cc // mostly from create_file(...) and format from main() SndfileHandle file; const int channels = 1; const int srate = FS; const int format = SF_FORMAT_WAV | LIB_SF_FORMAT; file = SndfileHandle(fname, SFM_WRITE, format, channels, srate); // using vector instead of buffer array // C++ vector elements are stored in contiguous memory, same as array // &v[0] is pointer to first element of vector file.write(&v[0], v.size()); return true; } int main(int argc, char *argv[]) { // generate a 440 Hz, full 1.0 amplitude sine wave for one second std::vector<MY_TYPE> vsamps = gen_sin(440.0, 1.0, 1.0); samples2wavfile("hw721_a440sine_amp1.wav", vsamps); // generate a 440 Hz, half 0.5 amplitude sine wave for one second vsamps.clear(); vsamps = gen_sin(440.0, 0.5, 1.0); samples2wavfile("hw721_a440sine_amp1half.wav", vsamps); return 0; }
- Notes
- You may get warnings about const variables not used. You'll use them to complete the code.
The typedef and #define statements near the beginning let you choose between using float32 (float) or float64 (double) types.
Most modern computers use 64 bit CPUs and contain 64 bit floating point processors.
High end DSP filters and effects might use float64 for more accurate internal computation but later output float32 audio.
Float32 is slightly faster.
Some 3rd party libraries choose either float32 or float64 and using the native library format will eliminate type narrowing warnings.
Common abbreviations is DSP code
NAME | SYMBOL | ALTERNATE |
Sample Rate | FS (Frequency of Samples) | SR |
Period | T (time), 1/FS | P |
Frequency | f | frq, freq |
Number of Samples | N | nSamp |
Radian/Angular Frequency | ω | w, 2 π f |
π | M_PI in <cmath> | kPI |
2 π | 2*M_PI | k2PI |
2 π T, 2 π/FS | 2*M_PI*T, 2*M_PI/FS | k2PIT, k2PIoverFS |
Build
cd $HOME312/cs312/hw72/hw721_gensine/build # build cmake .. && make
Run Make sure you've implemented the gen_sin() function.
# run and play # cd build ./hw721_gensin
After running the program you'll see two WAV files appear in the build folder. 0pen and view the files in Audacity. Plot the spectrum. Verify results. They should be the same as the files Audacity generated, except for the duration.
hw722_gensin_phasor
Phasors
See Fig. 2 and 3 at https://en.wikipedia.org/wiki/Phasor for an animated demonstration of phasor/sine wave equivalence.
Read this for more information.
Phasors.pdf
Setup
cd $HOME312/cs312/hw72 cp -R hw721_gensin/ hw722_gensin_phasor cd hw722_gensin_phasor mv hw721_gensin.cpp hw722_gensin_phasor.cpp sed -i '' s/hw721_gensin/hw722_gensin_phasor/g CMakeLists.txt
In hw721_gensin we noted that the period of a 440Hz sine wave was 100.23 samples at a sampling rate of 44100Hz. Here are formulas to convert from waveform periods in samples to waveform periods in radians.
\[\eqalign{ & 1Hz = 2\pi\ {\rm{\ radians/second}} \cr & freq = 2\pi f\ {\rm{ radians/second}} \cr & {\rm{phase\ increment = }}{{2\pi f{\rm{ radians}}} \over {\sec }} \cdot {{1\sec } \over {44100{\rm{ samples}}}} = {{2\pi f} \over {FS}}{\rm{ radians/sample}} \cr} \]
CMakeLists.txt should be ready.
Copy this code into hw722_gensin_phasor.cpp
#include "sndfile.hh" #include <iostream> #include <vector> #include <cmath> // for M_PI // either_or typedef float MY_TYPE; #define LIB_SF_FORMAT SF_FORMAT_FLOAT; // SF_FORMAT_MY_TYPE defiend in "sndfile.h" // typedef double MY_TYPE; // #define LIB_SF_FORMAT SF_FORMAT_DOUBLE; // SF_FORMAT_MY_TYPE defiend in "sndfile.h" // END either_or const int FS = 44100; // CD sample rate const MY_TYPE T = 1.0 / FS; // sample period const MY_TYPE k2PI = M_PI * 2.0; const MY_TYPE k2PIT = k2PI * T; std::vector<MY_TYPE> gen_sin_phasor(MY_TYPE freq, MY_TYPE ampl, MY_TYPE secs) { std::vector<MY_TYPE> v; std::cout << "You need to implement gen_sin_phasor(...)\n"; /*---------------------------------------------------- declare variable MY_TYPE phzNow declare variable MY_TYPE phzIncr set phzNow to zero calculate the phase increment in radians for freq BEGIN_LOOP from 0 to number of samples in secs paramater push ampl*sin(phzNow) to vector v increment phzNow check if phzNow greater than 2pi if so wrap around (phzNow = phzNow - 2pi) END_LOOP ----------------------------------------------------*/ return v; } bool samples2wavfile(const char *fname, const std::vector<MY_TYPE> &v) { // code borrowed and modified from libsndfile example sndfilehandle.cc // mostly from create_file(...) and format from main() SndfileHandle file; const int channels = 1; const int srate = FS; const int format = SF_FORMAT_WAV | LIB_SF_FORMAT; file = SndfileHandle(fname, SFM_WRITE, format, channels, srate); // using vector instead of buffer array // C++ vector elements are stored in contiguous memory, same as array // &v[0] is pointer to first element of vector file.write(&v[0], v.size()); return true; } int main(int argc, char *argv[]) { std::vector<MY_TYPE> vsamps = gen_sin_phasor(440.0, 1.0, 1.5); samples2wavfile("hw722_a440sine_phasor.wav", vsamps); return 0; }
Build
cd $HOME312/cs312/hw72/hw722_gensine_phasor/build # emptyBuildFolder.sh # build cmake .. && make
You'll get a message to implement the gen_sin_phasor(…) function. The k2PIoverFS warning should disappear after you implement the gen_sin_phasor(…) function. When the program builds and runs.
Open and view the hw722_a440sine_phasor.wav file in Audacity. Plot the spectrum. Verify results.
- If you don't see a sine wave a your gen_sin_phasor() function is not correct.
- If you plot the spectrum and the peak frequency is not 440Hz your gen_sin_phasor() function is not correct.
hw723_gensin_beatDemo
Adding two waveforms is as simple as adding their samples, taking care that the result stays within the -1.0 to +1.0 range.
Beats and tuning
Two sinewaves (musical notes) are said to be "in tune" when there are no perceptible "beat" frequencies. The greater the difference between the two frequencies, the faster the beats. Here's a demo.
Setup
cd $HOME312/cs312/hw72 cp -R hw722_gensin_phasor/ hw723_gensin_beatDemo cd hw723_gensin_beatDemo mv hw722_gensin_phasor.cpp hw723_gensin_beatDemo.cpp sed -i '' s/hw722_gensin_phasor/hw723_gensin_beatDemo/g CMakeLists.txt sed -i '' s/hw722_gensin_phasor.wav/hw723_gensin_beatDemo.wav/g hw723_gensin_beatDemo.cpp emptyBuildFolder.sh
Replace main() with this.
int main(int argc, char *argv[]) { // beats std::vector<MY_TYPE> v1 = gen_sin_phasor(220.0, 0.5, 5.0); // note the sum of the two amplitudes are <= 1.0 std::vector<MY_TYPE> v2 = gen_sin_phasor(220.5, 0.5, 5.0); std::vector<MY_TYPE> vout; for (auto n = 0; n != v1.size(); ++n) vout.push_back(v1.at(n) + v2.at(n)); samples2wavfile("hw723_a220sine_beats.wav", vout); // reset for no beats v1 = gen_sin_phasor(220.0, 0.5, 2.5); // note the sum of the two amplitudes are <= 1.0 v2 = gen_sin_phasor(220, 0.5, 2.5); vout.clear(); for (auto n = 0; n != v1.size(); ++n) vout.push_back(v1.at(n) + v2.at(n)); samples2wavfile("hw723_a220sine_nobeats.wav", vout); return 0; }
Build and run
# cd hw723_gensin_beatDemo/build cd build emptyBuildFolder.sh cmake .. && make && ./hw723_gensin_beatDemo
Open and play in Audacity.
Beats
Modulation
Modulation is the process of varying one or more properties of a periodic waveform by another waveform called the modulator. Modulation requires two waveforms called the modulating wave and the carrier wave. The carrier wave is modulated (modified) by the modulator wave. Typically the carrier wave is the higher frequency. A musician modulates a tone (a periodic waveform) on a musical instrument through touch and pressure. Digital modulation is sample by sample multiplication.
Bipolar and Unipolar waveforms
The modulating wave can be either bipolar or unipolar.
- Bipolar waveform amplitudes pass above and below 0 on the y axis.
- Unipolar waveform amplitudes are never negative.
We'll briefly look at three forms of modulation.
- Ring modulation
- multiplication of two bipolar waveforms sample by sample.
- Amplitude modulation
- multiplying the amplitude of a waveform by a second waveform.
- Frequency/Phase modulation
- modifying the phase of a waveform by a second waveform.
hw724_gensin_ringmod
Ring Modulation is the simplest form of modulation. It multiplies two bipolar signals sample by sample. If the two signals are sinusoidal, the output of the ring modulator would be the sum and difference of the two frequencies. The fundamental frequencies of the two sinusoids will disappear. This corresponds to the trigonometric identity for multiplying two sine or cosine waves.
\[\sin A*\sin B = \frac{1}{2}\left[ {\cos \left( {A - B} \right) + {\mathop{\rm Cos}\nolimits} (A + B)} \right]\]
Setup
cd $HOME312/cs312/hw72 cp -R hw722_gensin_phasor/ hw724_gensin_ringmod cd hw724_gensin_ringmod mv hw722_gensin_phasor.cpp hw724_gensin_ringmod.cpp sed -i '' s/hw722_gensin_phasor/hw724_gensin_ringmod/g CMakeLists.txt sed -i '' s/hw723_gensin_beatDemo.wav/hw724_gensin_ringmod.wav/g hw724_gensin_ringmod.cpp emptyBuildFolder.sh
Replace the main() function with this.
int main(int argc, char *argv[]) { /*---------------------------------------------- modulating sine wave at 400 Hz, ampl 1.0, duration 1.5 seconds carrier sine wave at 700 Hz, ampl 1.0, duration 1.5 seconds output wave file name: "hw724_gensin_ringmod.wav" ----------------------------------------------*/ std::cout << "You need to implement ring modulation in main()\n"; return 0; }
Build and run
cd ~/cs312/hw72/hw724_gensin_ringmod/build # emptyBuildFolder.sh cmake .. && make
Open hw724_gensin_ringmod.wav in Audacity.
Time domain
Frequency domain
The spectrum shows the sum and difference of the two frequencies. Neither original frequency is present in the output.
Sine wave | Frequency Hz |
A | 400 |
B | 700 |
A-B | 300 |
A+B | 1100 |
Amplitude Modulation
Amplitude Modulation (AM) modulates the amplitude of the carrier wave by the modulating wave. It's the principle used in AM radio. In audio it's called AM synthesis. The general formula for an AM waveform is shown below.
\[\eqalign{ & {A_m} = 2\pi {f_m}T \cr & {F_c} = 2\pi {f_c}T \cr & M \in [0,1]{\text{ is the modulation index}} \cr & samp(n) = [1 + M*\sin ({A_m}*n)]*{A_c}\sin ({F_c}*n) \cr} \]
Notes:
- f_sub_m is the modulation frequency
- f_sub_c is the carrier frequency
- A_sub_m and F_sub_c are constants that can be calculated outside of any loop.
- The site below limits M to between 0 and 1. Other sites say between -1 and +1. Still others allow values above and below plus/minus 1.
One of the best sources available for audio DSP of all types is
https://ccrma.stanford.edu/~jos/mdft/Sinusoidal_Amplitude_Modulation_AM.html
hw725_gensin_ammod
Setup
Execute these commands in Terminal.
cd $HOME312/cs312/hw72 cp -r hw724_gensin_ringmod hw725_gensin_ammod cd hw725_gensin_ammod mv hw724_gensin_ringmod.cpp hw725_gensin_ammod.cpp sed -i '' s/hw724_gensin_ringmod/hw725_gensin_ammod/g CMakeLists.txt sed -i '' s/hw724_gensin_ringmod.wav/hw725_gensin_ammod.wav/g hw725_gensin_ammod.cpp emptyBuildFolder.sh
CMakeLists.txt
Should already configured to compile hw725_gensin_ammod.
hw725_gensin_ammod.cpp
Add a new function am_modulation(…)
// Fc carrierFrequncy, Ac carrierAmplitude, // Fm modulationFrequency, Am modulationAmplitude, // M modulation Index, secs duration in seconds std::vector<MY_TYPE> am_modulation(MY_TYPE Fc, MY_TYPE Ac, MY_TYPE Fm, MY_TYPE Am, MY_TYPE M, MY_TYPE secs) { std::vector<MY_TYPE> v; const MY_TYPE kAM = k2PIT * Fm; const MY_TYPE kFC = k2PIT * Fc; /*---------------------------------------------------- implement the FM formula on the class web page kAM = 2*pi*T*Fm is a constant that can be pre-calculated outside of the loop kFC = 2*pi*T*Fc is a constant that can be pre-calculated outside of the loop loop for samples 0 to total number of samples samp = (1 + M * sin(kAM * n)) * Ac * sin(kFC * n); vout.push_back(samp); end loop ----------------------------------------------------*/ std::cout << "You need to implement std::vector<float> am_modulation(...)\n"; return v; }
Replace main() with this code.
int main(int argc, char *argv[]) { std::vector<MY_TYPE> vout; // params are // carrierFreq, carrierAmplitude, modulatorFreq, modulatorAmplitude, modulation Index, duration vout = am_modulation(400, 0.4, 97, 0.4, 1.25, 2.5); if (!samples2wavfile("hw725_m_test.wav", vout)) std::cout << "Could not write am_test.wav"; /*---------------------------------------------------- These are the settings I used to generate the over and under modulated figures on the web page. Once you get it working try other settings. ----------------------------------------------------*/ vout.clear(); vout = am_modulation(700, 0.3, 7, 0.5, -0.5, 2.5); if (!samples2wavfile("hw725_am_undermod.wav", vout)) std::cout << "Could not write am_undermod.wav"; vout.clear(); vout = am_modulation(700, 0.2, 7, 0.2, 3, 2.5); if (!samples2wavfile("hw725_am_overmod.wav", vout)) std::cout << "Could not write am_overmod.wav"; return 0; }
Build and run
cmake .. && make && ./hw725_gensin_ammod
Open the three wav files in Audacity. Compare your waveforms with the ones below.
am_test
Under modulated
Over modulated
Try variations
Test variations in mod amplitude and frequency.
- am_modulation(700, 0.5, 400, 0.5, .57, 2.5)
- am_modulation(400, 0.5, 3.0, 0.8, -1.0, 2.5)
- am_modulation(400, 0.5, 25.0, 0.8, 1.25, 2.5)
- etc.
Frequency Modulation
History
From: https://en.wikipedia.org/wiki/Frequency_modulation_synthesis
The technique of the digital implementation of frequency modulation was developed by John Chowning...at Stanford University in 1967–68 and patented in 1975. Prior to that, the FM synthesis algorithm was licensed to Japanese company Yamaha in 1973.[1] It was initially designed for radios to transmit voice by modulating one waveform's frequency with another's. This is why FM radio is called FM (frequency modulation). ... With the expiration of the Stanford University FM patent in 1995, digital FM synthesis can now be implemented freely by other manufacturers. The FM synthesis patent brought Stanford $20 million before it expired, making it (in 1994) "the second most lucrative licensing agreement in Stanford's history".
This is Chowning's 1973 paper. Many of the examples in hw726_gensin_fmmod come from that paper.
https://ccrma.stanford.edu/sites/default/files/user/jc/fm_synthesispaper-2.pdf
If you modulate the frequency of a carrier waveform, by a modulation waveform at radio frequencies the technique is called Frequency Modulation (FM). Using audio frequencies the technique is called FM synthesis. Chowning's general formula for a FM waveform is shown below.
\[\eqalign{ & sample(n) = A\sin \left[ {2\pi {f_c}nT + I\sin (2\pi {f_m}nT)} \right] \cr & {\text{A = amplitude}} \cr & {{\text{f}}_{\text{c}}}{\text{ = carrier}}\;{\text{frequency}} \cr & {{\text{f}}_{\text{m}}}{\text{ = modulation}}\;{\text{frequency}} \cr & {\text{n = sample}}\;{\text{number}} \cr & {\text{T = }}\frac{1}{{SampleRate}} \cr & {\text{I = modulation}}\;{\text{index}} \cr & {\text{I = }}\frac{{\Delta {f_m}}}{{{f_m}}} \cr & \Delta {f_m}\;{\text{ = frequency}}\;{\text{deviation}}\;{\text{from}}\;{{\text{f}}_{\text{m}}} \cr & \Delta {f_m} \geqslant 0 \cr & \Delta {f_m} \leqslant {f_m} \leqslant \Delta {f_m} \cr} \]
hw726_gensin_fmmod
Setup
cd $HOME312/cs312/hw72 cp -r hw725_gensin_ammod hw726_gensin_fmmod cd hw726_gensin_fmmod mv hw725_gensin_ammod.cpp hw726_gensin_fmmod.cpp sed -i '' s/hw725_gensin_ammod/hw726_gensin_fmmod/g CMakeLists.txt emptyBuildFolder.sh
CMakeLists.txt
Should already configured to compile hw726_gensin_fmmod. Here it is for reference.
hw726_gensin_fmmod.cpp
Copy/paste and implement.
#include "sndfile.hh" #include <iostream> #include <vector> #include <cmath> // for M_PI // either_or typedef float MY_TYPE; #define LIB_SF_FORMAT SF_FORMAT_FLOAT; // SF_FORMAT_MY_TYPE defiend in "sndfile.h" // typedef double MY_TYPE; // #define LIB_SF_FORMAT SF_FORMAT_DOUBLE; // SF_FORMAT_MY_TYPE defiend in "sndfile.h" // END either_or const int FS = 44100; // CD sample rate const MY_TYPE T = 1.0 / FS; // sample period const MY_TYPE k2PI = M_PI * 2.0; const MY_TYPE k2PIT = k2PI * T; std::vector<MY_TYPE> gen_sin_phasor(MY_TYPE freq, MY_TYPE ampl, MY_TYPE secs) { std::vector<MY_TYPE> v; MY_TYPE phzNow = 0; MY_TYPE phzIncr = k2PIT * freq; for (int n = 0; n < secs * FS; n++) { v.push_back(ampl * sin(phzNow)); phzNow += phzIncr; // wrap around 2pi if (phzNow > k2PI) phzNow -= k2PI; } return v; } bool samples2wavfile(const char *fname, const std::vector<MY_TYPE> &v) { // code borrowed and modified from libsndfile example sndfilehandle.cc // mostly from create_file(...) and format from main() SndfileHandle file; const int channels = 1; const int srate = FS; const int format = SF_FORMAT_WAV | LIB_SF_FORMAT; file = SndfileHandle(fname, SFM_WRITE, format, channels, srate); // using vector instead of buffer array // C++ vector elements are stored in contiguous memory, same as array // &v[0] is pointer to first element of vector file.write(&v[0], v.size()); return true; } std::vector<MY_TYPE> fm_modulation(MY_TYPE A, MY_TYPE Fc, MY_TYPE Fm, MY_TYPE dFm, MY_TYPE secs) { // Function Parameters // A: amplitude // Fc: carrierFrequency // Fm: modulationFrequency // secs: duration in floating point seconds // dFm: deltaFm // Variables/constants needed // samp: a single sample value // n: sample number as in samp[n] // Fs: sampling frequency 44100 // T: sampling period 1/FS // I: modulationIndex std::vector<MY_TYPE> vout; /*--------------------------------------------------- implement the FM formula on the class web page carF = 2*pi*Fc*T is a constant that can be pre-calculated outside of the loop carM = 2*pi*Fm*T is a constant that can be pre-calculated outside of the loop loop for samples 0 to total number of samples samp = A * sin( carF * n + I* sin( carM * n ) ); vout.push_back(samp); end loop ----------------------------------------------------*/ std::cout << "You need to implement fm_modulation(...)\n"; return vout; } void write_fm_file(const std::string &fname, const std::vector<MY_TYPE> &v) { if (!samples2wavfile(fname.c_str(), v)) std::cout << fname << std::endl; } int main(int argc, char *argv[]) { // DO NOT MODIFY /*-------------------------------------------------- Chowning examples without Envelopes Parameters on class web page Chowning used A=1000 (amplitude) but did not indicate units Test with one example at a time until you get all to work Compare your results with mp3 files on web page. --------------------------------------------------*/ MY_TYPE A = 1.0; std::vector<MY_TYPE> vout = fm_modulation(A, 100, 100, 400, 1.5); write_fm_file("Example1.wav", vout); // vout.clear(); // vout = fm_modulation(A, 440, 440, 2200, 1.5); // write_fm_file("Example2.wav", vout); // vout = fm_modulation(A, 900, 300, 600, 1.5); // write_fm_file("Example3a.wav", vout); // vout = fm_modulation(A, 500, 100, 150, 1.5); // write_fm_file("Example3b.wav", vout); // vout.clear(); // vout = fm_modulation(A, 900, 600, 2400, 1.5); // write_fm_file("Example3c.wav", vout); // vout.clear(); // vout = fm_modulation(A, 200, 280, 2800, 6); // write_fm_file("Example4a.wav", vout); // vout.clear(); // vout = fm_modulation(A, 200, 280, 560, 0.2); // write_fm_file("Example4b.wav", vout); // vout.clear(); // vout = fm_modulation(A, 80, 55, 375, 2.0); // write_fm_file("Example4c.wav", vout); /*-------------------------------------------------- JE examples --------------------------------------------------*/ // vout.clear(); // vout = fm_modulation(0.75, 549, 387, 300, 1.5); // write_fm_file("hw725_fm_bell.wav", vout); // vout.clear(); // vout = fm_modulation(0.75, 440, 3.0, 3.0, 3.0); // write_fm_file("hw725_fm_vibrato.wav", vout); // vout.clear(); // vout = fm_modulation(0.75, 440, 10, 10 * 20, 2.0); // write_fm_file("hw725_fm_raygun.wav", vout); /*-------------------------------------------------- Create four of your own fm modulation examples below --------------------------------------------------*/ return 0; }
Note:
An important synthesis function is missing which is missing when you listen to these examples, namely an Envelope. We'll look at envelopes in a future lab. Here's an example of my fm_bell with and without an envelope applied to the samples.
No envelope
hw726_fm_bell.wav
With exponential decay envelope
hw818_fm_bell_env.mp3
Build and run
These examples are directly from the Chowning paper above.
P4-P8 are the parameters Chowning using in the paper.
A, Fc, Fm, dFm are the equivalent parameters used in the homework.
dFm (delta FM) is the numerator of the Modulation Index I.
Example 1 p.3
P4 | P5 | P6 | P7_P8 | P7_P8 |
---|---|---|---|---|
A | Fc | Fm | dFm | I=dFm/Fm |
1.0 | 100 | 100 | 400 | 4 |
Waveform
Spectrum
Compare with Figure 4c, p.3
Brass-like Tones
Chowning states on p.7 that
1) The frequencies in the spectrum are in the harmonic series 2) Both odd and even numbered harmonics are at some times present
Example 2
P5 | P4 | P6 | (P4) | P7_P8 | P7_P8 |
---|---|---|---|---|---|
Fc | Ac | Fm | Am | dFm | I=dFm/Fm |
440 | 1.0 | 440 | (Ac) | 2200 | 5 |
Spectrum
Woodwind-like Tones
Chowning states on p.7 that
1) The frequencies in the spectrum are in the harmonic series and for some woodwind tones are predominantly odd numbered harmonics
Example 3a
Example 3b (bassoon-like)
Example 3c (clarinet-like)
P5 | P4 | P6 | (P4) | P7_P8 | P7_P8 |
---|---|---|---|---|---|
Fc | Ac | Fm | Am | dFm | I=dFm/Fm |
900 | 1.0 | 600 | (Ac) | 2400 (1200) | 4 (2) |
Spectrum
Percussive Sounds
Example 4a Bell-like
P5 | P4 | P6 | (P4) | P7_P8 | P7_P8 | P3 |
---|---|---|---|---|---|---|
Fc | Ac | Fm | Am | dFm | I=dFm/Fm | Secs |
200 | 1.0 | 280 | (Ac) | 2800 | 10 | 15 |
Spectrum
Example 4b Drum
P5 | P4 | P6 | (P4) | P7_P8 | P7_P8 | P3 |
---|---|---|---|---|---|---|
Fc | Ac | Fm | Am | dFm | I=dFm/Fm | Secs |
200 | 1.0 | 280 | (Ac) | 560 | 2 | 0.2 |
Spectrum
Example 4c Wood Drum
P5 | P4 | P6 | (P4) | P7_P8 | P7_P8 | P3 |
---|---|---|---|---|---|---|
Fc | Ac | Fm | Am | dFm | I=dFm/Fm | Secs |
80 | 1.0 | 55 | (Ac) | 1375 | 25 | 2 |
Spectrum
JE examples
Assignment 726
Create four of your own examples below the code section
/*-------------------------------------------------- Create four of your own fm modulation examples below --------------------------------------------------*/
Submission Format
Feel free to email jellinge@carleton.edu on any part of the homework that is unclear to you. Chances are if you have questions other students do too. I will answer those questions by email to everyone in the class. The original sender will remain anonymous. I have tried to provide clues to the homework in the web pages, reading assignments, and labs. Sometimes what seems obvious to me is not obvious to students just learning C++.
Create a folder named hwNN_LastnameFirstname_LastnameFirstname.
Substitute your name and your partner's name for LastnameFirstname_LastnameFirstname.
Remember
- Boilerplate header at top of every file.
- Make sure your program compiles before submitting.
- Programs that compile and produce partially correct output will be graded.
- You can send notes to me in your homework files by enclosing them in block comments.
- Programs that do not compile get an automatic F (59%).
- Submit the homework in only one of the partner's folders.
- Empty your build or build-debug folders using the command emptyBuildFolder.sh
- Only include .h, .cpp, Makefiles, CMakeLists.txt
- Do not include these folders/files
.git
.vscode Double check for the above two folders using this command
ls -a hwNN_LastnameFirstname1_LastnameFirstname2 # if either .git or .vscode exist in the folder you're submitting remove them with # rm -fR .git # rm -fR .vscode
Hand-in folder
$HOME312/common ├── RtMidi │ ├── RtMidi.cpp │ └── RtMidi.h ├── hw332_CMidiPacket.cpp ├── hw332_CMidiPacket.h ├── hw411_rand_int.cpp ├── hw411_rand_int.h ├── hw421_CDelayMs.cpp ├── hw421_CDelayMs.h ├── hw422_CAppleMidiSynth.cpp ├── hw422_CAppleMidiSynth.h ├── hw423_CMidiTrack.cpp ├── hw423_CMidiTrack.h ├── hw511_CInstrument.cpp ├── hw511_CInstrument.h └── libsndfile ├── sndfile.h └── sndfile.hh $HOME312/cs312/hw72 ├── hw721_gensin │ ├── CMakeLists.txt │ ├── build │ └── hw721_gensin.cpp ├── hw722_gensin_phasor │ ├── CMakeLists.txt │ ├── build │ └── hw722_gensin_phasor.cpp ├── hw723_gensin_beatDemo │ ├── CMakeLists.txt │ ├── build │ └── hw723_gensin_beatDemo.cpp ├── hw724_gensin_ringmod │ ├── CMakeLists.txt │ ├── build │ └── hw724_gensin_ringmod.cpp ├── hw725_gensin_ammod │ ├── CMakeLists.txt │ ├── build │ └── hw725_gensin_ammod.cpp └── hw726_gensin_fmmodJE ├── CMakeLists.txt ├── build └── hw726_gensin_fmmod.cpp