mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
Merge pull request #303 from DSheirer/278-wave-metadata
278 Add Metadata to WAV audio recordings
This commit is contained in:
@@ -63,6 +63,8 @@ public class Metadata
|
||||
protected Boolean mDoNotRecord;
|
||||
private Set<BroadcastChannel> mBroadcastChannels;
|
||||
|
||||
private long mTimestamp = System.currentTimeMillis();
|
||||
|
||||
|
||||
/**
|
||||
* Channel metadata. Contains all attributes that reflect the state and current attribute values for a channel
|
||||
@@ -82,6 +84,14 @@ public class Metadata
|
||||
mMetadataID = metadataID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp when this metadata was created
|
||||
* @return milliseconds since epoch
|
||||
*/
|
||||
public long getTimestamp()
|
||||
{
|
||||
return mTimestamp;
|
||||
}
|
||||
/**
|
||||
* Unique string identifier for this metadata that is comprised of the channel ID and the primary TO address.
|
||||
*
|
||||
|
||||
@@ -34,6 +34,7 @@ import java.util.concurrent.ScheduledExecutorService;
|
||||
public class AlwaysUnsquelchedDecoderState extends DecoderState
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AlwaysUnsquelchedDecoderState.class);
|
||||
private static final String NO_SQUELCH = "No Squelch";
|
||||
|
||||
private DecoderType mDecoderType;
|
||||
private String mChannelName;
|
||||
@@ -82,7 +83,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
|
||||
{
|
||||
//Each time we're reset, set the PRIMARY TO attribute back to the channel name, otherwise we won't have
|
||||
//a primary ID for any audio produced by this state.
|
||||
broadcast(new AttributeChangeRequest<String>(Attribute.PRIMARY_ADDRESS_TO, mChannelName));
|
||||
broadcast(new AttributeChangeRequest<String>(Attribute.PRIMARY_ADDRESS_TO, NO_SQUELCH));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +96,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
{
|
||||
broadcast(new AttributeChangeRequest<String>(Attribute.PRIMARY_ADDRESS_TO, mChannelName));
|
||||
broadcast(new AttributeChangeRequest<String>(Attribute.PRIMARY_ADDRESS_TO, NO_SQUELCH));
|
||||
broadcast(new DecoderStateEvent(this, Event.ALWAYS_UNSQUELCH, State.IDLE));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
/*
|
||||
* *********************************************************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
* *********************************************************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.record;
|
||||
|
||||
import io.github.dsheirer.audio.AudioPacket;
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.record.wave.AudioPacketWaveRecorder;
|
||||
import io.github.dsheirer.record.wave.ComplexBufferWaveRecorder;
|
||||
import io.github.dsheirer.record.wave.RealBufferWaveRecorder;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.OverflowableTransferQueue;
|
||||
import io.github.dsheirer.sample.real.IOverflowListener;
|
||||
@@ -45,7 +44,7 @@ public class RecorderManager implements Listener<AudioPacket>
|
||||
public static final int AUDIO_SAMPLE_RATE = 8000;
|
||||
public static final long IDLE_RECORDER_REMOVAL_THRESHOLD = 6000; //6 seconds
|
||||
|
||||
private Map<String,RealBufferWaveRecorder> mRecorders = new HashMap<>();
|
||||
private Map<String,AudioPacketWaveRecorder> mRecorders = new HashMap<>();
|
||||
private OverflowableTransferQueue<AudioPacket> mAudioPacketQueue = new OverflowableTransferQueue<>(1000, 100);
|
||||
private ScheduledFuture<?> mBufferProcessorFuture;
|
||||
|
||||
@@ -121,15 +120,15 @@ public class RecorderManager implements Listener<AudioPacket>
|
||||
|
||||
if(mRecorders.containsKey(identifier))
|
||||
{
|
||||
RealBufferWaveRecorder recorder = mRecorders.get(identifier);
|
||||
AudioPacketWaveRecorder recorder = mRecorders.get(identifier);
|
||||
|
||||
if(audioPacket.getType() == AudioPacket.Type.AUDIO)
|
||||
{
|
||||
recorder.receive(audioPacket.getAudioBuffer());
|
||||
recorder.receive(audioPacket);
|
||||
}
|
||||
else if(audioPacket.getType() == AudioPacket.Type.END)
|
||||
{
|
||||
RealBufferWaveRecorder finished = mRecorders.remove(identifier);
|
||||
AudioPacketWaveRecorder finished = mRecorders.remove(identifier);
|
||||
finished.stop();
|
||||
}
|
||||
}
|
||||
@@ -139,15 +138,15 @@ public class RecorderManager implements Listener<AudioPacket>
|
||||
{
|
||||
String filePrefix = getFilePrefix(audioPacket);
|
||||
|
||||
RealBufferWaveRecorder recorder = null;
|
||||
AudioPacketWaveRecorder recorder = null;
|
||||
|
||||
try
|
||||
{
|
||||
recorder = new RealBufferWaveRecorder(AUDIO_SAMPLE_RATE, filePrefix);
|
||||
recorder = new AudioPacketWaveRecorder(filePrefix, audioPacket.getMetadata());
|
||||
|
||||
recorder.start(ThreadPool.SCHEDULED);
|
||||
|
||||
recorder.receive(audioPacket.getAudioBuffer());
|
||||
recorder.receive(audioPacket);
|
||||
mRecorders.put(identifier, recorder);
|
||||
}
|
||||
catch(Exception ioe)
|
||||
@@ -176,11 +175,11 @@ public class RecorderManager implements Listener<AudioPacket>
|
||||
*/
|
||||
private void removeIdleRecorders()
|
||||
{
|
||||
Iterator<Map.Entry<String,RealBufferWaveRecorder>> it = mRecorders.entrySet().iterator();
|
||||
Iterator<Map.Entry<String,AudioPacketWaveRecorder>> it = mRecorders.entrySet().iterator();
|
||||
|
||||
while(it.hasNext())
|
||||
{
|
||||
Map.Entry<String,RealBufferWaveRecorder> entry = it.next();
|
||||
Map.Entry<String,AudioPacketWaveRecorder> entry = it.next();
|
||||
|
||||
if(entry.getValue().getLastBufferReceived() + IDLE_RECORDER_REMOVAL_THRESHOLD < System.currentTimeMillis())
|
||||
{
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* *********************************************************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
* *********************************************************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.record.wave;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.audio.AudioPacket;
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.sample.ConversionUtils;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.util.TimeStamp;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* WAVE audio recorder module for recording audio buffers to a wave file
|
||||
*/
|
||||
public class AudioPacketWaveRecorder extends Module implements Listener<AudioPacket>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioPacketWaveRecorder.class);
|
||||
|
||||
private WaveWriter mWriter;
|
||||
private String mFilePrefix;
|
||||
private Path mFile;
|
||||
private AudioFormat mAudioFormat;
|
||||
|
||||
private BufferProcessor mBufferProcessor;
|
||||
private ScheduledFuture<?> mProcessorHandle;
|
||||
private LinkedBlockingQueue<AudioPacket> mAudioPackets = new LinkedBlockingQueue<>(500);
|
||||
private Metadata mMetadata;
|
||||
private long mLastBufferReceived;
|
||||
|
||||
private AtomicBoolean mRunning = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* Wave audio recorder for AudioPackets
|
||||
* @param filePrefix
|
||||
* @param metadata
|
||||
*/
|
||||
public AudioPacketWaveRecorder(String filePrefix, Metadata metadata)
|
||||
{
|
||||
mMetadata = metadata;
|
||||
mFilePrefix = filePrefix;
|
||||
mAudioFormat = AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the recorder is currently running.
|
||||
*/
|
||||
public boolean isRunning()
|
||||
{
|
||||
return mRunning.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp of when the latest buffer was received by this recorder
|
||||
*/
|
||||
public long getLastBufferReceived()
|
||||
{
|
||||
return mLastBufferReceived;
|
||||
}
|
||||
|
||||
public Path getFile()
|
||||
{
|
||||
return mFile;
|
||||
}
|
||||
|
||||
public void start(ScheduledExecutorService executor)
|
||||
{
|
||||
if(mRunning.compareAndSet(false, true))
|
||||
{
|
||||
if(mBufferProcessor == null)
|
||||
{
|
||||
mBufferProcessor = new BufferProcessor();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(mFilePrefix);
|
||||
sb.append("_");
|
||||
sb.append(TimeStamp.getLongTimeStamp("_"));
|
||||
sb.append(".wav");
|
||||
|
||||
mFile = Paths.get(sb.toString());
|
||||
|
||||
mWriter = new WaveWriter(mAudioFormat, mFile);
|
||||
|
||||
/* Schedule the processor to run every 500 milliseconds */
|
||||
mProcessorHandle = executor.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
catch(IOException io)
|
||||
{
|
||||
mLog.error("Error starting real buffer recorder", io);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void stop()
|
||||
{
|
||||
if(mRunning.compareAndSet(true, false))
|
||||
{
|
||||
try
|
||||
{
|
||||
//Finish writing any residual audio buffers
|
||||
write();
|
||||
|
||||
//Append the LIST and ID3 metadata to the end
|
||||
if(mMetadata != null)
|
||||
{
|
||||
mWriter.writeMetadata(WaveMetadata.createFrom(mMetadata));
|
||||
mMetadata = null;
|
||||
}
|
||||
|
||||
if(mWriter != null)
|
||||
{
|
||||
mWriter.close();
|
||||
mWriter = null;
|
||||
}
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
mLog.error("Error writing final audio buffers to recording during shutdown", ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary input method for receiving audio packets to enqueue for later writing to the wav file.
|
||||
*/
|
||||
@Override
|
||||
public void receive(AudioPacket audioPacket)
|
||||
{
|
||||
if(mRunning.get())
|
||||
{
|
||||
boolean success = mAudioPackets.offer(audioPacket);
|
||||
|
||||
if(!success)
|
||||
{
|
||||
mLog.error("recorder buffer overflow - purging [" + mFile.toFile().getAbsolutePath() + "]");
|
||||
mAudioPackets.clear();
|
||||
}
|
||||
|
||||
mLastBufferReceived = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes all audio currently in the queue to the file. Captures any audio metadata from the packet and retains a
|
||||
* copy of the latest metadata to append to the end of the recording when stop() is invoked.
|
||||
*
|
||||
* @throws IOException if there are any errors writing the audio
|
||||
*/
|
||||
private void write() throws IOException
|
||||
{
|
||||
AudioPacket audioPacket = mAudioPackets.poll();
|
||||
|
||||
while(audioPacket != null && audioPacket.getType() == AudioPacket.Type.AUDIO)
|
||||
{
|
||||
if(audioPacket.hasMetadata())
|
||||
{
|
||||
mMetadata = audioPacket.getMetadata();
|
||||
}
|
||||
|
||||
mWriter.writeData(ConversionUtils.convertToSigned16BitSamples(audioPacket.getAudioBuffer()));
|
||||
audioPacket = mAudioPackets.poll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scheduled runnable to periodically write the enqueued audio packets to the file
|
||||
*/
|
||||
public class BufferProcessor implements Runnable
|
||||
{
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
write();
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
/* Stop this module if/when we get an IO exception */
|
||||
mAudioPackets.clear();
|
||||
stop();
|
||||
|
||||
mLog.error("IO Exception while trying to write to the wave writer", ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,7 +188,7 @@ public class ComplexBufferWaveRecorder extends Module
|
||||
}
|
||||
else
|
||||
{
|
||||
mWriter.write( ConversionUtils.convertToSigned16BitSamples( buffer ) );
|
||||
mWriter.writeData( ConversionUtils.convertToSigned16BitSamples( buffer ) );
|
||||
buffer = mBuffers.poll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ public class RealBufferWaveRecorder extends Module
|
||||
|
||||
while(buffer != null)
|
||||
{
|
||||
mWriter.write(ConversionUtils.convertToSigned16BitSamples(buffer));
|
||||
mWriter.writeData(ConversionUtils.convertToSigned16BitSamples(buffer));
|
||||
buffer = mBuffers.poll();
|
||||
}
|
||||
}
|
||||
|
||||
418
src/main/java/io/github/dsheirer/record/wave/WaveMetadata.java
Normal file
418
src/main/java/io/github/dsheirer/record/wave/WaveMetadata.java
Normal file
@@ -0,0 +1,418 @@
|
||||
package io.github.dsheirer.record.wave;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.PatchGroupAlias;
|
||||
import io.github.dsheirer.channel.metadata.AliasedIdentifier;
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class WaveMetadata
|
||||
{
|
||||
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyyMMddHHmmssSSS");
|
||||
private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
|
||||
private static final Charset UTF_8 = Charset.forName("UTF-8");
|
||||
private static final byte NULL_TERMINATOR = (byte)0x00;
|
||||
private static final String LIST_CHUNK_IDENTIFIER = "LIST";
|
||||
private static final String INFO_TYPE_IDENTIFIER = "INFO";
|
||||
private static final String ID3_CHUNK_IDENTIFIER = "ID3 ";
|
||||
private static final String ID3V2_IDENTIFIER = "ID3";
|
||||
private static final byte ID3V2_MAJOR_VERSION = (byte)0x4;
|
||||
private static final byte ID3V2_MINOR_VERSION = (byte)0x0;
|
||||
private static final byte ID3V2_FLAGS = (byte)0x0;
|
||||
private static final byte ID3V2_ISO_8859_1_ENCODING = (byte)0x0;
|
||||
|
||||
private Map<WaveMetadataType, String> mMetadataMap = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Audio Wave File - Metadata Chunk. Supports creating a map of metadata key:value pairs and
|
||||
* converting the metadata to WAV audio recording INFO/LIST and ID3 v2.4.0 metadata chunks.
|
||||
*/
|
||||
public WaveMetadata()
|
||||
{
|
||||
}
|
||||
|
||||
public ByteBuffer getID3Chunk()
|
||||
{
|
||||
ByteBuffer subChunks = getID3Frames();
|
||||
int subChunksLength = subChunks.capacity();
|
||||
|
||||
int overallLength = subChunksLength + 18;
|
||||
|
||||
//Pad the overall length to make it an even 32-bit boundary
|
||||
int padding = 0;
|
||||
|
||||
if(overallLength % 4 != 0)
|
||||
{
|
||||
padding = 4 - (overallLength % 4);
|
||||
}
|
||||
|
||||
ByteBuffer chunk = ByteBuffer.allocate(overallLength + padding).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
int chunkLength = subChunksLength + 10 + padding;
|
||||
|
||||
chunk.put(ID3_CHUNK_IDENTIFIER.getBytes());
|
||||
chunk.putInt(chunkLength);
|
||||
chunk.put(ID3V2_IDENTIFIER.getBytes());
|
||||
chunk.put(ID3V2_MAJOR_VERSION);
|
||||
chunk.put(ID3V2_MINOR_VERSION);
|
||||
chunk.put(ID3V2_FLAGS);
|
||||
chunk.put(getID3EncodedLength(subChunksLength));
|
||||
|
||||
chunk.put(subChunks);
|
||||
|
||||
chunk.position(0);
|
||||
|
||||
return chunk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an ID3 v2.4.0 compatible frame set from the metadata
|
||||
*/
|
||||
public ByteBuffer getID3Frames()
|
||||
{
|
||||
int length = 0;
|
||||
List<ByteBuffer> buffers = new ArrayList<>();
|
||||
|
||||
//List the primary metadata tags first
|
||||
for(Map.Entry<WaveMetadataType, String> entry: mMetadataMap.entrySet())
|
||||
{
|
||||
if(entry.getKey().isPrimaryTag())
|
||||
{
|
||||
ByteBuffer buffer = getID3Frame(entry.getKey(), entry.getValue());
|
||||
|
||||
if(buffer != null)
|
||||
{
|
||||
length += buffer.capacity();
|
||||
buffers.add(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(Map.Entry<WaveMetadataType, String> entry: mMetadataMap.entrySet())
|
||||
{
|
||||
if(!entry.getKey().isPrimaryTag())
|
||||
{
|
||||
ByteBuffer buffer = getID3Frame(entry.getKey(), entry.getValue());
|
||||
|
||||
if(buffer != null)
|
||||
{
|
||||
length += buffer.capacity();
|
||||
buffers.add(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuffer concatenatedBuffer = ByteBuffer.allocate(length);
|
||||
|
||||
for(ByteBuffer buffer: buffers)
|
||||
{
|
||||
concatenatedBuffer.put(buffer);
|
||||
}
|
||||
|
||||
concatenatedBuffer.position(0);
|
||||
|
||||
return concatenatedBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an ID3 v2.4.0 compatible metadata frame
|
||||
* @param type of metadata
|
||||
* @param value of the metadata
|
||||
* @return frame byte buffer or null if the value is empty or null
|
||||
*/
|
||||
public ByteBuffer getID3Frame(WaveMetadataType type, String value)
|
||||
{
|
||||
if(value != null && !value.isEmpty())
|
||||
{
|
||||
ByteBuffer encodedValue = ISO_8859_1.encode(value);
|
||||
|
||||
//Length is 4 bytes for tag ID, 4 bytes for length, value, and null terminator
|
||||
int chunkLength = encodedValue.capacity() + 11;
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(chunkLength).order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer.put(type.getID3Tag().getBytes());
|
||||
buffer.put(getID3EncodedLength(encodedValue.capacity() + 1));
|
||||
buffer.put(ID3V2_FLAGS);
|
||||
buffer.put(ID3V2_FLAGS);
|
||||
buffer.put(ID3V2_ISO_8859_1_ENCODING);
|
||||
buffer.put(encodedValue);
|
||||
|
||||
buffer.position(0);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a wave LIST chunk from the metadata tags
|
||||
*/
|
||||
public ByteBuffer getLISTChunk()
|
||||
{
|
||||
ByteBuffer metadataBuffer = getLISTSubChunks();
|
||||
int tagsLength = metadataBuffer.capacity();
|
||||
|
||||
int overallLength = tagsLength + 8;
|
||||
|
||||
//Pad the overall length to make it an event 32-bit boundary
|
||||
int padding = 0;
|
||||
|
||||
if(overallLength % 4 != 0)
|
||||
{
|
||||
padding = 4 - (overallLength % 4);
|
||||
}
|
||||
|
||||
ByteBuffer chunk = ByteBuffer.allocate(overallLength + padding).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
chunk.put(LIST_CHUNK_IDENTIFIER.getBytes());
|
||||
chunk.putInt(tagsLength + padding);
|
||||
chunk.put(metadataBuffer);
|
||||
|
||||
chunk.position(0);
|
||||
|
||||
return chunk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats all metadata key:value pairs in the metadata map into a wave INFO compatible format
|
||||
*/
|
||||
private ByteBuffer getLISTSubChunks()
|
||||
{
|
||||
int length = INFO_TYPE_IDENTIFIER.length();
|
||||
|
||||
List<ByteBuffer> buffers = new ArrayList<>();
|
||||
|
||||
//Add the primary tags first
|
||||
for(Map.Entry<WaveMetadataType, String> entry: mMetadataMap.entrySet())
|
||||
{
|
||||
if(entry.getKey().isPrimaryTag())
|
||||
{
|
||||
ByteBuffer buffer = getLISTSubChunk(entry.getKey(), entry.getValue());
|
||||
length += buffer.capacity();
|
||||
buffers.add(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
for(Map.Entry<WaveMetadataType, String> entry: mMetadataMap.entrySet())
|
||||
{
|
||||
if(!entry.getKey().isPrimaryTag())
|
||||
{
|
||||
ByteBuffer buffer = getLISTSubChunk(entry.getKey(), entry.getValue());
|
||||
length += buffer.capacity();
|
||||
buffers.add(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuffer joinedBuffer = ByteBuffer.allocate(length);
|
||||
|
||||
joinedBuffer.put(INFO_TYPE_IDENTIFIER.getBytes());
|
||||
|
||||
for(ByteBuffer buffer: buffers)
|
||||
{
|
||||
joinedBuffer.put(buffer);
|
||||
}
|
||||
|
||||
joinedBuffer.position(0);
|
||||
|
||||
return joinedBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the metadata type and value into a Wave LIST compatible byte array.
|
||||
*
|
||||
* @param type of metadata
|
||||
* @param value for the metadata
|
||||
* @return metadata and value formatted for wave chunk
|
||||
*/
|
||||
private ByteBuffer getLISTSubChunk(WaveMetadataType type, String value)
|
||||
{
|
||||
if(value != null && !value.isEmpty())
|
||||
{
|
||||
//Length is 4 bytes for tag ID, 4 bytes for length, value, and null terminator
|
||||
ByteBuffer encodedValue = UTF_8.encode(value);
|
||||
int chunkLength = encodedValue.capacity() + 9;
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(chunkLength).order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer.put(type.getLISTTag().getBytes());
|
||||
buffer.putInt(encodedValue.capacity() + 1);
|
||||
buffer.put(encodedValue);
|
||||
buffer.put(NULL_TERMINATOR);
|
||||
|
||||
buffer.position(0);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the metadata key:value pair to the metadata map. Note: since this is a map, any existing
|
||||
* key:value will be overwritten.
|
||||
* @param type identifying the metadata type
|
||||
* @param value to associate with the metadata type
|
||||
*/
|
||||
public void add(WaveMetadataType type, String value)
|
||||
{
|
||||
if(value != null)
|
||||
{
|
||||
mMetadataMap.put(type, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a WAVE recording metadata chunk from the audio metadata argument
|
||||
* @param audioMetadata to create the wave metadata from
|
||||
* @return wave metadata instance
|
||||
*/
|
||||
public static WaveMetadata createFrom(Metadata audioMetadata)
|
||||
{
|
||||
WaveMetadata waveMetadata = new WaveMetadata();
|
||||
|
||||
waveMetadata.add(WaveMetadataType.SOFTWARE, SystemProperties.getInstance().getApplicationName());
|
||||
waveMetadata.add(WaveMetadataType.DATE_CREATED, SDF.format(new Date(audioMetadata.getTimestamp())));
|
||||
waveMetadata.add(WaveMetadataType.ARTIST_NAME, audioMetadata.getChannelConfigurationSystem());
|
||||
waveMetadata.add(WaveMetadataType.ALBUM_TITLE, audioMetadata.getChannelConfigurationSite());
|
||||
waveMetadata.add(WaveMetadataType.TRACK_TITLE, audioMetadata.getChannelConfigurationName());
|
||||
waveMetadata.add(WaveMetadataType.COMMENTS, audioMetadata.getPrimaryDecoderType().getDisplayString());
|
||||
waveMetadata.add(WaveMetadataType.CHANNEL_ID, audioMetadata.getChannelFrequencyLabel());
|
||||
waveMetadata.add(WaveMetadataType.CHANNEL_FREQUENCY, String.valueOf(audioMetadata.getChannelFrequency()));
|
||||
|
||||
AliasedIdentifier networkID1 = audioMetadata.getNetworkID1();
|
||||
|
||||
if(networkID1 != null)
|
||||
{
|
||||
waveMetadata.add(WaveMetadataType.NETWORK_ID_1, networkID1.getIdentifier());
|
||||
}
|
||||
|
||||
AliasedIdentifier networkID2 = audioMetadata.getNetworkID2();
|
||||
|
||||
if(networkID2 != null)
|
||||
{
|
||||
waveMetadata.add(WaveMetadataType.NETWORK_ID_2, networkID2.getIdentifier());
|
||||
}
|
||||
|
||||
AliasedIdentifier primaryFrom = audioMetadata.getPrimaryAddressFrom();
|
||||
|
||||
if(primaryFrom != null)
|
||||
{
|
||||
waveMetadata.add(WaveMetadataType.TALKGROUP_PRIMARY_FROM, primaryFrom.getIdentifier());
|
||||
|
||||
Alias alias = primaryFrom.getAlias();
|
||||
|
||||
if(alias != null)
|
||||
{
|
||||
waveMetadata.add(WaveMetadataType.TALKGROUP_PRIMARY_FROM_ALIAS, alias.getName());
|
||||
waveMetadata.add(WaveMetadataType.TALKGROUP_PRIMARY_FROM_ICON, alias.getIconName());
|
||||
}
|
||||
}
|
||||
|
||||
AliasedIdentifier primaryTo = audioMetadata.getPrimaryAddressTo();
|
||||
|
||||
if(primaryTo != null)
|
||||
{
|
||||
waveMetadata.add(WaveMetadataType.TALKGROUP_PRIMARY_TO, primaryTo.getIdentifier());
|
||||
|
||||
Alias alias = primaryTo.getAlias();
|
||||
|
||||
if(alias != null)
|
||||
{
|
||||
waveMetadata.add(WaveMetadataType.TALKGROUP_PRIMARY_TO_ALIAS, alias.getName());
|
||||
waveMetadata.add(WaveMetadataType.TALKGROUP_PRIMARY_TO_ICON, alias.getIconName());
|
||||
|
||||
if(alias instanceof PatchGroupAlias)
|
||||
{
|
||||
PatchGroupAlias patchGroupAlias = (PatchGroupAlias)alias;
|
||||
|
||||
List<Alias> patchedAliases = patchGroupAlias.getPatchedAliases();
|
||||
|
||||
if(patchedAliases.size() >= 1)
|
||||
{
|
||||
Alias patch = patchedAliases.get(0);
|
||||
waveMetadata.add(WaveMetadataType.TALKGROUP_PRIMARY_PATCHED_1, patch.getName());
|
||||
}
|
||||
if(patchedAliases.size() >= 2)
|
||||
{
|
||||
Alias patch = patchedAliases.get(1);
|
||||
waveMetadata.add(WaveMetadataType.TALKGROUP_PRIMARY_PATCHED_2, patch.getName());
|
||||
}
|
||||
if(patchedAliases.size() >= 3)
|
||||
{
|
||||
Alias patch = patchedAliases.get(2);
|
||||
waveMetadata.add(WaveMetadataType.TALKGROUP_PRIMARY_PATCHED_3, patch.getName());
|
||||
}
|
||||
if(patchedAliases.size() >= 4)
|
||||
{
|
||||
Alias patch = patchedAliases.get(3);
|
||||
waveMetadata.add(WaveMetadataType.TALKGROUP_PRIMARY_PATCHED_4, patch.getName());
|
||||
}
|
||||
if(patchedAliases.size() >= 5)
|
||||
{
|
||||
Alias patch = patchedAliases.get(4);
|
||||
waveMetadata.add(WaveMetadataType.TALKGROUP_PRIMARY_PATCHED_5, patch.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AliasedIdentifier secondaryFrom = audioMetadata.getSecondaryAddressFrom();
|
||||
|
||||
if(secondaryFrom != null)
|
||||
{
|
||||
waveMetadata.add(WaveMetadataType.TALKGROUP_SECONDARY_FROM, secondaryFrom.getIdentifier());
|
||||
|
||||
Alias alias = secondaryFrom.getAlias();
|
||||
|
||||
if(alias != null)
|
||||
{
|
||||
waveMetadata.add(WaveMetadataType.TALKGROUP_SECONDARY_FROM_ALIAS, alias.getName());
|
||||
waveMetadata.add(WaveMetadataType.TALKGROUP_SECONDARY_FROM_ICON, alias.getIconName());
|
||||
}
|
||||
}
|
||||
|
||||
AliasedIdentifier secondaryTo = audioMetadata.getSecondaryAddressTo();
|
||||
|
||||
if(secondaryTo != null)
|
||||
{
|
||||
waveMetadata.add(WaveMetadataType.TALKGROUP_SECONDARY_TO, secondaryTo.getIdentifier());
|
||||
|
||||
Alias alias = secondaryTo.getAlias();
|
||||
|
||||
if(alias != null)
|
||||
{
|
||||
waveMetadata.add(WaveMetadataType.TALKGROUP_SECONDARY_TO_ALIAS, alias.getName());
|
||||
waveMetadata.add(WaveMetadataType.TALKGROUP_SECONDARY_TO_ICON, alias.getIconName());
|
||||
}
|
||||
}
|
||||
|
||||
return waveMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the integer length to an ID3 compatible length field where each byte only uses the 7 least significant
|
||||
* bits for a maximum of 28 bits used out of the 32-bit representation.
|
||||
* @param length
|
||||
* @return
|
||||
*/
|
||||
public static byte[] getID3EncodedLength(int length)
|
||||
{
|
||||
byte[] value = new byte[4];
|
||||
value[0] = (byte)((length >> 21) & 0x7F);
|
||||
value[1] = (byte)((length >> 14) & 0x7F);
|
||||
value[2] = (byte)((length >> 7) & 0x7F);
|
||||
value[3] = (byte)(length & 0x7F);
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* *********************************************************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
* *********************************************************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.record.wave;
|
||||
|
||||
/**
|
||||
* WAVE audio metadata tags.
|
||||
*
|
||||
* Metadata tag details:
|
||||
* LIST: https://sno.phy.queensu.ca/~phil/exiftool/TagNames/RIFF.html#Info
|
||||
* ID3: http://id3.org/id3v2.3.0
|
||||
*
|
||||
*/
|
||||
public enum WaveMetadataType
|
||||
{
|
||||
//Primary tags
|
||||
ARTIST_NAME("TPE1", "IART", true), //System
|
||||
ALBUM_TITLE("TALB", "IPRD", true), //Site
|
||||
TRACK_TITLE("TIT2", "INAM", true), //Channel Name
|
||||
COMMENTS("COMM", "ICMT", true),
|
||||
DATE_CREATED("TDRC", "ICRD", true),
|
||||
GENRE("TCON", "IGNR", true),
|
||||
SOFTWARE("TSSE", "ISFT", true), //sdrtrunk application name
|
||||
SOURCE_FORM("TMED", "ISRF", true), //protocol
|
||||
|
||||
//Secondary tags
|
||||
ALIAS_LIST_NAME("TALN", "ALLN", false),
|
||||
CHANNEL_FREQUENCY("TCHF", "CHFQ", false),
|
||||
CHANNEL_ID("TCHI", "CHID", false),
|
||||
CHANNEL_TIMESLOT("TCHT", "CHTS", false),
|
||||
NETWORK_ID_1("TNT1", "NTW1", false),
|
||||
NETWORK_ID_2("TNT2", "NTW2", false),
|
||||
TALKGROUP_PRIMARY_PATCHED_1("TPP1", "TPP1", false),
|
||||
TALKGROUP_PRIMARY_PATCHED_2("TPP2", "TPP2", false),
|
||||
TALKGROUP_PRIMARY_PATCHED_3("TPP3", "TPP3", false),
|
||||
TALKGROUP_PRIMARY_PATCHED_4("TPP4", "TPP4", false),
|
||||
TALKGROUP_PRIMARY_PATCHED_5("TPP5", "TPP5", false),
|
||||
TALKGROUP_PRIMARY_FROM("TPFM", "TPFM", false),
|
||||
TALKGROUP_PRIMARY_FROM_ALIAS("TPFA", "TPFA", false),
|
||||
TALKGROUP_PRIMARY_FROM_ICON("TPFI", "TPFI", false),
|
||||
TALKGROUP_PRIMARY_TO("TPTO", "TPTO", false),
|
||||
TALKGROUP_PRIMARY_TO_ALIAS("TOTA", "TPTA", false),
|
||||
TALKGROUP_PRIMARY_TO_ICON("TPTI", "TPTI", false),
|
||||
TALKGROUP_SECONDARY_FROM("TSFM", "TSFM", false),
|
||||
TALKGROUP_SECONDARY_FROM_ALIAS("TSFA", "TSFA", false),
|
||||
TALKGROUP_SECONDARY_FROM_ICON("TSFI", "TSFI", false),
|
||||
TALKGROUP_SECONDARY_TO("TSTO", "TSTO", false),
|
||||
TALKGROUP_SECONDARY_TO_ALIAS("TSTA", "TSTA", false),
|
||||
TALKGROUP_SECONDARY_TO_ICON("TSTI", "TSTI", false);
|
||||
|
||||
private String mID3Tag;
|
||||
private String mLISTTag;
|
||||
private boolean mPrimaryTag;
|
||||
|
||||
/**
|
||||
* Wave file LIST and ID3 chunk metadata tag types
|
||||
* @param id3Tag used in the ID3 chunk
|
||||
* @param listTag used in the LIST chunk
|
||||
* @param PrimaryTag indicates if this is a custom LIST tag (true) or standard LIST tag (false)
|
||||
*/
|
||||
WaveMetadataType(String id3Tag, String listTag, boolean PrimaryTag)
|
||||
{
|
||||
mID3Tag = id3Tag;
|
||||
mLISTTag = listTag;
|
||||
mPrimaryTag = PrimaryTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag to use when this metadata type is included in a WAV ID3 chunk
|
||||
*/
|
||||
public String getID3Tag()
|
||||
{
|
||||
return mID3Tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag to use when this metadata type is included in a WAV INFO/LIST chunk
|
||||
*/
|
||||
public String getLISTTag()
|
||||
{
|
||||
return mLISTTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this is a primary tag that should be listed first, before the secondary tags
|
||||
*/
|
||||
public boolean isPrimaryTag()
|
||||
{
|
||||
return mPrimaryTag;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
/*
|
||||
* *********************************************************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
* *********************************************************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.record.wave;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
@@ -24,90 +23,125 @@ import java.nio.ByteOrder;
|
||||
|
||||
public class WaveUtils
|
||||
{
|
||||
public static final byte[] RIFF_CHUNK = {(byte)0x52, (byte)0x49, (byte)0x46, (byte)0x46}; //RIFF
|
||||
public static final byte[] WAV_FORMAT = {(byte)0x57, (byte)0x41, (byte)0x56, (byte)0x45}; //WAVE
|
||||
public static final byte[] CHUNK_FORMAT = {(byte)0x66, (byte)0x6D, (byte)0x74, (byte)0x20}; //fmt space
|
||||
public static final byte[] CHUNK_DATA = {(byte)0x64, (byte)0x61, (byte)0x74, (byte)0x61}; //data
|
||||
public static final String RIFF_ID = "RIFF";
|
||||
public static final String WAVE_ID = "WAVE";
|
||||
public static final int WAVE_HEADER_SIZE = 54; //Includes the 8 byte data chunk header
|
||||
|
||||
public static final String FORMAT_CHUNK = "fmt ";
|
||||
public static final int FORMAT_CHUNK_SIZE = 16;
|
||||
public static final short FORMAT_UNCOMPRESSED_PCM = 1;
|
||||
|
||||
public static final String DATA_CHUNK = "data";
|
||||
|
||||
public static final byte[] RIFF_CHUNK = {(byte)0x52, (byte)0x49, (byte)0x46, (byte)0x46}; //RIFF
|
||||
public static final byte[] WAV_FORMAT = {(byte)0x57, (byte)0x41, (byte)0x56, (byte)0x45}; //WAVE
|
||||
public static final byte[] CHUNK_FORMAT = {(byte)0x66, (byte)0x6D, (byte)0x74, (byte)0x20}; //fmt space
|
||||
public static final byte[] CHUNK_DATA = {(byte)0x64, (byte)0x61, (byte)0x74, (byte)0x61}; //data
|
||||
public static final int PCM_FORMAT = 1;
|
||||
|
||||
/**
|
||||
* Creates a wave file header with the RIFF chunk descriptor and the WAVE
|
||||
* format chunk 1 and chunk 2 header
|
||||
*/
|
||||
public static ByteBuffer getWaveHeader( AudioFormat format )
|
||||
{
|
||||
ByteBuffer descriptor = ByteBuffer.allocate( 44 );
|
||||
descriptor.order( ByteOrder.LITTLE_ENDIAN );
|
||||
/**
|
||||
* Creates a wave file header with the RIFF chunk descriptor and the WAVE
|
||||
* format chunk 1 and chunk 2 header
|
||||
*/
|
||||
public static ByteBuffer getWaveHeader(AudioFormat format)
|
||||
{
|
||||
ByteBuffer descriptor2 = ByteBuffer.allocate(36).order(ByteOrder.LITTLE_ENDIAN);
|
||||
descriptor2.put(RIFF_ID.getBytes());
|
||||
descriptor2.putInt(WAVE_HEADER_SIZE);
|
||||
descriptor2.put(WAVE_ID.getBytes());
|
||||
|
||||
/* Chunk ID = RIFF */
|
||||
descriptor.put( (byte)0x52 ); //R
|
||||
descriptor.put( (byte)0x49 ); //I
|
||||
descriptor.put( (byte)0x46 ); //F
|
||||
descriptor.put( (byte)0x46 ); //F
|
||||
//Chunk #1 - format descriptor
|
||||
descriptor2.put(FORMAT_CHUNK.getBytes());
|
||||
descriptor2.putInt(FORMAT_CHUNK_SIZE);
|
||||
descriptor2.putShort(FORMAT_UNCOMPRESSED_PCM);
|
||||
descriptor2.putShort((short)format.getChannels());
|
||||
descriptor2.putInt((int)format.getSampleRate());
|
||||
|
||||
/* Chunk Size = 36 bytes (initial empty size) */
|
||||
descriptor.put( (byte)36 );
|
||||
descriptor.put( (byte)0 );
|
||||
descriptor.put( (byte)0 );
|
||||
descriptor.put( (byte)0 );
|
||||
int frameByteRate2 = format.getChannels() * format.getSampleSizeInBits() / 8;
|
||||
|
||||
/* Format = WAVE */
|
||||
descriptor.put( (byte)0x57 ); //W
|
||||
descriptor.put( (byte)0x41 ); //A
|
||||
descriptor.put( (byte)0x56 ); //V
|
||||
descriptor.put( (byte)0x45 ); //E
|
||||
/* Byte Rate = sample rate * channels * bits per sample / 8 */
|
||||
int byteRate2 = (int)(format.getSampleRate() * frameByteRate2);
|
||||
descriptor2.putInt(byteRate2);
|
||||
|
||||
/* Sub Chunk 1 ID */
|
||||
descriptor.put( (byte)0x66 ); //f
|
||||
descriptor.put( (byte)0x6D ); //m
|
||||
descriptor.put( (byte)0x74 ); //t
|
||||
descriptor.put( (byte)0x20 ); //space
|
||||
/* Block Align */
|
||||
descriptor2.putShort((short)frameByteRate2);
|
||||
|
||||
/* SubChunk1Size = 1 */
|
||||
descriptor.put( (byte)0x10 );
|
||||
descriptor.put( (byte)0x0 );
|
||||
descriptor.put( (byte)0x0 );
|
||||
descriptor.put( (byte)0x0 );
|
||||
/* Bits per Sample */
|
||||
descriptor2.putShort((short)format.getSampleSizeInBits());
|
||||
//Finished with the header here
|
||||
|
||||
/* Audio format = 1 (Uncompressed PCM) */
|
||||
descriptor.put( (byte)0x1 );
|
||||
descriptor.put( (byte)0x0 );
|
||||
|
||||
/* Number of Channels */
|
||||
descriptor.asShortBuffer().put( (short)format.getChannels() );
|
||||
descriptor.position( descriptor.position() + 2 );
|
||||
ByteBuffer descriptor = ByteBuffer.allocate(44);
|
||||
descriptor.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
/* Sample Rate - assumes integral sample rate */
|
||||
descriptor.asIntBuffer().put( (int)format.getSampleRate() );
|
||||
descriptor.position( descriptor.position() + 4 );
|
||||
/* Chunk ID = RIFF */
|
||||
descriptor.put((byte)0x52); //R
|
||||
descriptor.put((byte)0x49); //I
|
||||
descriptor.put((byte)0x46); //F
|
||||
descriptor.put((byte)0x46); //F
|
||||
|
||||
int frameByteRate = format.getChannels() * format.getSampleSizeInBits() / 8;
|
||||
|
||||
/* Byte Rate = sample rate * channels * bits per sample / 8 */
|
||||
int byteRate = (int)( format.getSampleRate() * frameByteRate );
|
||||
descriptor.asIntBuffer().put( byteRate );
|
||||
descriptor.position( descriptor.position() + 4 );
|
||||
/* Chunk Size = 36 bytes (initial empty size) */
|
||||
descriptor.put((byte)36);
|
||||
descriptor.put((byte)0);
|
||||
descriptor.put((byte)0);
|
||||
descriptor.put((byte)0);
|
||||
|
||||
/* Block Align */
|
||||
descriptor.asShortBuffer().put( (short)frameByteRate );
|
||||
descriptor.position( descriptor.position() + 2 );
|
||||
/* Format = WAVE */
|
||||
descriptor.put((byte)0x57); //W
|
||||
descriptor.put((byte)0x41); //A
|
||||
descriptor.put((byte)0x56); //V
|
||||
descriptor.put((byte)0x45); //E
|
||||
|
||||
/* Bits per Sample */
|
||||
descriptor.asShortBuffer().put( (short)( format.getSampleSizeInBits() ) );
|
||||
descriptor.position( descriptor.position() + 2 );
|
||||
|
||||
/* Sub Chunk 2 ID */
|
||||
descriptor.put( (byte)0x64 ); //d
|
||||
descriptor.put( (byte)0x61 ); //a
|
||||
descriptor.put( (byte)0x74 ); //t
|
||||
descriptor.put( (byte)0x61 ); //a
|
||||
/* Sub Chunk 1 ID */
|
||||
descriptor.put((byte)0x66); //f
|
||||
descriptor.put((byte)0x6D); //m
|
||||
descriptor.put((byte)0x74); //t
|
||||
descriptor.put((byte)0x20); //space
|
||||
|
||||
/* Sub Chunk 2 Size */
|
||||
descriptor.put( (byte)0x0 );
|
||||
descriptor.put( (byte)0x0 );
|
||||
descriptor.put( (byte)0x0 );
|
||||
descriptor.put( (byte)0x0 );
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
/* SubChunk1Size = 1 */
|
||||
descriptor.put((byte)0x10);
|
||||
descriptor.put((byte)0x0);
|
||||
descriptor.put((byte)0x0);
|
||||
descriptor.put((byte)0x0);
|
||||
|
||||
/* Audio format = 1 (Uncompressed PCM) */
|
||||
descriptor.put((byte)0x1);
|
||||
descriptor.put((byte)0x0);
|
||||
|
||||
/* Number of Channels */
|
||||
descriptor.asShortBuffer().put((short)format.getChannels());
|
||||
descriptor.position(descriptor.position() + 2);
|
||||
|
||||
/* Sample Rate - assumes integral sample rate */
|
||||
descriptor.asIntBuffer().put((int)format.getSampleRate());
|
||||
descriptor.position(descriptor.position() + 4);
|
||||
|
||||
int frameByteRate = format.getChannels() * format.getSampleSizeInBits() / 8;
|
||||
|
||||
/* Byte Rate = sample rate * channels * bits per sample / 8 */
|
||||
int byteRate = (int)(format.getSampleRate() * frameByteRate);
|
||||
descriptor.asIntBuffer().put(byteRate);
|
||||
descriptor.position(descriptor.position() + 4);
|
||||
|
||||
/* Block Align */
|
||||
descriptor.asShortBuffer().put((short)frameByteRate);
|
||||
descriptor.position(descriptor.position() + 2);
|
||||
|
||||
/* Bits per Sample */
|
||||
descriptor.asShortBuffer().put((short)(format.getSampleSizeInBits()));
|
||||
descriptor.position(descriptor.position() + 2);
|
||||
|
||||
/* Sub Chunk 2 ID */
|
||||
descriptor.put((byte)0x64); //d
|
||||
descriptor.put((byte)0x61); //a
|
||||
descriptor.put((byte)0x74); //t
|
||||
descriptor.put((byte)0x61); //a
|
||||
|
||||
/* Sub Chunk 2 Size */
|
||||
descriptor.put((byte)0x0);
|
||||
descriptor.put((byte)0x0);
|
||||
descriptor.put((byte)0x0);
|
||||
descriptor.put((byte)0x0);
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.apache.commons.lang3.Validate;
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -32,253 +33,430 @@ import java.util.Arrays;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class WaveWriter implements AutoCloseable
|
||||
public class WaveWriter implements AutoCloseable
|
||||
{
|
||||
private static final Pattern FILENAME_PATTERN = Pattern.compile( "(.*_)(\\d+)(\\.wav)" );
|
||||
public static final long MAX_WAVE_SIZE = 2l * (long)Integer.MAX_VALUE;
|
||||
|
||||
private AudioFormat mAudioFormat;
|
||||
private int mFileRolloverCounter = 1;
|
||||
private long mMaxSize;
|
||||
private Path mFile;
|
||||
private FileChannel mFileChannel;
|
||||
|
||||
/**
|
||||
* Constructs a new wave writer that is open with a complete header, ready
|
||||
* for writing buffers of PCM sample data.
|
||||
*
|
||||
* Each time the maximum file size is reached, a new file is created with a
|
||||
* series suffix appended to the file name.
|
||||
*
|
||||
* @param format - audio format (channels, sample size, sample rate)
|
||||
* @param file - wave file to write
|
||||
* @param maxSize - maximum file size ( range: 1 - 4,294,967,294 bytes )
|
||||
* @throws IOException - if there are any IO issues
|
||||
*/
|
||||
public WaveWriter( AudioFormat format, Path file, long maxSize ) throws IOException
|
||||
{
|
||||
Validate.isTrue(format != null);
|
||||
Validate.isTrue( file != null );
|
||||
|
||||
mAudioFormat = format;
|
||||
mFile = file;
|
||||
|
||||
if( 0 < maxSize && maxSize <= MAX_WAVE_SIZE )
|
||||
{
|
||||
mMaxSize = maxSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
mMaxSize = MAX_WAVE_SIZE;
|
||||
}
|
||||
|
||||
open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new wave writer that is open with a complete header, ready
|
||||
* for writing buffers of PCM sample data. The maximum file size is limited
|
||||
* to the max size specified in the wave file format: max unsigned integer
|
||||
*
|
||||
* @param format - audio format (channels, sample size, sample rate)
|
||||
* @param file - wave file to write
|
||||
* @throws IOException - if there are any IO issues
|
||||
*/
|
||||
public WaveWriter( AudioFormat format, Path file ) throws IOException
|
||||
{
|
||||
this( format, file, Integer.MAX_VALUE * 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the file and writes a wave header.
|
||||
*/
|
||||
private void open() throws IOException
|
||||
{
|
||||
int version = 2;
|
||||
|
||||
while( Files.exists( mFile ) )
|
||||
{
|
||||
mFile = Paths.get( mFile.toFile().getAbsolutePath().replace( ".wav", "_" + version + ".wav" ) );
|
||||
version++;
|
||||
}
|
||||
|
||||
mFileChannel = (FileChannel.open( mFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW ) );
|
||||
public static final String RIFF_ID = "RIFF";
|
||||
public static final int INITIAL_TOTAL_LENGTH = 4;
|
||||
public static final String WAVE_ID = "WAVE";
|
||||
|
||||
ByteBuffer header = WaveUtils.getWaveHeader( mAudioFormat );
|
||||
|
||||
header.flip();
|
||||
public static final String FORMAT_CHUNK_ID = "fmt ";
|
||||
public static final int FORMAT_CHUNK_LENGTH = 16;
|
||||
public static final short FORMAT_UNCOMPRESSED_PCM = 1;
|
||||
|
||||
while( header.hasRemaining() )
|
||||
{
|
||||
mFileChannel.write( header );
|
||||
}
|
||||
}
|
||||
public static final String DATA_CHUNK_ID = "data";
|
||||
|
||||
/**
|
||||
* Closes the file
|
||||
*/
|
||||
public void close() throws IOException
|
||||
{
|
||||
mFileChannel.force( true );
|
||||
mFileChannel.close();
|
||||
mFileChannel = null;
|
||||
}
|
||||
private static final Pattern FILENAME_PATTERN = Pattern.compile("(.*_)(\\d+)(\\.wav)");
|
||||
public static final long MAX_WAVE_SIZE = 2l * (long)Integer.MAX_VALUE;
|
||||
|
||||
private AudioFormat mAudioFormat;
|
||||
private int mFileRolloverCounter = 1;
|
||||
private long mMaxSize;
|
||||
private Path mFile;
|
||||
private FileChannel mFileChannel;
|
||||
private boolean mDataChunkOpen = false;
|
||||
private long mDataChunkSizeOffset = 0;
|
||||
private int mDataChunkSize = 0;
|
||||
|
||||
/**
|
||||
* Constructs a new wave writer that is open with a complete header, ready
|
||||
* for writing buffers of PCM sample data.
|
||||
*
|
||||
* Each time the maximum file size is reached, a new file is created with a
|
||||
* series suffix appended to the file name.
|
||||
*
|
||||
* @param format - audio format (channels, sample size, sample rate)
|
||||
* @param file - wave file to write
|
||||
* @param maxSize - maximum file size ( range: 1 - 4,294,967,294 bytes )
|
||||
* @throws IOException - if there are any IO issues
|
||||
*/
|
||||
public WaveWriter(AudioFormat format, Path file, long maxSize) throws IOException
|
||||
{
|
||||
Validate.isTrue(format != null);
|
||||
Validate.isTrue(file != null);
|
||||
|
||||
mAudioFormat = format;
|
||||
mFile = file;
|
||||
|
||||
if(0 < maxSize && maxSize <= MAX_WAVE_SIZE)
|
||||
{
|
||||
mMaxSize = maxSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
mMaxSize = MAX_WAVE_SIZE;
|
||||
}
|
||||
|
||||
open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new wave writer that is open with a complete header, ready
|
||||
* for writing buffers of PCM sample data. The maximum file size is limited
|
||||
* to the max size specified in the wave file format: max unsigned integer
|
||||
*
|
||||
* @param format - audio format (channels, sample size, sample rate)
|
||||
* @param file - wave file to write
|
||||
* @throws IOException - if there are any IO issues
|
||||
*/
|
||||
public WaveWriter(AudioFormat format, Path file) throws IOException
|
||||
{
|
||||
this(format, file, Integer.MAX_VALUE * 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the file and writes a wave header.
|
||||
*/
|
||||
private void open() throws IOException
|
||||
{
|
||||
int version = 2;
|
||||
|
||||
while(Files.exists(mFile))
|
||||
{
|
||||
mFile = Paths.get(mFile.toFile().getAbsolutePath().replace(".wav", "_" + version + ".wav"));
|
||||
version++;
|
||||
}
|
||||
|
||||
mFileChannel = (FileChannel.open(mFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW));
|
||||
|
||||
ByteBuffer header = getWaveHeader(mAudioFormat);
|
||||
|
||||
while(header.hasRemaining())
|
||||
{
|
||||
mFileChannel.write(header);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the file
|
||||
*/
|
||||
public void close() throws IOException
|
||||
{
|
||||
mFileChannel.force(true);
|
||||
mFileChannel.close();
|
||||
mFileChannel = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws IOException
|
||||
{
|
||||
mFileChannel.force( true );
|
||||
mFileChannel.force(true);
|
||||
mFileChannel.close();
|
||||
mFileChannel = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the buffer contents to the file. Assumes that the buffer is full
|
||||
* and the first byte of data is at position 0.
|
||||
*/
|
||||
public void write( ByteBuffer buffer ) throws IOException
|
||||
{
|
||||
buffer.position( 0 );
|
||||
* Writes the buffer contents to the file. Assumes that the buffer is full
|
||||
* and the first byte of data is at position 0.
|
||||
*/
|
||||
public void writeData(ByteBuffer buffer) throws IOException
|
||||
{
|
||||
buffer.position(0);
|
||||
|
||||
/* Write the full buffer if there is room, respecting the max file size */
|
||||
if( mFileChannel.size() + buffer.capacity() < mMaxSize )
|
||||
{
|
||||
while( buffer.hasRemaining() )
|
||||
{
|
||||
mFileChannel.write( buffer );
|
||||
}
|
||||
|
||||
updateWaveFileSize();
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Split the buffer to finish filling the current file and then put
|
||||
* the leftover into a new file */
|
||||
int remaining = (int)( mMaxSize - mFileChannel.size() );
|
||||
openDataChunk();
|
||||
|
||||
/* Ensure we write full frames to fill up the remaining size */
|
||||
remaining -= (int)( remaining % mAudioFormat.getFrameSize() );
|
||||
|
||||
byte[] bytes = buffer.array();
|
||||
|
||||
ByteBuffer current = ByteBuffer.wrap( Arrays.copyOf( bytes, remaining ) );
|
||||
/* Write the full buffer if there is room, respecting the max file size */
|
||||
if(mFileChannel.size() + buffer.capacity() < mMaxSize)
|
||||
{
|
||||
while(buffer.hasRemaining())
|
||||
{
|
||||
mDataChunkSize += mFileChannel.write(buffer);
|
||||
}
|
||||
|
||||
ByteBuffer next = ByteBuffer.wrap( Arrays.copyOfRange( bytes,
|
||||
remaining, bytes.length ) );
|
||||
updateTotalSize();
|
||||
updateDataChunkSize();
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Split the buffer to finish filling the current file and then put
|
||||
* the leftover into a new file */
|
||||
int remaining = (int)(mMaxSize - mFileChannel.size());
|
||||
|
||||
while( current.hasRemaining() )
|
||||
{
|
||||
mFileChannel.write( current );
|
||||
}
|
||||
|
||||
updateWaveFileSize();
|
||||
|
||||
rollover();
|
||||
/* Ensure we write full frames to fill up the remaining size */
|
||||
remaining -= (int)(remaining % mAudioFormat.getFrameSize());
|
||||
|
||||
while( next.hasRemaining() )
|
||||
{
|
||||
mFileChannel.write( next );
|
||||
}
|
||||
|
||||
updateWaveFileSize();
|
||||
}
|
||||
}
|
||||
byte[] bytes = buffer.array();
|
||||
|
||||
/**
|
||||
* Closes out the current file, appends an incremented sequence number to
|
||||
* the file name and opens up a new file.
|
||||
*/
|
||||
private void rollover() throws IOException
|
||||
{
|
||||
close();
|
||||
|
||||
mFileRolloverCounter++;
|
||||
|
||||
updateFileName();
|
||||
|
||||
open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the overall and the chunk2 sizes
|
||||
*/
|
||||
private void updateWaveFileSize() throws IOException
|
||||
{
|
||||
/* Update overall wave size (total size - 8 bytes) */
|
||||
ByteBuffer buffer = getUnsignedIntegerBuffer( mFileChannel.size() - 8 );
|
||||
ByteBuffer current = ByteBuffer.wrap(Arrays.copyOf(bytes, remaining));
|
||||
|
||||
mFileChannel.write( buffer, 4 );
|
||||
ByteBuffer next = ByteBuffer.wrap(Arrays.copyOfRange(bytes,
|
||||
remaining, bytes.length));
|
||||
|
||||
ByteBuffer buffer2 = getUnsignedIntegerBuffer( mFileChannel.size() - 44 );
|
||||
|
||||
mFileChannel.write( buffer2, 40 );
|
||||
}
|
||||
while(current.hasRemaining())
|
||||
{
|
||||
mDataChunkSize += mFileChannel.write(current);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a little-endian 4-byte buffer containing an unsigned 32-bit
|
||||
* integer value derived from the 4 least significant bytes of the argument.
|
||||
*
|
||||
* The buffer's position is set to 0 to prepare it for writing to a channel.
|
||||
*/
|
||||
protected static ByteBuffer getUnsignedIntegerBuffer( long size )
|
||||
{
|
||||
ByteBuffer buffer = ByteBuffer.allocate( 4 );
|
||||
|
||||
buffer.put( (byte)( size & 0xFFl ) );
|
||||
buffer.put( (byte)( Long.rotateRight( size & 0xFF00l, 8 ) ) );
|
||||
buffer.put( (byte)( Long.rotateRight( size & 0xFF0000l, 16 ) ) );
|
||||
|
||||
/* This side-steps an issue with right shifting a signed long by 32
|
||||
* where it produces an error value. Instead, we right shift in two steps. */
|
||||
buffer.put( (byte)Long.rotateRight(
|
||||
Long.rotateRight( size & 0xFF000000l, 16 ), 8 ) );
|
||||
|
||||
buffer.position( 0 );
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public static String toString( ByteBuffer buffer )
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
byte[] bytes = buffer.array();
|
||||
|
||||
for( byte b: bytes )
|
||||
{
|
||||
sb.append(String.format("%02X ", b));
|
||||
sb.append( " " );
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current file name with the rollover counter series suffix
|
||||
*/
|
||||
private void updateFileName()
|
||||
{
|
||||
String filename = mFile.toString();
|
||||
updateTotalSize();
|
||||
updateDataChunkSize();
|
||||
|
||||
if( mFileRolloverCounter == 2 )
|
||||
{
|
||||
filename = filename.replace( ".wav", "_2.wav" );
|
||||
}
|
||||
else
|
||||
{
|
||||
Matcher m = FILENAME_PATTERN.matcher( filename );
|
||||
rollover();
|
||||
|
||||
if( m.find() )
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append( m.group( 1 ) );
|
||||
sb.append( mFileRolloverCounter );
|
||||
sb.append( m.group( 3 ) );
|
||||
|
||||
filename = sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
mFile = Paths.get( filename );
|
||||
}
|
||||
openDataChunk();
|
||||
|
||||
while(next.hasRemaining())
|
||||
{
|
||||
mDataChunkSize += mFileChannel.write(next);
|
||||
}
|
||||
|
||||
updateTotalSize();
|
||||
updateDataChunkSize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the current data chunk
|
||||
*/
|
||||
private void closeDataChunk()
|
||||
{
|
||||
mDataChunkOpen = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a new data chunk if a data chunk is not currently open. This method can be invoked repeatedly as an
|
||||
* assurance that the data chunk header has been written.
|
||||
*
|
||||
* @throws IOException if there is an error writing the data chunk header.
|
||||
*/
|
||||
private void openDataChunk() throws IOException
|
||||
{
|
||||
if(!mDataChunkOpen)
|
||||
{
|
||||
if(mFileChannel.size() + 32 >= mMaxSize)
|
||||
{
|
||||
rollover();
|
||||
}
|
||||
|
||||
ByteBuffer formatChunk = getFormatChunk(mAudioFormat);
|
||||
formatChunk.position(0);
|
||||
|
||||
while(formatChunk.hasRemaining())
|
||||
{
|
||||
mFileChannel.write(formatChunk);
|
||||
}
|
||||
|
||||
ByteBuffer dataHeader = getDataHeader();
|
||||
dataHeader.position(0);
|
||||
|
||||
while(dataHeader.hasRemaining())
|
||||
{
|
||||
mFileChannel.write(dataHeader);
|
||||
}
|
||||
|
||||
mDataChunkSizeOffset = mFileChannel.size() - 4;
|
||||
mDataChunkSize = 0;
|
||||
mDataChunkOpen = true;
|
||||
|
||||
updateTotalSize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the metadata to the end of the file if there is sufficient space without exceeding the
|
||||
* max file size.
|
||||
*/
|
||||
public void writeMetadata(WaveMetadata metadata) throws IOException
|
||||
{
|
||||
ByteBuffer listChunk = metadata.getLISTChunk();
|
||||
|
||||
if(mFileChannel.size() + listChunk.capacity() >= mMaxSize)
|
||||
{
|
||||
throw new IOException("Cannot write LIST metadata chunk - insufficient file space remaining");
|
||||
}
|
||||
|
||||
closeDataChunk();
|
||||
|
||||
listChunk.position(0);
|
||||
|
||||
while(listChunk.hasRemaining())
|
||||
{
|
||||
mFileChannel.write(listChunk);
|
||||
}
|
||||
|
||||
updateTotalSize();
|
||||
|
||||
ByteBuffer id3Chunk = metadata.getID3Chunk();
|
||||
|
||||
if(mFileChannel.size() + id3Chunk.capacity() >= mMaxSize)
|
||||
{
|
||||
throw new IOException("Cannot write ID3 metadata chunk - insufficient file space remaining");
|
||||
}
|
||||
|
||||
id3Chunk.position(0);
|
||||
|
||||
while(id3Chunk.hasRemaining())
|
||||
{
|
||||
mFileChannel.write(id3Chunk);
|
||||
}
|
||||
|
||||
updateTotalSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes out the current file, appends an incremented sequence number to
|
||||
* the file name and opens up a new file.
|
||||
*/
|
||||
private void rollover() throws IOException
|
||||
{
|
||||
closeDataChunk();
|
||||
close();
|
||||
|
||||
mFileRolloverCounter++;
|
||||
|
||||
updateFileName();
|
||||
|
||||
open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the overall and the chunk2 sizes
|
||||
*/
|
||||
private void updateTotalSize() throws IOException
|
||||
{
|
||||
/* Update overall wave size (total size - 8 bytes) */
|
||||
ByteBuffer buffer = getUnsignedIntegerBuffer(mFileChannel.size() - 8);
|
||||
mFileChannel.write(buffer, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the data chunk size
|
||||
*/
|
||||
private void updateDataChunkSize() throws IOException
|
||||
{
|
||||
if(!mDataChunkOpen)
|
||||
{
|
||||
throw new IOException("Can't update data chunk size - data chunk is not currently open");
|
||||
}
|
||||
|
||||
/* Update overall wave size (total size - 8 bytes) */
|
||||
ByteBuffer size = getUnsignedIntegerBuffer(mDataChunkSize);
|
||||
mFileChannel.write(size, mDataChunkSizeOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a little-endian 4-byte buffer containing an unsigned 32-bit
|
||||
* integer value derived from the 4 least significant bytes of the argument.
|
||||
*
|
||||
* The buffer's position is set to 0 to prepare it for writing to a channel.
|
||||
*/
|
||||
protected static ByteBuffer getUnsignedIntegerBuffer(long size)
|
||||
{
|
||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||
|
||||
buffer.put((byte)(size & 0xFFl));
|
||||
buffer.put((byte)(Long.rotateRight(size & 0xFF00l, 8)));
|
||||
buffer.put((byte)(Long.rotateRight(size & 0xFF0000l, 16)));
|
||||
|
||||
/* This side-steps an issue with right shifting a signed long by 32
|
||||
* where it produces an error value. Instead, we right shift in two steps. */
|
||||
buffer.put((byte)Long.rotateRight(Long.rotateRight(size & 0xFF000000l, 16), 8));
|
||||
|
||||
buffer.position(0);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public static String toString(ByteBuffer buffer)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
byte[] bytes = buffer.array();
|
||||
|
||||
for(byte b : bytes)
|
||||
{
|
||||
sb.append(String.format("%02X ", b));
|
||||
sb.append(" ");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current file name with the rollover counter series suffix
|
||||
*/
|
||||
private void updateFileName()
|
||||
{
|
||||
String filename = mFile.toString();
|
||||
|
||||
if(mFileRolloverCounter == 2)
|
||||
{
|
||||
filename = filename.replace(".wav", "_2.wav");
|
||||
}
|
||||
else
|
||||
{
|
||||
Matcher m = FILENAME_PATTERN.matcher(filename);
|
||||
|
||||
if(m.find())
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(m.group(1));
|
||||
sb.append(mFileRolloverCounter);
|
||||
sb.append(m.group(3));
|
||||
|
||||
filename = sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
mFile = Paths.get(filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a data chunk header with the chunk size initialized to zero
|
||||
*/
|
||||
public static ByteBuffer getDataHeader()
|
||||
{
|
||||
ByteBuffer header = ByteBuffer.allocate(8);
|
||||
header.put(DATA_CHUNK_ID.getBytes());
|
||||
header.position(0);
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a wave file header with a format descriptor chunk
|
||||
*/
|
||||
public static ByteBuffer getWaveHeader(AudioFormat format)
|
||||
{
|
||||
ByteBuffer header = ByteBuffer.allocate(12).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
//RIFF/WAVE header and size
|
||||
header.put(RIFF_ID.getBytes());
|
||||
header.putInt(INITIAL_TOTAL_LENGTH);
|
||||
header.put(WAVE_ID.getBytes());
|
||||
|
||||
//Reset the buffer pointer to 0
|
||||
header.position(0);
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an audio format chunk
|
||||
*/
|
||||
public static ByteBuffer getFormatChunk(AudioFormat format)
|
||||
{
|
||||
ByteBuffer header = ByteBuffer.allocate(24).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
//Format descriptor
|
||||
header.put(FORMAT_CHUNK_ID.getBytes());
|
||||
header.putInt(FORMAT_CHUNK_LENGTH);
|
||||
header.putShort(FORMAT_UNCOMPRESSED_PCM);
|
||||
header.putShort((short)format.getChannels());
|
||||
header.putInt((int)format.getSampleRate());
|
||||
|
||||
//Byte Rate = sample rate * channels * bits per sample / 8
|
||||
int frameByteRate = format.getChannels() * format.getSampleSizeInBits() / 8;
|
||||
int byteRate = (int)(format.getSampleRate() * frameByteRate);
|
||||
header.putInt(byteRate);
|
||||
|
||||
//Block Align
|
||||
header.putShort((short)frameByteRate);
|
||||
|
||||
//Bits per Sample
|
||||
header.putShort((short)format.getSampleSizeInBits());
|
||||
|
||||
//Reset the buffer pointer to 0
|
||||
header.position(0);
|
||||
|
||||
return header;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user