Psoc 6 Audio Sensor

About the project

Truly listen to what is around you, with the use of some electronics of course. Microphones can 'hear' more than your ears. This project will guide you through measurement, calculations and interpretation of that!

The Great PSoC 6 Design Challenge Contest WinnerThe Great PSoC 6 Design Challenge contest winner

Project info

Difficulty: Difficult

Platforms: CypressInfineon

Estimated time: 2 weeks

License: GNU General Public License, version 3 or later (GPL3+)

Items used in this project

Hardware components

Psoc6 Wifi- Bt Ultra-low-power Kit Psoc6 Wifi- Bt Ultra-low-power Kit WiFi BT Prototyping Kit (CY8CPROTO-062-4343W) x 1

Software apps and online services

InfluxDB InfluxDB To store our data in a timescale database
PuTTY PuTTY Where you can see the printf() responses from our program
Eclipse IDE for ModusToolbox Eclipse IDE for ModusToolbox Our programming and debug environment
Grafana Grafana For advanced graphing

Story

For the "The Great PSoC 6 Design Challenge", Electromaker did provide me a Cypress PSoC 6 - WiFi BT Prototyping Kit (CY8CPROTO-062-4343W). A development board that is roughly 12 by 8 centimeter in size. The MCU on this board is of course the PSoC 6 chipset with dual cores: one 150-MHz Arm Cortex-M4 and one 100-MHz Arm Cortex-M0+.

One of the nice features here, is that once your prototyping is complete, the board can be broken into 6 pieces, and you only keep the parts that you need for your project. The main board is 12 by 2.7 centimeter and has all you need for basics: the MCU, WiFi, Bluetooth, a button and an LED. A plethora of pins are broken out around it.

