通过MediaExtractor获取音频的信息。
package com.goobird.common.basic.utils.audio;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.util.Arrays;
import android.annotation.SuppressLint;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.media.MediaRecorder;
import android.os.Environment;
import android.util.Log;
@SuppressLint("NewApi")
public class SoundFile {
private ProgressListener mProgressListener = null;
private File mInputFile = null;
// Member variables representing frame data
private String mFileType;
private int mFileSize;
private int mAvgBitRate; // Average bit rate in kbps.
private int mSampleRate;
private int mChannels;
private int mNumSamples; // total number of samples per channel in audio file
private ByteBuffer mDecodedBytes; // Raw audio data
private ShortBuffer mDecodedSamples; // shared buffer with mDecodedBytes.
// mDecodedSamples has the following format:
// {s1c1, s1c2, ..., s1cM, s2c1, ..., s2cM, ..., sNc1, ..., sNcM}
// where sicj is the ith sample of the jth channel (a sample is a signed short)
// M is the number of channels (e.g. 2 for stereo) and N is the number of samples per channel.
// Member variables for hack (making it work with old version, until app just uses the samples).
private int mNumFrames;
private int[] mFrameGains;
private int[] mFrameLens;
private int[] mFrameOffsets;
private float mNumFramesFloat;
public float getmNumFramesFloat() {
return mNumFramesFloat;
}
// Progress listener interface.
public interface ProgressListener {
/**
* Will be called by the SoundFile class periodically
* with values between 0.0 and 1.0. Return true to continue
* loading the file or recording the audio, and false to cancel or stop recording.
*/
boolean reportProgress(double fractionComplete);
}
// Custom exception for invalid inputs.
public class InvalidInputException extends Exception {
// Serial version ID generated by Eclipse.
private static final long serialVersionUID = -2505698991597837165L;
public InvalidInputException(String message) {
super(message);
}
}
// TODO(nfaralli): what is the real list of supported extensions? Is it device dependent?
public static String[] getSupportedExtensions() {
return new String[] {"mp3", "wav", "3gpp", "3gp", "amr", "aac", "m4a", "ogg"};
}
public static boolean isFilenameSupported(String filename) {
String[] extensions = getSupportedExtensions();
for (int i=0; i<extensions.length; i++) {
if (filename.endsWith("." + extensions[i])) {
return true;
}
}
return false;
}
// Create and return a SoundFile object using the file fileName.
public static SoundFile create(String fileName,
ProgressListener progressListener)
throws java.io.FileNotFoundException,
IOException, InvalidInputException {
// First check that the file exists and that its extension is supported.
File f = new File(fileName);
if (!f.exists()) {
throw new java.io.FileNotFoundException(fileName);
}
String name = f.getName().toLowerCase();
String[] components = name.split("\\.");
if (components.length < 2) {
return null;
}
if (!Arrays.asList(getSupportedExtensions()).contains(components[components.length - 1])) {
return null;
}
SoundFile soundFile = new SoundFile();
soundFile.setProgressListener(progressListener);
soundFile.ReadFile(f);
return soundFile;
}
// Create and return a SoundFile object by recording a mono audio stream.
public static SoundFile record(ProgressListener progressListener) {
if (progressListener == null) {
// must have a progessListener to stop the recording.
return null;
}
SoundFile soundFile = new SoundFile();
soundFile.setProgressListener(progressListener);
soundFile.RecordAudio();
return soundFile;
}
public String getFiletype() {
return mFileType;
}
public int getFileSizeBytes() {
return mFileSize;
}
public int getAvgBitrateKbps() {
return mAvgBitRate;
}
public int getSampleRate() {
return mSampleRate;
}
public int getChannels() {
return mChannels;
}
public int getNumSamples() {
return mNumSamples; // Number of samples per channel.
}
// Should be removed when the app will use directly the samples instead of the frames.
public int getNumFrames() {
return mNumFrames;
}
// Should be removed when the app will use directly the samples instead of the frames.
public int getSamplesPerFrame() {
return 16000/50; // just a fixed value here...
// return 1024/2; // just a fixed value here...
}
// Should be removed when the app will use directly the samples instead of the frames.
public int[] getFrameGains() {
return mFrameGains;
}
public ShortBuffer getSamples() {
if (mDecodedSamples != null) {
return mDecodedSamples.asReadOnlyBuffer();
} else {
return null;
}
}
// A SoundFile object should only be created using the static methods create() and record().
private SoundFile() {
}
private void setProgressListener(ProgressListener progressListener) {
mProgressListener = progressListener;
}
private void ReadFile(File inputFile)
throws java.io.FileNotFoundException,
IOException, InvalidInputException {
MediaExtractor extractor = new MediaExtractor();
MediaFormat format = null;
int i;
mInputFile = inputFile;
String[] components = mInputFile.getPath().split("\\.");
mFileType = components[components.length - 1];
mFileSize = (int)mInputFile.length();
extractor.setDataSource(mInputFile.getPath());
int numTracks = extractor.getTrackCount();
// find and select the first audio track present in the file.
for (i=0; i<numTracks; i++) {
format = extractor.getTrackFormat(i);
if (format.getString(MediaFormat.KEY_MIME).startsWith("audio/")) {
extractor.selectTrack(i);
break;
}
}
if (i == numTracks) {
throw new InvalidInputException("No audio track found in " + mInputFile);
}
mChannels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
mSampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
// Expected total number of samples per channel.
int expectedNumSamples =
(int)((format.getLong(MediaFormat.KEY_DURATION) / 100.f) * mSampleRate + 0.5f);
// (int)((format.getLong(MediaFormat.KEY_DURATION) / 1000000.f) * mSampleRate + 0.5f);
MediaCodec codec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME));
codec.configure(format, null, null, 0);
codec.start();
int decodedSamplesSize = 0; // size of the output buffer containing decoded samples.
byte[] decodedSamples = null;
ByteBuffer[] inputBuffers = codec.getInputBuffers();
ByteBuffer[] outputBuffers = codec.getOutputBuffers();
int sample_size;
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
long presentation_time;
int tot_size_read = 0;
boolean done_reading = false;
// Set the size of the decoded samples buffer to 1MB (~6sec of a stereo stream at 44.1kHz).
// For longer streams, the buffer size will be increased later on, calculating a rough
// estimate of the total size needed to store all the samples in order to resize the buffer
// only once.
mDecodedBytes = ByteBuffer.allocate(1<<20);
Boolean firstSampleData = true;
while (true) {
// read data from file and feed it to the decoder input buffers.
int inputBufferIndex = codec.dequeueInputBuffer(100);
if (!done_reading && inputBufferIndex >= 0) {
sample_size = extractor.readSampleData(inputBuffers[inputBufferIndex], 0);
if (firstSampleData
&& format.getString(MediaFormat.KEY_MIME).equals("audio/mp4a-latm")
&& sample_size == 2) {
// For some reasons on some devices (e.g. the Samsung S3) you should not
// provide the first two bytes of an AAC stream, otherwise the MediaCodec will
// crash. These two bytes do not contain music data but basic info on the
// stream (e.g. channel configuration and sampling frequency), and skipping them
// seems OK with other devices (MediaCodec has already been configured and
// already knows these parameters).
extractor.advance();
tot_size_read += sample_size;
} else if (sample_size < 0) {
// All samples have been read.
codec.queueInputBuffer(
inputBufferIndex, 0, 0, -1, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
done_reading = true;
} else {
presentation_time = extractor.getSampleTime();
codec.queueInputBuffer(inputBufferIndex, 0, sample_size, presentation_time, 0);
extractor.advance();
tot_size_read += sample_size;
if (mProgressListener != null) {
if (!mProgressListener.reportProgress((float)(tot_size_read) / mFileSize)) {
// We are asked to stop reading the file. Returning immediately. The
// SoundFile object is invalid and should NOT be used afterward!
extractor.release();
extractor = null;
codec.stop();
codec.release();
codec = null;
return;
}
}
}
firstSampleData = false;
}
// Get decoded stream from the decoder output buffers.
int outputBufferIndex = codec.dequeueOutputBuffer(info, 100);
if (outputBufferIndex >= 0 && info.size > 0) {
if (decodedSamplesSize < info.size) {
decodedSamplesSize = info.size;
decodedSamples = new byte[decodedSamplesSize];
}
outputBuffers[outputBufferIndex].get(decodedSamples, 0, info.size);
outputBuffers[outputBufferIndex].clear();
// Check if buffer is big enough. Resize it if it's too small.
if (mDecodedBytes.remaining() < info.size) {
// Getting a rough estimate of the total size, allocate 20% more, and
// make sure to allocate at least 5MB more than the initial size.
int position = mDecodedBytes.position();
int newSize = (int)((position * (1.0 * mFileSize / tot_size_read)) * 1.2);
if (newSize - position < info.size + 5 * (1<<20)) {
newSize = position + info.size + 5 * (1<<20);
}
ByteBuffer newDecodedBytes = null;
// Try to allocate memory. If we are OOM, try to run the garbage collector.
int retry = 10;
while(retry > 0) {
try {
newDecodedBytes = ByteBuffer.allocate(newSize);
break;
} catch (OutOfMemoryError oome) {
// setting android:largeHeap="true" in <application> seem to help not
// reaching this section.
retry--;
}
}
if (retry == 0) {
// Failed to allocate memory... Stop reading more data and finalize the
// instance with the data decoded so far.
break;
}
//ByteBuffer newDecodedBytes = ByteBuffer.allocate(newSize);
mDecodedBytes.rewind();
newDecodedBytes.put(mDecodedBytes);
mDecodedBytes = newDecodedBytes;
mDecodedBytes.position(position);
}
mDecodedBytes.put(decodedSamples, 0, info.size);
codec.releaseOutputBuffer(outputBufferIndex, false);
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
outputBuffers = codec.getOutputBuffers();
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Subsequent data will conform to new format.
// We could check that codec.getOutputFormat(), which is the new output format,
// is what we expect.
}
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0
|| (mDecodedBytes.position() / (2 * mChannels)) >= expectedNumSamples) {
// We got all the decoded data from the decoder. Stop here.
// Theoretically dequeueOutputBuffer(info, ...) should have set info.flags to
// MediaCodec.BUFFER_FLAG_END_OF_STREAM. However some phones (e.g. Samsung S3)
// won't do that for some files (e.g. with mono AAC files), in which case subsequent
// calls to dequeueOutputBuffer may result in the application crashing, without
// even an exception being thrown... Hence the second check.
// (for mono AAC files, the S3 will actually double each sample, as if the stream
// was stereo. The resulting stream is half what it's supposed to be and with a much
// lower pitch.)
break;
}
}
mNumSamples = mDecodedBytes.position() / (mChannels * 2); // One sample = 2 bytes.
mDecodedBytes.rewind();
mDecodedBytes.order(ByteOrder.LITTLE_ENDIAN);
mDecodedSamples = mDecodedBytes.asShortBuffer();
mAvgBitRate = (int)((mFileSize * 8) * ((float)mSampleRate / mNumSamples) / 1000);
extractor.release();
extractor = null;
codec.stop();
codec.release();
codec = null;
// Temporary hack to make it work with the old version.
mNumFrames = mNumSamples / getSamplesPerFrame();
mNumFramesFloat = (float)mNumSamples / getSamplesPerFrame();
System.out.println(mNumSamples+"sstest"+getSamplesPerFrame()+"--"+mNumFramesFloat);
if (mNumSamples % getSamplesPerFrame() != 0){
mNumFrames++;
}
mFrameGains = new int[mNumFrames];
mFrameLens = new int[mNumFrames];
mFrameOffsets = new int[mNumFrames];
int j;
int gain, value;
int frameLens = (int)((1000 * mAvgBitRate / 8) *
((float)getSamplesPerFrame() / mSampleRate));
for (i=0; i<mNumFrames; i++){
gain = -1;
for(j=0; j<getSamplesPerFrame(); j++) {
value = 0;
for (int k=0; k<mChannels; k++) {
if (mDecodedSamples.remaining() > 0) {
value += Math.abs(mDecodedSamples.get());
}
}
value /= mChannels;
if (gain < value) {
gain = value;
}
}
mFrameGains[i] = (int)Math.sqrt(gain); // here gain = sqrt(max value of 1st channel)...
mFrameLens[i] = frameLens; // totally not accurate...
mFrameOffsets[i] = (int)(i * (1000 * mAvgBitRate / 8) * // = i * frameLens
((float)getSamplesPerFrame() / mSampleRate));
}
mDecodedSamples.rewind();
// DumpSamples(); // Uncomment this line to dump the samples in a TSV file.
}
private void RecordAudio() {
if (mProgressListener == null) {
// A progress listener is mandatory here, as it will let us know when to stop recording.
return;
}
mInputFile = null;
mFileType = "wav";
mFileSize = 0;
mSampleRate = 16000;
mChannels = 1; // record mono audio.
short[] buffer = new short[1024]; // buffer contains 1 mono frame of 1024 16 bits samples
int minBufferSize = AudioRecord.getMinBufferSize(
mSampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
// make sure minBufferSize can contain at least 1 second of audio (16 bits sample).
if (minBufferSize < mSampleRate * 2) {
minBufferSize = mSampleRate * 2;
}
AudioRecord audioRecord = new AudioRecord(
MediaRecorder.AudioSource.DEFAULT,
mSampleRate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
minBufferSize
);
// Allocate memory for 20 seconds first. Reallocate later if more is needed.
mDecodedBytes = ByteBuffer.allocate(20 * mSampleRate * 2);
mDecodedBytes.order(ByteOrder.LITTLE_ENDIAN);
mDecodedSamples = mDecodedBytes.asShortBuffer();
audioRecord.startRecording();
while (true) {
// check if mDecodedSamples can contain 1024 additional samples.
if (mDecodedSamples.remaining() < 1024) {
// Try to allocate memory for 10 additional seconds.
int newCapacity = mDecodedBytes.capacity() + 10 * mSampleRate * 2;
ByteBuffer newDecodedBytes = null;
try {
newDecodedBytes = ByteBuffer.allocate(newCapacity);
} catch (OutOfMemoryError oome) {
break;
}
int position = mDecodedSamples.position();
mDecodedBytes.rewind();
newDecodedBytes.put(mDecodedBytes);
mDecodedBytes = newDecodedBytes;
mDecodedBytes.order(ByteOrder.LITTLE_ENDIAN);
mDecodedBytes.rewind();
mDecodedSamples = mDecodedBytes.asShortBuffer();
mDecodedSamples.position(position);
}
// TODO(nfaralli): maybe use the read method that takes a direct ByteBuffer argument.
audioRecord.read(buffer, 0, buffer.length);
mDecodedSamples.put(buffer);
// Let the progress listener know how many seconds have been recorded.
// The returned value tells us if we should keep recording or stop.
if (!mProgressListener.reportProgress(
(float)(mDecodedSamples.position()) / mSampleRate)) {
break;
}
}
audioRecord.stop();
audioRecord.release();
mNumSamples = mDecodedSamples.position();
mDecodedSamples.rewind();
mDecodedBytes.rewind();
mAvgBitRate = mSampleRate * 16 / 1000;
// Temporary hack to make it work with the old version.
mNumFrames = mNumSamples / getSamplesPerFrame();
if (mNumSamples % getSamplesPerFrame() != 0){
mNumFrames++;
}
mFrameGains = new int[mNumFrames];
mFrameLens = null; // not needed for recorded audio
mFrameOffsets = null; // not needed for recorded audio
int i, j;
int gain, value;
for (i=0; i<mNumFrames; i++){
gain = -1;
for(j=0; j<getSamplesPerFrame(); j++) {
if (mDecodedSamples.remaining() > 0) {
value = Math.abs(mDecodedSamples.get());
} else {
value = 0;
}
if (gain < value) {
gain = value;
}
}
mFrameGains[i] = (int)Math.sqrt(gain); // here gain = sqrt(max value of 1st channel)...
}
mDecodedSamples.rewind();
// DumpSamples(); // Uncomment this line to dump the samples in a TSV file.
}
// should be removed in the near future...
public void WriteFile(File outputFile, int startFrame, int numFrames)
throws IOException {
float startTime = (float)startFrame * getSamplesPerFrame() / mSampleRate;
float endTime = (float)(startFrame + numFrames) * getSamplesPerFrame() / mSampleRate;
WriteFile(outputFile, startTime, endTime);
}
public void WriteFile(File outputFile, float startTime, float endTime)
throws IOException {
int startOffset = (int)(startTime * mSampleRate) * 2 * mChannels;
int numSamples = (int)((endTime - startTime) * mSampleRate);
// Some devices have problems reading mono AAC files (e.g. Samsung S3). Making it stereo.
int numChannels = (mChannels == 1) ? 2 : mChannels;
String mimeType = "audio/mp4a-latm";
int bitrate = 64000 * numChannels; // rule of thumb for a good quality: 64kbps per channel.
MediaCodec codec = MediaCodec.createEncoderByType(mimeType);
MediaFormat format = MediaFormat.createAudioFormat(mimeType, mSampleRate, numChannels);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
codec.start();
// Get an estimation of the encoded data based on the bitrate. Add 10% to it.
int estimatedEncodedSize = (int)((endTime - startTime) * (bitrate / 8) * 1.1);
ByteBuffer encodedBytes = ByteBuffer.allocate(estimatedEncodedSize);
ByteBuffer[] inputBuffers = codec.getInputBuffers();
ByteBuffer[] outputBuffers = codec.getOutputBuffers();
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
boolean done_reading = false;
long presentation_time = 0;
int frame_size = 1024; // number of samples per frame per channel for an mp4 (AAC) stream.
byte buffer[] = new byte[frame_size * numChannels * 2]; // a sample is coded with a short.
mDecodedBytes.position(startOffset);
numSamples += (2 * frame_size); // Adding 2 frames, Cf. priming frames for AAC.
int tot_num_frames = 1 + (numSamples / frame_size); // first AAC frame = 2 bytes
if (numSamples % frame_size != 0) {
tot_num_frames++;
}
int[] frame_sizes = new int[tot_num_frames];
int num_out_frames = 0;
int num_frames=0;
int num_samples_left = numSamples;
int encodedSamplesSize = 0; // size of the output buffer containing the encoded samples.
byte[] encodedSamples = null;
while (true) {
// Feed the samples to the encoder.
int inputBufferIndex = codec.dequeueInputBuffer(100);
if (!done_reading && inputBufferIndex >= 0) {
if (num_samples_left <= 0) {
// All samples have been read.
codec.queueInputBuffer(
inputBufferIndex, 0, 0, -1, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
done_reading = true;
} else {
inputBuffers[inputBufferIndex].clear();
if (buffer.length > inputBuffers[inputBufferIndex].remaining()) {
// Input buffer is smaller than one frame. This should never happen.
continue;
}
// bufferSize is a hack to create a stereo file from a mono stream.
int bufferSize = (mChannels == 1) ? (buffer.length / 2) : buffer.length;
if (mDecodedBytes.remaining() < bufferSize) {
for (int i=mDecodedBytes.remaining(); i < bufferSize; i++) {
buffer[i] = 0; // pad with extra 0s to make a full frame.
}
mDecodedBytes.get(buffer, 0, mDecodedBytes.remaining());
} else {
mDecodedBytes.get(buffer, 0, bufferSize);
}
if (mChannels == 1) {
for (int i=bufferSize - 1; i >= 1; i -= 2) {
buffer[2*i + 1] = buffer[i];
buffer[2*i] = buffer[i-1];
buffer[2*i - 1] = buffer[2*i + 1];
buffer[2*i - 2] = buffer[2*i];
}
}
num_samples_left -= frame_size;
inputBuffers[inputBufferIndex].put(buffer);
presentation_time = (long) (((num_frames++) * frame_size * 1e6) / mSampleRate);
codec.queueInputBuffer(
inputBufferIndex, 0, buffer.length, presentation_time, 0);
}
}
// Get the encoded samples from the encoder.
int outputBufferIndex = codec.dequeueOutputBuffer(info, 100);
if (outputBufferIndex >= 0 && info.size > 0 && info.presentationTimeUs >=0) {
if (num_out_frames < frame_sizes.length) {
frame_sizes[num_out_frames++] = info.size;
}
if (encodedSamplesSize < info.size) {
encodedSamplesSize = info.size;
encodedSamples = new byte[encodedSamplesSize];
}
outputBuffers[outputBufferIndex].get(encodedSamples, 0, info.size);
outputBuffers[outputBufferIndex].clear();
codec.releaseOutputBuffer(outputBufferIndex, false);
if (encodedBytes.remaining() < info.size) { // Hopefully this should not happen.
estimatedEncodedSize = (int)(estimatedEncodedSize * 1.2); // Add 20%.
ByteBuffer newEncodedBytes = ByteBuffer.allocate(estimatedEncodedSize);
int position = encodedBytes.position();
encodedBytes.rewind();
newEncodedBytes.put(encodedBytes);
encodedBytes = newEncodedBytes;
encodedBytes.position(position);
}
encodedBytes.put(encodedSamples, 0, info.size);
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
outputBuffers = codec.getOutputBuffers();
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Subsequent data will conform to new format.
// We could check that codec.getOutputFormat(), which is the new output format,
// is what we expect.
}
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
// We got all the encoded data from the encoder.
break;
}
}
int encoded_size = encodedBytes.position();
encodedBytes.rewind();
codec.stop();
codec.release();
codec = null;
// Write the encoded stream to the file, 4kB at a time.
buffer = new byte[4096];
try {
FileOutputStream outputStream = new FileOutputStream(outputFile);
outputStream.write(MP4Header.getMP4Header(mSampleRate, numChannels, frame_sizes, bitrate));
while (encoded_size - encodedBytes.position() > buffer.length) {
encodedBytes.get(buffer);
outputStream.write(buffer);
}
int remaining = encoded_size - encodedBytes.position();
if (remaining > 0) {
encodedBytes.get(buffer, 0, remaining);
outputStream.write(buffer, 0, remaining);
}
outputStream.close();
} catch (IOException e) {
Log.e("Ringdroid", "Failed to create the .m4a file.");
Log.e("Ringdroid", getStackTrace(e));
}
}
// Method used to swap the left and right channels (needed for stereo WAV files).
// buffer contains the PCM data: {sample 1 right, sample 1 left, sample 2 right, etc.}
// The size of a sample is assumed to be 16 bits (for a single channel).
// When done, buffer will contain {sample 1 left, sample 1 right, sample 2 left, etc.}
private void swapLeftRightChannels(byte[] buffer) {
byte left[] = new byte[2];
byte right[] = new byte[2];
if (buffer.length % 4 != 0) { // 2 channels, 2 bytes per sample (for one channel).
// Invalid buffer size.
return;
}
for (int offset = 0; offset < buffer.length; offset += 4) {
left[0] = buffer[offset];
left[1] = buffer[offset + 1];
right[0] = buffer[offset + 2];
right[1] = buffer[offset + 3];
buffer[offset] = right[0];
buffer[offset + 1] = right[1];
buffer[offset + 2] = left[0];
buffer[offset + 3] = left[1];
}
}
// should be removed in the near future...
public void WriteWAVFile(File outputFile, int startFrame, int numFrames)
throws IOException {
float startTime = (float)startFrame * getSamplesPerFrame() / mSampleRate;
float endTime = (float)(startFrame + numFrames) * getSamplesPerFrame() / mSampleRate;
WriteWAVFile(outputFile, startTime, endTime);
}
public void WriteWAVFile(File outputFile, float startTime, float endTime)
throws IOException {
int startOffset = (int)(startTime * mSampleRate) * 2 * mChannels;
int numSamples = (int)((endTime - startTime) * mSampleRate);
// Start by writing the RIFF header.
FileOutputStream outputStream = new FileOutputStream(outputFile);
outputStream.write(WAVHeader.getWAVHeader(mSampleRate, mChannels, numSamples));
// Write the samples to the file, 1024 at a time.
byte buffer[] = new byte[1024 * mChannels * 2]; // Each sample is coded with a short.
mDecodedBytes.position(startOffset);
int numBytesLeft = numSamples * mChannels * 2;
while (numBytesLeft >= buffer.length) {
if (mDecodedBytes.remaining() < buffer.length) {
// This should not happen.
for (int i = mDecodedBytes.remaining(); i < buffer.length; i++) {
buffer[i] = 0; // pad with extra 0s to make a full frame.
}
mDecodedBytes.get(buffer, 0, mDecodedBytes.remaining());
} else {
mDecodedBytes.get(buffer);
}
if (mChannels == 2) {
swapLeftRightChannels(buffer);
}
outputStream.write(buffer);
numBytesLeft -= buffer.length;
}
if (numBytesLeft > 0) {
if (mDecodedBytes.remaining() < numBytesLeft) {
// This should not happen.
for (int i = mDecodedBytes.remaining(); i < numBytesLeft; i++) {
buffer[i] = 0; // pad with extra 0s to make a full frame.
}
mDecodedBytes.get(buffer, 0, mDecodedBytes.remaining());
} else {
mDecodedBytes.get(buffer, 0, numBytesLeft);
}
if (mChannels == 2) {
swapLeftRightChannels(buffer);
}
outputStream.write(buffer, 0, numBytesLeft);
}
outputStream.close();
}
// Debugging method dumping all the samples in mDecodedSamples in a TSV file.
// Each row describes one sample and has the following format:
// "<presentation time in seconds>\t<channel 1>\t...\t<channel N>\n"
// File will be written on the SDCard under media/audio/debug/
// If fileName is null or empty, then the default file name (samples.tsv) is used.
private void DumpSamples(String fileName) {
String externalRootDir = Environment.getExternalStorageDirectory().getPath();
if (!externalRootDir.endsWith("/")) {
externalRootDir += "/";
}
String parentDir = externalRootDir + "media/audio/debug/";
// Create the parent directory
File parentDirFile = new File(parentDir);
parentDirFile.mkdirs();
// If we can't write to that special path, try just writing directly to the SDCard.
if (!parentDirFile.isDirectory()) {
parentDir = externalRootDir;
}
if (fileName == null || fileName.isEmpty()) {
fileName = "samples.tsv";
}
File outFile = new File(parentDir + fileName);
// Start dumping the samples.
BufferedWriter writer = null;
float presentationTime = 0;
mDecodedSamples.rewind();
String row;
try {
writer = new BufferedWriter(new FileWriter(outFile));
for (int sampleIndex = 0; sampleIndex < mNumSamples; sampleIndex++) {
presentationTime = (float)(sampleIndex) / mSampleRate;
row = Float.toString(presentationTime);
for (int channelIndex = 0; channelIndex < mChannels; channelIndex++) {
row += "\t" + mDecodedSamples.get();
}
row += "\n";
writer.write(row);
}
} catch (IOException e) {
Log.w("Ringdroid", "Failed to create the sample TSV file.");
Log.w("Ringdroid", getStackTrace(e));
}
// We are done here. Close the file and rewind the buffer.
try {
writer.close();
} catch (Exception e) {
Log.w("Ringdroid", "Failed to close sample TSV file.");
Log.w("Ringdroid", getStackTrace(e));
}
mDecodedSamples.rewind();
}
// Helper method (samples will be dumped in media/audio/debug/samples.tsv).
private void DumpSamples() {
DumpSamples(null);
}
// Return the stack trace of a given exception.
private String getStackTrace(Exception e) {
StringWriter writer = new StringWriter();
e.printStackTrace(new PrintWriter(writer));
return writer.toString();
}
}
画自定义view。按照音频时间画横轴,然后纵轴是音频的频谱。
package com.goobird.common.application.widget;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ScrollView;
import android.widget.Scroller;
import com.goobird.common.R;
import com.goobird.common.basic.utils.CommonThreadPool;
import com.goobird.common.basic.utils.LogUtils;
import com.goobird.common.basic.utils.ScreenUtils;
import com.goobird.common.basic.utils.audio.SoundFile;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
/**
* create by Jiang HongLi
* create on 2019/1/4 0004
* description 音频类频谱进度条,可拖拽
*/
public class AudioProgressBarDrag extends View implements View.OnTouchListener {
private int viewHeight;
private int viewWidth;
private long duration;
private static final int DEFAULT_BAR_WIDTH = 2;//一个波形的默认宽度,单位 dp
private static final float DEFAULT_BAR_RATE = 0.7f;
private int barColor;
//绘制未着色的波峰
private Paint barPaint;
//波峰+间隔的 宽度
private float barWidthFull;
//波峰宽度
private float barWidth;
//波峰宽度的一半
private float barWidthHalf;
//能绘制的波峰数
private int barCount;
private Random random;
// 波形 权重
private float[] waveWeight = new float[]{0.3f, 0.2f, 0.21f, 0.22f, 0.23f, 0.24f, 0.30f, 0.25f, 0.26f, 0.27f, 0.28f, 0.29f, 0.3f};
private int speed;
private Timer timer;
private TimerTask task;
private int distance;
private long size;
private float[] mCurData;
private int currentDis;
private GestureDetector mygesture;
private AudioDragListener draglistener;
private float mOffset;
private float rawX;
private float rawY;
private float downY;
private AudioDragDistanceListener distanceListener;
private int anInt;
private float[] totalLines;
private float progressX;// 水瓶滑动的距离
private int count;
private Scroller mScroller;
private boolean isPrepare;
private Paint linePaint;
private SoundFile mSoundFile;
private int mSampleRate;
private int mSamplesPerFrame;
private int[] mHeightsAtThisZoomLevel;
private double[] mZoomFactorByZoomLevel;
private int mZoomLevel;
private int mNumZoomLevels;
private double[][] mValuesByZoomLevel;
private int[] mLenByZoomLevel;
private boolean mInitialized;
private boolean useFalseData = false;// 是否用假数据
public AudioProgressBarDrag(@NonNull Context context) {
super(context);
initView(context);
}
public AudioProgressBarDrag(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public AudioProgressBarDrag(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
private void initView(Context context) {
random = new Random();
speed = 15;
barColor = getResources().getColor(R.color.audio_line);
setClickable(true);
//构建手势探测器
mygesture = new GestureDetector(context, mSimpleOnGestureListener);
setOnTouchListener(this);
anInt = ScreenUtils.dp2px(context, 25);
mScroller = new Scroller(getContext());
linePaint = new Paint();
linePaint.setStyle(Paint.Style.FILL);
linePaint.setStrokeWidth(ScreenUtils.dp2px(getContext(), 2));
linePaint.setColor(barColor);
}
public void reSet() {
distance = 0;
scrollTo(0, 0);
}
/**
* 设置是否准备好,没有准备好就直线
*
* @param isPrepare
*/
public void setPrepareState(boolean isPrepare) {
this.isPrepare = isPrepare;
invalidate();
}
public void setDuration(long duration) {
this.duration = duration;
post(new Runnable() {
@Override
public void run() {
int measuredWidth = getMeasuredWidth();
init();
}
});
}
public void setSoundFile(SoundFile soundFile) {
if (soundFile == null) {
useFalseData = true;
} else {
useFalseData = false;
mSoundFile = soundFile;
mSampleRate = mSoundFile.getSampleRate();
mSamplesPerFrame = mSoundFile.getSamplesPerFrame();
computeDoublesForAllZoomLevels();
mHeightsAtThisZoomLevel = null;
}
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
getViewData();
if (!isPrepare) {
// drawOneLine(canvas);
} else {
if (duration == 0 || mCurData == null || (!useFalseData && mSoundFile == null)) {
return;
}
if (useFalseData) {
drawLines(canvas);
} else {
drawReallyLines(canvas);
}
}
}
/**
* 画真实的线
*
* @param canvas
*/
private void drawReallyLines(Canvas canvas) {
if (mHeightsAtThisZoomLevel == null)
computeIntsForThisZoomLevel();
int ctr = getMeasuredHeight() / 2;
canvas.save();
canvas.translate(getScrollX(), 0);
size = maxPos();
// 获取屏幕最左侧的波峰起始位置
int start = (int) (getScrollX() / barWidthFull);
start = start < 0 ? 0 : (int) (start > size ? size : start);
// 获取屏幕最右侧的波峰结束位置
int end = start + barCount > size ? (int) size : start + barCount;
// 屏幕内画多少个波峰
int mSize = start + barCount > size ? (int) (size - start) : barCount;
// 左侧波峰的偏移量
int v = getScrollX() % (int) (barWidthFull);
for (int j = 0; j < mSize; j++) {
float x = j * barWidthFull - (v * barWidthFull / 10);
float topY = ctr - mHeightsAtThisZoomLevel[start + j];
float bottomY = ctr + 1 + mHeightsAtThisZoomLevel[start + j];
canvas.drawLine(x, topY, x, bottomY, barPaint);
}
canvas.restore();
}
/**
* 画一条直线
*
* @param canvas
*/
private void drawOneLine(Canvas canvas) {
canvas.drawLine(0, viewHeight / 2, ScreenUtils.getScreenWidth(getContext()), viewHeight / 2, linePaint);
}
/**
* 画指定 位置的线
*
* @param canvas
*/
private void drawLines(Canvas canvas) {
canvas.save();
canvas.translate(getScrollX(), 0);
// 获取屏幕最左侧的波峰起始位置
int start = (int) (getScrollX() / barWidthFull);
start = start < 0 ? 0 : (int) (start > size ? size : start);
// 获取屏幕最右侧的波峰结束位置
int end = start + barCount > size ? (int) size : start + barCount;
// 屏幕内画多少个波峰
int mSize = start + barCount > size ? (int) (size - start) : barCount;
// 左侧波峰的偏移量
int v = getScrollX() % (int) (barWidthFull);
for (int j = 0; j < mSize; j++) {
float x = j * barWidthFull - (v * barWidthFull / 10);
float topY = mCurData[start + j] * viewHeight + anInt;
float bottomY = viewHeight - mCurData[start + j] * viewHeight - anInt;
canvas.drawLine(x, topY, x, bottomY, barPaint);
}
canvas.restore();
}
private void getViewData() {
viewHeight = getMeasuredHeight();
viewWidth = getMeasuredWidth();
}
private void init() {
if (barCount == 0 && getMeasuredWidth() != 0) {
// 计算最大波峰数,保证为偶数,不考虑波峰数为奇数的情况
final float scale = getContext().getResources().getDisplayMetrics().density;
barWidth = DEFAULT_BAR_WIDTH * scale + 0.5f;
barCount = (int) ((float) getMeasuredWidth() / barWidth);
if (barCount % 2 != 0) {
barCount++;
}
barWidthFull = (float) getMeasuredWidth() / barCount;
barWidth = (float) getMeasuredWidth() / barCount * DEFAULT_BAR_RATE;
barWidthHalf = barWidth / 2;
setPaint(barWidth);
initSize();
}
}
/**
* 计算多少个波峰
*/
private void initSize() {
int time = speed * getMeasuredWidth();
if (barCount != 0) {
size = duration / (time / barCount);
}
mCurData = new float[(int) size];
for (int i = 0; i < size; i++) {
int finalI = i;
CommonThreadPool.getThreadPool().addFixedTask(new Runnable() {
@Override
public void run() {
if (mCurData[finalI] == 0) {
mCurData[finalI] = waveWeight[random.nextInt(waveWeight.length)];
}
handler.sendEmptyMessage(0);
}
});
}
}
@SuppressLint("HandlerLeak")
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
count = count + 1;
// 在前一个屏幕数量的波峰情况下 先刷新 然后其他的等到 全部加载完了 再刷新一遍
if (count <= barCount) {
invalidate();
}
// 这是都算完了一起显示加载
if (count == mCurData.length) {
invalidate();
}
}
};
private void setPaint(float barWidth) {
barPaint = new Paint();
barPaint.setAntiAlias(false);
barPaint.setStyle(Paint.Style.FILL);
barPaint.setStrokeWidth(barWidth);
barPaint.setColor(barColor);
}
private void computeDoublesForAllZoomLevels() {
int numFrames = mSoundFile.getNumFrames();
int[] frameGains = mSoundFile.getFrameGains();
double[] smoothedGains = new double[numFrames];
if (numFrames == 1) {
smoothedGains[0] = frameGains[0];
} else if (numFrames == 2) {
smoothedGains[0] = frameGains[0];
smoothedGains[1] = frameGains[1];
} else if (numFrames > 2) {
smoothedGains[0] = (double) (
(frameGains[0] / 2.0) +
(frameGains[1] / 2.0));
for (int i = 1; i < numFrames - 1; i++) {
smoothedGains[i] = (double) (
(frameGains[i - 1] / 3.0) +
(frameGains[i] / 3.0) +
(frameGains[i + 1] / 3.0));
}
smoothedGains[numFrames - 1] = (double) (
(frameGains[numFrames - 2] / 2.0) +
(frameGains[numFrames - 1] / 2.0));
}
double maxGain = 1.0;
for (int i = 0; i < numFrames; i++) {
if (smoothedGains[i] > maxGain) {
maxGain = smoothedGains[i];
}
}
double scaleFactor = 1.0;
if (maxGain > 255.0) {
scaleFactor = 255 / maxGain;
}
maxGain = 0;
int gainHist[] = new int[256];
for (int i = 0; i < numFrames; i++) {
int smoothedGain = (int) (smoothedGains[i] * scaleFactor);
if (smoothedGain < 0)
smoothedGain = 0;
if (smoothedGain > 255)
smoothedGain = 255;
if (smoothedGain > maxGain)
maxGain = smoothedGain;
gainHist[smoothedGain]++;
}
double minGain = 0;
int sum = 0;
while (minGain < 255 && sum < numFrames / 20) {
sum += gainHist[(int) minGain];
minGain++;
}
sum = 0;
while (maxGain > 2 && sum < numFrames / 100) {
sum += gainHist[(int) maxGain];
maxGain--;
}
if (maxGain <= 50) {
maxGain = 80;
} else if (maxGain > 50 && maxGain < 120) {
maxGain = 142;
} else {
maxGain += 10;
}
double[] heights = new double[numFrames];
double range = maxGain - minGain;
for (int i = 0; i < numFrames; i++) {
double value = (smoothedGains[i] * scaleFactor - minGain) / range;
if (value < 0.0)
value = 0.0;
if (value > 1.0)
value = 1.0;
heights[i] = value * value;
}
mNumZoomLevels = 5;
mLenByZoomLevel = new int[5];
mZoomFactorByZoomLevel = new double[5];
mValuesByZoomLevel = new double[5][];
// Level 0 is doubled, with interpolated values
mLenByZoomLevel[0] = numFrames * 2;
System.out.println("ssnum" + numFrames);
mZoomFactorByZoomLevel[0] = 2.0;
mValuesByZoomLevel[0] = new double[mLenByZoomLevel[0]];
if (numFrames > 0) {
mValuesByZoomLevel[0][0] = 0.5 * heights[0];
mValuesByZoomLevel[0][1] = heights[0];
}
for (int i = 1; i < numFrames; i++) {
mValuesByZoomLevel[0][2 * i] = 0.5 * (heights[i - 1] + heights[i]);
mValuesByZoomLevel[0][2 * i + 1] = heights[i];
}
// Level 1 is normal
mLenByZoomLevel[1] = numFrames;
mValuesByZoomLevel[1] = new double[mLenByZoomLevel[1]];
mZoomFactorByZoomLevel[1] = 1.0;
for (int i = 0; i < mLenByZoomLevel[1]; i++) {
mValuesByZoomLevel[1][i] = heights[i];
}
// 3 more levels are each halved
for (int j = 2; j < 5; j++) {
mLenByZoomLevel[j] = mLenByZoomLevel[j - 1] / 2;
mValuesByZoomLevel[j] = new double[mLenByZoomLevel[j]];
mZoomFactorByZoomLevel[j] = mZoomFactorByZoomLevel[j - 1] / 2.0;
for (int i = 0; i < mLenByZoomLevel[j]; i++) {
mValuesByZoomLevel[j][i] =
0.5 * (mValuesByZoomLevel[j - 1][2 * i] +
mValuesByZoomLevel[j - 1][2 * i + 1]);
}
}
if (numFrames > 5000) {
mZoomLevel = 3;
} else if (numFrames > 1000) {
mZoomLevel = 2;
} else if (numFrames > 300) {
mZoomLevel = 1;
} else {
mZoomLevel = 0;
}
mInitialized = true;
}
public int maxPos() {
return mLenByZoomLevel[mZoomLevel];
}
private void computeIntsForThisZoomLevel() {
int halfHeight = (getMeasuredHeight() * 1 / 5) - 1;
mHeightsAtThisZoomLevel = new int[mLenByZoomLevel[mZoomLevel]];
for (int i = 0; i < mLenByZoomLevel[mZoomLevel]; i++) {
mHeightsAtThisZoomLevel[i] =
(int) (mValuesByZoomLevel[mZoomLevel][i] * halfHeight);
}
}
@Override
public boolean onTouch(View v, MotionEvent event) {
mygesture.onTouchEvent(event);
return super.onTouchEvent(event);
}
private boolean isFling;
private GestureDetector.SimpleOnGestureListener mSimpleOnGestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
if (draglistener != null) {
draglistener.onDragCallBack(false);
}
return super.onDown(e);
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
LogUtils.d("手势==" + "手指离开");
return super.onSingleTapUp(e);
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
LogUtils.d("手势==" + "手指离开22");
return super.onSingleTapConfirmed(e);
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
mOffset = getScrollX();
float xDown = e1.getX();
float x2 = e2.getX();
if (getScrollX() < 0) {
scrollTo(0, 0);
invalidate();
} else if (getScrollX() > size * barWidthFull) {
scrollTo((int) (size * barWidthFull), 0);
invalidate();
} else {
scrollBy((int) distanceX, 0);
invalidate();
}
if (distanceListener != null) {
distanceListener.onDragDistance(getScrollX(), (int) ((duration * getScrollX()) / (size * barWidthFull)));
}
return true;
}
@Override
public void onLongPress(MotionEvent e) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
mScroller.fling((int) getScrollX(), 0, -(int) velocityX, 0, 0, (int) (size * barWidthFull), 0, 0);
isFling = true;
invalidate();
return true;
}
};
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());//第三步
invalidate();
if (distanceListener != null) {
distanceListener.onDragDistance(getScrollX(), (int) ((duration * getScrollX()) / (size * barWidthFull)));
}
}
if (isFling && mScroller.isFinished()) {
isFling = false;
}
}
/**
* 设置是否禁止viewpager滑动的监听
*
* @param listener
*/
public void setAudioDragListener(AudioDragListener listener) {
this.draglistener = listener;
}
/**
* 设置滑动位置监听
*
* @param listenr
*/
public void setAudioDragDistanceListenr(AudioDragDistanceListener listenr) {
this.distanceListener = listenr;
}
/**
* 设置目前滚动的时间
*
* @param curTime
*/
public void setCurrenTime(int curTime) {
float v = (curTime * size * barWidthFull) / duration;
scrollTo((int) v, 0);
invalidate();
}
/**
* 清空布局
*/
public void clearLayout() {
}
public interface AudioDragListener {
void onDragCallBack(boolean canDrag);
}
public interface AudioDragDistanceListener {
void onDragDistance(int distance, int currentTime);
}
}