5

I recorded voice samples from my microphone using Adobe Flash Builder 4.6 / AIR, voice recorded successfully. I first converted voice data(byte array) to base64 format in actionscript then I converted that base64 data to WAV file using my PHP code. but that WAV file throw file corrupted issue in RiffPad.

RIFFPad is a viewer for RIFF formatted files like WAV, AVI.

expected wav file specification:

sampling Rate : 22KHZ

    // -- saves the current audio data as a .wav file
    protected function onSubmit( event:Event ):void {
        alertBox.show("Processing ... please wait.");

        stopPlayback();
        stopRecording();
        playBtn.enabled = recordBtn.enabled = submitBtn.enabled = false;
        var position:int = capture.buffer.position;
        var wavWriter:WAVWriter = new WAVWriter()
        var wavWriter1:WaveEncoder = new WaveEncoder()
        wavWriter.numOfChannels = 1;
        wavWriter.samplingRate = 22050;
        wavWriter.sampleBitRate = 16; 
        var wavBytes:ByteArray = new ByteArray;
        capture.buffer.position = 0;
        wavWriter.processSamples(wavBytes, capture.buffer, capture.microphone.rate * 1000, 1);
        Settings.alertBox3.show("RATE :"+capture.microphone.rate); //Here show RATE: 8
        //wavWriter.processSamples(wavBytes, capture.buffer, 22050, 1);
        //wavBytes = wavWriter1.encode( capture.buffer, 1, 16, 22050);
        capture.buffer.position = position;
        wavBytes.position=0;
        submitVoiceSample(Base64_new.encodeByteArray(wavBytes));
    }

WAV Writer header function:

public var samplingRate = 22050;
public var sampleBitRate:int = 8;
public var numOfChannels:int = 2;
private var compressionCode:int = 1;

private function header(dataOutput:IDataOutput, fileSize:Number):void
{
    dataOutput.writeUTFBytes("RIFF");
    dataOutput.writeUnsignedInt(fileSize); // Size of whole file
    dataOutput.writeUTFBytes("WAVE");
    // WAVE Chunk
    dataOutput.writeUTFBytes("fmt ");   // Chunk ID
    dataOutput.writeUnsignedInt(16);    // Header Chunk Data Size
    dataOutput.writeShort(compressionCode); // Compression code - 1 = PCM
    dataOutput.writeShort(numOfChannels); // Number of channels
    dataOutput.writeUnsignedInt(samplingRate); // Sample rate
    dataOutput.writeUnsignedInt(samplingRate * numOfChannels * sampleBitRate / 8); // Byte Rate == SampleRate * NumChannels * BitsPerSample/8       
    dataOutput.writeShort(numOfChannels * sampleBitRate / 8); // Block align == NumChannels * BitsPerSample/8
    dataOutput.writeShort(sampleBitRate); // Bits Per Sample
}

WAV file Writer function:

