Introduction
I'm no musician or songwriter, so I'll avoid the blurb about where I got my inspiration from. This Pico project just morphed out of my last project working with a 4-digit 7-segment display and the HC-SR04 ultrasonic sensor... I had a notion to develop something where the HC-SR04 sensor would trigger some audio.
This blog is about that journey of discovery. So I hope some of you find this blog useful.
Using Arduino IDE
As with my segment display project, I'm using the Arduino IDE as my development platform and, as I'm sure you all well know, the Arduino programming language includes a built-in tone function.
There is even a couple of tone examples included in the default example menu (inside the 02. Digital folder). These all work fine from any digital pin when using the main core (there's an issue with the tone function if trying to use in core 1).
However, the issue using the tone library is that I would have to create my own tone files to create a tune and as I'm tone deaf (excuse the pun) this wouldn't work for my project.
The alternative, which has been demonstrated to work countless times with Arduino UNO's, is to use an audio WAV file where these audio WAV files are stored on an SD card and an SD card reader is attached to the Arduino and using SPI the data is read and played back through an onboard DAC and an op-amp.
And the good news is that you do not even have to build one from scratch. One of my very first Arduino shields was the Adafruit Wave Shield, which is still available for purchase.
https://learn.adafruit.com/adafruit-wave-shield-audio-shield-for-arduino
But taking this route would be just too easy and besides, using a shield fails to take advantage of some of the unique features found on the Pico.
I wanted to try something different.
I then found this blog on Hackster, which described a technique of using PWM to generate the audio signals. This isn't new of course, or unique to Pico, as I could just as easily have done the same with an Arduino UNO. The difference here is that the Pico processor clocks at a much higher frequency than the UNO (133MHz vs. 16MHz) and so the quality of the audio is that much better.
Unfortunately, the code provided in the blog is utilising the Pico SDK library and was not created using the Arduino programming language.
Ah, but is this really a problem.
Well, thankfully it's not.
In most cases you can simply copy and paste the c code directly into the Arduino IDE and it will compile this code (once you've made a few changes, e.g. changing int main to void setup and adding in the loop function) and it will also upload the new sketch onto the Pico for you.
So this is what I tried.
However, there was one snag. The example code uses a command set_sys_clock_khz to change the clock speed of the Pico and this is one of the few commands which is not available via the Arduino IDE.
Thankfully I found a solution to this problem, which was offered as a response to a similar issue raised within the Arduino mbed-core GitHub repository.
All I needed to do was add this code snippet into my project and this then allows overclocking:
/*********************************************************************************** * Inline functions to enable overclocking * ************************************************************************************/ void set_sys_clock_pll(uint32_t vco_freq, uint post_div1, uint post_div2) { if (!running_on_fpga()) { clock_configure(clk_sys, CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLKSRC_CLK_SYS_AUX, CLOCKS_CLK_SYS_CTRL_AUXSRC_VALUE_CLKSRC_PLL_USB, 48 * MHZ, 48 * MHZ); pll_init(pll_sys, 1, vco_freq, post_div1, post_div2); uint32_t freq = vco_freq / (post_div1 * post_div2); // Configure clocks // CLK_REF = XOSC (12MHz) / 1 = 12MHz clock_configure(clk_ref, CLOCKS_CLK_REF_CTRL_SRC_VALUE_XOSC_CLKSRC, 0, // No aux mux 12 * MHZ, 12 * MHZ); // CLK SYS = PLL SYS (125MHz) / 1 = 125MHz clock_configure(clk_sys, CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLKSRC_CLK_SYS_AUX, CLOCKS_CLK_SYS_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS, freq, freq); clock_configure(clk_peri, 0, // Only AUX mux on ADC CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLKSRC_PLL_USB, 48 * MHZ, 48 * MHZ); } } bool check_sys_clock_khz(uint32_t freq_khz, uint *vco_out, uint *postdiv1_out, uint *postdiv_out) { uint crystal_freq_khz = clock_get_hz(clk_ref) / 1000; for (uint fbdiv = 320; fbdiv >= 16; fbdiv--) { uint vco = fbdiv * crystal_freq_khz; if (vco < 400000 || vco > 1600000) continue; for (uint postdiv1 = 7; postdiv1 >= 1; postdiv1--) { for (uint postdiv2 = postdiv1; postdiv2 >= 1; postdiv2--) { uint out = vco / (postdiv1 * postdiv2); if (out == freq_khz && !(vco % (postdiv1 * postdiv2))) { *vco_out = vco * 1000; *postdiv1_out = postdiv1; *postdiv_out = postdiv2; return true; } } } } return false; } static inline bool set_sys_clock_khz(uint32_t freq_khz, bool required) { uint vco, postdiv1, postdiv2; if (check_sys_clock_khz(freq_khz, &vco, &postdiv1, &postdiv2)) { set_sys_clock_pll(vco, postdiv1, postdiv2); return true; } else if (required) { panic("System clock of %u kHz cannot be exactly achieved", freq_khz); } return false; } /********************************************************************/
The Arduino code is pretty much as is:
/*********************************************************************************** * MIT License Copyright (c) 2021 Robin Grosset Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. **********************************************************************************/ #include <stdio.h> #include "stdlib.h" // stdlib #include "hardware/irq.h" // interrupts #include "hardware/pwm.h" // pwm #include "hardware/sync.h" // wait for interrupt #include "hardware/pll.h" #include "hardware/clocks.h" // Audio PIN is to match some of the design guide shields. #define AUDIO_PIN 18 // you can change this to whatever you like /* * This include brings in static arrays which contain audio samples. * if you want to know how to make these please see the python code * for converting audio samples into static arrays. */ #include "sample.h" int wav_position = 0; /*********************************************************************************** * Inline functions to enable overclocking * ************************************************************************************/ void set_sys_clock_pll(uint32_t vco_freq, uint post_div1, uint post_div2) { if (!running_on_fpga()) { clock_configure(clk_sys, CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLKSRC_CLK_SYS_AUX, CLOCKS_CLK_SYS_CTRL_AUXSRC_VALUE_CLKSRC_PLL_USB, 48 * MHZ, 48 * MHZ); pll_init(pll_sys, 1, vco_freq, post_div1, post_div2); uint32_t freq = vco_freq / (post_div1 * post_div2); // Configure clocks // CLK_REF = XOSC (12MHz) / 1 = 12MHz clock_configure(clk_ref, CLOCKS_CLK_REF_CTRL_SRC_VALUE_XOSC_CLKSRC, 0, // No aux mux 12 * MHZ, 12 * MHZ); // CLK SYS = PLL SYS (125MHz) / 1 = 125MHz clock_configure(clk_sys, CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLKSRC_CLK_SYS_AUX, CLOCKS_CLK_SYS_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS, freq, freq); clock_configure(clk_peri, 0, // Only AUX mux on ADC CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLKSRC_PLL_USB, 48 * MHZ, 48 * MHZ); } } bool check_sys_clock_khz(uint32_t freq_khz, uint *vco_out, uint *postdiv1_out, uint *postdiv_out) { uint crystal_freq_khz = clock_get_hz(clk_ref) / 1000; for (uint fbdiv = 320; fbdiv >= 16; fbdiv--) { uint vco = fbdiv * crystal_freq_khz; if (vco < 400000 || vco > 1600000) continue; for (uint postdiv1 = 7; postdiv1 >= 1; postdiv1--) { for (uint postdiv2 = postdiv1; postdiv2 >= 1; postdiv2--) { uint out = vco / (postdiv1 * postdiv2); if (out == freq_khz && !(vco % (postdiv1 * postdiv2))) { *vco_out = vco * 1000; *postdiv1_out = postdiv1; *postdiv_out = postdiv2; return true; } } } } return false; } static inline bool set_sys_clock_khz(uint32_t freq_khz, bool required) { uint vco, postdiv1, postdiv2; if (check_sys_clock_khz(freq_khz, &vco, &postdiv1, &postdiv2)) { set_sys_clock_pll(vco, postdiv1, postdiv2); return true; } else if (required) { panic("System clock of %u kHz cannot be exactly achieved", freq_khz); } return false; } /********************************************************************/ /* * PWM Interrupt Handler which outputs PWM level and advances the * current sample. * * We repeat the same value for 8 cycles this means sample rate etc * adjust by factor of 8 (this is what bitshifting <<3 is doing) * */ void pwm_interrupt_handler() { pwm_clear_irq(pwm_gpio_to_slice_num(AUDIO_PIN)); if (wav_position < (WAV_DATA_LENGTH<<3) - 1) { // set pwm level // allow the pwm value to repeat for 8 cycles this is >>3 pwm_set_gpio_level(AUDIO_PIN, WAV_DATA[wav_position>>3]); wav_position++; } else { // reset to start wav_position = 0; } } void setup() { /* Overclocking for fun but then also so the system clock is a * multiple of typical audio sampling rates. */ //stdio_init_all(); -- this is not needed in Arduino (it will return a 'was not declared in this scope' error) set_sys_clock_khz(176000, true); gpio_set_function(AUDIO_PIN, GPIO_FUNC_PWM); int audio_pin_slice = pwm_gpio_to_slice_num(AUDIO_PIN); // Setup PWM interrupt to fire when PWM cycle is complete pwm_clear_irq(audio_pin_slice); pwm_set_irq_enabled(audio_pin_slice, true); // set the handle function above irq_set_exclusive_handler(PWM_IRQ_WRAP, pwm_interrupt_handler); irq_set_enabled(PWM_IRQ_WRAP, true); // Setup PWM for audio output pwm_config config = pwm_get_default_config(); /* Base clock 176,000,000 Hz divide by wrap 250 then the clock divider further divides * to set the interrupt rate. * * 11 KHz is fine for speech. Phone lines generally sample at 8 KHz * * * So clkdiv should be as follows for given sample rate * 8.0f for 11 KHz * 4.0f for 22 KHz * 2.0f for 44 KHz etc */ pwm_config_set_clkdiv(&config, 8.0f); pwm_config_set_wrap(&config, 250); pwm_init(audio_pin_slice, &config, true); pwm_set_gpio_level(AUDIO_PIN, 0); } void loop() { __wfi(); // Wait for Interrupt }
I then wired up my Pico according the example, but the audio output was not as good as I hoped for.
So, I used a fair bit of trial and error and added in a few bits more to create my own filtered digital output, which also included an old TS922 op-amp (only because I had one). This is what I came up with and it seems to work for me.
{gallery}Pico PWM Digital Audio Output Circuit |
---|
And here is the audio output from that example (video captured using mobile phone):
And where is the speaker? Well, its actually attached underneath the Ikea cardboard box lid, which I'm using as my sound box. This speaker, which I had purchased a good few years ago, is a transducer speaker which vibrates the surface to produce sound. It's pretty neat, in my humble opinion and the sound output isn't bad either.
Now that I was able to play audio from an embedded file, I wanted to explore options that did not involve an SD card.
Using the MbedOS USBMSD API
If you open up the Examples menu in Arduino IDE, you should see a section called "Examples for Raspberry Pi Pico" if you have the Raspberry Pi Pico board selected within the Boards Manager tool. As shown below, two examples are provided under the category "USB Mass Storage".
The one that caught my eye was titled "AccessFlashAsUSBDisk".
This was exactly what I wanted.
Unfortunately, this example returned a "fatal error: QSPIFBlockDevice.h: No such file or directory" when using the Pico board.
As I could not find anything online, I raised the issue on GitHub and the response received explained the reason: "on RP2040 based boards the QSPI flash is also the 'system' flash, so it's not recommended to access it bypassing the internal flash APIs". A solution was also provided suggesting that the FlashIAPBlockDevice driver could safely be used instead, which proved to be the case. I was able to quickly create a working example, which was as follows:
#include "PluggableUSBMSD.h" #include "FlashIAPBlockDevice.h" static FlashIAPBlockDevice bd(XIP_BASE + 0x100000, 0x100000); USBMSD MassStorage(&bd); static FILE *f = nullptr; // Note that file is written to the actual root directory as created when USBMSD is mounted // File is not stored within a folder called root. // File is only found when you remove usb and replace again const char *fname = "/root/myfile.txt"; void USBMSD::begin() { int err = getFileSystem().mount(&bd); if (err) { Serial.println(" filesystem mount failed\ntry to reformat device..."); err = getFileSystem().reformat(&bd); } if (err) { Serial.println("Error: Unable to format/mount the device."); while(1); } } mbed::FATFileSystem &USBMSD::getFileSystem() { static mbed::FATFileSystem fs("root"); return fs; } void writeContents() { f = fopen(fname, "w+"); if (f != nullptr) { fprintf(f, "Hello World\n"); fflush(f); fclose(f); Serial.println("Written to File"); } else { Serial.println("File not found"); } } void readContents() { f = fopen(fname, "r"); if (f != nullptr) { fclose(f); Serial.println("File found"); } else { Serial.println("File not found"); } } void setup() { // put your setup code here, to run once: Serial.begin(115200); MassStorage.begin(); // If you do not want to wait for Serial Monitor to load then use a long delay here. // Found a delay helps if you want to capture the initial serial output. //while(!Serial) {;;} delay(1000); Serial.println("MassStorage mounted"); writeContents(); //readContents(); } void loop() { // put your main code here, to run repeatedly: MassStorage.process(); }
Parsing Audio WAV Files
All that was left to do was create some code that would be able to read and parse an Audio WAV File.
Once again there's plenty of information online to help. For example, I found this link which provided a good introduction to the WAV header format: http://soundfile.sapp.org/doc/WaveFormat/
I also found this stackoverflow post, which provided me with the actual WAV audio parsing code.
I then found one code gotcha, when trying to use WAV audio generated from Audacity.
Embedded within Audacity WAV audio files are LIST chunks found within the RIFF header. Once again the Internet was my helper and I found this useful reference to guide me: https://www.recordingblogs.com/wiki/list-chunk-of-a-wave-file
Once that was sorted, it was basically a task of putting it all together. And this is my result:
/*********************************************************************************** * MIT License * * Application Copyright (c) C Gerrish (BigG/Gerrikoio) - relates to this code * * Copyright (c) 2021 Robin Grosset - relates to PWM audio * https://github.com/rgrosset/pico-pwm-audio * * Source on how to read header info from WAV audio file: * https://stackoverflow.com/questions/13660777/c-reading-the-data-part-of-a-wav-file * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. **********************************************************************************/ #include <stdio.h> #include "stdlib.h" // stdlib #include "hardware/irq.h" // interrupts #include "hardware/pwm.h" // pwm #include "hardware/sync.h" // wait for interrupt #include "hardware/pll.h" #include "hardware/clocks.h" #include "PluggableUSBMSD.h" #include "FlashIAPBlockDevice.h" // Note that file is written to the actual root directory as created when USBMSD is mounted // File is not stored within a folder called root. // File is only found when you remove usb and replace again const char *fname = "/root/sound1.wav"; static FlashIAPBlockDevice bd(XIP_BASE + 0x100000, 0x100000); USBMSD MassStorage(&bd); static FILE *f = nullptr; // Audio PIN is to match some of the design guide shields. #define AUDIO_PIN 18 // you can change this to whatever you like uint32_t wav_position = 0; uint32_t ChunkSize = 0; // this is the actual sound data size uint8_t* WAV_DATA; typedef struct WAV_HEADER { /* RIFF Chunk Descriptor */ uint8_t RIFF[4]; // RIFF Header Magic header uint32_t ChunkSize; // RIFF Chunk Size uint8_t WAVE[4]; // WAVE Header /* "fmt" sub-chunk */ uint8_t fmt[4]; // FMT header uint32_t Subchunk1Size; // Size of the fmt chunk uint16_t AudioFormat; // Audio format 1=PCM,6=mulaw,7=alaw, 257=IBM Mu-Law, 258=IBM A-Law, 259=ADPCM uint16_t NumOfChan; // Number of channels 1=Mono 2=Sterio uint32_t SamplesPerSec; // Sampling Frequency in Hz uint32_t bytesPerSec; // bytes per second uint16_t blockAlign; // 2=16-bit mono, 4=16-bit stereo uint16_t bitsPerSample; // Number of bits per sample /* "data" sub-chunk */ uint8_t Subchunk2ID[4]; // "data" string uint32_t Subchunk2Size; // Sampled data length } wav_hdr; /*********************************************************************************** * Inline functions to enable overclocking * ************************************************************************************/ void set_sys_clock_pll(uint32_t vco_freq, uint post_div1, uint post_div2) { if (!running_on_fpga()) { clock_configure(clk_sys, CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLKSRC_CLK_SYS_AUX, CLOCKS_CLK_SYS_CTRL_AUXSRC_VALUE_CLKSRC_PLL_USB, 48 * MHZ, 48 * MHZ); pll_init(pll_sys, 1, vco_freq, post_div1, post_div2); uint32_t freq = vco_freq / (post_div1 * post_div2); // Configure clocks // CLK_REF = XOSC (12MHz) / 1 = 12MHz clock_configure(clk_ref, CLOCKS_CLK_REF_CTRL_SRC_VALUE_XOSC_CLKSRC, 0, // No aux mux 12 * MHZ, 12 * MHZ); // CLK SYS = PLL SYS (125MHz) / 1 = 125MHz clock_configure(clk_sys, CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLKSRC_CLK_SYS_AUX, CLOCKS_CLK_SYS_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS, freq, freq); clock_configure(clk_peri, 0, // Only AUX mux on ADC CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLKSRC_PLL_USB, 48 * MHZ, 48 * MHZ); } } bool check_sys_clock_khz(uint32_t freq_khz, uint *vco_out, uint *postdiv1_out, uint *postdiv_out) { uint crystal_freq_khz = clock_get_hz(clk_ref) / 1000; for (uint fbdiv = 320; fbdiv >= 16; fbdiv--) { uint vco = fbdiv * crystal_freq_khz; if (vco < 400000 || vco > 1600000) continue; for (uint postdiv1 = 7; postdiv1 >= 1; postdiv1--) { for (uint postdiv2 = postdiv1; postdiv2 >= 1; postdiv2--) { uint out = vco / (postdiv1 * postdiv2); if (out == freq_khz && !(vco % (postdiv1 * postdiv2))) { *vco_out = vco * 1000; *postdiv1_out = postdiv1; *postdiv_out = postdiv2; return true; } } } } return false; } static inline bool set_sys_clock_khz(uint32_t freq_khz, bool required) { uint vco, postdiv1, postdiv2; if (check_sys_clock_khz(freq_khz, &vco, &postdiv1, &postdiv2)) { set_sys_clock_pll(vco, postdiv1, postdiv2); return true; } else if (required) { panic("System clock of %u kHz cannot be exactly achieved", freq_khz); } return false; } /********************************************************************/ void USBMSD::begin() { int err = getFileSystem().mount(&bd); if (err) { //String FN = getFileSystem().getName(); Serial.println(" filesystem mount failed. Try to reformat device..."); err = getFileSystem().reformat(&bd); } if (err) { Serial.println("Error: Unable to format/mount the device."); while(1); } } mbed::FATFileSystem &USBMSD::getFileSystem() { static mbed::FATFileSystem fs("root"); return fs; } /* * PWM Interrupt Handler which outputs PWM level and advances the * current sample. * * We repeat the same value for 8 cycles this means sample rate etc * adjust by factor of 8 (this is what bitshifting <<3 is doing) * */ void pwm_interrupt_handler() { pwm_clear_irq(pwm_gpio_to_slice_num(AUDIO_PIN)); if (wav_position < (ChunkSize<<3) - 1) { // set pwm level // allow the pwm value to repeat for 8 cycles this is >>3 pwm_set_gpio_level(AUDIO_PIN, WAV_DATA[wav_position>>3]); wav_position++; } else { // reset to start wav_position = 0; } } void readContents() { wav_hdr wavHeader; int headerSize = sizeof(wav_hdr); f = fopen(fname, "r"); if (f != nullptr) { size_t bytesRead = fread(&wavHeader, 1, headerSize, f); Serial.print("Header Read "); Serial.print(bytesRead); Serial.println(" bytes."); if (bytesRead > 0) { //Read the data uint16_t bytesPerSample = wavHeader.bitsPerSample / 8; //Number of bytes per sample uint64_t numSamples = wavHeader.ChunkSize / bytesPerSample; //How many samples are in the wav file? //static const uint16_t BUFFER_SIZE = 4096; Serial.println(); Serial.print("RIFF header :"); Serial.print((char)wavHeader.RIFF[0]); Serial.print((char)wavHeader.RIFF[1]); Serial.print((char)wavHeader.RIFF[2]); Serial.println((char)wavHeader.RIFF[3]); Serial.print("Chunk Size :"); Serial.print(wavHeader.ChunkSize); Serial.print(" TOTAL: "); Serial.print(wavHeader.ChunkSize+8); Serial.print(" DATA: "); Serial.println(wavHeader.ChunkSize-(36 + wavHeader.Subchunk2Size)-8); Serial.print("WAVE header :"); Serial.print((char)wavHeader.WAVE[0]); Serial.print((char)wavHeader.WAVE[1]); Serial.print((char)wavHeader.WAVE[2]); Serial.println((char)wavHeader.WAVE[3]); Serial.print("Subchunk1 ID (fmt) :"); Serial.print((char)wavHeader.fmt[0]); Serial.print((char)wavHeader.fmt[1]); Serial.print((char)wavHeader.fmt[2]); Serial.println((char)wavHeader.fmt[3]); Serial.print("Subchunk1 size :"); Serial.print(wavHeader.Subchunk1Size); Serial.println(wavHeader.Subchunk1Size==16?" PCM":""); // Display the sampling Rate from the header Serial.print("Audio Format :"); Serial.println(wavHeader.AudioFormat==1?"PCM":(wavHeader.AudioFormat==6?"mulaw":(wavHeader.AudioFormat==7?"alaw":(wavHeader.AudioFormat==257?"IBM Mu-Law":(wavHeader.AudioFormat==258?"IBM A-Law":"ADPCM"))))); Serial.print("Number of channels :"); Serial.println(wavHeader.NumOfChan==1?"Mono":(wavHeader.NumOfChan==2?"Mono":"Other")); Serial.print("Sampling Rate :"); Serial.println(wavHeader.SamplesPerSec); Serial.print("Number of bytes per second :"); Serial.println(wavHeader.bytesPerSec); Serial.print("Block align :"); Serial.print(wavHeader.blockAlign); Serial.print(" validate: "); Serial.println(bytesPerSample * wavHeader.NumOfChan); Serial.print("Number of bits per sample :"); Serial.println(wavHeader.bitsPerSample); Serial.print("Data length :"); Serial.println(wavHeader.Subchunk2Size); // Audio format 1=PCM,6=mulaw,7=alaw, 257=IBM Mu-Law, 258=IBM A-Law, 259=ADPCM Serial.print("Data string (Subchunk2 ID) :"); Serial.print((char)wavHeader.Subchunk2ID[0]); Serial.print((char)wavHeader.Subchunk2ID[1]); Serial.print((char)wavHeader.Subchunk2ID[2]); Serial.println((char)wavHeader.Subchunk2ID[3]); Serial.print("Subchunk2 size :"); Serial.print(wavHeader.Subchunk2Size); Serial.print(" validate: "); Serial.println(numSamples * wavHeader.NumOfChan * bytesPerSample); if (memcmp((char*)wavHeader.Subchunk2ID,"LIST",4) == 0) { Serial.println("List chunk (of a RIFF file):"); uint8_t ListType[wavHeader.Subchunk2Size]; // RIFF Header Magic header bytesRead = fread(&ListType, 1, wavHeader.Subchunk2Size, f); if ( bytesRead > 0 ) { Serial.print(" --- List type ID :"); Serial.print((char)ListType[0]); Serial.print((char)ListType[1]); Serial.print((char)ListType[2]); Serial.println((char)ListType[3]); if (memcmp((char*)ListType,"INFO",4) == 0) { // INFO tag Serial.print(" --- --- INFO1 type ID :"); Serial.print((char)ListType[4]); Serial.print((char)ListType[5]); Serial.print((char)ListType[6]); Serial.println((char)ListType[7]); uint8_t sizeTxt = (ListType[11] << 24) | (ListType[10] << 16) | (ListType[9] << 8) | ListType[8]; Serial.print(" --- --- SizeD :"); Serial.print(sizeTxt); Serial.print(" Validate: "); Serial.println(wavHeader.Subchunk2Size - sizeTxt); } for (uint8_t x = 12; x < wavHeader.Subchunk2Size; x++) { if (ListType[x] >= 32 && ListType[x] <= 126) { Serial.print((char)ListType[x]); } else if (ListType[x] > 0) { Serial.print("0x"); Serial.print(ListType[x],HEX); } Serial.print(" "); } Serial.println(""); // Checking for data bytesRead = fread(&ListType, 1, 4, f); if (bytesRead > 0) { Serial.print("Data string (Subchunk3 ID) :"); for (uint8_t x = 0; x < 4; x++) { if (ListType[x] >= 32 && ListType[x] <= 126) { Serial.print((char)ListType[x]); } else if (ListType[x] > 0) { Serial.print("0x"); Serial.print(ListType[x],HEX); } } Serial.println(""); } bytesRead = fread(&ChunkSize, 1, 4, f); if (bytesRead > 0) { Serial.print("Subchunk3 size :"); Serial.print(ChunkSize); Serial.print(" validate: "); Serial.println(8+numSamples * wavHeader.NumOfChan * bytesPerSample); } } } else { ChunkSize = wavHeader.Subchunk2Size; } WAV_DATA = new uint8_t[ChunkSize]; //int8_t* buffer = new int8_t[BUFFER_SIZE]; bytesRead = fread(WAV_DATA, sizeof WAV_DATA[0], ChunkSize / (sizeof WAV_DATA[0]), f); if (bytesRead) { Serial.println("Sound File Data Read "); int fileSize = 0; fseek(f, 0, SEEK_END); fileSize = ftell(f); fseek(f, 0, SEEK_SET); Serial.print("File size is: "); Serial.print(fileSize); Serial.println(" bytes."); fclose(f); gpio_set_function(AUDIO_PIN, GPIO_FUNC_PWM); int audio_pin_slice = pwm_gpio_to_slice_num(AUDIO_PIN); // Setup PWM interrupt to fire when PWM cycle is complete pwm_clear_irq(audio_pin_slice); pwm_set_irq_enabled(audio_pin_slice, true); // set the handle function above irq_set_exclusive_handler(PWM_IRQ_WRAP, pwm_interrupt_handler); irq_set_enabled(PWM_IRQ_WRAP, true); // Setup PWM for audio output pwm_config config = pwm_get_default_config(); /* Base clock 176,000,000 Hz divide by wrap 250 then the clock divider further divides * to set the interrupt rate. * * 11 KHz is fine for speech. Phone lines generally sample at 8 KHz * * * So clkdiv should be as follows for given sample rate * 8.0f for 11 KHz * 4.0f for 22 KHz * 2.0f for 44 KHz etc */ pwm_config_set_clkdiv(&config, 8.0f); pwm_config_set_wrap(&config, 250); pwm_init(audio_pin_slice, &config, true); pwm_set_gpio_level(AUDIO_PIN, 0); } //delete [] buffer; //buffer = nullptr; } } else { Serial.println("File not found"); } } void setup() { set_sys_clock_khz(176000, true); // put your setup code here, to run once: Serial.begin(115200); MassStorage.begin(); // If you do not want to wait for Serial Monitor to load then use a long delay here. // Found a delay helps if you want to capture the initial serial output. while(!Serial) {;;} //delay(1000); Serial.println("\r\nMassStorage mounted"); Serial.println(""); Serial.flush(); //writeContents(); readContents(); } void loop() { // put your main code here, to run repeatedly: __wfi(); // Wait for Interrupt }
You will notice in the code a "while (!Serial) {;;}". This is basically to pause playing the audio until the serial monitor is opened. This is not required and is simply there for demo purposes.
Another important point to make about the code is that I am buffering the whole file into RAM. This limits the size of the audio file to about 10 seconds for 8 bit sampling rate at 11kHz. This was simply a short cut to get me started. Please note that there are no size checks in the code, so it's best to amend if larger audio files want to be tested.