com.andrewkroh.cicso.rtp.AudioFileStreamer.java Source code

Java tutorial

Introduction

Here is the source code for com.andrewkroh.cicso.rtp.AudioFileStreamer.java

Source

/*
 * Copyright 2013 Andrew Kroh
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.andrewkroh.cicso.rtp;

import java.io.IOException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.Random;
import java.util.concurrent.TimeUnit;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.AbstractScheduledService;

/**
 * Streams an audio file using an {@link RtpSession} as the distribution
 * mechanism.
 *
 * @author akroh
 */
public class AudioFileStreamer extends AbstractScheduledService {
    /**
     * Supported encoding types of this class.
     */
    public enum EncodingType {
        /**
         * RFC3551 specifies payload type 8 for PCMA (G.711), 8000 Hz, 1 channel.
         */
        ALAW(8),

        /**
         * Dynamic payload type for L16, 8000 Hz, 1 channel.
         * This is a custom type defined here.
         */
        PCM16(96),

        /**
         * RFC3551 specifies payload type 0 for PCMU, 8000 Hz, 1 channel.
         */
        ULAW(0);

        private EncodingType(int payloadType) {
            this.payloadType = payloadType;
        }

        public int getPayloadType() {
            return payloadType;
        }

        private final int payloadType;
    }

    /**
     * SLF4J Logger for this class.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(AudioFileStreamer.class);

    /**
     * AudioFormat of the output stream.
     */
    private final AudioFormat outputFormat;

    /**
     * EncodingType used for the output.
     */
    private final EncodingType outputEncodingType;

    /**
     * Playback length of the audio data in a single
     * packet given in milliseconds.
     */
    private final long outputPacketLengthMs;

    /**
     * Number of samples in one packet.
     */
    private final int numSamplesPerPacket;

    /**
     * Payload size of a single packet given in bytes.
     */
    private final int payloadSizeBytes;

    /**
     * {@code RtpSession} used for streaming.
     */
    private final RtpSession rtpSession;

    /**
     * {@code ByteBuffer} containing the source's complete audio data in
     * the output encoding.
     */
    private final ByteBuffer outputDataBuffer;

    /**
     * URL of the source file.
     */
    private final URL sourceUrl;

    /**
     * Random number generator used to generate the starting
     * values for fields within RTP packets.
     */
    private final Random randomNumberGen = new Random();

    /**
     * SSRC that is used in RtpPackets.
     */
    private final int ssrc = randomNumberGen.nextInt();

    /**
     * Timestamp that is used in RtpPackets. Per RFC3550 the timestamp
     * is initialized to a random value.
     */
    private int timestamp = randomNumberGen.nextInt();

    /**
     * Sequence number that is used in RtpPackets.
     */
    private int sequenceNumber = 0;

    /**
     * Constructs a new AudioFileStreamer whose source data will be read from
     * the specified URL. The encoding on the output stream will be the
     * specified {@code outputEncoding} value. Each RTP packet will contain the
     * corresponding number of samples to represent the amount of time given in
     * {@code outputPacketLengthMs}.
     *
     * @param sourceUrl
     *            URL of the source file
     * @param outputEncoding
     *            encoding type to use for the output data
     * @param outputPacketLengthMs
     *            amount of data to put into each packet
     * @param rtpSession
     *            {@code RtpSession} to use for streaming the data
     * @throws UnsupportedAudioFileException
     *             if the source file is in an unsupported format or if the
     *             source file cannot be converted to the specifed encoding type
     * @throws IOException
     *             if there is problem reading the source file
     */
    public AudioFileStreamer(URL sourceUrl, EncodingType outputEncoding, long outputPacketLengthMs,
            RtpSession rtpSession) throws UnsupportedAudioFileException, IOException {
        this.sourceUrl = Preconditions.checkNotNull(sourceUrl, "Audio file source URL cannot be null.");
        this.outputEncodingType = Preconditions.checkNotNull(outputEncoding,
                "Output encoding type cannot be null.");
        this.rtpSession = Preconditions.checkNotNull(rtpSession, "RtpSession cannot be null.");
        this.outputPacketLengthMs = outputPacketLengthMs;

        // Read input source:
        AudioInputStream sourceStream = AudioSystem.getAudioInputStream(sourceUrl);
        AudioFormat conversionFormat = getConversionFormat(sourceStream.getFormat(), outputEncoding);
        LOGGER.debug("Input format: {}", audioFormatToString(sourceStream.getFormat()));
        LOGGER.debug("Conversion format: {}", audioFormatToString(conversionFormat));

        // Convert to output format:
        AudioInputStream outputStream = AudioSystem.getAudioInputStream(conversionFormat, sourceStream);
        outputFormat = outputStream.getFormat();
        LOGGER.debug("Output format: {}", audioFormatToString(outputFormat));

        // Buffer the output data:
        outputDataBuffer = ByteBuffer.wrap(IOUtils.toByteArray(outputStream));

        // Calculate packet size:
        numSamplesPerPacket = getNumberOfSamplesPerTimePeriod(outputFormat, outputPacketLengthMs,
                TimeUnit.MILLISECONDS);
        int sampleSizeBytes = outputStream.getFormat().getSampleSizeInBits() / 8;
        payloadSizeBytes = numSamplesPerPacket * sampleSizeBytes;
    }

    /**
     * Returns the AudioFormat of the output stream.
     *
     * @return AudioFormat of the output stream
     */
    public AudioFormat getOutputFormat() {
        return outputFormat;
    }