public function processSamples(dataOutput:IDataOutput, dataInput:ByteArray, inputSamplingRate:int, inputNumChannels:int = 1):void
{
    if (!dataInput || dataInput.bytesAvailable <= 0) // Return if null
        throw new Error("No audio data");

    // 16 bit values are between -32768 to 32767.
    var bitResolution:Number = (Math.pow(2, sampleBitRate)/2)-1;
    var soundRate:Number = samplingRate / inputSamplingRate;
    var dataByteLength:int = ((dataInput.length/4) * soundRate * sampleBitRate/8);
    // data.length is in 4 bytes per float, where we want samples * sampleBitRate/8 for bytes
    //var fileSize:int = 32 + 8 + dataByteLength;
    var fileSize:int = 32 + 4 + dataByteLength;
    // WAV format requires little-endian
    dataOutput.endian = Endian.LITTLE_ENDIAN;  
    // RIFF WAVE Header Information
    header(dataOutput, fileSize);
    // Data Chunk Header
    dataOutput.writeUTFBytes("data");
    dataOutput.writeUnsignedInt(dataByteLength); // Size of whole file

    // Write data to file
    dataInput.position = 0;
    var tempData:ByteArray = new ByteArray();
    tempData.endian = Endian.LITTLE_ENDIAN;

    // Write to file in chunks of converted data.
    while (dataInput.bytesAvailable > 0) 
    {
        tempData.clear();
        // Resampling logic variables
        var minSamples:int = Math.min(dataInput.bytesAvailable/4, 8192);
        var readSampleLength:int = minSamples;//Math.floor(minSamples/soundRate);
        var resampleFrequency:int = 100;  // Every X frames drop or add frames
        var resampleFrequencyCheck:int = (soundRate-Math.floor(soundRate))*resampleFrequency;
        var soundRateCeil:int = Math.ceil(soundRate);
        var soundRateFloor:int = Math.floor(soundRate);
        var jlen:int = 0;
        var channelCount:int = (numOfChannels-inputNumChannels);
        /*
        trace("resampleFrequency: " + resampleFrequency + " resampleFrequencyCheck: " + resampleFrequencyCheck
            + " soundRateCeil: " + soundRateCeil + " soundRateFloor: " + soundRateFloor);
        */
        var value:Number = 0;
        // Assumes data is in samples of float value
        for (var i:int = 0;i < readSampleLength;i+=4)
        {
            value = dataInput.readFloat();
            // Check for sanity of float value
            if (value > 1 || value < -1)
                throw new Error("Audio samples not in float format");

            // Special case with 8bit WAV files
            if (sampleBitRate == 8)
                value = (bitResolution * value) + bitResolution;
            else
                value = bitResolution * value;

            // Resampling Logic for non-integer sampling rate conversions
            jlen = (resampleFrequencyCheck > 0 && i % resampleFrequency < resampleFrequencyCheck) ? soundRateCeil : soundRateFloor; 
            for (var j:int = 0; j < jlen; j++)
            {
                writeCorrectBits(tempData, value, channelCount);
            }
        }
        dataOutput.writeBytes(tempData);
    }
}

I send that base64 data to my service request php side i got the '$this->request->voiceSample' parameter and decode base64 to .wav file

 file_put_contents('name.wav', base64_decode($this->request->voiceSample));

After load that "name.wav" file in Riffpad I got issue

There is extra junk at the end of the file.

Any one please give me the advice to solve this issue...

ketan
  • 19,129
  • 42
  • 60
  • 98
