Analysis of Surface Electromyography-Signals


Hello!
For my bachelor’s-thesis I examine the electric activity of human forearm muscles (especcially M. palmaris longus). I have been able to record those signals with Audacity using Surface Electromyography.
I already talked to some people in the “Adding New Features to Audacity”-section of the forum what possibilities there might be to further analyse the signals.

(https://forum.audacityteam.org/t/surface-elektromyography/24455/1)

They told me to precisely describe what I actually need. So that’s what I wanna do now:

  • I have 3 incoming channels (each from a different muscle)
  • For each of those channels I would like to apply the RMS-algorithm (based on samples of about 50ms duration) and then display the resulting curve(s)
    a)in a separate track in waveform and
    b)in a table (e.g. Excel) , time/intensity

I’d be very happy about every kind of help!

Niklas

The easiest way to run simple Nyquist commands is in the “Nyquist Prompt” effect (Effect menu).

Many Nyquist commands work on just one audio channel at a time, so it is often convenient to split stereo tracks into mono. To do that, click on the track name and select “Split Stereo to Mono” from the track drop down menu.

This command will calculate the rms signal using a 0.01 second square window.

(rms s)

“rms” is the function.
“S” is a special global variable that passes audio data from the audio track selection to Nyquist.
For mono tracks, “S” will contain the “sound” from the track.
For stereo tracks “S” will be an array with the sound from the left channel as the first element and the sound from the right channel as the second element.
(Nyquist is case insensitive)

“rms” is an example of a function that only works on single channel sounds. If you need to run it on a stereo sound, then either split the stereo track into two mono tracks (as described previously) or apply the function to each element of the array:

(vector
  (rms (aref s 0))
  (rms (aref s 1)))

You will notice that when applying (rms s) to a mono track, the processed track will be about 480 times shorter than the original. Each 0.01 second window produces 1 sample. Assuming that the track sample rate is 48 kHz (as per your example file) then 10 seconds of input will produce 1000 output samples which will occupy about 21 ms in a 48 kHz track.

For a different window size, the syntax for “rms” is:
(rms sound [rate window-size])
By default the window-size is 1/rate, so for a 50 ms window the rate needs to be 1/0.05 = 20
“rate” and “size” are optional parameters (defaults are 100 and 0.01 respectively)
So for a 50 ms window you can use:

(rms s 20)

How long are the audio tracks that you want to analyse?
If only a few seconds then we can just output to the Nyquist Prompt debug window and you will be able to copy/paste from there (most simple solution). If they are much longer, producing thousands of values, then we will need to output to a file.

The easiest way to see the curves is to just zoom in to fit the waveform to the screen.
If you want to see it on the same time scale as the original audio it will be necessary to resample the rms output to the track sample rate.

(force-srate *sound-srate* (rms s 20))

:smiley: - After sending my post I just realized that you already got more answers than expected while I was still writing. Here is my version, please excuse if great parts are double, but as I had promised, in the Nyquist section of the forum your question will be found by more people than under “Adding new Features”…

To display the RMS value of an Audacity audio track in the same audio track, select the entire signal in the audio track, then in the Audacity “Effect” menu, open the “Nyquist Prompt” (German Audacity: Nyquist Eingabeaufforderung), and copy the following code into the text field of the “Nyquist Prompt” window, then click “OK”.

For a mono audio track use this code:

(force-srate *sound-srate* (rms s 20))

For a stereo audio track (like the signal in your WAV file), use this code:

