Teensy 3.1 based sound recorder and digital delay line

Status
Not open for further replies.

uTasker

Well-known member
Hi All

I have updated the USB-MSD demo that I posted here: http://forum.pjrc.com/threads/25367-USB-MSD-with-SD-card-(Teensy-3-1)
One reson being that someone said that it would be of no interest to use the Teensy 3.1 as a memory stick, so I am hoping that this extension shows some use for that function too.

Essentially, by connecting a microphone to the ADC input and an SD card for storage it allows the Teensy 3.1 to be used to record sound, speech (like a digital dictation device). Due to the large storage space it should allow recoding for almost 72 hours on a 4GByte card. To play the recording back, simply connect via USB to a PC and open the file so that the Windows Media Player (or whatever) can play it back.



This is how it works and how to use it:

- This extends the USB-MSD project to include ADC/DAC use.
- The ADC input is A12 (ADC0_DP3), which is a single ended input (0..3.3V) and the DAC output is DAC/A14 (DAC0).
- The ADC input is sampled at 8kHz and each sample saved to an internal SRAM buffer. The PDB is used as time base and DMA is used to save each sample to memory.
- The SRAM buffer is 16kBytes in size (enough for 1s of input in 16 bit ADC mode).
- The DMA transfer generates an interrupt every half buffer (500ms) to signal that half of the buffer has been filled and the second half is now being filled. The circular buffer is handled automatically by the DMA controller.
- The buffer is also copied to the DAC output, but with a 1s delay. It also uses DMA and is timed the same as the ADC (using PDB) and the result is a 1s digital delay line between ADC and DAC. This is interesting even when not using the SD card since it uses no CPU power due to the DMA operation.
- When there is an SD-card attached, the signal can be saved to the SD card. To start a recording the input PTD5 (pin 20/A6) is pulled low for an instance (falling edge interrupt - debounced) and a file called Teensy_ADC.wav is created in the root directory of the SD-card if it doesn't already exist.
- The ADC buffer is saved to this file each time that it is half-full. During the recording the LED flashes quickly (normally it blinks slowly)
- When the input PTD5 (pin 20/A6) is pulsed low again the operation stops and the file is closed (and LED blinks slowly again).
The ADC data content is given a WAV header according to the saved length and format. It is important to stop the recording and not just reset the board since the WAV header will only be updated correctly when stopped correctly...
- The Teensy can then be connected to a PC to play back the WAV file (the recording that was made) via the USB-MSD mode whereby the SD-card appears as a hard-disk.
- After listening to the sound the file can be deleted (or renamed or saved etc.). If not deleted, further recordings are added on to the file and the header updated each time to match the complete content length.

A microphone can be connected to the ADC (with some amplification to get a decent signal) to build a dictation device!

The ADC/DAC/PDB/DMA operation is explained in this document: http://www.utasker.com/docs/uTasker/uTaskerADC.pdf (chapter 2)
The FAT used is documented here: http://www.utasker.com/docs/uTasker/uTasker_utFAT.PDF
The USB stack used is documented here: http://www.utasker.com/docs/uTasker/USB_User_Guide.PDF

To get the WAVE file recoding to work I had to study the RIFF WAV format header and then add code to handle the start/stop and disk interface. This required about 40 lines of new C-code to complete. I then tested some recordings and things seemed OK. however I would use an experimental SD card (no imporant data on it) because I can't guarantee that it will really achieve 72 hours ;-)

It is to be noted that the device will appear as a hard disk when powered via USB from the PC but the PC will not see the file (or its size change) until reconnected again later. This is normal since there is no synchronisation mechanism between the PC using the SD card and the internal application using it.
The use of USB-MSD is mainly to retrieve data from such a device after it has been used in a standalone environment. It may also be an idea to avoid recording when the PC has just been connected to - or is using the SD card since it may be using up some of the SD card read/write bandwidth and in the worst case could cause a buffer save to be missed since it couldn't be saved in time.

Hope you like the software.

Regards

Mark
 

Attachments

  • Teensy_3_1_delayline_recorder_V1.zip
    63.8 KB · Views: 294
This sounds like a very useful program, thank you!
I wonder, would it be possible for it to automatically finalize the WAV file after some time period (say 1 hour) and immediately start a new recording file, without loosing any data at the changeover?
 
Hi JBeale

Yes, that is a good idea, although I could imagine that often short recordings would be made and so a more frequent header update would be appropriate.

The actual internal workings are as follows:
- when the file is created, a RIFF WAVE header with zero data length is written
- each 500ms there are 8k of data (4k x 16 bits) to be saved and so these are written to the card, whereby there are three options:
1) - the data is written and the FAT entry is updated when new clusters are added (twice since there are always 2 FAT copies), the file object is updated and the RIFF WAVE header is overwritten with a new values (two locations contain infomation about the new length) [updating the header may also cause the file object to be updated again with the newest file time/date]. This requires writing to about 20 SD-card sectors each time as well as reading sectors a number of times in the process. This is however the 'safest' method since in the worst case only the present write would fail if there were a reset, but is the most time consuming to complete
2) - the data is written and the FAT entry updated, the file object is updated, but the RIFF WAVE header is not updated. This saves a couple of sector reads and writes. In the case of a reset the worst case for the data is that the last write in progress woudl be lost but the RIFF WAVE header would report the content to be shorter than it actually is (or zero if never updated) which means that there would be no data (unless the length entry was manipulated - which is in fact easy to do to correct).
3) - the data is written and the FAT entry updated, but the data object is only updated on close, along with the RIFF WAVE header. This is the most efficient method. Of course, if the close doesn't take place the file is still empty (the data is on the disk but in clusters that are not assigned to a file and so are not known by the reader). Possibly the PC will report that there is an error since the file length doesn't match with the physical cluster chain (which the PC can also repair)