VijayRagavan
  • 380
  • 1
  • 5
  • 17
  • Check if your base64 string encoder is correct by comparing with publicly enabled encoders. Also check if your PHP decoder is correctly written (unless it's a built in function). Also check if your Flash side depends on byte endian (there is a hidden dependency in `c = data[int(i++)] << 16 | data[int(i++)] << 8 | data[int(i++)];` part at the very least). – Vesper May 25 '15 at 15:26
  • Hi vesper, I am using encoding [base64](http://www.sociodox.com/base64.html) lib for byteArray to base64 conversion. In PHP side, I am using built in function. – VijayRagavan May 26 '15 at 05:54
  • When you view the decoded file via hex editor, does it have a `RIFF` signature in first 4 bytes? If not, you'll have to debug your conversion routine(s). – Vesper May 26 '15 at 06:34
  • Our webservice support only _RIFF based waveform audio file format_ files only. I think the issue found in **WAV Data Writer function**. RIFF Chunk specification not formed correctly. – VijayRagavan May 26 '15 at 10:32
  • 1
    So, the file received has a RIFF signature intact? Okay, then it's not base64 conversion. Sorry I can't help in debugging the signature writing routine, maybe someone else can. (Hmm, maybe writing UTFBytes drops a zero in there somewhere? I suggest writing an int instead.) Also, once you will discover where the error is, please post an answer in this question. – Vesper May 26 '15 at 10:45
  • 1
    Nice question! :) I think the best option is to load wav file into flash, decode it, then use your functions to encode and convert it and send it to server. Compare the result with what you see in hex editor - maybe you miss something from the signature.. – Andrey Popov May 28 '15 at 12:12

1 Answers1

2

There is an inherent mistake in this line:

 wavWriter.processSamples(wavBytes, capture.buffer, capture.microphone.rate * 1000, 1);

The Microphone.rate manual states that actual sampling frequency differs from microphone.rate*1000 as expected by this code. The actual table is as follows:

rate   Actual frequency
44     44,100 Hz
22     22,050 Hz
11     11,025 Hz
8      8,000 Hz
5      5,512 Hz

So, while your code comments state that rate is reported as 8, this might not be the case on the client side in general, so perform the lookup prior to passing the deduced sampling rate into wavWriter.processSamples().

Next, you are precalculating dataByteLength via floating point calculation, this might end up being inaccurate as you then sample the data byte by byte, so it's better to first resample, then gather data length and only then write all the data into dataOutput, like this:

public function processSamples(dataOutput:IDataOutput, dataInput:ByteArray, inputSamplingRate:int, inputNumChannels:int = 1):void
{
    if (!dataInput || dataInput.bytesAvailable <= 0) // Return if null
        throw new Error("No audio data");

    // 16 bit values are between -32768 to 32767.
    var bitResolution:Number = (Math.pow(2, sampleBitRate)/2)-1;
    // var soundRate:Number = samplingRate / inputSamplingRate;
    // var fileSize:int = 32 + 4 + dataByteLength; kept for reference
    // fmt tag is 4+4+16, data header is 8 bytes in size, and 4 bytes for WAVE
    // but the data length is not yet determined
    // WAV format requires little-endian
    dataOutput.endian = Endian.LITTLE_ENDIAN;  
    // Prepare data for data to file
    dataInput.position = 0;
    var tempData:ByteArray = new ByteArray();
    tempData.endian = Endian.LITTLE_ENDIAN;
    // Writing in chunks is no longer possible, because we don't have the header ready

    // Let's precalculate the data needed in the loop
    var step:Number=inputSamplingRate / samplingRate; // how far we should step into the input data to get next sample
    var totalOffset:Number=1.0-1e-8; // accumulator for step
    var oldChannels:Array=[];
    var i:int;
    for (i=0;i<numOfChannels;i++) oldChannels.push(0.0);
    // previous channels' sample holder
    var newChannels:Array=oldChannels.slice(); // same for new channels that are to be read from byte array
    // reading first sample set from input byte array
    if (dataInput.bytesAvailable>=inputNumChannels*4) {
        for (i=0;i<inputNumChannels;i++) {
            var buf:Number=dataInput.readFloat();
            if (buf > 1) buf=1; if (buf < -1) buf=-1;
            newChannels[i]=buf;
        }
        // if there's one channel, copy data to other channels
        if ((inputNumChannels==1) && (numOfChannels>1)) {
            for (i=1;i<numOfChannels;i++) newChannels[i]=newChannels[0];                
        }
    }
    while ((dataInput.bytesAvailable>=inputNumChannels*4) || (totalOffset<1.0))
    {
        // sample next value for output wave file
        var value:Number;
        for (i=0;i<numOfChannels;i++) {
            value = (totalOffset*newChannels[i])+(1.0-totalOffset)*oldChannels[i];
            // linear interpolation between old sample and new sample
            // Special case with 8bit WAV files
            if (sampleBitRate == 8)
                value = (bitResolution * value) + bitResolution;
            else
                value = bitResolution * value; 
            // writing one channel into tempData
            writeCorrectBits(tempData, value, 0);
        }
        totalOffset+=step; // advance per output sample
        while ((totalOffset>1) && (dataInput.bytesAvailable>=inputNumChannels*4)) {
            // we need a new sample, and have a sample to process in input
            totalOffset-=1;
            for (i=0;i<numOfChannels;i++) oldChannels[i]=newChannels[i]; // store old sample
            // get another sample, copypasted from above
            for (i=0;i<inputNumChannels;i++) {
                value=dataInput.readFloat();
                if (value > 1) value=1; if (value < -1) value=-1; // sanity check
                // I made it clip instead of throwing exception, replace if necessary
                // if (value > 1 || value < -1) throw new Error("Audio samples not in float format");
                newChannels[i]=value;
            }
            if ((inputNumChannels==1) && (numOfChannels>1)) {
                for (i=1;i<numOfChannels;i++) newChannels[i]=newChannels[0];
            }
        } // end advance by totalOffset
    } // end main loop
    var dataBytesLength:uint=tempData.length; // now the length will be correct by definition
    header(dataOutput, 32+4+dataBytesLength);
    dataOutput.writeUTFBytes("data");
    dataOutput.writeUnsignedInt(dataBytesLength);
    dataOutput.writeBytes(tempData);

}

I have rewritten the resample routine to use sliding window algorithm (works best if new sample rate is higher than old, but accepts any ratio). This algorithm uses linear interpolation between samples instead of plainly re-using old sample over the length of the interpolated sequence. Feel free to replace with your own loop. The principal that should be retained is that you first compile full tempData and only then write the header with now correctly defined data length.

Please report issues if there are any.

Vesper
  • 18,599
  • 6
  • 39
  • 61
  • I have used the your rewritten routine it generates the audio file without any junk but audio file size is very low and the file not that we recorded no audio was heared when we played it – VijayRagavan Jun 02 '15 at 10:09
  • Ouch, there was a typo in determining `step` value, I've divided by a wrong variable, I should have divided it by `samplingRate` not `soundRate`. Fixed. – Vesper Jun 02 '15 at 10:18
  • Now the audio file has been generated but the file is too long that is 375kb and also when i tried to play the audio the sound was not exactly we recorded – VijayRagavan Jun 02 '15 at 10:28
  • Hmm. If you examine the file, what do you receive as output by RIFFPad? How many channels, what encoding, what length in samples and in seconds? Does it have 8-bit PCM or 16-bit? What kind of sound distortion you experience? – Vesper Jun 02 '15 at 11:06
  • Channels - 1, nSamplespersec - 22050 – VijayRagavan Jun 02 '15 at 11:14
  • RIFFPad Results offset - 20, id - fmt, dwsize - 16, wformattag-1, nchannels -1, nsamplepersec - 22050,navgbytespersec - 44100, nblockalign - 2,wbitspersample-16 – VijayRagavan Jun 02 '15 at 11:17
  • Hmm. 375kb /44100 = 8.5s, I doubt it's too long. Do you mind tracing what data you have recorded on the client side (`capture.buffer.length`, `capture.microphone.rate` and elapsed time of capture)? Do these correlate with what WAV file you receive? And again, what do you mean "the sound was not exactly we recorded"? Was it too slow, too fast, high-pitched, low-pitched, garbled, unrecognizable or completely different? – Vesper Jun 02 '15 at 14:29
  • (thumbsup) Thanks a lot vesper upvote +1 will test further and will approve the answer – VijayRagavan Jun 02 '15 at 14:59
  • Please provide data on what does `writeCorrectBits()` does with its third parameter. There was a weird value passed as third parameter, but with the rebuilt routine I call this function for each channel, not once per processed sample. This can be the source of wrongly received sound. I have edited the post and put **zero** as third parameter, as I expect it to be the number of extra samples to write to `tempData` besides the first one. Another possible values are `1` and `i`, but choosing the proper value requires knowledge of what the function does, as there is no code for it in the question. – Vesper Jun 02 '15 at 15:23
  • Thanks a lot @Visper issue fixed, I updated "processSamples" method above – VijayRagavan Jun 03 '15 at 05:50