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...
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With