(force-srate *sound-srate* (multichan-expand #'rms s 20))

The waveform in the audio track should be replaced by the RMS value afterwards.

If you want two audio tracks, one with the unprocessed original signal and one with the RMS value, then you must duplicate the original track with “Edit > Duplicate” (German Audacity: "Bearbeiten > In neue Tonspur kopieren) to get two identical audio tracks and then use the code from the “Nyquist Prompt” on only one of these tracks.

Explanation of the Nyquist code:

The RMS function is described under http://www.cs.cmu.edu/~rbd/doc/nyquist/part8.html#index532

The s after rms in (rms s 20) ist the audio signal of the Audacity track. The s variable contains a pointer to the first block of samples. The processing of the audio samples works automatically, you only need to care about the other parameters.

50 milliseconds = 0.05 seconds, where 1/0.05 seconds = 20 Hertz. That’s where the 20 in (rms s 20) come from.

(rms s 20) produces a 20Hz signal, containing the RMS values of blocks of samples contained in 50 milliseconds each. Because Nyquist can’t change the Audacity track sample frequency, the 20Hz RMS signal must be transformed back to the Audacity track sample frequency (usually 44100Hz), that is stored in the sound-srate variable. Using the sound-srate variable instead of a hardcoded frequency value will work with all track sample frequencies automatically, not only with 44100Hz.

(Only for stereo tracks: The MULTICHAN-EXPAND macro is not described in the Nyquist manual, but in the Audacity Wiki under “Nyquist Plug-ins Reference > Stereo Tracks” is explained how it works.)

The FORCE-SRATE function is described under http://www.cs.cmu.edu/~rbd/doc/nyquist/part8.html#index327

The force-srate function transforms the 20Hz RMS signal back into the Audacity track sample frequency, so it is displayed in the correct time units (seconds), according to the (visual) length of the original signal. The transformed signal then replaces the original signal in the Audacity audio track.

I will look-up now how to write these values into a CSV file for Excel…

  • edgar

With a window size of 50 ms there will be 20 values per second of audio, 1200 per minute. For short audio tracks we can output a few hundred samples to the Nyquist Prompt debug window. Obviously this gets a bit impractical for long tracks.

The following code will output a simple list of rms values (50 ms window)

(setf s (rms s 20))
(dotimes (num (snd-length s ny:all))
   (print (snd-fetch s)))
(format nil "Done.~%See debug window for results")

To use this code in the Nyquist Prompt, copy and paste the code into the Nyquist Prompt effect then click the “Debug” button.

The “format” command allows us to output to either a pop-up window or to the debug window.
On Windows, copy/paste of text does not work from( a pop-up window, so we need to output the data to the debug window where it can be copied.

(format nil “text string”) will output to a pop-up dialogue.
(format t “text string”) will output to the debug window.

To format the output as comma separated variables:

(setf s (rms s 20))
(dotimes (num (snd-length s ny:all))
   (format t "~a, " (snd-fetch s)))
(format nil "Done.~%See debug window for results")

If you need to process longer tracks we will need to add file output. This can get a bit tricky to do it in a cross-platform way, so if you need this please say what operating system you use.

[update: I see Edgar has posted while I was typing, so apologies for any duplication]

I’ve just read Edgar’s reply. Almost identical solutions :smiley:

Just to clarify one difference in the detail:



Both of these do basically the same thing.
Putting the two snippets of my code together gives:

(force-srate *sound-srate*
  (vector
    (rms (aref s 0))
    (rms (aref s 1))))

In this code the first line is forcing the sample rate of the processed audio back to the same sample rate as the track. sound-srate is a special global variable which in Audacity is the audio track sample rate.
“vector” creates an array (necessary for a stereo track)
The two elements are (rms (aref s 0)) and (rms (aref s 1))

This code will only work for stereo tracks because we are specifically processing two array elements (aref s 0) and (aref s 1).

In Edgar’s code:

(force-srate *sound-srate* (multichan-expand #'rms s 20)))

he is “expanding” (rms s 20) for each element (channel) of “S”. This will work for mono, stereo or multi-channel sounds (though Audacity currently only supports mono or stereo).

@steve: Do we have somewhere a prefabricated framework for writing a text file in the user’s HOME directory? I remember the discussion about how to find the HOME directory under different operating systems (where I assume that Niklas uses Windows if he works with Excel) and how to write or append to text files with Nyquist in Audacity, and there had been plugins to write sample values to disk, but had this ever been generalized into a set of file-i/o functions? Or do you remember a plugin where these functions just can be copied from? We need to write the RMS values into a CSV file.

  • edgar

OK, here’s an example for writing to a file. Note that this example was written to run on Linux.

(setq outfile "/home/<username>/Desktop/output.csv")
(setf s (rms s 20))

(setq maxvalues ny:all)
(setq f (open outfile :direction :output))
(dotimes (num (snd-length s maxvalues))
   (format f "~a, " (snd-fetch s)))
(close f)
(format nil "Results written to:~%~a" outfile)

The first line sets the destination file. This must be a fully qualified file name.
Windows uses “” as a file separator, but that character is also used as an “escape character” so cannot be used in the code.
An example that works on (English) Windows XP for writing “output.csv” to “My Documents” would be:

(setq outfile "C:/Documents and Settings/<user name>/My Documents/output.csv")

Note that the usual “” has been replace with “/”
would need to be changed to your actual user name.

Note that this code has no error checking. If you enter an invalid file path the code will fail.
Note also that this code is for processing one mono track. It will fail on stereo tracks.
Each time the code is run it will overwrite the same output file unless you manually rename the file.


(setf s (rms s 20))

This line computes the rms of the sound “S”.

(setq maxvalues ny:all)

This sets the maximum number of samples to be processed to “ny:all” which in Audacity is 1 billion (about 5 hours at 48 kHz). Note that processing long selections of audio will be slow and will produce large csv files.

The rest of the code is similar to before except that we are printing to the file rather than to the debug window.

Not yet.
I’ve got close to completing one but the task is complicated by platform dependent file naming rules, particularly Windows weirdness.
(Did you know that Windows handles the file separator after a drive letter differently from anywhere else in the path, and that it is not consistent about how it does this but can be different on one drive to another? :confused: Have you seen the weird way that OS X can silently change “” and “:” characters? :confused: )

The simple solution (as in my previous post) is for the user to enter the full path and file name (correctly). If necessary error checking can be added to flag up an invalid path or file name by checking if the file is writeable.

The following code, if copied and executed from the Audacity Nyquist Prompt, replaces the original signal in the Audacity audio track by the RMS signal, while writing all RMS values into a CSV file with one data set per channel (= two data sets with stereo signals). The filename in the last line (scroll down in the code window to see what I mean) must be replaced by the path and name of the desired CSV file. Click “Debug” instead of “OK” in the Nyquist Prompt window to see the error messages if no file is written.

(defun write-file (filename)
  (let ((rms-signal (multichan-expand #'rms s 20))
        (file-stream (open filename :direction :output)))
    (if (not file-stream)
        (format t "Error while opening file: ~a~%" filename)
        (unwind-protect
          (multichan-expand
            #'(lambda (snd stream)
                (let ((first-sample-p t))
                  (dotimes (num (snd-length snd ny:all))
                    (let ((sample (snd-fetch snd)))
                      (if first-sample-p
                          (progn (format stream "~a" sample)
                                 (setf first-sample-p nil))
                          (format stream ", ~a" sample))))
                  (format stream "~%")))
            (multichan-expand #'snd-copy rms-signal)
            file-stream)
          (if file-stream
              (progn (close file-stream)
                     (format t "Results written to: ~a~%" filename))
              (format t "Error while writing file: ~a~%" filename))))
    (force-srate *sound-srate* rms-signal)))

(write-file "/home/edgar/audacity.csv")

If this code works as wanted we can consider writing an Audacity plugin that does this automatically. Open Office Calc can read the CSV data without problems, but maybe we need to change the CSV file format to make it compatible to MS-Excel. I have no Excel here so I can’t test this.

Niklas may also please tell if he needs e.g. the exact time values or any other additional data in the CSV file for Excel.

  • edgar

Hey guys,
thank you for your effort!
I tried the code Edgar posted last and it works well! :smiley:

For one stereo track I receive a openofficecalc-file, that shows two lines of data. But for two mono tracks it seems like it puts all samples in just one line. Unfortunately, my recordigs are 3 or 4 mono tracks.

My recordings are about 10 seconds long (maybe 20).

In order to compare signals it would be helpful to integrate over the signals (=>area underneath the RMS-graph) with varying intervalls. An intervall could be the entire selection ( which the RMS-command was applyied to), but more useful might be to divide the selection into multiple intervalls and to receive one value per intervall.
I guess for all kind of values (single RMS-value or integration-value of an intervall)it would be very good to have the corresponding time values given out.

Have good day,
Niklas :wink:

Then it may be easier to go with the more simple code that outputs to the debug window and then copy and paste from the debug window into your csv file.

(setf s (rms s 20))
(dotimes (num (snd-length s ny:all))
   (format t "~a, " (snd-fetch s)))
(format nil "Done.~%See debug window for results")

If this approach is acceptable then a second (separate) piece of code could be run to return the additional data.
By using short snippets of code rather than an all-in-one plug-in it is much easier to customise the code to suit your needs.

Do you know what intervals you need?


Time values in Nyquist are relative to the start of the selection. Nyquist does not know the absolute time as it appears in the Audacity Timeline.
How much precision do you need? milliseconds?

What do you mean by “into your csv file”? Pasting it into calcfile results into having one cell containing all values.

I suppose 10-50ms intervals. I will have to see which intervals lead to the most meaningful results.

Milliseconds would be great :slight_smile:

A CSV file is nothing but a plain-text file, where one text line equals one record, and the values of each cell are seperated by commas. This means that you can edit a CSV file with every simple text editor.

A CSV file with the following plain-text contents:

1.0, 2.0, 3.0
4.0, 5.0, 6.0

produces a spreadsheet that looks like this:

           cell 1  cell 2  cell 3
         +-------+-------+-------+
record 1 |  1.0  |  2.0  |  3.0  |
         +-------+-------+-------+
record 2 |  4.0  |  5.0  |  6.0  |
         +-------+-------+-------+

Every text line from the CSV file will be interpreted as a record, and all values between the commas are the cell values. You can add more records by pasting more lines to the end of a CSV file with a simple text editor.

  • edgar

Open NotePad or any other text editor.
Copy and Paste the data from the Nyquist Prompt debug window into NotePad.
Apply any additional formatting as required.
Save with a .csv file extension.

It would be advantageous to use a better text editor than NotePad so that you can do search and replace if necessary. For Windows I’d recommend Notepad++.

The code that I wrote puts a comma followed by a space between values.
If just a comma and no space is preferred the quoted text in this line can be altered:

   (format t "~a, " (snd-fetch s)))

to

   (format t "~a," (snd-fetch s)))

or for semi-colon separated variables

   (format t "~a;" (snd-fetch s)))

Explanation:
“format” is the command that outputs the text data as formatted text.
“t” tells “format” to send the formatted text to the debug window.
The quoted text is the text data.
“~a” is a special character pair that is replaced by a value.
“(snd-fetch s)” is the command that fetches the sample value and the sample value replaces ~a in the text string.

This is for mono tracks only. The output is in the debug window.

(setq window-size 50)   ; rms window ms
(setq ws (/ window-size 1000.0))
(setf s (rms s (/ ws)))
(setq times ())
(format t ""rms values",")
(dotimes (num (snd-length s ny:all))
  (format t "~a," (snd-fetch s))
  (setq times (cons (* num ws) times)))

(setq times (reverse times))
(setq *float-format* "%1.3f")
(format t "~%"Time (seconds)",")

(dotimes (time (length times))
  (format t "~a," (nth time times)))

(format nil "Done.~%See debug window for results")

This will give you the rms values and the time (seconds).

Explanation:

(setq window-size 50) ; rms window ms : sets the rms window size in milliseconds.

(format t ““rms values”,”) : puts “rms values” as a label in the first column of the first row.
(format t “~%“Time (seconds)”,”) : puts “Time (seconds)” as a label in the first column of the second row.

(setq times ()) : creates an empty list which the time values will be added to.

(format t “~a,” (snd-fetch s)) : prints the sample values of the rms signal
(setq times (cons ( num ws) times)))* : adds time value to the “times” list

(setq times (reverse times)) : reverses the “times” list so that it is in the right order

(setq float-format “%1.3f”) : sets the output format for floating point (decimal) values to 3 decimal places (milliseconds)

(dotimes (num (snd-length s ny:all))
  (format t "~a," (snd-fetch s))
  (setq times (cons (* num ws) times)))

: Prints the time values to the second row.

Tested in Gnumeric and OpenOffice Calc.



Does that make sense?
The rms value for a 50 ms window is just a single point on the “graph”, so the area under it is zero. :confused:

The Nyquist SND-AVG function has the option to compute the RMS value of a “block” of samples. I assume that this is what we need here.

SND-AVG computes either the peak of a block of samples, or the average of a block of samples, depending on whether the final argument evaluates to 1 or 2 (op-average or op-peak respectively).

To calculate the rms using SND-AVG:

(snd-sqrt (snd-avg (mult sound sound) ws ws op-average))

where “sound” is the sound being analysed and “ws” is the window size in samples, so for a 50 ms window

(snd-sqrt (snd-avg (mult s s) (round (* 0.05 *sound-srate*)) (round (* 0.05 *sound-srate*)) op-average))

is the same as

(rms s 20)

I’m not sure what Niklas wants to compute, perhaps the integral of the square of the sound over a given window size.

This code will calculate the square of the sound, then add up the sample values within the window and divide by the size of the window.
For a square wave of amplitude 1 this will give an output of 1 regardless of the window size or frequency. For a sine wave it will give an output of about 0.5 (provided that the window size is significantly larger than the waveform period)

(setq window-size 50)   ; window size ms
(setq ws (/ window-size 1000.0))
(setq num-wins (truncate (/ (get-duration 1) ws)))

(dotimes (i num-wins)
  (let* ((test-s (extract-abs (* i ws) (* (1+ i) ws) (cue (mult s s))))
        (int-s (peak (integrate test-s) ny:all)))
    (format t "~a," (/ int-s ws))))
(format nil "Done.~%See debug window for output")



The RMS value already is “the area below the absolute value of a signal”. To get the “area underneath the RMS-graph” of an interval you just simply need to compute the RMS value of the original signal with different interval steps.

@Niklas: or do you need the statistical average “sum-of-RMS-values divided by number-of-RMS-values” per interval?

  • edgar