I have updated the binary V1.1 for two reasons:
- I found that only half of the data was being saved in the first version (I didn't notice this when testing with a signal generator but once speech is used it was causing gaps).
- I have opened the file with an option to only commit the file object on close. This is case 3 and so the fastest, but with more risk involved. For experimentation this case is interesting but I may go for case 1 for production code once worst case timing is known.

Since the RIFF WAVE header write is controlled by the application it could be updated periodically as you suggest rather than at the end.
Another possibility would be to automatically 'repair' a RIFF WAVE header on start-up if it doesn't match the data length. In fact I find this an elegant method since the file length itself is in fact adequate to know the data content size (there is no data written that is not sound, as could be the case I believe).

It is also possible to open a file with a data cache (can reduce data writes) but this doesn't help in this case due to the fact that each data block written is a natural size for the SD-card (a multiple of 512 byte sectors).

I have reproduced the application code below as reference. It is called once every 500ms when there is a half-full ADC sample buffer (alternating between first and second half of the buffer).

Regards

Mark


Code:
static void fnSaveWavToDisk(signed short *ptrInput, unsigned short usBufferLength)
{
    static UTDIRECTORY *ptr_utWavDirectory = 0;                         // pointer to a directory object
    static UTFILE utWavFile = {0};                                      // local file object
    static unsigned long ulWaveLength = 0;
    static RIFF_WAVE_HEADER wav_header = {
        {'R', 'I', 'F', 'F'},
        0,
        {'W', 'A', 'V', 'E'},
        {'f', 'm', 't', ' '},
        16,                                                              // this must be 16
        FORMAT_PCM,
        1,                                                               // mono
        8000,                                                            // 8000 samples per second
        (8000 * 2),                                                      // bytes per second
        2,                                                               // 2 bytes for each sample
        16,                                                              // 16 bits samples
        {'d', 'a', 't', 'a'},
        0,                                                               // initially the data rate is zero
    };

    if (iStopRecording != 0) {                                           // close the file
        if (ptr_utWavDirectory != 0) {                                   // valid disk present
            wav_header.ulRiffLength = (ulWaveLength - 8);                // the header length field values
            wav_header.ulDataLength = (ulWaveLength - sizeof(wav_header));
            utSeek(&utWavFile, 0, UTFAT_SEEK_SET);                       // move back to start of the file
            utWriteFile(&utWavFile, (unsigned char *)&wav_header, sizeof(wav_header)); // write new header
            utCloseFile(&utWavFile);                                     // work complete
            ulWaveLength = 0;                                            // reset ready for next recording session
            ptr_utWavDirectory = 0;
            uTaskerMonoTimer(TASK_WATCHDOG, (DELAY_LIMIT)(0.20 * SEC), 0); // set standard LED blinking rate again
        }
        if (--iStopRecording == 0) {                                     // debounce the stop interrupt
            iStartRecording = 0;
        }
    }
    else if (iStartRecording != 0) {                                     // save buffer to disk
        if (ptr_utWavDirectory == 0) {                                   // directory has not yet been opened
            ptr_utWavDirectory = utAllocateDirectory(DISK_D, 0);         // allocate a directory for use by this module associated with D: use root
        }
        if (!(ptr_utWavDirectory->usDirectoryFlags & UTDIR_VALID)) {     // directory not valid
            if (utOpenDirectory(0, ptr_utWavDirectory) != UTFAT_SUCCESS) {  // open the root directory
                return;                                                  // do nothing until a card is detected
            }
            if (UTFAT_PATH_IS_FILE != utOpenFile("Teensy_ADC.wav", &utWavFile, ptr_utWavDirectory, (UTFAT_CREATE | UTFAT_OPEN_FOR_WRITE | UTFAT_OPEN_FOR_READ | UTFAT_APPEND | UTFAT_COMMIT_FILE_ON_CLOSE))) {
                return;
            }
            uTaskerMonoTimer(TASK_WATCHDOG, (DELAY_LIMIT)(0.05 * SEC), 0); // speed up LED during recording
            utSeek(&utWavFile, 0, UTFAT_SEEK_SET);                       // set to start of the file
            if (utWavFile.ulFileSize >= sizeof(wav_header)) {            // see whether there is already a header in the file
                utReadFile(&utWavFile, (unsigned char *)&wav_header, sizeof(wav_header)); // load existing header
                ulWaveLength = (wav_header.ulRiffLength + 8);            // valid length
                utSeek(&utWavFile, ulWaveLength, UTFAT_SEEK_SET);        // move to the end of the existing valid wave data
            }
        }
        if (utWavFile.ulFileSize == 0) {                                 // empty file so set the initial header
            if (!(utWavFile.ulFileMode & UTFAT_OPEN_FOR_WRITE)) {        // file has been closed so ignore further data
                return;
            }
            utWriteFile(&utWavFile, (unsigned char *)&wav_header, sizeof(wav_header)); // write an inital header with zero lengths
            ulWaveLength = sizeof(wav_header);                           // initial file content length
        }
        if (utWriteFile(&utWavFile, (unsigned char *)ptrInput, usBufferLength) == UTFAT_SUCCESS) { // save the data block
            ulWaveLength += usBufferLength;                              // new complete valid file length
        }
        if (iStartRecording < 4) {                                       // debounce start interrupt
            iStartRecording++;
        }
    }
}
 

Attachments

  • Teensy_3_1_delayline_recorder_V1_1.zip
    63.7 KB · Views: 226
Last edited:
Status
Not open for further replies.
Back
Top