Then you have 5 parts that you can mix-'n-match for your project needs:

  1. An SD Card Reader & Flash Memory
  2. Two PDM Microphones and a Thermistor
  3. A CapSense Slider and Buttons
  4. A Digilent  PMOD Interface to easily connect external boards (once headers are soldered on)
  5. The KitProg3 on-board Programmer & Debugger (of course once your project is complete, you don't need this part anymore)

Image source: Jeremy S. Cook

 

What will I make with that?

There is always one big question when you subscribe to a contest; "What will you make?"

My plan was to use this board with the stereo microphone, to capture sounds and process that to find out if there are sonar calls from bats. Results are to be uploaded to an Influx database, possibly via NodeRED for data manipulation. And graphing can be done in Grafana. If time permitted, I wanted to extend this to find out if I can distinguish different types of bats.

Well, that went a bit differently... I did see the last bats flying in November, but when I got the board in December, bats where all in their winter hibernation. And a technical issue; the microphones would not be able to process sonar sounds (more on that later).  So that would be a nice programming experience, with no testing evidence...

No worries, I started working with the board anyway to get the microphones working. First the example sketches for the CapSense and Audio/Volume, then combined the two. Next added WiFi connection to it, and then sent the volume to InfluxDB. It took some time to get used to the Eclipse IDE for ModusToolbox, but after some weeks of playing, it worked quite steady.

But I wasn't satisfied yet, I wanted to do a more with the Sound/Audio part, more realtime, and more detail. And that was a wasp nest! To help you achieve the same, and don't get stung by the wasps, this project is to share my learned knowledge with you all!

You can watch this video that goes through all of this, or read the full project. The video will show you a live demo, including me playing saxophone (first time again since 25 years...)

 

Sound or Audio?

Sound is a vibration of air (or other medium), that by our ears and brains can be perceived as tones. It can be caused by any source.

Audio is referring to sound coming from a recording, transmission or electronic device.

As sound is recorded by the microphone and then turns into audio, when I talk about what happens in code, I call it audio. When it is about how your ears perceive something, I call it sound.

 

What is Sound?

Besides the short description above, lets explain that a bit deeper. Starting with how we perceive it with our ears and brains. Starting simple: somebody plays a flute in a steady tone, not too loud. This starts a vibration of the air. For now, think of that as a single wave pattern, we call that a frequency. If you  draw this, this is a wave that goes up and down in a uniform looking form; a sine wave. The higher the tone, the higher the frequency is; shorter waves. The loudness defines how high or low the wave is. When somebody else plays a trumpet at a steady tone and somewhat louder, you will hear the tone height is different. That means it is in another frequency, presumably a lower frequency, and the energy is higher. And add a piano as well, also with a single steady tone, but very loud. We now have 3 different tones, 3 frequencies and a different loudness.

But frequencies is something we perceive with our ears, or if only one instrument at a time plays. At this point, we just talked about vibration of the air of single instrument. With our 3 instruments at a different frequencies and loudness, the overall air pressure will be a sum of the 3. So it looks more like this:

And that is the start of everything that is used for Sound and Audio processing!

 

Your ears: sound processing

Looking at the anatomy of your ears, air pressure causes the eardrum or membrane to vibrate. Which sets the three tiny bones in the middle ear into motion. The motion of the bones causes the fluid in the cochlea to moveThe cochlea works as a frequency filter and sends detailed signals to your brains. The cochlea can register sounds from 20 Hz to 20 kHz. The NIH made a nice animation of this.

The voiced speech of a typical adult male will have a fundamental frequency from 85 to 180 Hz, and that of a typical adult female from 165 to 255 Hz [Source]. So that is what we at least need to capture crisp and clear.

At the edges the registration is less (compared to technical sound measurement), but in the middle around 2.5 kHz there is actually an amplifying peak. When you grow older, these edges do register less and less sound, mainly at the high frequency side. So you don't hear the high tones as good as a youngster anymore.

But it doesn’t tell you if you hear a piano, flute  or trumpet! That is what your brains will do, based on lot's of other factors.

 

Microphones: audio processing

Our PSoC 6 Prototyping Kit has two Knowles SPK0838HT4H-B microphones, with a Frequency Response of 20 Hz - 20 kHz. As we did see from the cochlea, this is ideal for voice input! But we are missing out everything in higher frequencies, like the sonar sounds of the bats who use calls between 25 kHz and 120 kHz...

Most of the microphones used today, for example in your mobile phone or voice assistant, are MEMS (Micro-Electro-Mechanical System) microphones.

How do these work?

These microphones are little boxes on your PCB, sometimes looking like a little tin. The one on the PSoC board is a grey box (see the right-hand image below). At the top you can see a little hole. But there are also versions where the hole is at the bottom side, through the PCB.MEMS  diaphragm registers the air pressure differences as an analog signal. It sends these analog signals to the ASIC (Application-Specific Integrated Circuit). The ASIC then translates this to a digital signal in PDM format.

PDM (Pulse-Density Modulation)

Pulse-Density Modulation is used to represent an analog signal with a binary signal. That means it does send LOW and HIGH pulses, in a frequency that was agreed upfront. The relative density of the pulses, corresponds to the analog signal amplitude. This is a modulation used for transmitting audio signals in a digital way. If you would make a picture of the signal, it looks kind of like a bar-code.

PCM (Pulse-Code Modulation)

To make it easier for our program to work with the signals, PDM is translated to Pulse-Code Modulation. This is still digital, but instead of LOW and HIGH, this presented with numbers. That makes it easier to let it look like our air pressure way of presentation. Depending on the number of readings, the line might be as fluid as here, or look more blocky.

Audio Processing raw data

Of course you can write a program to listen to the PDM signals that are received, and translate that into PCM data. Luckily there is a standard for that, and Cypress did even give as a tool for it in ModusToolbox: the PDM-PCM conversion!

The output we get is in 16 bit word length, which we can convert to floats. The result we get now, is data between -1 and 1, with the center at 0.

As a recap; this is what our ears would hear with some help from our brains:

But this is what the microphone 'hears':

And that is exactly what we need for next steps!

 

Fourier Transform

In the world of mathematics, there is always somebody who finds a formula that fixes your problem. In this case, around 1822, a French mathematician Joseph Fourier, found a way transform time based data, into frequency based data, by splitting it into many repeatable functions. Although only long after that, this was valued to it's worth and got the name Fourier Transform. This mathematical solution is seen as one of the most important algorithms that exist. It is used for so many things around us. Just thinking of today's technology: voice calls, music, streaming video, image storing, file compression, etc.

Sources I used to understand this, are this YouTube video from 3Blue1Brown that explains this very well, it made so many things clear for me! Then we have this page from Elan Ness-Cohn, with clear animated images, based on the information from 3Blue1Brown. And when you want to take a really deep dive into Fourier Transforms, watch this YouTube video series from Steve L. Brunton.

Hann Window

One of the inputs for the Fourier Transform, is the repeatable function. These are called Window functions. For audio transform, the Hann Window (also "Von Hann Window" or "Hanning Window") is most widely used. Remember our PDM-PCM input after the IntegerToFloat conversion was all between -1 and 1? In very simple words, the Hann Window makes sure all negative values are raised above 0, in fact to data between 0 and 1, with the center at 0.5. 

Quick and very dirty Fourier Transform

If you wrap our data input as a circle around a mid-point, and calculate the Center of Mass of it, you will see it ends up somewhere close to the middle in most cases. If you then start to change the winding speed (the frequency) of that wrapped data, you will see that there are peaks occurring. This is the energy of that exact frequency. By going over half of the dataset, this will give a view of all our frequencies, and the energy for each of them.

 

Octaves

To make something meaningful of that, we can bin the data into Octaves. In music, an octave is the interval between one musical pitch and another with double its frequency.

But for sound and audio processing, there are used Octave-bands. In our audible range of 20 Hz to 20 kHz, there are 10 Octave-bands, that have overlapping outer ranges.

In the current code, all input is taken 100% from one octave to the next. So this could be improved!

What we effectively do, is that our Fourier Transform result, is split into equal pieces. Notice that above Octave-band visualization uses logarithmic horizontal ax, and in below visualization there is a normal horizontal ax. That is why the size of the octaves looks like it is getting larger.

Then we sum the results of each piece together, divide it by the number of inputs, and we can see the total Sound Pressure per octave:

 

Frequency Weighting

Microphones are designed to hear everything within technical range

Your ears are designed to hear everything that is important

And with important, we mean "everything we need to survive". And you can see that in how our cochlea works: it can register sounds from 20 Hz to 20 kHz, with lower registration at the edges and amplification in the middle.

As our microphone did register everything that it could hear at a technical level, and that is different compared to what our ears would do before sending it off to your brains. The high and low tones have a much lower energy registration by our ears. So we have to correct our received audio for that.

This is what we call frequency weighting, where we mainly use 3 weightings:

  • A-Weighting is used for quiet environments
  • C-Weighting is used for noisy environments
  • Z-Weighting is the “zero” line that is our microphone source

A full table of applied Weightings, can be found on the website from NTI-audio.

By limiting our readings following the A-Weighting, we get back to what our ears would have perceived:

Instead of a table with a single value per (third of an) octave, we could also calculate with a formula what the detailed weighting would be in each reading of our dataset. But we would have to do that before we do split into the octave-bands. So the order of steps will be changed by that.

 

Now you know all you need for Sound and Audio processing to get to the next steps. Let's start digging into our software!

 

Single thread vs. Real Time Operating System

In classic "single thread" operating systems, all tasks will be executed one-by-one, in a predefined order.

There usually is a "setup()" class that is used to initiate objects and configuration that is needed for all following code and only need to happen once. Then you use the "main()" or "loop()" class for the actual program. As the name says, the code loops through this endlessly. And everything happens after each other, there never is something that happens at the same time as something else. If we write our program very clever, the loop will be super fast and each step is touched within milliseconds. It could even serve steps into other processes/classes. But still, it has to wait for the process/class to complete before it goes to the next step.

Using for example a button for momentary input, would be very hard. The program will once in each loop check for the state of the button. And although in milliseconds, we might miss it being pressed. Most programming languages offer Interrupts for that. If a change is detected at the button, it will set a flag that is checked in the next run of the loop. But that might still take a little before that happens, because your loop might take a little longer this time when a bigger task it assigned.

That is where Real Time Operating Systems come into play! Still the processor does 1 task at a time, but these operating systems are able to give little sub-tasks a little bit of processor time, and keep track of their states and priorities. It does offer functionality to let one task talk to another task with queues and semaphores. And tasks can suspend or resume each other. The delay() function that would cause a full system halt, is replaced by a function that halts the current task only. So a lot of clever stuff is going on here, but most important, multiple tasks can run at the same time!

One of these Real Time Operating Systems is FreeRTOS, and does support our PSoC 6 processor. So that is what will be used in this project.

 

Splitting our program into tasks

To make optimal use of tasks, we have to define what the tasks are that have to run in parallel.

For us that will be (also see the RTOS example picture):

  • WiFi task: at startup, create a WiFi connection and receive the current datetime stamp from an NTP server, and store that in the RTC. If connection is lost, create a new connection.
  • CapSense tasks: 'listen' to the CapSense slider and buttons. Once something happens, immediately publish the values to let other tasks use it.
  • Audio task: get data from the microphone, do many calculations with the data and show the results
  • Influx task: data collection and sending the data over TCP to the InfluxDB API when our queue/stack is full

Each task will get it's own priority, meaning FreeRTOS will decide who goes first if two tasks ask for processing time at the same moment:

  • As the CapSense task has to monitor the slider/button state, these get the highest priority. On the other hand, they do not have to do a lot, so they will most time be in a dormant mode.
  • Then we have the Audio Task that might consume most calculation power. And I want this to happen fast, to get to the next audio frame as quickly as possible.
  • The Influx task is just collecting data, and once every so many seconds does really upload the collected data, so the priority is low.
  • The WiFi task seems to need quite some priority at the moment of starting up, but once connection is created, the priority is low. That is why I've added a little trick here: it will suspend all other tasks until the connection is complete and the timestamp is processed. Then it resumes the other tasks.

 

Deeper into the code

Now it is time to look into the code. I'll pick some highlights per function and will skip the header files, use the download/viewer at the bottom of this page to get the full details.

main.c:

This is where we start our task management.

1: First we create the queues. See the respective data-types for details of queue contents. There is one used for the CapSense interrupt changes that is only 1 large as each new command should replace a non-processed one, and one for the communication between the Audio task and Influx task we have a queue of 200 messages as these are the audio readings and we want them all to be processed:

  1. capsense_command_q = xQueueCreate(SINGLE_ELEMENT_QUEUE, sizeof(capsense_command_t));
  2. influx_command_data_q = xQueueCreate(200, sizeof(influx_command_data_t));

2: Then we create the 4 tasks and start them. See the explanations in previous paragraph for more details of these tasks.

  1. xTaskCreate(capsense_task, "CapSense Task", TASK_CAPSENSE_STACK_SIZE, NULL, TASK_CAPSENSE_PRIORITY, NULL);
  2. xTaskCreate(audio_task, "Audio Task", TASK_AUDIO_STACK_SIZE, NULL, TASK_AUDIO_PRIORITY, NULL);
  3. xTaskCreate(wifi_task, "WIFI Task", TASK_WIFI_STACK_SIZE, NULL, TASK_WIFI_PRIORITY, NULL);
  4. xTaskCreate(influx_task, "Influx Task", TASK_INFLUX_STACK_SIZE, NULL, TASK_INFLUX_PRIORITY, NULL);
  5. vTaskStartScheduler();

Once the four tasks are started by the scheduler, we should never return into this part of the code. If we do return here, something went wrong and we can exit.

wifi_task.c

This file does handle the WiFi connection, and receives data from an NTP (Network Time Protocal) server through UDP (). Let's break the code into pieces.

1: Task management: let the 3 other tasks start, and suspend themselves. I've added this because my WiFi connection seems to freeze sometimes. I couldn't find why so far, but suspending everything else made it at least a little bit more reliant. If you have a tip or trick for me, let me know! 

Then the FOR loop is entered. I do have that here, because this is also used in case of a disconnect. First step is suspending the other tasks if they didn't do that on their own. Then I use the WiFi_Connect_To_AP() function for getting the WiFi connection. And if that is successful, I use the NTP_Get_Time() function. If both these are successful, the other 3 tasks are started, and the WiFi Task is suspended. It will only be woken by a disconnect, which should not happen too often.

  1. /* wait a little bit to give the other tasks time to suspend on their own */
  2. vTaskDelay(pdMS_TO_TICKS(1000u));
  3. /* Obtain the handle of a task from its name and resume the tasks. */
  4. xHandle_Audio = xTaskGetHandle( "Audio Task" );
  5. xHandle_Influx = xTaskGetHandle( "Influx Task" );
  6. xHandle_CapSense = xTaskGetHandle( "CapSense Task" );
  7. xHandle_WIFI = xTaskGetHandle( "WIFI Task" );
  8.  
  9. /* "Repeatedly" running part of the task; after 1 run it does suspend,
  10. * but a resume could wake it up, suspending other tasks temporarily */
  11. for(;;) {
  12.     /* As Wifi needs full system resources to successfully start,
  13.      * suspend all other tasks for a moment. */
  14. vTaskSuspend( xHandle_Audio );
  15. vTaskSuspend( xHandle_Influx );
  16. vTaskSuspend( xHandle_CapSense );
  17.  
  18. vTaskDelay(pdMS_TO_TICKS(1000u));
  19.  
  20. /* Connect to WiFi AP */
  21.     if(wifi_connect_to_ap() != CY_RSLT_SUCCESS ) {
  22. printf("33[91mWIFI: Failed to connect to AP.33[mn");
  23. CY_ASSERT(0);
  24.     }
  25.  
  26.     /* Get current time from NTP server and store in RTC */
  27.     if(ntp_get_time() != CY_RSLT_SUCCESS ) {
  28. printf("33[91mNTP: Failed to get current time.33[mn");
  29. CY_ASSERT(0);
  30.     }
  31.  
  32.     /* Wifi and NTP/RTC completed, now resume the tasks. */
  33.     vTaskResume( xHandle_CapSense );
  34.     vTaskResume( xHandle_Influx );
  35.  
  36.     vTaskDelay(pdMS_TO_TICKS(1000u));
  37.     vTaskResume( xHandle_Audio );
  38.  
  39.     /* Wifi task can suspend now, waiting for a renewed connection attempt */
  40.     vTaskSuspend( xHandle_WIFI );
  41. }

2: Connecting to the WiFi AP: based on the credentials that are entered in the wifi_task.h file, a connection is configured. There is a callback that is used to print the IP address at connection, or will start this task over when a disconnect is registered. 

  1. /* Initialize WiFi connection manager. */
  2. result = cy_wcm_init(&wifi_config);
  3. if (result != CY_RSLT_SUCCESS) {
  4.     printf("33[91mWIFI: Connection Manager initialization failed!33[mn");
  5.     return result;
  6. }
  7.  
  8. /* Set the WiFi SSID, password and security type. */
  9. memset(&wifi_conn_param, 0, sizeof(cy_wcm_connect_params_t));
  10. memset(&ip_address, 0, sizeof(cy_wcm_ip_address_t));
  11. memcpy(wifi_conn_param.ap_credentials.SSID, WIFI_SSID, sizeof(WIFI_SSID));
  12. memcpy(wifi_conn_param.ap_credentials.password, WIFI_PASSWORD, sizeof(WIFI_PASSWORD));
  13. wifi_conn_param.ap_credentials.security = WIFI_SECURITY_TYPE;
  14.  
  15. /* Join the WiFi AP. */
  16. for(uint32_t conn_retries = 0; conn_retries < MAX_WIFI_CONN_RETRIES; conn_retries++ ) {
  17.     result = cy_wcm_connect_ap(&wifi_conn_param, &ip_address);
  18.  
  19.     if(result == CY_RSLT_SUCCESS) {
  20.         printf("33[94mWIFI: Successfully connected to network '%s'.33[mn",
  21.                 wifi_conn_param.ap_credentials.SSID);
  22.  
  23.         /* Callback event that shows disconnects and reconnects. */
  24.         cy_wcm_register_event_callback(wifi_network_event_callback);
  25.  
  26.         /* We are successfully connected, exit the function with a success message */
  27.         return CY_RSLT_SUCCESS;
  28.     }
  29.     printf("33[91mWIFI: Connection to network failed with error code %d. Retrying in %d ms...33[mn",
  30.             (int)result, WIFI_CONN_RETRY_INTERVAL_MSEC);
  31.     vTaskDelay(pdMS_TO_TICKS(WIFI_CONN_RETRY_INTERVAL_MSEC));
  32. }
  33.  
  34. /* Stop retrying after maximum retry attempts. */
  35. printf("33[91mWIFI: Exceeded maximum connection attempts.33[mn");

3: Getting the NTP time: this is where I had a lot of struggles with. There are complete NTP libraries available for C++, but not an easy implementation for C. Based on snippets from here and there, and the UDP example sketch from ModusToolbox, I was able to get this working. The NTP connection creates an interrupt for receiving data. Once the interrupt happens and was able to correctly process the NTP packet into the RTC, the main class will continue.

  1. /* IP address and UDP port number of the UDP server */
  2. cy_socket_sockaddr_t udp_server_addr = {
  3.     .ip_address.ip.v4 = NTP_SERVER_IP_ADDRESS,
  4.     .ip_address.version = CY_SOCKET_IP_VER_V4,
  5.     .port = NTP_SERVER_PORT
  6.     };
  7. /* Prepare the NTP packet that we will send:
  8. * Set the first byte's bits to 01,100,011;
  9.  * LI = 1 (01) (Leap Indicator "last minute of the day has 61 seconds",
  10.  *               ignored in request)
  11. * VN = 4 (100) (Version Number)
  12. * Mode = 3 (011) (Mode = 3: client)
  13.  */
  14. memset( &packet, 0, sizeof( ntp_packet ) );
  15. *( ( char * ) &packet + 0 ) = 0b01100011;
  16. /* Sockets initialization */
  17. cy_socket_init();
  18. /* Create a UDP socket. */
  19. cy_socket_create(CY_SOCKET_DOMAIN_AF_INET, CY_SOCKET_TYPE_DGRAM,
  20.                     CY_SOCKET_IPPROTO_UDP, &udp_client_handle);
  21. /* Variable used to set socket receive callback function. */
  22. cy_socket_opt_callback_t udp_recv_option = {
  23. .callback = ntp_client_recv_handler,
  24. .arg = NULL};
  25. /* Register the callback function to handle messages received from UDP client. */
  26. cy_socket_setsockopt(udp_client_handle, CY_SOCKET_SOL_SOCKET, CY_SOCKET_SO_RECEIVE_CALLBACK,
  27.              &udp_recv_option, sizeof(cy_socket_opt_callback_t));
  28. /* Send NTP data to Server */
  29. cy_socket_sendto(udp_client_handle, (char*) &packet, sizeof(ntp_packet), CY_SOCKET_FLAGS_NONE,
  30. &udp_server_addr, sizeof(cy_socket_sockaddr_t), &bytes_sent);
  31. /* When we are here, we did successfully send a message to the NTP server.
  32. * Now we wait for a returning message */
  33. for(;;) {
  34.     if(received_time_from_server) {
  35.         /* Return message is received and time is extracted, so jump out of the FOR loop */
  36.         break;
  37.     }
  38.     vTaskDelay(RTOS_TASK_TICKS_TO_WAIT);
  39. }
  40. /* Read the time stored in the RTC */
  41. cyhal_rtc_read(&rtc_obj, &date_time);
  42. time_t t_of_day = mktime(&date_time);
  43. printf("33[94mRTC: %s33[m", asctime(&date_time));
  44. printf("33[94mRTC: seconds since the Epoch: %ld33[mn", (long) t_of_day);
  45. /* All done for the UDP/NTP/RTC functions! */
  46. /* Disconnect the UDP client. */
  47. cy_socket_disconnect(udp_client_handle, 0);
  48. /* Free the resources allocated to the socket. */
  49. cy_socket_delete(udp_client_handle);

Within the interrupt we handle unraveling the NTP message and storing it into the RTC:

  1. /* To hold the NTP time */
  2. struct tm * new_time = {0};
  3. /* Receive incoming message from UDP server. */
  4. result = cy_socket_recvfrom(udp_client_handle, &packet, MAX_UDP_RECV_BUFFER_SIZE,
  5. CY_SOCKET_FLAGS_RECVFROM_NONE, NULL, 0, &bytes_received);
  6. if ( bytes_received < 0 ) {
  7. printf( "33[91mUDP: error reading from socket33[mn" );
  8.     return result;
  9. }
  10. /* These two fields contain the time-stamp seconds as the packet left the NTP server.
  11. * The number of seconds correspond to the seconds passed since 1900.
  12. * ntohl() converts the bit/byte order from the network's to host's "endianness".
  13. */
  14. packet.txTm_s = ntohl( packet.txTm_s ); // Time-stamp seconds.
  15. packet.txTm_f = ntohl( packet.txTm_f ); // Time-stamp fraction of a second.
  16. /* Extract the 32 bits that represent the time-stamp seconds (since NTP epoch)
  17. * from when the packet left the server.
  18. * Subtract 70 years worth of seconds from the seconds since 1900.
  19. * This leaves the seconds since the UNIX epoch of 1970.
  20. * (1900)------------------
  21. * (1970)**************************************(Time Packet Left the Server)
  22. */
  23. time_t txTm = ( time_t ) ( packet.txTm_s - NTP_TIMESTAMP_DELTA );
  24. /* Print the time we got from the server, in UTC time. */
  25. printf( "33[94mNTP: time received: %s33[m", ctime( ( const time_t* ) &txTm ) );
  26. /* Initialize RTC */
  27. cyhal_rtc_init(&rtc_obj);
  28. new_time = gmtime(&txTm);
  29. /* Write the time to the RTC */
  30. cyhal_rtc_write(&rtc_obj, new_time);
  31. printf("33[94mRTC: Time updated.33[mn");
  32. received_time_from_server = true;

capsense_task.c

This is where all the Slider and Button handling happens. And beside some very little changes, this actually is exactly the code from the ModusToolbox example.

The task goes into suspend mode immediately and will wait there until the WiFi connection is completed. To show that it started, print something to the debug lines.

  1. /* Immediately suspend the task to give WIFI connection priority */
  2. vTaskSuspend( NULL );
  3. printf("33[94mCapSense: task started!33[mn");

Then the CapSense is tuned and initialized. Ready for creating the endless FOR loop to check for changes.

  1. BaseType_t rtos_api_result;
  2. cy_status result = CY_RSLT_SUCCESS;
  3. capsense_command_t capsense_cmd;
  4. /* Initialize timer for periodic CapSense scan */
  5. scan_timer_handle = xTimerCreate ("Scan Timer", CAPSENSE_SCAN_INTERVAL_MS,
  6.              pdTRUE, NULL, capsense_timer_callback);
  7. /* Setup communication between Tuner GUI and PSoC 6 MCU */
  8. tuner_init();
  9. /* Initialize CapSense block */
  10. capsense_init();
  11. /* Start the timer */
  12. xTimerStart(scan_timer_handle, 0u);
  13. /* Repeatedly running part of the task */
  14. for(;;) {

Here we use the FreeRTOS Queue function to receive data that the interrupt generated. There are two types of messages we can receive in the queue: SCAN and PROCESS. In case of the second one, the interrupt did receive updated data, so we will process that. The process does compare the previous button states, and if a difference is found, the "CapSense_slider_value" is updated with the new value. And that variable is used later on in the Audio task. This CapSense task doesn't have to do anything else. Which is a good thing and it's purpose: it should be very fast in responding to a touch of the sensor!

  1. /* Block until a CapSense command has been received over queue */
  2. rtos_api_result = xQueueReceive(capsense_command_q, &capsense_cmd,
  3. portMAX_DELAY);
  4. /* Command has been received from capsense_cmd */
  5. if(rtos_api_result == pdTRUE) {
  6.     /* Check if CapSense is busy with a previous scan */
  7. if(CY_CAPSENSE_NOT_BUSY == Cy_CapSense_IsBusy(&cy_capsense_context)) {
  8.     switch(capsense_cmd) {
  9.             case CAPSENSE_SCAN:
  10. {
  11.                 /* Start scan */
  12.                 Cy_CapSense_ScanAllWidgets(&cy_capsense_context);
  13.                 break;
  14.             }
  15.             case CAPSENSE_PROCESS:
  16.             {
  17.                 /* Process all widgets */
  18.                 Cy_CapSense_ProcessAllWidgets(&cy_capsense_context);
  19.                 process_touch();
  20.                 /* Establishes synchronized operation between the CapSense
  21.                  * middleware and the CapSense Tuner tool. */
  22.                 Cy_CapSense_RunTuner(&cy_capsense_context);
  23.                 break;
  24.             }
  25.         }
  26.     }
  27. }
  28. /* Task has timed out and received no data during an interval of
  29. * portMAXDELAY ticks. */
  30. else {
  31. /* You could handle a timeout here */
  32. }

 

This is the actual touch processing code, which uses the Slider values to put a threshold on the LED in the audio task. Left button for is equal to minimum value (1), and Right button for maximum value (100).

  1. static void process_touch(void)
  2. {
  3. /* Variables used to store touch information */
  4. uint32_t button0_status = 0;
  5. uint32_t button1_status = 0;
  6. uint16_t slider_pos = 0;
  7. uint8_t slider_touched = 0;
  8. cy_stc_capsense_touch_t *slider_touch;
  9. uint32_t CapSense_slider_reading = 1;
  10. /* Variables used to store previous touch information */
  11. static uint32_t button0_status_prev = 0;
  12. static uint32_t button1_status_prev = 0;
  13. static uint16_t slider_pos_perv = 0;
  14. /* Get button 0 status */
  15. button0_status = Cy_CapSense_IsSensorActive(
  16. CY_CAPSENSE_BUTTON0_WDGT_ID,
  17. CY_CAPSENSE_BUTTON0_SNS0_ID,
  18. &cy_capsense_context);
  19. /* Get button 1 status */
  20. button1_status = Cy_CapSense_IsSensorActive(
  21. CY_CAPSENSE_BUTTON1_WDGT_ID,
  22. CY_CAPSENSE_BUTTON1_SNS0_ID,
  23. &cy_capsense_context);
  24. /* Get slider status */
  25. slider_touch = Cy_CapSense_GetTouchInfo(
  26. CY_CAPSENSE_LINEARSLIDER0_WDGT_ID,
  27. &cy_capsense_context);
  28. slider_pos = slider_touch->ptrPosition->x;
  29. slider_touched = slider_touch->numPosition;
  30. /* Detect new touch on Button0 */
  31. if((0u != button0_status) && (0u == button0_status_prev)) {
  32. CapSense_slider_value = 1;
  33. }
  34. /* Detect new touch on Button1 */
  35. if((0u != button1_status) && (0u == button1_status_prev)) {
  36. CapSense_slider_value = 100;
  37. }
  38. /* Detect new touch on slider */
  39. if((0u != slider_touched) && (slider_pos_perv != slider_pos )) {
  40. CapSense_slider_reading = (slider_pos * 100)
  41. / cy_capsense_context.ptrWdConfig[CY_CAPSENSE_LINEARSLIDER0_WDGT_ID].xResolution;
  42. CapSense_slider_value = (CapSense_slider_reading < 1) ? 1 : CapSense_slider_reading;
  43. }
  44. /* Update previous touch status */
  45. button0_status_prev = button0_status;
  46. button1_status_prev = button1_status;
  47. slider_pos_perv = slider_pos;
  48. }

audio_task.c

Now we get into the interesting part. All the sound recording and audio calculation functions!

For the calculation part, I've spend quite some time googling for examples. And I have to say, there is nothing right out of the box 100% working to find. On the logical order, these were good pieces of information: [Stack Overflow] and [Stack Overflow]. Then code examples, but although each with their own things that didn't tie out perfectly and that I changed in my code: [Noise Level Meter] and [LoRaSoundkit]. And I've used [ArduinoFFT], where I only used the parts needed for this project, to keep it simple to follow (and was less work to convert it into a ModusToolbox working function).

My sound input is a 44.100 Hz samples/second, resulting in 4096 data points. That means we can sample is 10.7666 Hz per step. As we can only use half of the data points for steps (2048 steps), that means we have a range of 22 kHz. We start by setting up the microphone with these settings. The decimation rate is used in the PDM to PCM conversion. The system clock hz is used to synchronize the clock speed with the pdm/pcm clocks.

  1. /* Define how many samples in a frame */
  2. #define FRAME_SIZE 4096u
  3. /* Desired sample rate. Typical values: 8/16/22.05/32/44.1/48 kHz */
  4. #define SAMPLE_RATE_HZ 44100u
  5. /* Decimation Rate of the PDM/PCM block. Typical value is 64. DEC_RATE = 2 x SINC_RATE */
  6. #define DECIMATION_RATE 64u
  7.  
  8. /* Audio Subsystem Clock. Typical values depends on the desire sample rate:
  9. - 8/16/48kHz : 24.576 MHz
  10. - 22.05/44.1kHz : 22.574 MHz */
  11. #define AUDIO_SYS_CLOCK_HZ 22574000u
  12.  
  13. const cyhal_pdm_pcm_cfg_t pdm_pcm_cfg =
  14. {
  15. .sample_rate = SAMPLE_RATE_HZ,
  16. .decimation_rate = DECIMATION_RATE,
  17. .mode = AUDIO_MODE,
  18. .word_length = 16, /* bits */
  19. .left_gain = GAIN_LEFT , /* dB */
  20. .right_gain = GAIN_RIGHT , /* dB */
  21. };
  22. /* Init the clocks */
  23. clock_init();
  24. /* Initialize the PDM/PCM block */
  25. cyhal_pdm_pcm_init(&pdm_pcm, PDM_DATA, PDM_CLK, &audio_clock, &pdm_pcm_cfg);
  26. cyhal_pdm_pcm_register_callback(&pdm_pcm, pdm_pcm_isr_handler, NULL);
  27. cyhal_pdm_pcm_enable_event(&pdm_pcm, CYHAL_PDM_PCM_ASYNC_COMPLETE, CYHAL_ISR_PRIORITY_DEFAULT, true);
  28. cyhal_pdm_pcm_start(&pdm_pcm);

In above code, 3rd last line, there is a callback created. That callback does happen when recording an audioframe is completed and data is available. At that moment, only the flag "pdm_pcm_flag" is set to true.

As this Audio Task will use the onboard LED to show the total dB of the last reading, we need to init that as well:

  1. /* Initialize a PWM resource for driving an LED. */
  2. cyhal_pwm_init(&pwm_led, CYBSP_USER_LED, NULL);
  3. cyhal_pwm_set_duty_cycle(&pwm_led, GET_DUTY_CYCLE(LED_MAX_BRIGHTNESS), PWM_LED_FREQ_HZ);
  4. cyhal_pwm_start(&pwm_led);

And now we get to the real fun in the FOR loop!

First step is to check if the "pdm_pcm_flag" is set. If not, it will skip all the code to the end, and waits for 1 millisecond before trying again. Using this 1 millisecond delay, gives other tasks the opportunity to do their things as well (like uploading the data to InfluxDB). When the flag is set, immediately get the RTC time so that we know when this reading happened. We use that time to compare with previous readings to see if we are still in the same second. When we jumped into the next second, first average all previous readings and stack them into the Influx queue. More on what happens there later on.

  1. if (pdm_pcm_flag) {
  2.     /* Clear the PDM/PCM flag */
  3.     pdm_pcm_flag = false;
  4.  
  5.     /* Read current time from the RTC to test of reachable*/
  6.     cyhal_rtc_read(&rtc_obj, &date_time);
  7.  
  8.     if ((long) mktime(&date_time) > (long) mktime(&avg_date_time)) {
  9.         /* We are in the next second, calculate averages of the previous second! */
  10.         for ( int i = 0; i < OCTAVES; i++) {
  11.             audio_z_avg[i] /= (float) avg_count;
  12.             audio_a_avg[i] /= (float) avg_count;
  13.         }
  14.  
  15.         /* calculate the dB of the averages */
  16.         soundsensor_toDecibel(audio_z_avg, OCTAVES);
  17.         soundsensor_toDecibel(audio_a_avg, OCTAVES);
  18.  
  19.         /* Send off the Z weighted readings to the Influx task */
  20.         influx_cmd_data.influx_command = INFLUX_DATA;
  21.         influx_cmd_data.audio_weighting = "z";
  22.         influx_cmd_data.audio_octave01 = audio_z_avg[0];
  23.         /* same happens for octave 2-11 */
  24. influx_cmd_data.audio_volume = audio_z_avg[OCTAVES];
  25. influx_cmd_data.sec_since_epoch = (long) mktime(&avg_date_time);
  26. xQueueSendToBack(influx_command_data_q, &influx_cmd_data, 0u);
  27.  
  28. /* Send off the A weighted readings to the Influx task */
  29.  
  30.         /* reset the spectrum output */
  31.         avg_count = 0;
  32.         avg_date_time = date_time;
  33.         for ( int i = 0; i < OCTAVES; i++) {
  34.             audio_z_avg[i] = 0.0;
  35.     audio_a_avg[i] = 0.0;
  36. }
  37.     }
  38. /* Last second average completed, now process new audio data */

And yes, there it is, here is where the audio calculations happen!

  1. /* Convert the 16 bit integer to a float */
  2. soundsensor_integerToFloat(audio_frame, audio_real, audio_imag, FRAME_SIZE);
  3.  
  4. /* apply HANN window, optimal for energy calculations */
  5. arduinoFFT_Windowing(FFT_WIN_TYP_HANN, FFT_FORWARD, audio_real, FRAME_SIZE);
  6.  
  7. /* do FFT processing */
  8. arduinoFFT_Compute(FFT_FORWARD, audio_real, audio_imag, FRAME_SIZE);
  9.  
  10. /* calculate energy */
  11. soundsensor_calculateEnergy(audio_real, audio_imag, FRAME_SIZE);
  12.  
  13. /* sum up energy in bin for each octave */
  14. soundsensor_sumEnergy(audio_real, audio_energy, FRAME_SIZE, OCTAVES);
  15.  
  16. /* update the spectrum with new measurements */
  17. soundsensor_loudnessWeighting(audio_energy, zWeighting, audio_z_spectrum, OCTAVES);
  18. soundsensor_loudnessWeighting(audio_energy, aWeighting, audio_a_spectrum, OCTAVES);
  19.  
  20. /* Add the reading to the AVG */
  21. avg_count += 1;
  22. for ( int i = 0; i < OCTAVES; i++) {
  23.     audio_z_avg[i] += audio_z_spectrum[i];
  24.     audio_a_avg[i] += audio_a_spectrum[i];
  25. }
  26.  
  27. /* calculate the dB */
  28. soundsensor_toDecibel(audio_z_spectrum, OCTAVES);
  29. soundsensor_toDecibel(audio_a_spectrum, OCTAVES);

As you see, these are just references to the classes where the logic happens. Let's go through them;

1: integerToFloat: Convert 16 bits from integer to float. But it turns out, our PDM/PCM doesn't store 16 bits. It only stores 8 bits. (even if we did set it up that way...) And it doesn't put these bits in the high bytes as others do, but at the low bytes. Which makes it easier! As we work with FFT in complex data, there is a REAL and IMAG part. As this is the first step, we fill in the REAL and zero the IMAG.

  1. void soundsensor_integerToFloat(int16_t *samples, float *vReal, float *vImag, uint16_t size) {
  2.     int bit_size = 8;
  3. for (uint16_t i = 0; i < size; i++) {
  4. vReal[i] = (float)((samples[i] / pow(2,bit_size-1)));
  5. vImag[i] = 0.0;
  6.     }
  7. }

2: FFT_Windowing: The floats from the previous step end up in the range between -1 and 1. For FFT we need values between 0 and 1. The HANN window works best for that, so we convert our data with that setting.

  1. void arduinoFFT_Windowing(uint8_t windowType, uint8_t dir, float *vReal, uint16_t size) {
  2.     double samplesMinusOne = (double) size - 1.0;
  3.     for (uint16_t i = 0; i < (size >> 1); i++) {
  4.         double indexMinusOne = (double) i;
  5.         double ratio = (indexMinusOne / samplesMinusOne);
  6.         double weightingFactor = 1.0;
  7.         /* Compute and record weighting factor for windowing */
  8.         switch (windowType) {
  9.             case FFT_WIN_TYP_HANN:
  10.     {
  11.                 weightingFactor = 0.54 * (1.0 - cos(twoPi * ratio));
  12.                 break;
  13.             }
  14. }
  15.         if (dir == FFT_FORWARD) {
  16.             vReal[i] *= weightingFactor;
  17.             vReal[size - (i + 1)] *= weightingFactor;
  18.         }
  19. else {
  20.     vReal[i] /= weightingFactor;
  21.     vReal[size - (i + 1)] /= weightingFactor;
  22. }
  23.     }
  24. }

3: FFT_ComputeComputes in-place complex-to-complex FFT. This does the heavy complex calculations for us.

  1. void arduinoFFT_Compute(uint8_t dir, float *vReal, float *vImag, uint16_t size){
  2.     /* Reverse bits */
  3.     uint16_t j = 0;
  4.     for (uint16_t i = 0; i < (size - 1); i++) {
  5.         if (i < j) {
  6.             arduinoFFT_Swap(&vReal[i], &vReal[j]);
  7.             if(dir==FFT_REVERSE)
  8.                 arduinoFFT_Swap(&vImag[i], &vImag[j]);
  9.         }
  10.         uint16_t k = (size >> 1);
  11.         while (k <= j) {
  12.             j -= k;
  13.             k >>= 1;
  14.         }
  15.         j += k;
  16.     }
  17.     /* Compute the FFT */
  18.     double c1 = -1.0;
  19.     double c2 = 0.0;
  20.     uint16_t l2 = 1;
  21.     for (uint8_t l = 0; (l < arduinoFFT_Exponent(size)); l++) {
  22.         uint16_t l1 = l2;
  23.         l2 <<= 1;
  24.         double u1 = 1.0;
  25.         double u2 = 0.0;
  26.         for (j = 0; j < l1; j++) {
  27.             for (uint16_t i = j; i < size; i += l2) {
  28.                 uint16_t i1 = i + l1;
  29.                 double t1 = u1 * vReal[i1] - u2 * vImag[i1];
  30.                 double t2 = u1 * vImag[i1] + u2 * vReal[i1];
  31.                 vReal[i1] = vReal[i] - t1;
  32.                 vImag[i1] = vImag[i] - t2;
  33.                 vReal[i] += t1;
  34.                 vImag[i] += t2;
  35.             }
  36.             double z = ((u1 * c1) - (u2 * c2));
  37.             u2 = ((u1 * c2) + (u2 * c1));
  38.             u1 = z;
  39.         }
  40.         c2 = sqrt((1.0 - c1) / 2.0);
  41.         c1 = sqrt((1.0 + c1) / 2.0);
  42.         if (dir == FFT_FORWARD) {
  43.             c2 = -c2;
  44.         }
  45.     }
  46.     /* Scaling for reverse transform */
  47.     if (dir != FFT_FORWARD) {
  48.         for (uint16_t i = 0; i < size; i++) {
  49.             vReal[i] /= size;
  50.             vImag[i] /= size;
  51.         }
  52.     }
  53. }

4: calculateEnergy: Calculates energy from Real and Imaginary parts and place it back in the Real part. Only for half of the dataset!

  1. void soundsensor_calculateEnergy(float *vReal, float *vImag, uint16_t size) {
  2.     for (uint16_t i = 0; i < size/2 ; i++) {
  3.         vReal[i] = sqrt(sq(vReal[i]) + sq(vImag[i]));
  4.     }
  5. }

5: sumEnergy: Sums up energy in whole octave bins.

NOTE: octave bins are square blocks in this code, with the determined Hz value being in the middle of the block. but in real life they have curves and overlap. That isn't implemented here! That is a TODO, but would require to swap the sumEnergy and loudnessWeighting steps.

  1. void soundsensor_sumEnergy(const float *vReal, float *vEnergy, uint16_t size, uint16_t octaves) {
  2.     int bin_size = 1;
  3.     int bin = bin_size;
  4.     for (int octave = 0; octave < octaves; octave++){
  5.         float sum = 0.0;
  6.         for (int i = 0; i < bin_size && bin < size/2; i++){
  7.             sum += vReal[bin++];
  8.         }
  9.         vEnergy[octave] = (sum / (bin_size/2.0));
  10.         bin_size *= 2;
  11.     }
  12. }

6: loudnessWeighting: Calculates the loudness input, according to A/C/Z-weighting scale. Negative weightings don't make sense. So in that case we assume there is no sound on this level and set everything below 1 to 1.

  1. /* 16Hz 31.5Hz 63Hz 125Hz 250Hz 500Hz 1kHz 2kHz 4kHz 8kHz 16kHz */
  2. static float aWeighting[] = { -56.7, -39.4, -26.2, -16.1, -8.6, -3.2, 0.0, 1.2, 1.0, -1.1, -6.6 };
  3. static float cWeighting[] = { -8.5, -3.0, -0.8, -0.2, 0.0, 0.0, 0.0, -0.2, -0.8, -3.0, -8.5 };
  4. static float zWeighting[] = { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 };
  5.  
  6. void soundsensor_loudnessWeighting(float *vEnergy, float *weighting, float *spectrum, uint16_t octaves) {
  7.     float calc_energy = 0.0;
  8.     for (int i = 0; i < octaves; i++) {
  9.         calc_energy = vEnergy[i] * sqrt(pow(10, weighting[i] / 10.0));
  10.         spectrum[i] = (calc_energy > 1 ? calc_energy : 1);
  11.     } }

7: toDecibel: Almost there! We now convert our readings into Decibels (dB) for each octave bin. There is also calculated a dB for the total and stored in an extra bit of the spectrum. If in the calculateEnergy() step, we didn't SQRT vReal it would be 10*log10(). But we did SQRT the value there, so we need to use 20*log10() here

  1. void soundsensor_toDecibel(float *spectrum, uint16_t octaves) {
  2.     float full_spectrum = 0.0;
  3.  
  4.     /* calculate for each octave the dB */
  5.     for ( int i = 0; i < octaves; i++) {
  6.         full_spectrum += spectrum[i];
  7.         spectrum[i] = 20.0 * log10(spectrum[i]);
  8.     }
  9.     /* this is the total over all octaves */
  10.     spectrum[octaves] = 20.0 * log10(full_spectrum);
  11. }

That was all the special calculations applied. What is left, is printing the results to a terminal for a direct view of the sound/audio measurements.

  1. /* Print the spectrum dB values from the A-Weighting.
  2. * Skip the 16Hz bin as we cannot hear that anyway */
  3. for (int i = 1; i < OCTAVES; i++) {
  4.     printf(" %8.1f", audio_a_spectrum[i]);
  5. }
  6.  
  7. /* Print the total dB value */
  8. printf(" %8.1f ", audio_a_spectrum[OCTAVES]);
  9.  
  10. /* Create a bar to show the total dB values, divided by 10 to keep it readable.
  11. * Range is up to 150, which means you will be deaf by that time... */
  12. printf("[33[93m"); /* foreground color to "Bright Yellow" */
  13. for (int index = 0; index < 15; index++) {
  14.     printf("%s", (index < floor(audio_a_spectrum[OCTAVES]/10) ? "=" : " "));
  15. }
  16. printf("33[m]"); /* reset background color to standard */

And we present the total volume on the LED, where the CapSense slider value is used for a threshold.

  1. /* Calculate the brightness of the LED */
  2. corrected_volume = floor(audio_a_spectrum[OCTAVES]) > CapSense_slider_value ? (uint32_t) floor(audio_a_spectrum[OCTAVES]) - CapSense_slider_value : 0;
  3.  
  4. if (corrected_volume <= 0) {
  5.     /* Stop PWM to turn the LED off */
  6.     cyhal_pwm_stop(&pwm_led);
  7. }
  8. else {
  9.     /* make sure the LED brightness is between the MIN and MAX */
  10.     corrected_volume = corrected_volume < LED_MIN_BRIGHTNESS ? LED_MIN_BRIGHTNESS : corrected_volume;
  11.     corrected_volume = corrected_volume > LED_MAX_BRIGHTNESS ? LED_MAX_BRIGHTNESS : corrected_volume;
  12.  
  13.     /* Start PWM to turn the LED on and drive the LED with brightness */
  14.     cyhal_pwm_set_duty_cycle(&pwm_led, GET_DUTY_CYCLE(corrected_volume), PWM_LED_FREQ_HZ);
  15.     cyhal_pwm_start(&pwm_led);
  16. }

And last but not least, instruct our PDM microphone to record us another sample!

  1. /* Read the next audio frame */
  2. cyhal_pdm_pcm_read_async(&pdm_pcm, audio_frame, FRAME_SIZE);

That is all we need inside the audio task at this moment.

I have ideas to enhance it further, mainly on how the octaves are calculated (change that to octave-bands), and how the weighting is done based on full octaves, and not the precise values.

influx_task.c

Last part of the code, and as there are some repeating tricks, I will skip some of them.

This task is used for stacking the queued data, and process it when there are several in the queue. I've chosen to collect 20 readings, both A and Z together. As there was averaged per second, that means this contains 10 seconds of data. If you are not looking live at the graphs, you could collect for a longer time, but be aware of memory overflow issues.

For the InfluxDB data, the API expects us to deliver something like this:

  1. POST http://192.1668.1.245:9999/api/v2/write?org=MyFlux&bucket=AudioSensor&precision=s HTTP/1.0
  2. Authorization: Token 541556ad4t56we455h41er566y744543653y41h56h4r
  3. Content-Type: text/plain
  4. Content-Length: 4012
  5.  
  6. PSoc6,sensor=audio,weighting=a tot=23.5,oct01=0.2,oct02=3.7,oct03=4.2,oct04=12.1,oct05=13.5,oct06=15.1,oct07=12.8,oct08=11.7,oct09=8.6,oct10=5.2,oct11=0.9 1611864129
  7. PSoc6,sensor=audio,weighting=z tot=23.5,oct01=0.2,oct02=3.7,oct03=4.2,oct04=12.1,oct05=13.5,oct06=15.1,oct07=12.8,oct08=11.7,oct09=8.6,oct10=5.2,oct11=0.9 1611864129
  8. PSoc6,sensor=audio,weighting=a tot=23.5,oct01=0.2,oct02=3.7,oct03=4.2,oct04=12.1,oct05=13.5,oct06=15.1,oct07=12.8,oct08=11.7,oct09=8.6,oct10=5.2,oct11=0.9 1611864130
  9. PSoc6,sensor=audio,weighting=z tot=23.5,oct01=0.2,oct02=3.7,oct03=4.2,oct04=12.1,oct05=13.5,oct06=15.1,oct07=12.8,oct08=11.7,oct09=8.6,oct10=5.2,oct11=0.9 1611864130

As you see, there is one line for the A-weighting, and one line for Z-weighting, with the same epoch timestamp at the end of the line. And that it repeats 1 second up each time. I have a field called sensor=audio, which is there in preparation for if I want to upload something additionally, for example the thermistor data from this board.

The way how I'm this merging data here, is not proper programming. The compiler even gives me a warning of it. But as it works with keeping possible memory overflows in mind, I've left it as is for now. If you have a good example of how to solve this, let me know! 

  1. /*empty the buffers at start */
  2. tx_buffer[0] = '';
  3. tx_body[0] = '';
  4.  
  5. /* Empty the queue before starting */
  6. xQueueReset( influx_command_data_q );
  7.  
  8. /* Set the start of the loop to -4, as first couple of audio readings have an invalid data reading */
  9. int i_upload = -4;
  10.  
  11. /* Repeatedly running part of the task */
  12. for(;;) {
  13.     /* Block until a command has been received over queue */
  14.     rtos_api_result = xQueueReceive(influx_command_data_q, &influx_cmd_data, portMAX_DELAY);
  15.  
  16.     /* Command has been received from queue */
  17.     if(rtos_api_result == pdTRUE) {
  18.         switch(influx_cmd_data.influx_command) {
  19.             /* Check the command and process the steps for it. */
  20.             case INFLUX_DATA:
  21.             {
  22.                 /* InfluxDB API data line */
  23.                 sprintf(tx_body,
  24.                         "%sPSoc6,sensor=audio,weighting=%s tot=%.1f,oct01=%.1f,oct02=%.1f,oct03=%.1f,oct04=%.1f,oct05=%.1f,oct06=%.1f,oct07=%.1f,oct08=%.1f,oct09=%.1f,oct10=%.1f,oct11=%.1f %ld",
  25. tx_body, influx_cmd_data.audio_weighting,
  26. influx_cmd_data.audio_volume, influx_cmd_data.audio_octave01, influx_cmd_data.audio_octave02, influx_cmd_data.audio_octave03, influx_cmd_data.audio_octave04, influx_cmd_data.audio_octave05, influx_cmd_data.audio_octave06, influx_cmd_data.audio_octave07, influx_cmd_data.audio_octave08, influx_cmd_data.audio_octave09, influx_cmd_data.audio_octave10, influx_cmd_data.audio_octave11,
  27. influx_cmd_data.sec_since_epoch);
  28.  
  29.                 /* At 20, we are going to upload! (2 spectrums per second,
  30.                  * so this is 10 seconds of data) */
  31.                 if (i_upload == 20) {
  32.                     /* InfluxDB API header line */
  33.                     tx_buffer[0] = '';
  34.                     sprintf(tx_buffer,
  35.             "POST /api/v2/write?org=%s&bucket=%s&precision=%s HTTP/1.0rnAuthorization: Token %srnContent-Type: text/plainrnContent-Length: %drnrn%s",
  36.     INFLUX_ORGANISATION, INFLUX_BUCKET, INFLUX_PRECISSION, INFLUX_TOKEN, strlen(tx_body), tx_body);
  37.                     result = tcp_send_to_server(tcp_server_address, tx_buffer);
  38.     if (result == CY_RSLT_SUCCESS) {
  39.                         tx_body[0] = '';
  40.     }
  41.                     i_upload = 0;
  42. }
  43.  
  44.                 /* Above 0, we assume this is just a reading to be added to previous readings */
  45.                 else if (i_upload >= 0) {
  46.                     sprintf(tx_body, "%sn", tx_body);
  47.                     i_upload++;
  48.                 }
  49.  
  50.                 /* Below 0, this is a reading just after start, ignore it! */
  51. else {
  52.                     tx_body[0] = '';
  53.     i_upload++;
  54. }
  55.   break;
  56.             }
  57.   }
  58.     }
  59.  
  60.     /* delay for a very short time, to let the other tasks go ahead */
  61.     vTaskDelay(pdMS_TO_TICKS(1u));
  62. }

At the count of 20, the package is handed over to the function to actually send the package:

  1. for(uint32_t conn_retries = 0; conn_retries < MAX_TCP_SERVER_CONN_RETRIES; conn_retries++) {
  2. /* Initialize the Sockets Library. */
  3. cy_socket_init();
  4.  
  5. /* Create a new TCP socket. */
  6. cy_socket_create(CY_SOCKET_DOMAIN_AF_INET, CY_SOCKET_TYPE_STREAM,
  7.     CY_SOCKET_IPPROTO_TCP, &tcp_socket_handle);
  8.  
  9. /* Set the SEND timeout to 3000 milliseconds */
  10. cy_socket_setsockopt(tcp_socket_handle, CY_SOCKET_SOL_SOCKET,
  11.         CY_SOCKET_SO_SNDTIMEO, 3000, sizeof(3000));
  12.  
  13. /* Set the RECEIVE timeout to 5000 milliseconds */
  14. cy_socket_setsockopt(tcp_socket_handle, CY_SOCKET_SOL_SOCKET,
  15. CY_SOCKET_SO_RCVTIMEO, 5000, sizeof(5000));
  16.  
  17. /* No need for a receive callback */
  18. /* No need for a disconnection callback */
  19.  
  20. result = cy_socket_connect(tcp_socket_handle, &tcp_server_address,
  21.                                         sizeof(cy_socket_sockaddr_t));
  22. if (result == CY_RSLT_SUCCESS) {
  23. /* Send a message to the TCP server. */
  24. conn_result = cy_socket_send(tcp_socket_handle, tx_buffer,
  25.                                 strlen(tx_buffer), CY_SOCKET_FLAGS_NONE, &bytes_sent);
  26. if (conn_result == CY_RSLT_SUCCESS) {
  27. printf("33[32mTCP: sent %ld bytes to InfluxDB server.33[mn",
  28.                                     bytes_sent);
  29. }
  30.  
  31. /* Disconnect the TCP client. */
  32. cy_socket_disconnect(tcp_socket_handle, 0);
  33.  
  34. /* Free the resources allocated to the socket. */
  35. cy_socket_delete(tcp_socket_handle);
  36.  
  37. return conn_result;
  38. }
  39.  
  40. printf("33[91mTCP: Could not connect to TCP server. Trying to reconnect...33[mn");
  41.  
  42. /* The resources allocated during the socket creation (cy_socket_create)
  43.          * should be deleted. */
  44. cy_socket_delete(tcp_socket_handle);
  45. }

 

Once we are able to run this, data is being uploaded to InfluxDB.

 

InfluxDB and Grafana

For my home automation data logger, I do use InfluxDB because it is a time scale database. For example a normal relational database like Microsoft SQL Server, MySQL, etc, is designed to store data in multiple customized tables that you can link together. You can write your custom SQL code to get the data out. Perfect for the Content Management System of your website, or to store your financial administration. But for storing sensor readings, we are not interested in all that customization. We want something that can very fast read data for a given period of time. And it should now how to get weighted averages, sums, min/max values. And that is where a database like InfluxDB is good at. You can host it on a relatively simple system, even on a Raspberry Pi (but keep in mind that a database does a lot of read/write actions and will wear out your SD card).

In the examples here, I use InfluxDB 2.0.0, which was not official yet at time of install, and had many changes in the current version. For example the port of my server is different from the current standard. It is very easy to install, and I recommend you to follow the documentation from Influx.

In older InfluxDB versions, the graphing tools were very simple and not really nice to look at. Usually I use it to see if the data is good, and I build my queries with it. For example this is the graph of A-weighted data. But as it is all 10 octaves at the same time, it looks quite messy.

For advanced graphs and dashboards, I do use Grafana. The two products are very tied together in available functionality. Again, I recommend to follow the Grafana documentation for installing it. In this example, I've split the graphs per octave, and now you can better see in which octave more sound is registered:

In the video for this project, I do go deeper into using and reading the graphs. So watch that ;)

 

But what can you do with this?

Good question! In the program that is here today, you can Analyze, Learn, Play with sound/audio data. But what more can you do with it? First some enhancement ideas, then some usage ideas.

Simple enhancement: Averaging

The PDM/PCM output is at a very detailed level (multiple readings per second), which is too much detail to be useful. I do already average the data to the second, but probably an even wider average would help:

  • Smooth out sudden peaks/dips.
  • Reduce the amount of data to send over WiFi, resulting in less power consumption.
  • Store less datapoints on our InfluxDB, which will make it faster to analyze.

As you might have seen on the InfluxDB/Grafana dashboards, I did add an averaging function there for 5 seconds to get the smoother lines. But that is after publishing the data, and doesn't give less WiFi power consumption and less datapoints.

Advanced enhancement: Octaves & Weighting

My octaves are square blocks with the octave Hz roughly in the middle. Official octaves-bands have curves and overlap. The weighting is applied based on these whole octaves, which are an equal sum of a lot of readings, and thus miss out detail.

  • By first applying the weighting formula to the detailed readings
  • And then applying the octave-band formula to bin them into octaves
  • The outcome would become more realistic

Usage: weighting data in real life

As I store both original and A-Weighted data, next is to visualize the sounds our ears are not able to hear. And for people with hearing loss, you can create custom Weighting models. Then you can tell (or warn!) what they are missing compared to others.

 

Did I hear ideas, questions, anything else?

The project has a working result, I'm satisfied with what I achieved. And I did learn an enormous amount of new things about sound, audio, ears, microphones, Fourier Transforms and much much more.

Credits go to all these pages that did help me getting to understand parts of it, summing together to this project. I've read so many pages, that I'm I did forgot some of them to list here.

Information about Sound, Audio and processing it:

Technical information and code examples:

 

But that doesn't mean I'm done with it. I will make changes and enhancements. And if you have questions or additions, I'm happy to have a look at what you are doing and think along with you!

 

 

Code

Github: bastiaanslee/PSoC6-AudioSensor

All code is uploaded on Github!

Credits

Photo of bastiaanslee

bastiaanslee

Tinkerer in the field of Home Automation, with the main goal: fun! Playing with Raspberry Pi, Python, EmonCMS, meter reading, OpenTherm, Weather Station, IoT, cloud-server, Pebble Time, LoRaWAN, WeMos, InfluxDB, Grafana,NodeRed, Google Assistant, 3D Printing (Snapmaker2)

   

Leave your feedback...