Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Audio recorded file corrupted issue in Actionscript

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...

like image 681
VijayRagavan Avatar asked Nov 01 '22 03:11

VijayRagavan


1 Answers

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.

like image 77
Vesper Avatar answered Nov 05 '22 13:11

Vesper