    /**
     * Returns the playback length in milliseconds of the data contained in each
     * output packet.
     *
     * @return playback length in milliseconds of the data contained in each
     *         output packet
     */
    public long getOutputPacketLengthMs() {
        return outputPacketLengthMs;
    }

    /**
     * Returns the RtpSession used for streaming data.
     *
     * @return RtpSession used for streaming data
     */
    public RtpSession getRtpSession() {
        return rtpSession;
    }

    /**
     * Returns the URL of the source audio file.
     *
     * @return URL of the source audio file
     */
    public URL getSourceUrl() {
        return sourceUrl;
    }

    @Override
    protected void runOneIteration() throws Exception {
        try {
            sendAudioData();
        } catch (RuntimeException e) {
            LOGGER.error("An exception occurred while sending audio data.", e);
        }
    }

    @Override
    protected Scheduler scheduler() {
        return Scheduler.newFixedRateSchedule(0, outputPacketLengthMs, TimeUnit.MILLISECONDS);
    }

    /**
     * Sends a single packet of audio data. It reads from the
     * {@link #outputDataBuffer} and will rewind that buffer when it reaches the
     * end so that it continuously streams the source file in a loop.
     */
    private void sendAudioData() {
        ByteBuffer packetDataBuffer = ByteBuffer.allocate(payloadSizeBytes);

        while (packetDataBuffer.hasRemaining()) {
            if (!outputDataBuffer.hasRemaining()) {
                outputDataBuffer.rewind();
            }

            packetDataBuffer.put(outputDataBuffer.get());
        }

        timestamp += numSamplesPerPacket;

        RtpPacket packet = new RtpPacket();
        packet.setPayloadType(outputEncodingType.getPayloadType());
        packet.setSSRC(ssrc);
        packet.setSequenceNumber(++sequenceNumber);
        packet.setTimestamp(timestamp);
        packet.setRtpPayloadData(packetDataBuffer.array());

        rtpSession.sendData(packet);
    }

    /**
     * Utility method to convert an {@link AudioFormat} object to a String.
     * {@code AudioFormat} does implement a toString method, but it's output
     * varies depending upon the contents. I find it more useful to always print
     * the value of all fields.
     *
     * @param format
     *            {@code AudioFormat} to convert to a String
     * @return {@code AudioFormat} object as a String
     */
    private static String audioFormatToString(AudioFormat format) {
        return new ToStringBuilder(format).append("encoding", format.getEncoding())
                .append("sampleRate", format.getSampleRate())
                .append("sampleSizeInBits", format.getSampleSizeInBits()).append("channels", format.getChannels())
                .append("frameSize", format.getFrameSize()).append("frameRate", format.getFrameRate())
                .append("isBigEndian", format.isBigEndian()).toString();
    }

    /**
     * Builds an AudioFormat object used for converting to the specified output
     * encoding type. The format will be used with
     * {@link AudioSystem#getAudioInputStream(AudioFormat, AudioInputStream)}.
     *
     * @param source
     *            {@code AudioFormat} of the source
     * @param outputEncoding
     *            encoding type of the output
     * @return audio format used for converting to the specified
     *         {@code outputEncoding} type
     */
    private static AudioFormat getConversionFormat(AudioFormat source, EncodingType outputEncoding) {
        Preconditions.checkNotNull(source, "Source AudioFormat cannot be null.");
        Preconditions.checkNotNull(outputEncoding, "Output EncodingType cannot be null.");

        switch (outputEncoding) {
        case ALAW:
            return toAlawFormat(source);
        case PCM16:
            return toPcm16Format(source, false);
        case ULAW:
            return toUlawFormat(source);
        default:
            throw new IllegalArgumentException("Unhandled EncodingType: " + outputEncoding.name());
        }
    }

    /**
     * Returns the number of samples that represent the specified time period.
     *
     * @param outputFormat
     *            format of the data, needed to obtain the sample rate
     * @param timePeriod
     *            period of time
     * @param timeUnit
     *            TimeUnit of the {@code timePeriod}
     * @return number of samples that represent the specified time period
     */
    @VisibleForTesting
    public static int getNumberOfSamplesPerTimePeriod(AudioFormat outputFormat, long timePeriod,
            TimeUnit timeUnit) {
        double timePeriodSec = timeUnit.toNanos(timePeriod) / 1E9;
        double numberOfSamples = timePeriodSec * outputFormat.getSampleRate();
        return (int) Math.ceil(numberOfSamples);
    }

    private static AudioFormat toAlawFormat(AudioFormat source) {
        Preconditions.checkNotNull(source, "Source AudioFormat cannot be null.");

        return new AudioFormat(AudioFormat.Encoding.ALAW, source.getSampleRate(), 8, // sample size in bits
                source.getChannels(), 1, // frame size in bytes
                source.getFrameRate(), source.isBigEndian());
    }

    private static AudioFormat toPcm16Format(AudioFormat source, boolean bigEndianOutput) {
        Preconditions.checkNotNull(source, "Source AudioFormat cannot be null.");

        return new AudioFormat(AudioFormat.Encoding.PCM_UNSIGNED, source.getSampleRate(), 16, // sample size in bits
                source.getChannels(), 2, // frame size in bytes
                source.getFrameRate(), bigEndianOutput);
    }

    private static AudioFormat toUlawFormat(AudioFormat source) {
        Preconditions.checkNotNull(source, "Source AudioFormat cannot be null.");

        return new AudioFormat(AudioFormat.Encoding.ULAW, source.getSampleRate(), 8, // sample size in bits
                source.getChannels(), 1, // frame size in bytes
                source.getFrameRate(), source.isBigEndian());
    }
}