diff --git a/.idea/dictionaries/denny.xml b/.idea/dictionaries/denny.xml index 31ec5bc1..0df9f405 100644 --- a/.idea/dictionaries/denny.xml +++ b/.idea/dictionaries/denny.xml @@ -1,10 +1,12 @@ + bahroo esnid fleetsync ident polyphase + shoutcast talkgroup tgid diff --git a/.idea/libraries/imports.xml b/.idea/libraries/imports.xml index 8ac29dbb..7e05f72d 100644 --- a/.idea/libraries/imports.xml +++ b/.idea/libraries/imports.xml @@ -21,9 +21,10 @@ - + + diff --git a/.idea/libraries/jmbe_interface_0_3_3.xml b/.idea/libraries/jmbe_interface_0_3_3.xml deleted file mode 100644 index 1b73f0a9..00000000 --- a/.idea/libraries/jmbe_interface_0_3_3.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/jump3r_1_0_3.xml b/.idea/libraries/jump3r_1_0_3.xml deleted file mode 100644 index 7b55f0d4..00000000 --- a/.idea/libraries/jump3r_1_0_3.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/imports/java-lame-3.98.4.jar b/imports/java-lame-3.98.4.jar new file mode 100644 index 00000000..026d96eb Binary files /dev/null and b/imports/java-lame-3.98.4.jar differ diff --git a/imports/jump3r-1.0.3.jar b/imports/jump3r-1.0.3.jar deleted file mode 100644 index 12d8234a..00000000 Binary files a/imports/jump3r-1.0.3.jar and /dev/null differ diff --git a/sdrtrunk.iml b/sdrtrunk.iml index 1c1d0bed..b65a79a9 100644 --- a/sdrtrunk.iml +++ b/sdrtrunk.iml @@ -20,7 +20,5 @@ - - \ No newline at end of file diff --git a/src/audio/AudioUtils.java b/src/audio/AudioUtils.java new file mode 100644 index 00000000..fac396f9 --- /dev/null +++ b/src/audio/AudioUtils.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 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 + * + ******************************************************************************/ +package audio; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import record.wave.RealBufferWaveRecorder; +import sample.ConversionUtils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +public class AudioUtils +{ + private final static Logger mLog = LoggerFactory.getLogger(AudioUtils.class); + + /** + * Converts the audio packets into a byte array of 16-bit, little-endian audio samples + */ + public static byte[] convertTo16BitSamples(List audioPackets) + { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + + try + { + for(AudioPacket packet: audioPackets) + { + //Converting from 32-bit floats to signed 16-bit samples + ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(packet.getAudioBuffer().getSamples()); + stream.write(buffer.array()); + } + } + catch(IOException e) + { + mLog.error("Error writing converted PCM bytes to output stream"); + } + + return stream.toByteArray(); + } +} diff --git a/src/audio/stream/StreamManager.java b/src/audio/broadcast/AudioBroadcastManager.java similarity index 78% rename from src/audio/stream/StreamManager.java rename to src/audio/broadcast/AudioBroadcastManager.java index a1a87ca4..c0598a13 100644 --- a/src/audio/stream/StreamManager.java +++ b/src/audio/broadcast/AudioBroadcastManager.java @@ -16,18 +16,17 @@ * along with this program. If not, see * ******************************************************************************/ -package audio.stream; +package audio.broadcast; import audio.AudioPacket; -import audio.IAudioPacketListener; import controller.ThreadPoolManager; import sample.Listener; -public class StreamManager implements IAudioPacketListener, Listener +public class AudioBroadcastManager implements Listener { private ThreadPoolManager mThreadPoolManager; - public StreamManager(ThreadPoolManager threadPoolManager) + public AudioBroadcastManager(ThreadPoolManager threadPoolManager) { mThreadPoolManager = threadPoolManager; } @@ -37,10 +36,4 @@ public class StreamManager implements IAudioPacketListener, Listener getAudioPacketListener() - { - return this; - } } diff --git a/src/audio/stream/StreamModel.java b/src/audio/broadcast/AudioBroadcastModel.java similarity index 75% rename from src/audio/stream/StreamModel.java rename to src/audio/broadcast/AudioBroadcastModel.java index 9e6b99ed..b5819cf7 100644 --- a/src/audio/stream/StreamModel.java +++ b/src/audio/broadcast/AudioBroadcastModel.java @@ -16,24 +16,26 @@ * along with this program. If not, see * ******************************************************************************/ -package audio.stream; +package audio.broadcast; + +import audio.broadcast.configuration.BroadcastConfiguration; import javax.swing.table.AbstractTableModel; import java.util.ArrayList; import java.util.List; -public class StreamModel extends AbstractTableModel +public class AudioBroadcastModel extends AbstractTableModel { private static final int COLUMN_CHANNEL_NAME = 0; private static final int COLUMN_CHANNEL_FORMAT = 1; private static final int COLUMN_SERVER_NAME = 2; - private List mStreams = new ArrayList<>(); + private List mBroadcastConfigurations = new ArrayList<>(); @Override public int getRowCount() { - return mStreams.size(); + return mBroadcastConfigurations.size(); } @Override @@ -45,20 +47,20 @@ public class StreamModel extends AbstractTableModel @Override public Object getValueAt(int rowIndex, int columnIndex) { - if( rowIndex <= mStreams.size()) + if( rowIndex <= mBroadcastConfigurations.size()) { - Stream stream = mStreams.get(rowIndex); + BroadcastConfiguration configuration = mBroadcastConfigurations.get(rowIndex); - if(stream != null) + if(configuration != null) { switch(columnIndex) { case COLUMN_CHANNEL_NAME: - return stream.getChannel().getName(); + return configuration.getAlias(); case COLUMN_CHANNEL_FORMAT: - return stream.getChannel().getSourceFormat().name(); + return configuration.getBroadcastFormat().name(); case COLUMN_SERVER_NAME: - return stream.getServer().getName(); + return configuration.getHost(); default: break; } diff --git a/src/audio/broadcast/Broadcaster.java b/src/audio/broadcast/Broadcaster.java new file mode 100644 index 00000000..7e107969 --- /dev/null +++ b/src/audio/broadcast/Broadcaster.java @@ -0,0 +1,205 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 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 + * + ******************************************************************************/ +package audio.broadcast; + +import audio.AudioPacket; +import audio.IAudioPacketListener; +import audio.broadcast.configuration.BroadcastConfiguration; +import audio.broadcast.handler.BroadcastHandler; +import audio.broadcast.handler.BroadcastState; +import controller.ThreadPoolManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Listener; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public class Broadcaster implements IAudioPacketListener, Listener +{ + private final static Logger mLog = LoggerFactory.getLogger( Broadcaster.class ); + + private ThreadPoolManager mThreadPoolManager; + private ScheduledFuture mScheduledTask; + private AudioQueueProcessor mAudioQueueProcessor; + private LinkedTransferQueue mAudioQueue = new LinkedTransferQueue<>(); + private BroadcastHandler mBroadcastHandler; + private boolean mEnabled = true; + + /** + * Broadcaster for sending broadcast audio to a remote broadcast server. + */ + public Broadcaster(ThreadPoolManager threadPoolManager, BroadcastHandler broadcastHandler) + { + mThreadPoolManager = threadPoolManager; + mBroadcastHandler = broadcastHandler; + mAudioQueueProcessor = new AudioQueueProcessor(); + + //Add broadcast state listener to stop processing if we have an error + mBroadcastHandler.addListener(new BroadcastStateMonitor()); + } + + /** + * Broadcast configuration used by this broadcaster + */ + public BroadcastConfiguration getBroadcastConfiguration() + { + return mBroadcastHandler.getBroadcastConfiguration(); + } + + /** + * Current state of the broadcaster's connection with the remote server + */ + public BroadcastState getBroadcastState() + { + return mBroadcastHandler.getBroadcastState(); + } + + /** + * Registers the listener to receive broadcast state changes + */ + public void addListener(Listener listener) + { + mBroadcastHandler.addListener(listener); + } + + /** + * Removes the listener from receiving broadcast state changes + */ + public void removeListener(Listener listener) + { + mBroadcastHandler.removeListener(listener); + } + + /** + * IAudioPacketListener interface method + */ + @Override + public Listener getAudioPacketListener() + { + return this; + } + + /** + * Disconnects the broadcaster and halts all broadcasting. + */ + public void stop() + { + mEnabled = false; + stopProcessor(); + mBroadcastHandler.disconnect(); + } + + /** + * Pauses the broadcaster. + * @param paused set to true to pause and false to continue + */ + public void setPaused(boolean paused) + { + if(mEnabled) + { + mBroadcastHandler.setPaused(paused); + } + } + + /** + * Primary audio packet input method. Audio packets are queued and then processed by the underlying broadcast + * handler for broadcast to the remote server. + */ + public void receive(AudioPacket packet) + { + if(mEnabled) + { + startProcessor(); + mAudioQueue.add(packet); + } + } + + /** + * Starts the audio queue processor. The audio packet queue is serviced every 100 milliseconds. Subsequent calls + * to this method once running are ignored. + */ + private void startProcessor() + { + if(mEnabled && mScheduledTask == null) + { + if(mThreadPoolManager != null) + { + mScheduledTask = mThreadPoolManager.scheduleFixedRate(ThreadPoolManager.ThreadType.AUDIO_PROCESSING, + mAudioQueueProcessor, 100, TimeUnit.MILLISECONDS ); + } + } + } + + /** + * Stops the audio queue processor + */ + private void stopProcessor() + { + if(mThreadPoolManager != null && mScheduledTask != null) + { + mThreadPoolManager.cancel(mScheduledTask); + } + } + + /** + * Monitors the broadcast handler for error state and halts audio broadcasting + */ + public class BroadcastStateMonitor implements Listener + { + @Override + public void receive(BroadcastState broadcastState) + { + if(broadcastState.isErrorState()) + { + stop(); + } + } + } + + /** + * Processes audio packets from the audio packet queue and broadcasts the audio + */ + public class AudioQueueProcessor implements Runnable + { + private AtomicBoolean mProcessing = new AtomicBoolean(); + + @Override + public void run() + { + if(mProcessing.compareAndSet(false, true)) + { + List audioPackets = new ArrayList<>(); + + mAudioQueue.drainTo(audioPackets); + + if(!audioPackets.isEmpty()) + { + mBroadcastHandler.broadcast(audioPackets); + } + + mProcessing.set(false); + } + } + } +} diff --git a/src/audio/broadcast/BroadcasterFactory.java b/src/audio/broadcast/BroadcasterFactory.java new file mode 100644 index 00000000..ce13702e --- /dev/null +++ b/src/audio/broadcast/BroadcasterFactory.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 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 + * + ******************************************************************************/ +package audio.broadcast; + +import audio.broadcast.configuration.BroadcastConfiguration; +import audio.broadcast.configuration.BroadcastFormat; +import audio.broadcast.configuration.ShoutcastV1Configuration; +import audio.broadcast.handler.ShoutcastV1Handler; +import audio.convert.IAudioConverter; +import audio.convert.MP3AudioConverter; +import controller.ThreadPoolManager; + +public class BroadcasterFactory +{ + public static final int MP3_MONO_16_KHZ_BITRATE = 16; + public static final boolean MP3_NO_VARIABLE_BITRATE = false; + + /** + * Creates an audio streaming broadcaster for the configuration + * @param threadPoolManager for creating an audio broadcasting connection thread + * @param configuration describing the server and audio types + * @return configured broadcaster or null + */ + public static Broadcaster getBroadcaster(ThreadPoolManager threadPoolManager, + BroadcastConfiguration configuration) + { + IAudioConverter converter = getAudioConverter(configuration.getBroadcastFormat()); + + if(converter != null) + { + switch(configuration.getBroadcastServerType()) + { + case BROADCASTIFY: + break; + case ICECAST: + break; + case SHOUTCAST_V1: + return new Broadcaster(threadPoolManager, + new ShoutcastV1Handler((ShoutcastV1Configuration)configuration, converter)); + case UNKNOWN: + break; + } + } + + return null; + } + + /** + * Creates an audio convert to convert from 8 kHz PCM audio to the specified format + * @param format to convert to + * @return audio convert or null + */ + public static IAudioConverter getAudioConverter(BroadcastFormat format) + { + switch(format) + { + case MP3: + return new MP3AudioConverter(MP3_MONO_16_KHZ_BITRATE, MP3_NO_VARIABLE_BITRATE); + + } + + return null; + } +} diff --git a/src/audio/broadcast/configuration/BroadcastConfiguration.java b/src/audio/broadcast/configuration/BroadcastConfiguration.java new file mode 100644 index 00000000..2044b727 --- /dev/null +++ b/src/audio/broadcast/configuration/BroadcastConfiguration.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 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 + * + ******************************************************************************/ +package audio.broadcast.configuration; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +public abstract class BroadcastConfiguration +{ + private BroadcastFormat mBroadcastFormat; + private String mAlias; + private String mHost; + private int mPort; + private String mPassword; + + + /** + * Broadcast audio streaming configuration. Describes the streaming server, broadcast user authentication and + * audio broadcast details to use when connecting to a streaming server and broadcasting audio. + */ + public BroadcastConfiguration(BroadcastFormat format) + { + mBroadcastFormat = format; + } + + public abstract BroadcastServerType getBroadcastServerType(); + + /** + * Alias identifying this broadcast configuration. + */ + public String getAlias() + { + return mAlias; + } + + /** + * Sets the alias name that identifies/describes this configuration + * @param alias + */ + public void setAlias(String alias) + { + mAlias = alias; + } + + /** + * Streaming server host name. + */ + public String getHost() + { + return mHost; + } + + /** + * Host name or IP address of the streaming server + * @param host + */ + public void setHost(String host) + { + mHost = host; + } + + /** + * Streaming server port number; + */ + public int getPort() + { + return mPort; + } + + /** + * Port number of the streaming server to where the broadcast will be directed. + * @param port + */ + public void setPort(int port) + { + mPort = port; + } + + public SocketAddress getAddress() + { + return new InetSocketAddress(getHost(), getPort()); + } + + /** + * Password to authenticate with the streaming server. + */ + public String getPassword() + { + return mPassword; + } + + /** + * Sets the password used to authenticate the broadcast broadcast user to the streaming server + * @param password + */ + public void setPassword(String password) + { + mPassword = password; + } + + /** + * Audio broadcast content type (e.g. mp3) + */ + public BroadcastFormat getBroadcastFormat() + { + return mBroadcastFormat; + } + + /** + * Audio broadcast content type (e.g. mp3) + * @param broadcastFormat + */ + public void setBroadcastFormat(BroadcastFormat broadcastFormat) + { + mBroadcastFormat = broadcastFormat; + } + +} diff --git a/src/audio/stream/shout/source/ShoutSource.java b/src/audio/broadcast/configuration/BroadcastConfigurationFactory.java similarity index 63% rename from src/audio/stream/shout/source/ShoutSource.java rename to src/audio/broadcast/configuration/BroadcastConfigurationFactory.java index 00d8184d..91bec018 100644 --- a/src/audio/stream/shout/source/ShoutSource.java +++ b/src/audio/broadcast/configuration/BroadcastConfigurationFactory.java @@ -16,19 +16,24 @@ * along with this program. If not, see * ******************************************************************************/ -package audio.stream.shout.source; +package audio.broadcast.configuration; -public abstract class ShoutSource implements IShoutSource +public class BroadcastConfigurationFactory { - private ShoutSourceFormat mFormat; + public static BroadcastConfiguration getConfiguration(BroadcastServerType serverType, BroadcastFormat format) + { + switch(serverType) + { + case BROADCASTIFY: + break; + case ICECAST: + break; + case SHOUTCAST_V1: + return new ShoutcastV1Configuration(format); + case UNKNOWN: + break; + } - public ShoutSource(ShoutSourceFormat format) - { - mFormat = format; - } - @Override - public ShoutSourceFormat getFormat() - { - return mFormat; + return null; } } diff --git a/src/audio/stream/shout/server/ShoutServer.java b/src/audio/broadcast/configuration/BroadcastFormat.java similarity index 78% rename from src/audio/stream/shout/server/ShoutServer.java rename to src/audio/broadcast/configuration/BroadcastFormat.java index 1020a5c9..25f97f37 100644 --- a/src/audio/stream/shout/server/ShoutServer.java +++ b/src/audio/broadcast/configuration/BroadcastFormat.java @@ -16,20 +16,21 @@ * along with this program. If not, see * ******************************************************************************/ -package audio.stream.shout.server; +package audio.broadcast.configuration; -public abstract class ShoutServer implements IShoutServer +public enum BroadcastFormat { - private String mName; + MP3("audio/mpeg"); - public ShoutServer(String name) + private String mValue; + + private BroadcastFormat(String value) { - mName = name; + mValue = value; } - @Override - public String getName() + public String getValue() { - return mName; + return mValue; } } diff --git a/src/audio/broadcast/configuration/BroadcastMetadata.java b/src/audio/broadcast/configuration/BroadcastMetadata.java new file mode 100644 index 00000000..917e7fb2 --- /dev/null +++ b/src/audio/broadcast/configuration/BroadcastMetadata.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 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 + * + ******************************************************************************/ +package audio.broadcast.configuration; + +public enum BroadcastMetadata +{ + AIM("icy-aim:"), + AUDIO_BIT_RATE("icy-br:"), + CHANNELS("icy-channels:"), + CONTENT_TYPE("Content-Type:"), + DESCRIPTION("icy-description:"), + GENRE("icy-genre:"), + ICE_AUDIO_INFO("ice-audio-info:"), + ICQ("icy-icq:"), + INTERNET_RELAY_CHAT("icy-irc:"), + METADATA_INTERVAL("icy-metaint:"), + METADATA_REQUEST("icy-metadata:"), + PRE_BUFFER("icy-prebuffer:"), + PUBLIC("icy-pub:"), + RESET("icy-reset:"), + SERVER("server:"), + STREAM_NAME("icy-name:"), + SONG_TITLE("icy-title:"), + URL("icy-url:"), + USER_AGENT("User-Agent:"), + UNKNOWN("Unknown"); + + private String mTag; + + private BroadcastMetadata(String tag) + { + mTag = tag; + } + + public static final String COMMAND_TERMINATOR = "\n"; + + public String toString() + { + return mTag; + } + + /** + * Encodes the metadata tag and value with an end-of-line character, appropriate for sending over the network. + */ + public String encode(String value) + { + if(value != null && !value.isEmpty()) + { + StringBuilder sb = new StringBuilder(); + sb.append(mTag).append(value).append(COMMAND_TERMINATOR); + return sb.toString(); + } + + return null; + } + + public String encode(boolean value) + { + StringBuilder sb = new StringBuilder(); + sb.append(mTag).append(value ? "1" : "0").append(COMMAND_TERMINATOR); + return sb.toString(); + } + + public String encode(BroadcastFormat broadcastFormat) + { + StringBuilder sb = new StringBuilder(); + sb.append(mTag).append(broadcastFormat.getValue()).append(COMMAND_TERMINATOR); + return sb.toString(); + } + + public String encode(int value) + { + StringBuilder sb = new StringBuilder(); + sb.append(mTag).append(String.valueOf(value)).append(COMMAND_TERMINATOR); + return sb.toString(); + } +} diff --git a/src/audio/stream/shout/source/IShoutSource.java b/src/audio/broadcast/configuration/BroadcastServerType.java similarity index 85% rename from src/audio/stream/shout/source/IShoutSource.java rename to src/audio/broadcast/configuration/BroadcastServerType.java index 198adeb4..c5c26d52 100644 --- a/src/audio/stream/shout/source/IShoutSource.java +++ b/src/audio/broadcast/configuration/BroadcastServerType.java @@ -16,9 +16,13 @@ * along with this program. If not, see * ******************************************************************************/ -package audio.stream.shout.source; +package audio.broadcast.configuration; -public interface IShoutSource +public enum BroadcastServerType { - public ShoutSourceFormat getFormat(); + BROADCASTIFY, + ICECAST, + SHOUTCAST_V1, + SHOUTCAST_V2, + UNKNOWN; } diff --git a/src/audio/broadcast/configuration/ShoutcastV1Configuration.java b/src/audio/broadcast/configuration/ShoutcastV1Configuration.java new file mode 100644 index 00000000..0a00d641 --- /dev/null +++ b/src/audio/broadcast/configuration/ShoutcastV1Configuration.java @@ -0,0 +1,141 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 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 + * + ******************************************************************************/ +package audio.broadcast.configuration; + +public class ShoutcastV1Configuration extends BroadcastConfiguration +{ + private String mStreamName; + private String mGenre; + private boolean mPublic; + private int mChannels; + private int mBitRate; + private String mURL; + + public ShoutcastV1Configuration(BroadcastFormat format) + { + super(format); + } + + @Override + public BroadcastServerType getBroadcastServerType() + { + return BroadcastServerType.SHOUTCAST_V1; + } + + /** + * Stream name + */ + public String getStreamName() + { + return mStreamName; + } + + /** + * Sets the stream name + * @param name + */ + public void setStreamName(String name) + { + mStreamName = name; + } + + /** + * Stream genre + */ + public String getGenre() + { + return mGenre; + } + + /** + * Stream genre + * @param genre + */ + public void setGenre(String genre) + { + mGenre = genre; + } + + /** + * Public visibility of the broadcast + */ + public boolean isPublic() + { + return mPublic; + } + + /** + * Sets public visibility of the broadcast + * @param isPublic indicates if the broadcast should be visible to the public + */ + public void setPublic(boolean isPublic) + { + mPublic = isPublic; + } + + /** + * Number of audio channels in the broadcast + */ + public int getChannels() + { + return mChannels; + } + + /** + * Sets the number of audio channels in the broadcast + */ + public void setChannels(int channels) + { + mChannels = channels; + } + + /** + * Bit rate in bits per second + */ + public int getBitRate() + { + return mBitRate; + } + + /** + * Bit rate + * @param bitRate in samples per second + */ + public void setBitRate(int bitRate) + { + mBitRate = bitRate; + } + + /** + * URL associated with the broadcast where users can find additional details. + */ + public String getURL() + { + return mURL; + } + + /** + * URL associated with the broadcast where users can find additional details. + * @param url + */ + public void setURL(String url) + { + mURL = url; + } +} diff --git a/src/audio/broadcast/configuration/ShoutcastV2Configuration.java b/src/audio/broadcast/configuration/ShoutcastV2Configuration.java new file mode 100644 index 00000000..f92f13ca --- /dev/null +++ b/src/audio/broadcast/configuration/ShoutcastV2Configuration.java @@ -0,0 +1,179 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 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 + * + ******************************************************************************/ +package audio.broadcast.configuration; + +public class ShoutcastV2Configuration extends BroadcastConfiguration +{ + private String mMountPoint; + private String mStreamName; + private String mSongTitle; + private String mGenre; + private boolean mPublic; + private int mChannels; + private int mBitRate; + private String mURL; + + + public ShoutcastV2Configuration(BroadcastFormat format) + { + super(format); + } + + @Override + public BroadcastServerType getBroadcastServerType() + { + return BroadcastServerType.SHOUTCAST_V1; + } + + public String getMountPoint() + { + return mMountPoint; + } + + /** + * Sets the optional mount point. Shoutcast v1.x servers don't require a mount point, but v2.x servers do require + * the mount point. + * + * Note: if the supplied mount point contains a leading slash (/) it will be removed. + * + * @param mountPoint + */ + public void setMountPoint(String mountPoint) + { + //Strip the leading slash from the mount point if the user supplies it + mMountPoint = mountPoint.replace("/", ""); + } + + /** + * Stream name + */ + public String getStreamName() + { + return mStreamName; + } + + /** + * Sets the stream name + * @param name + */ + public void setStreamName(String name) + { + mStreamName = name; + } + + /** + * Song title + */ + public String getSongTitle() + { + return mSongTitle; + } + + /** + * Set the song title + */ + public void setSongTitle(String title) + { + mSongTitle = title; + } + + /** + * Stream genre + */ + public String getGenre() + { + return mGenre; + } + + /** + * Stream genre + * @param genre + */ + public void setGenre(String genre) + { + mGenre = genre; + } + + /** + * Public visibility of the broadcast + */ + public boolean isPublic() + { + return mPublic; + } + + /** + * Sets public visibility of the broadcast + * @param isPublic indicates if the broadcast should be visible to the public + */ + public void setPublic(boolean isPublic) + { + mPublic = isPublic; + } + + /** + * Number of audio channels in the broadcast + */ + public int getChannels() + { + return mChannels; + } + + /** + * Sets the number of audio channels in the broadcast + */ + public void setChannels(int channels) + { + mChannels = channels; + } + + /** + * Bit rate in bits per second + */ + public int getBitRate() + { + return mBitRate; + } + + /** + * Bit rate + * @param bitRate in samples per second + */ + public void setBitRate(int bitRate) + { + mBitRate = bitRate; + } + + /** + * URL associated with the broadcast where users can find additional details. + */ + public String getURL() + { + return mURL; + } + + /** + * URL associated with the broadcast where users can find additional details. + * @param url + */ + public void setURL(String url) + { + mURL = url; + } +} diff --git a/src/audio/broadcast/handler/BroadcastHandler.java b/src/audio/broadcast/handler/BroadcastHandler.java new file mode 100644 index 00000000..8b3e33df --- /dev/null +++ b/src/audio/broadcast/handler/BroadcastHandler.java @@ -0,0 +1,140 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 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 + * + ******************************************************************************/ +package audio.broadcast.handler; + +import audio.AudioPacket; +import audio.broadcast.configuration.BroadcastConfiguration; +import audio.convert.IAudioConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Listener; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public abstract class BroadcastHandler +{ + private final static Logger mLog = LoggerFactory.getLogger( BroadcastHandler.class ); + + protected BroadcastConfiguration mBroadcastConfiguration; + protected IAudioConverter mAudioConverter; + private List> mBroadcastStateListeners = new CopyOnWriteArrayList<>(); + private BroadcastState mBroadcastState = BroadcastState.READY; + + /** + * Abstract audio broadcast handler. Protocol-specific implementations will manage a connection with the remote + * broadcast audio server, convert audio using the audio converter argument and dispatch the converted audio to + * the remote server. + * + * @param broadcastConfiguration for configuring the handler + * @param audioConverter to convert audio packets to the desired output format + */ + public BroadcastHandler(BroadcastConfiguration broadcastConfiguration, IAudioConverter audioConverter) + { + mBroadcastConfiguration = broadcastConfiguration; + mAudioConverter = audioConverter; + } + + public BroadcastConfiguration getBroadcastConfiguration() + { + return mBroadcastConfiguration; + } + + /** + * Audio converter + */ + protected IAudioConverter getAudioConverter() + { + return mAudioConverter; + } + + /** + * Processes and broadcasts the audio packets + */ + public abstract void broadcast(List audioPackets); + + /** + * Disconnects from the remote server and disables the broadcaster. + */ + public abstract void disconnect(); + + /** + * Pauses or unpauses the broadcast + */ + public void setPaused(boolean paused) + { + if(!getBroadcastState().isErrorState()) + { + if(paused) + { + setBroadcastState(BroadcastState.PAUSED); + } + else + { + setBroadcastState(BroadcastState.READY); + } + } + } + + /** + * Registers the listener to receive broadcast state changes + */ + public void addListener(Listener listener) + { + mBroadcastStateListeners.add(listener); + } + + /** + * Removes the listener from receiving broadcast state changes + */ + public void removeListener(Listener listener) + { + mBroadcastStateListeners.remove(listener); + } + + /** + * Sets the state of the broadcast connection + */ + protected void setBroadcastState(BroadcastState state) + { + if(mBroadcastState != state) + { + mLog.debug("Changing State to: " + state); + mBroadcastState = state; + + for(Listener listener: mBroadcastStateListeners) + { + listener.receive(state); + } + } + } + + /** + * Current state of the broadcast connection + */ + public BroadcastState getBroadcastState() + { + return mBroadcastState; + } + + public boolean connected() + { + return getBroadcastState() == BroadcastState.CONNECTED; + } +} diff --git a/src/audio/broadcast/handler/BroadcastState.java b/src/audio/broadcast/handler/BroadcastState.java new file mode 100644 index 00000000..ee9bb432 --- /dev/null +++ b/src/audio/broadcast/handler/BroadcastState.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 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 + * + ******************************************************************************/ +package audio.broadcast.handler; + +public enum BroadcastState +{ + /** + * Error while broadcasting stream data. Temporary error state to allow connection to be reset. + */ + BROADCAST_ERROR("Broadcast Error", false), + + /** + * Connected to the broadcast server and capable of streaming + */ + CONNECTED("Connected", false), + + /** + * Connection interrupted, attempting to reconnect. + */ + CONNECTING("Connecting", false), + + /** + * Disconnected from the broadcast server + */ + DISCONNECTED("Disconnected", true), + + /** + * Invalid password + */ + INVALID_PASSWORD("Invalid Password", true), + + /** + * Server is not known or reachable + */ + NO_SERVER("No Server", false), + + /** + * Connected but paused. No audio is streaming. + */ + PAUSED("Paused", false), + + /** + * Initial state with no connection attempted. + */ + READY("Ready", false), + + /** + * Server host name or port is invalid + */ + UNKNOWN_HOST("Unknown Host or Invalid Port", true), + + /** + * Unspecified error + */ + ERROR("Error", true); + + private String mLabel; + private boolean mErrorState; + + private BroadcastState(String label, boolean connected) + { + mLabel = label; + mErrorState = connected; + } + + public String toString() + { + return mLabel; + } + + public boolean isErrorState() + { + return mErrorState; + } +} diff --git a/src/audio/broadcast/handler/ShoutcastV1Handler.java b/src/audio/broadcast/handler/ShoutcastV1Handler.java new file mode 100644 index 00000000..ab01bd93 --- /dev/null +++ b/src/audio/broadcast/handler/ShoutcastV1Handler.java @@ -0,0 +1,379 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 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 + * + ******************************************************************************/ +package audio.broadcast.handler; + +import audio.AudioPacket; +import audio.broadcast.Broadcaster; +import audio.broadcast.BroadcasterFactory; +import audio.broadcast.configuration.BroadcastFormat; +import audio.broadcast.configuration.BroadcastMetadata; +import audio.broadcast.configuration.ShoutcastV1Configuration; +import audio.convert.IAudioConverter; +import controller.ThreadPoolManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import record.wave.AudioPacketMonoWaveReader; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +public class ShoutcastV1Handler extends BroadcastHandler +{ + private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV1Handler.class ); + + private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds + private long mLastConnectionAttempt = 0; + + private Socket mSocket; + private DataOutputStream mOutputStream; + private DataInputStream mInputStream; + + private static final int mBufferSize = 2000; + + /** + * Creates a Shoutcast Version 1 broadcast handler. + * @param configuration + * @param audioConverter + */ + public ShoutcastV1Handler(ShoutcastV1Configuration configuration, IAudioConverter audioConverter) + { + super(configuration, audioConverter); + } + + /** + * Shoutcast V1 Configuration information + */ + private ShoutcastV1Configuration getShoutcastConfiguration() + { + return (ShoutcastV1Configuration)getBroadcastConfiguration(); + } + + /** + * Process audio packets using Shoutcast V1 Protocol + * @param audioPackets + */ + @Override + public void broadcast(List audioPackets) + { + //If we're connected, send the audio, otherwise discard it + if(connect()) + { + byte[] convertedAudio = mAudioConverter.convert(audioPackets); + + if(convertedAudio != null) + { + try + { + send(convertedAudio); + } + catch(IOException e) + { + mLog.error("Error while dispatching audio", e); + setBroadcastState(BroadcastState.BROADCAST_ERROR); + } + } + } + } + + /** + * Indicates if the audio handler is currently connected to the remote server and capable of streaming audio. + * + * This method will attempt to establish a connection or reconnect to the streaming server. + * @return true if the audio handler can stream audio + */ + private boolean connect() + { + if(!connected()) + { + createConnection(); + } + + return connected(); + } + + + /** + * Disconnect from the remote server + */ + @Override + public void disconnect() + { + + } + + /** + * Creates a connnection to the remote server using the shoutcast configuration information. Once disconnected + * following a successful connection, attempts to reestablish a connection on a set interval + */ + private void createConnection() + { + if(!connected() && System.currentTimeMillis() - mLastConnectionAttempt >= RECONNECT_INTERVAL_MILLISECONDS) + { + createSocket(); + + if(mSocket.isConnected()) + { + mLog.debug("Sending credentials"); + + StringBuilder sb = new StringBuilder(); + + //Password + sb.append(getShoutcastConfiguration().getPassword()).append(BroadcastMetadata.COMMAND_TERMINATOR); + + //Metadata + sb.append(BroadcastMetadata.STREAM_NAME.encode(getShoutcastConfiguration().getStreamName())); + sb.append(BroadcastMetadata.URL.encode(getShoutcastConfiguration().getURL())); + sb.append(BroadcastMetadata.PUBLIC.encode(getShoutcastConfiguration().isPublic())); + sb.append(BroadcastMetadata.GENRE.encode(getShoutcastConfiguration().getGenre())); + sb.append(BroadcastMetadata.AUDIO_BIT_RATE.encode(getShoutcastConfiguration().getBitRate())); + + //End of connection string + sb.append(BroadcastMetadata.COMMAND_TERMINATOR); + + try + { + send(sb.toString()); + + if(isInvalidResponse()) + { + mLog.error("Invalid response after sending metadata"); + return; + } + else + { + setBroadcastState(BroadcastState.CONNECTED); + } + } + catch(IOException ioe) + { + mLog.debug("Error while connecting to server", ioe); + setBroadcastState(BroadcastState.ERROR); + } + + mLog.debug("Connection established"); + } + } + } + + /** + * Sends the string data to the remote server + * @param data to send + * @throws IOException if there is an error communicating with the remote server + */ + private void send(String data) throws IOException + { + mLog.debug("Sending [" + data + "]"); + if(data != null && !data.isEmpty() && mOutputStream != null) + { + mOutputStream.writeBytes(data); + } + } + + /** + * Sends the byte data to the remote server + * @param data to send + * @throws IOException if there is an error communicating with the remote server + */ + private void send(byte[] data) throws IOException + { + mLog.debug("Sending bytes:" + data.length); + if(connected()) + { + int sent = 0; + + while(sent < data.length) + { + int available = data.length - sent; + + int sending = available 0) + { + byte[] responseBuffer = new byte[bytesAvailable]; + + int bytesRead = 0; + + while(bytesRead < bytesAvailable) + { + bytesRead += mInputStream.read(responseBuffer, bytesRead, bytesAvailable - bytesRead); + } + + return new String(responseBuffer); + } + } + + return null; + } + + private boolean isInvalidResponse() + { + String response; + + try + { + response = getResponse(); + } + catch(IOException e) + { + mLog.error("Error while retrieving server response message", e); + setBroadcastState(BroadcastState.ERROR); + return false; + } + + if(response != null && !response.isEmpty()) + { + if(response.startsWith("OK")) + { + return false; + } + else if(response.startsWith("Invalid Password")) + { + setBroadcastState(BroadcastState.INVALID_PASSWORD); + return true; + } + + mLog.debug("Unrecognized server response:" + response); + return true; + } + + return false; + } + + /** + * Creates a socket connection to the remote server and sets the state to CONNECTING. + */ + private void createSocket() + { + if(mSocket == null) + { + mLog.debug("Creating socket"); + try + { + mSocket = new Socket(getShoutcastConfiguration().getHost(), + getShoutcastConfiguration().getPort()); + mOutputStream = new DataOutputStream(mSocket.getOutputStream()); + mInputStream = new DataInputStream(mSocket.getInputStream()); + } + catch(UnknownHostException uhe) + { + setBroadcastState(BroadcastState.UNKNOWN_HOST); + mLog.error("Unknown host or port. Unable to create connection to streaming server host[" + + getShoutcastConfiguration().getHost() + "] and port[" + + getShoutcastConfiguration().getPort() + "] - will reattempt connection periodically"); + return; + } + catch(IOException ioe) + { + setBroadcastState(BroadcastState.ERROR); + + mLog.error("Error connecting to streaming server host[" + + getShoutcastConfiguration().getHost() + "] and port[" + + getShoutcastConfiguration().getPort() + "]", ioe); + return; + } + } + + if(mSocket.isConnected()) + { + setBroadcastState(BroadcastState.CONNECTING); + } + else + { + try + { + SocketAddress address = getShoutcastConfiguration().getAddress(); + mSocket.connect(address); + setBroadcastState(BroadcastState.CONNECTING); + } + catch(UnknownHostException uhe) + { + setBroadcastState(BroadcastState.UNKNOWN_HOST); + } + catch(IOException e) + { + setBroadcastState(BroadcastState.ERROR); + + mLog.error("Error connecting to streaming server host[" + + getShoutcastConfiguration().getHost() + "] and port[" + + getShoutcastConfiguration().getPort() + "]", e); + } + } + } + + + public static void main(String[] args) + { + ShoutcastV1Configuration config = new ShoutcastV1Configuration(BroadcastFormat.MP3); + config.setAlias("Test Configuration"); + config.setHost("localhost"); + config.setPort(8000); + config.setPassword("denny3"); + config.setStreamName("Denny's Audio Broadcast Test"); + config.setGenre("Scanner Audio"); + config.setPublic(true); + config.setURL("http://localhost:8000"); + config.setBitRate(16); + + ThreadPoolManager threadPoolManager = new ThreadPoolManager(); + + final Broadcaster broadcaster = BroadcasterFactory.getBroadcaster(threadPoolManager,config); + + Path path = Paths.get("/home/denny/Music/PCM.wav"); + mLog.debug("Opening: " + path.toString()); + + mLog.debug("Registering and starting audio playback"); + + while(true) + { + mLog.debug("Playback started [" + path.toString() + "]"); + + try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true)) + { + reader.setListener(broadcaster); + reader.read(); + } + catch (IOException e) + { + mLog.error("Error", e); + } + + mLog.debug("Playback ended [" + path.toString() + "]"); + } + } +} diff --git a/src/audio/stream/shout/source/ShoutSourceFormat.java b/src/audio/convert/IAudioConverter.java similarity index 84% rename from src/audio/stream/shout/source/ShoutSourceFormat.java rename to src/audio/convert/IAudioConverter.java index bc447db8..86cdd98f 100644 --- a/src/audio/stream/shout/source/ShoutSourceFormat.java +++ b/src/audio/convert/IAudioConverter.java @@ -16,9 +16,13 @@ * along with this program. If not, see * ******************************************************************************/ -package audio.stream.shout.source; +package audio.convert; -public enum ShoutSourceFormat +import audio.AudioPacket; + +import java.util.List; + +public interface IAudioConverter { - MP3; + public byte[] convert(List audioPackets); } diff --git a/src/audio/convert/MP3AudioConverter.java b/src/audio/convert/MP3AudioConverter.java new file mode 100644 index 00000000..27b87637 --- /dev/null +++ b/src/audio/convert/MP3AudioConverter.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 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 + * + ******************************************************************************/ +package audio.convert; + +import audio.AudioFormats; +import audio.AudioPacket; +import audio.AudioUtils; +import net.sourceforge.lame.lowlevel.LameEncoder; +import net.sourceforge.lame.mp3.Lame; +import net.sourceforge.lame.mp3.MPEGMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.util.List; + +public class MP3AudioConverter implements IAudioConverter +{ + private final static Logger mLog = LoggerFactory.getLogger( MP3AudioConverter.class ); + + private LameEncoder mEncoder; + private ByteArrayOutputStream mMP3Stream = new ByteArrayOutputStream(); + private byte[] mMP3Buffer; + + /** + * Converts PCM 8kHz 16-bit Little Endian audio packets to Mono, Low Quality MP3 compressed audio. + * + * @param bitRate for converted MP3 audio + * @param variableBitRate (VBR) true or false for constant bit rate (CBR) + */ + public MP3AudioConverter(int bitRate, boolean variableBitRate) + { + mEncoder = new LameEncoder(AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO, + bitRate, MPEGMode.MONO, Lame.QUALITY_LOW, variableBitRate); + + mMP3Buffer = new byte[mEncoder.getPCMBufferSize()]; + } + + @Override + public byte[] convert(List audioPackets) + { + mMP3Stream.reset(); + + byte[] pcmBytes = AudioUtils.convertTo16BitSamples(audioPackets); + + int pcmBufferSize = Math.min(mMP3Buffer.length, pcmBytes.length); + + int mp3BufferSize = 0; + + int pcmBytesPosition = 0; + + while (0 < (mp3BufferSize = mEncoder.encodeBuffer(pcmBytes, pcmBytesPosition, pcmBufferSize, mMP3Buffer))) + { + pcmBytesPosition += pcmBufferSize; + pcmBufferSize = Math.min(mMP3Buffer.length, pcmBytes.length - pcmBytesPosition); + mMP3Stream.write(mMP3Buffer, 0, mp3BufferSize); + } + + return mMP3Stream.toByteArray(); + } +} diff --git a/src/audio/inverted/AudioAdapter.java b/src/audio/invert/AudioAdapter.java similarity index 98% rename from src/audio/inverted/AudioAdapter.java rename to src/audio/invert/AudioAdapter.java index f529fb06..db45c734 100644 --- a/src/audio/inverted/AudioAdapter.java +++ b/src/audio/invert/AudioAdapter.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see ******************************************************************************/ -package audio.inverted; +package audio.invert; import dsp.filter.FilterFactory; import dsp.filter.FloatFIRFilter; @@ -23,7 +23,7 @@ import dsp.filter.Window.WindowType; import dsp.mixer.Oscillator; /** - * Based on the AudioType applied, produces normal, muted, or audio inverted + * Based on the AudioType applied, produces normal, muted, or audio invert * audio output. */ public class AudioAdapter diff --git a/src/audio/inverted/AudioInverter.java b/src/audio/invert/AudioInverter.java similarity index 96% rename from src/audio/inverted/AudioInverter.java rename to src/audio/invert/AudioInverter.java index e14dbb15..f9ec3755 100644 --- a/src/audio/inverted/AudioInverter.java +++ b/src/audio/invert/AudioInverter.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see ******************************************************************************/ -package audio.inverted; +package audio.invert; import audio.InversionFrequency; import sample.real.RealSampleListener; @@ -24,7 +24,7 @@ import dsp.filter.FloatFIRFilter; import dsp.mixer.Oscillator; /** - * Applies audio inversion (or un-inversion) to a stream of float audio samples + * Applies audio inversion (or un-inversion) to a broadcast of float audio samples * by multiplying each successive sample by either a 1 or a -1 value. */ public class AudioInverter implements RealSampleListener diff --git a/src/audio/inverted/AudioType.java b/src/audio/invert/AudioType.java similarity index 99% rename from src/audio/inverted/AudioType.java rename to src/audio/invert/AudioType.java index 706ee683..0659b781 100644 --- a/src/audio/inverted/AudioType.java +++ b/src/audio/invert/AudioType.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see ******************************************************************************/ -package audio.inverted; +package audio.invert; public enum AudioType { diff --git a/src/audio/inverted/IAudioTypeListener.java b/src/audio/invert/IAudioTypeListener.java similarity index 97% rename from src/audio/inverted/IAudioTypeListener.java rename to src/audio/invert/IAudioTypeListener.java index 52321dcf..a55feec1 100644 --- a/src/audio/inverted/IAudioTypeListener.java +++ b/src/audio/invert/IAudioTypeListener.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see ******************************************************************************/ -package audio.inverted; +package audio.invert; public interface IAudioTypeListener diff --git a/src/audio/stream/Stream.java b/src/audio/stream/Stream.java deleted file mode 100644 index 2ab62edd..00000000 --- a/src/audio/stream/Stream.java +++ /dev/null @@ -1,55 +0,0 @@ -/******************************************************************************* - * sdrtrunk - * Copyright (C) 2014-2016 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 - * - ******************************************************************************/ -package audio.stream; - -import audio.AudioPacket; -import audio.stream.shout.channel.IShoutChannel; -import audio.stream.shout.server.IShoutServer; -import sample.Listener; - -/** - * Streaming channel encapsulates communication with a specific server and channel. - */ -public class Stream implements Listener -{ - private IShoutServer mServer; - private IShoutChannel mChannel; - - public Stream(IShoutServer server, IShoutChannel channel) - { - mServer = server; - mChannel = channel; - } - - public IShoutServer getServer() - { - return mServer; - } - - public IShoutChannel getChannel() - { - return mChannel; - } - - @Override - public void receive(AudioPacket audioPacket) - { - - } -} diff --git a/src/audio/stream/shout/channel/IShoutChannel.java b/src/audio/stream/shout/channel/IShoutChannel.java deleted file mode 100644 index 08505a87..00000000 --- a/src/audio/stream/shout/channel/IShoutChannel.java +++ /dev/null @@ -1,27 +0,0 @@ -/******************************************************************************* - * sdrtrunk - * Copyright (C) 2014-2016 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 - * - ******************************************************************************/ -package audio.stream.shout.channel; - -import audio.stream.shout.source.ShoutSourceFormat; - -public interface IShoutChannel -{ - public String getName(); - public ShoutSourceFormat getSourceFormat(); -} diff --git a/src/audio/stream/shout/server/IShoutServer.java b/src/audio/stream/shout/server/IShoutServer.java deleted file mode 100644 index f53c3697..00000000 --- a/src/audio/stream/shout/server/IShoutServer.java +++ /dev/null @@ -1,24 +0,0 @@ -/******************************************************************************* - * sdrtrunk - * Copyright (C) 2014-2016 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 - * - ******************************************************************************/ -package audio.stream.shout.server; - -public interface IShoutServer -{ - public String getName(); -} diff --git a/src/audio/stream/shout/source/MP3ShoutSource.java b/src/audio/stream/shout/source/MP3ShoutSource.java deleted file mode 100644 index c5749a4f..00000000 --- a/src/audio/stream/shout/source/MP3ShoutSource.java +++ /dev/null @@ -1,27 +0,0 @@ -/******************************************************************************* - * sdrtrunk - * Copyright (C) 2014-2016 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 - * - ******************************************************************************/ -package audio.stream.shout.source; - -public class MP3ShoutSource extends ShoutSource -{ - public MP3ShoutSource() - { - super(ShoutSourceFormat.MP3); - } -} diff --git a/src/gui/SDRTrunk.java b/src/gui/SDRTrunk.java index 40bfb51a..c3e6fa40 100644 --- a/src/gui/SDRTrunk.java +++ b/src/gui/SDRTrunk.java @@ -43,6 +43,7 @@ import javax.swing.JOptionPane; import javax.swing.JSeparator; import javax.swing.KeyStroke; +import audio.broadcast.AudioBroadcastManager; import map.IconManager; import map.MapService; import module.log.EventLogManager; @@ -152,6 +153,9 @@ public class SDRTrunk implements Listener AudioManager audioManager = new AudioManager( threadPoolManager, sourceManager.getMixerManager() ); channelProcessingManager.addAudioPacketListener( audioManager ); + AudioBroadcastManager audioBroadcastManager = new AudioBroadcastManager(threadPoolManager); + channelProcessingManager.addAudioPacketListener(audioBroadcastManager); + MapService mapService = new MapService( mSettingsManager ); channelProcessingManager.addMessageListener( mapService ); diff --git a/src/org/jma/encoder/audio/IAudioEncoder.java b/src/org/jma/encoder/audio/IAudioEncoder.java new file mode 100644 index 00000000..ab5eeef2 --- /dev/null +++ b/src/org/jma/encoder/audio/IAudioEncoder.java @@ -0,0 +1,18 @@ +package org.jma.encoder.audio; + +/** + * Retrieved from: http://livertmpjavapublisher.blogspot.com/2011/06/orgtritonuslowlevellameapiiaudioencoder.html + * on 5 Sep 2016 + */ +public interface IAudioEncoder +{ + public int encodeBuffer(byte[] input, int offset, int encodedCount, byte[] output); + + public int encodeFinish(byte[] input); + + public int getInputBufferSize(); + + public int getOutputBufferSize(); + + public void close(); +} diff --git a/src/record/mp3/MP3Recorder.java b/src/record/mp3/MP3Recorder.java new file mode 100644 index 00000000..bd5dcdc7 --- /dev/null +++ b/src/record/mp3/MP3Recorder.java @@ -0,0 +1,319 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 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 + * + ******************************************************************************/ +package record.mp3; + +import audio.AudioPacket; +import audio.IAudioPacketListener; +import audio.convert.MP3AudioConverter; +import module.Module; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import record.wave.AudioPacketMonoWaveReader; +import sample.Listener; +import util.TimeStamp; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +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; + +/** + * MP3 recorder for converting 8 kHz PCM audio packets to MP3 and writing to .mp3 file. + */ +public class MP3Recorder extends Module implements Listener, IAudioPacketListener +{ + private final static Logger mLog = LoggerFactory.getLogger(MP3Recorder.class); + + private MP3AudioConverter mMP3Converter; + private FileOutputStream mFileOutputStream; + private String mFilePrefix; + private Path mPath; + + private BufferProcessor mBufferProcessor; + private ScheduledFuture mProcessorHandle; + private LinkedBlockingQueue mAudioPacketQueue = new LinkedBlockingQueue<>(500); + private long mLastBufferReceived; + + private AtomicBoolean mRunning = new AtomicBoolean(); + + /** + * MP3 audio recorder module for converting audio packets to 16 kHz constant bit rate MP3 format and + * recording to a file. + */ + public MP3Recorder(String filePrefix) + { + mFilePrefix = filePrefix; + + int mp3BitRate = 16; //16 kHz + boolean variableBitRate = false; //CBR + mMP3Converter = new MP3AudioConverter(mp3BitRate, variableBitRate); + } + + /** + * Timestamp of when the latest buffer was received by this recorder + */ + public long getLastBufferReceived() + { + return mLastBufferReceived; + } + + public Path getPath() + { + return mPath; + } + + /** + * Starts this recorder using as a scheduled thread broadcast running under the executor argument + * + * @param executor to use in scheduling MP3 conversion and file writes. + */ + 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(".mp3"); + + mPath = Paths.get(sb.toString()); + mFileOutputStream = new FileOutputStream(mPath.toFile()); + + /* Schedule the handler to run every second */ + mProcessorHandle = executor.scheduleAtFixedRate(mBufferProcessor, 0, 200, TimeUnit.MILLISECONDS); + } + catch (IOException io) + { + mLog.error("Error starting real buffer recorder", io); + } + } + } + + /** + * Stops the recorder. + */ + public void stop() + { + mRunning.set(false); + + if (mProcessorHandle != null) + { + mProcessorHandle.cancel(true); + } + + mProcessorHandle = null; + + processAudioPacketQueue(); + + close(); + } + + /** + * Closes the recording file. + */ + private void close() + { + if (mFileOutputStream != null) + { + try + { + mFileOutputStream.close(); + } + catch (IOException e) + { + mLog.error("Error closing output stream", e); + } + } + } + + /** + * Primary data insert method designed to accept a stream of audio packets and a final ending audio packet. + * + * Audio packets received before the recorder is started or after the recorder is stopped will be ignored. + */ + @Override + public void receive(AudioPacket audioPacket) + { + if (mRunning.get()) + { + boolean success = mAudioPacketQueue.offer(audioPacket); + + if (!success) + { + mLog.error("recorder buffer overflow - purging [" + mPath.toFile().getAbsolutePath() + "]"); + + mAudioPacketQueue.clear(); + } + + mLastBufferReceived = System.currentTimeMillis(); + } + } + + /** + * IAudioPacketListener interface method. + */ + @Override + public Listener getAudioPacketListener() + { + return this; + } + + /** + * Disposes this audio recorder and prepares it for reclamation + */ + @Override + public void dispose() + { + stop(); + } + + /** + * Not implemented. Recorder modules are not appropriate for reset and reuse. + */ + @Override + public void reset() + { + } + + /** + * Processes the audio packet queue. + */ + private void processAudioPacketQueue() + { + try + { + List mPacketsToProcess = new ArrayList<>(); + mAudioPacketQueue.drainTo(mPacketsToProcess); + + if (!mPacketsToProcess.isEmpty()) + { + boolean stopRequested = false; + int stopPacketIndex = 0; + + //An END audio packet is a request to stop the recorder + for (AudioPacket packet : mPacketsToProcess) + { + if (packet.getType() == AudioPacket.Type.END) + { + stopRequested = true; + stopPacketIndex = mPacketsToProcess.indexOf(packet); + continue; + } + } + + if (stopRequested) + { + mRunning.set(false); + mAudioPacketQueue.clear(); + + if (stopPacketIndex > 0) + { + List remainingPacketsToProcess = new ArrayList<>(); + + for (int x = 0; x < stopPacketIndex; x++) + { + remainingPacketsToProcess.add(mPacketsToProcess.get(x)); + } + + write(remainingPacketsToProcess); + + remainingPacketsToProcess.clear(); + } + + stop(); + } + else + { + write(mPacketsToProcess); + } + + mPacketsToProcess.clear(); + } + } + catch (IOException ioe) + { + mAudioPacketQueue.clear(); + + mLog.error("IOException while processing audio packet queue for MP3 conversion and storage", ioe); + } + } + + /** + * Converts the PCM audio packets to MP3 format and writes the converted audio to the output stream. + */ + private void write(List audioPackets) throws IOException + { + byte[] mp3Audio = mMP3Converter.convert(audioPackets); + + mFileOutputStream.write(mp3Audio); + } + + public class BufferProcessor implements Runnable + { + private AtomicBoolean mProcessing = new AtomicBoolean(); + + public void run() + { + if (mProcessing.compareAndSet(false, true)) + { + processAudioPacketQueue(); + + mProcessing.set(false); + } + } + } + + public static void main(String[] args) + { + Path path = Paths.get("/home/denny/Music/PCM.wav"); + mLog.debug("Opening: " + path.toString()); + + final MP3Recorder recorder = new MP3Recorder("/home/denny/Music/denny_test"); + recorder.start(Executors.newSingleThreadScheduledExecutor()); + + try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true)) + { + reader.setListener(recorder); + reader.read(); + recorder.stop(); + } + catch (IOException e) + { + mLog.error("Error", e); + } + + mLog.debug("Finished"); + } +} diff --git a/src/record/wave/AudioPacketMonoWaveReader.java b/src/record/wave/AudioPacketMonoWaveReader.java new file mode 100644 index 00000000..b8a4af71 --- /dev/null +++ b/src/record/wave/AudioPacketMonoWaveReader.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 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 + * + ******************************************************************************/ +package record.wave; + +import audio.AudioPacket; +import audio.IAudioPacketListener; +import audio.metadata.AudioMetadata; +import sample.Listener; +import sample.real.RealBuffer; + +import java.io.IOException; +import java.nio.file.Path; + +public class AudioPacketMonoWaveReader extends MonoWaveReader implements Listener +{ + private AudioMetadata mMetadata = new AudioMetadata(0, false); + + private IAudioPacketListener mListener; + + /** + * Reads wav files and converts the contents to AudioPackets. + * + * @param path of file to read containing 8kHz, 16-bit signed litte endian PCM audio + * @param realTime causes the read() method to read and dispatch audio packets in real time. Note: when using + * real time playback, the thread calling the read() method will sleep as needed to effect real-time playback, + * therefore you should invoke read() on a separate thread if this will impact the calling thread. + * + * @throws IOException on any file IO errors + */ + public AudioPacketMonoWaveReader(Path path, boolean realTime) throws IOException + { + super(path, realTime); + setListener(this); + } + + public void setMetadata(AudioMetadata metadata) + { + mMetadata = metadata; + } + + public void setListener(IAudioPacketListener listener) + { + mListener = listener; + } + + @Override + public void receive(RealBuffer realBuffer) + { + if(mListener != null) + { + mListener.getAudioPacketListener().receive(new AudioPacket(realBuffer.getSamples(), mMetadata)); + } + } +} diff --git a/src/record/wave/MonoWaveReader.java b/src/record/wave/MonoWaveReader.java new file mode 100644 index 00000000..cd717d46 --- /dev/null +++ b/src/record/wave/MonoWaveReader.java @@ -0,0 +1,257 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 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 + * + ******************************************************************************/ +package record.wave; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.ConversionUtils; +import sample.Listener; +import sample.real.RealBuffer; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.ShortBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.regex.Pattern; + +public class MonoWaveReader implements AutoCloseable +{ + private final static Logger mLog = LoggerFactory.getLogger( MonoWaveReader.class); + + private static final Pattern FILENAME_PATTERN = Pattern.compile( "(.*_)(\\d+)(\\.wav)" ); + + private Path mPath; + private boolean mRealTime; + private InputStream mInputStream; + private int mDataByteSize = 0; + private Listener mListener; + private byte[] mDataBuffer = new byte[8000]; //500 milliseconds of audio + + /** + * Wave file reader for PCM 8kHz, 16-bit little-endian format. + * + * @param path of the wave file + * @param realTime to force replay to real-time. Note: you should run this reader on a separate thread if you + * choose real time since this reader will sleep the calling thread as needed to effect real-time playback. + */ + public MonoWaveReader(Path path, boolean realTime) throws IOException + { + assert( path != null ); + mPath = path; + mRealTime = realTime; + + open(); + } + + /** + * Opens the file + */ + private void open() throws IOException + { + if(!Files.exists(mPath)) + { + throw new IOException("File not found"); + } + + mInputStream = Files.newInputStream(mPath, StandardOpenOption.READ); + + //Check for RIFF header + byte[] buffer = new byte[4]; + mInputStream.read(buffer); + + if(!Arrays.equals(buffer, WaveUtils.RIFF_CHUNK)) + { + throw new IOException("File is not .wav format - missing RIFF chunk"); + } + + //Get file size + mInputStream.read(buffer); + int fileSize = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get(); + + //Check for WAVE format + mInputStream.read(buffer); + + if(!Arrays.equals(buffer, WaveUtils.WAV_FORMAT)) + { + throw new IOException("File is not .wav format - missing WAVE format"); + } + + //Check for format chunk + mInputStream.read(buffer); + + if(!Arrays.equals(buffer, WaveUtils.CHUNK_FORMAT)) + { + throw new IOException("File is not .wav format - missing format chunk"); + } + + //Get chunk size + mInputStream.read(buffer); + int chunkSize = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get(); + + //Get format + mInputStream.read(buffer); + + ShortBuffer shortBuffer = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer(); + short format = shortBuffer.get(); + if(format != WaveUtils.PCM_FORMAT) + { + throw new IOException("File format not supported - expecting PCM format"); + } + + //Get number of channels + short channels = shortBuffer.get(); + if(channels != 1) + { + throw new IOException("Unsupported channel count - mono audio only"); + } + + //Get samples per second + mInputStream.read(buffer); + int sampleRate = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get(); + + //Get bytes per second + mInputStream.read(buffer); + int bytesPerSecond = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get(); + + mInputStream.read(buffer); + + //Get frame size + shortBuffer = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer(); + short frameSize = shortBuffer.get(); + if(frameSize != 2) + { + throw new IOException("PCM frame size not supported - expecting 2 bytes per frame"); + } + + //Get bits per sample + short bitsPerSample = shortBuffer.get(); + if(bitsPerSample != 16) + { + throw new IOException("PCM sample size not supported - expecting 16 bits per sample"); + } + + mInputStream.read(buffer); + + if(!Arrays.equals(buffer, WaveUtils.CHUNK_DATA)) + { + throw new IOException("Unexpected chunk - expecting data chunk"); + } + + //Get data chunk size + mInputStream.read(buffer); + mDataByteSize = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get(); + } + + public void read() throws IOException + { + long start = System.currentTimeMillis(); + long samplesRead = 0; + + int totalBytesRead = 0; + + while(totalBytesRead < mDataByteSize) + { + int bytesRead = mInputStream.read(mDataBuffer); + + totalBytesRead += bytesRead; + + float[] samples = ConversionUtils.convertFromSigned16BitSamples(mDataBuffer); + + if(bytesRead != mDataBuffer.length) + { + samples = Arrays.copyOf(samples, bytesRead / 2); + } + + if(mRealTime) + { + samplesRead += samples.length; + + long actualElapsed = System.currentTimeMillis() - start; + + long audioElapsed = (long)(((double)samplesRead / 8000.0) * 1000.0); + + if(audioElapsed > actualElapsed) + { + try + { + Thread.sleep(audioElapsed - actualElapsed); + } + catch (InterruptedException e) + { + } + } + } + + if(mListener != null) + { + mListener.receive(new RealBuffer(samples)); + } + } + + close(); + } + + public void setListener(Listener listener) + { + mListener = listener; + } + + /** + * Closes the file + */ + public void close() throws IOException + { + if(mInputStream != null) + { + mInputStream.close(); + } + } + + public static void main(String[] args) + { + Path path = Paths.get("/home/denny/Music/PCM.wav"); + mLog.debug("Opening: " + path.toString()); + + try + { + MonoWaveReader reader = new MonoWaveReader(path, true); + reader.setListener(new Listener() + { + @Override + public void receive(RealBuffer realBuffer) + { + mLog.debug("Received buffer"); + } + }); + reader.read(); + } + catch(IOException e) + { + mLog.error("Error", e); + } + + mLog.debug("Finished"); + } +} diff --git a/src/record/wave/WaveUtils.java b/src/record/wave/WaveUtils.java index 050fa83f..68fb8d8e 100644 --- a/src/record/wave/WaveUtils.java +++ b/src/record/wave/WaveUtils.java @@ -17,14 +17,19 @@ ******************************************************************************/ package record.wave; +import javax.sound.sampled.AudioFormat; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import javax.sound.sampled.AudioFormat; - 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 int PCM_FORMAT = 1; + /** * Creates a wave file header with the RIFF chunk descriptor and the WAVE * format chunk 1 and chunk 2 header