Merge pull request #303 from DSheirer/278-wave-metadata

278 Add Metadata to WAV audio recordings
This commit is contained in:
Denny Sheirer
2017-12-25 07:17:08 -05:00
committed by GitHub
10 changed files with 1314 additions and 343 deletions

View File

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

View File

@@ -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));
}

View File

@@ -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())
{

View File

@@ -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);
}
}
}
}

View File

@@ -188,7 +188,7 @@ public class ComplexBufferWaveRecorder extends Module
}
else
{
mWriter.write( ConversionUtils.convertToSigned16BitSamples( buffer ) );
mWriter.writeData( ConversionUtils.convertToSigned16BitSamples( buffer ) );
buffer = mBuffers.poll();
}
}

View File

@@ -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();
}
}

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}