com.offbynull.voip.audio.gateways.io.AudioRunnable.java Source code

Java tutorial

Introduction

Here is the source code for com.offbynull.voip.audio.gateways.io.AudioRunnable.java

Source

/*
 * Copyright (c) 2015, Kasra Faghihi, All rights reserved.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3.0 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.
 */
package com.offbynull.voip.audio.gateways.io;

import com.offbynull.peernetic.core.shuttle.Address;
import com.offbynull.peernetic.core.shuttle.Message;
import com.offbynull.peernetic.core.shuttle.Shuttle;
import com.offbynull.peernetic.core.shuttles.simple.Bus;
import com.offbynull.voip.audio.gateways.io.internalmessages.CloseDevicesRequest;
import com.offbynull.voip.audio.gateways.io.internalmessages.ErrorResponse;
import com.offbynull.voip.audio.gateways.io.internalmessages.InputPcmBlock;
import com.offbynull.voip.audio.gateways.io.internalmessages.LoadDevicesRequest;
import com.offbynull.voip.audio.gateways.io.internalmessages.LoadDevicesResponse;
import com.offbynull.voip.audio.gateways.io.internalmessages.LoadDevicesResponse.InputDevice;
import com.offbynull.voip.audio.gateways.io.internalmessages.LoadDevicesResponse.OutputDevice;
import com.offbynull.voip.audio.gateways.io.internalmessages.OpenDevicesRequest;
import com.offbynull.voip.audio.gateways.io.internalmessages.OutputPcmBlock;
import com.offbynull.voip.audio.gateways.io.internalmessages.SuccessResponse;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import javax.sound.sampled.AudioFormat;
import static javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.Line;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.TargetDataLine;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

final class AudioRunnable implements Runnable {

    private static final Logger LOG = LoggerFactory.getLogger(AudioRunnable.class);

    private static final int SAMPLE_RATE = 16000; // 16k samples per second
    private static final int SAMPLE_SIZE = 8; // 8-bits per sample
    private static final int NUM_CHANNELS = 1; // mono

    // frame = set of samples for all channels at a given point in time
    private static final int FRAME_SIZE = NUM_CHANNELS * SAMPLE_SIZE / 8; // 1 byte per frame (because we're 1 chan (mono) & 8bits per chan)
    private static final int FRAME_RATE = FRAME_SIZE * SAMPLE_RATE;

    private static final int INPUT_BUFFER_SIZE = FRAME_RATE / 10; // read up to 100ms of data at a time
    private static final int OUTPUT_BUFFER_SIZE = FRAME_RATE / 5; // play up to 200ms of back data, otherwise will play latest 200ms

    private static final AudioFormat EXPECTED_FORMAT = new AudioFormat(PCM_SIGNED, SAMPLE_RATE, SAMPLE_SIZE,
            NUM_CHANNELS, FRAME_SIZE, FRAME_RATE, true);

    private final Bus bus;
    private final Map<String, Shuttle> outgoingShuttles;

    private int nextDeviceId;
    private Map<Integer, LineEntry> inputDevices;
    private Map<Integer, LineEntry> outputDevices;

    private Address openedFromAddress;
    private Address openedToAddress;

    private TargetDataLine openInputDevice;
    private Thread inputReadThread;

    private SourceDataLine openOutputDevice;
    private LinkedBlockingQueue<OutputData> outputQueue;
    private Thread outputWriteThread;

    public AudioRunnable(Bus bus) {
        Validate.notNull(bus);
        this.bus = bus;
        outgoingShuttles = new HashMap<>();
    }

    @Override
    public void run() {
        try {
            while (true) {
                // Poll for new messages
                List<Object> incomingObjects = bus.pull();
                Validate.notNull(incomingObjects);
                Validate.noNullElements(incomingObjects);

                for (Object incomingObj : incomingObjects) {
                    if (incomingObj instanceof Message) {
                        Message msg = (Message) incomingObj;

                        Address src = msg.getSourceAddress();
                        Address dst = msg.getDestinationAddress();
                        Object payload = msg.getMessage();

                        LOG.debug("Processing incoming message from {} to {}: {}", src, dst, payload);

                        if (payload instanceof LoadDevicesRequest) {
                            Object response = loadDevices();
                            sendMessage(src, dst, response);
                        } else if (payload instanceof OpenDevicesRequest) {
                            Object response = openDevices(src, dst, (OpenDevicesRequest) payload);
                            sendMessage(src, dst, response);
                        } else if (payload instanceof CloseDevicesRequest) {
                            Object response = closeDevices();
                            sendMessage(src, dst, response);
                        } else if (payload instanceof OutputPcmBlock) {
                            if (openedToAddress == null || openedFromAddress == null) {
                                LOG.warn("Output PCM block received but devices closed");
                                continue;
                            }

                            OutputPcmBlock outputPCMBlock = (OutputPcmBlock) payload;
                            byte[] data = outputPCMBlock.getData();
                            OutputData outputData = new OutputData(data);

                            outputQueue.put(outputData);
                        } else {
                            LOG.error("Unrecognized message: {}", payload);
                        }
                    } else if (incomingObj instanceof InputData) { // message from input read thread (microphone reader thread)
                        if (openedToAddress == null || openedFromAddress == null) {
                            LOG.warn("Input PCM block received but devices not opened");
                            continue;
                        }

                        InputData inputData = (InputData) incomingObj;
                        byte[] data = inputData.getData();
                        InputPcmBlock inputPCMBlock = new InputPcmBlock(data);

                        sendMessage(openedFromAddress, openedToAddress, inputPCMBlock);
                    } else if (incomingObj instanceof AddShuttle) {
                        AddShuttle addShuttle = (AddShuttle) incomingObj;
                        Shuttle shuttle = addShuttle.getShuttle();
                        Shuttle existingShuttle = outgoingShuttles.putIfAbsent(shuttle.getPrefix(), shuttle);
                        Validate.validState(existingShuttle == null);
                    } else if (incomingObj instanceof RemoveShuttle) {
                        RemoveShuttle removeShuttle = (RemoveShuttle) incomingObj;
                        String prefix = removeShuttle.getPrefix();
                        Shuttle oldShuttle = outgoingShuttles.remove(prefix);
                        Validate.validState(oldShuttle != null);
                    } else {
                        throw new IllegalStateException("Unexpected message type: " + incomingObj);
                    }
                }
            }
        } catch (InterruptedException ie) {
            LOG.debug("Audio gateway interrupted");
            Thread.interrupted();
        } catch (Exception e) {
            LOG.error("Internal error encountered", e);
        } finally {
            closeDevices();
            bus.close();
        }
    }

    private Object loadDevices() {
        Map<Integer, LineEntry> newOutputDevices = new HashMap<>();
        Map<Integer, LineEntry> newInputDevices = new HashMap<>();
        List<OutputDevice> respOutputDevices = new LinkedList<>();
        List<InputDevice> respInputDevices = new LinkedList<>();

        Mixer.Info[] mixerInfos;
        try {
            mixerInfos = AudioSystem.getMixerInfo();
        } catch (Exception e) {
            LOG.error("Unable to get mixers", e);
            return new ErrorResponse("Unable to get mixers: " + e);
        }

        for (Mixer.Info info : mixerInfos) {
            Mixer mixer;
            try {
                mixer = AudioSystem.getMixer(info);
            } catch (Exception e) {
                LOG.error("Unable to get mixer", e);
                continue;
            }

            Line.Info[] lineInfos;
            Map<Integer, LineEntry> newEntries;

            // out devices
            try {
                lineInfos = mixer.getSourceLineInfo(new Line.Info(SourceDataLine.class));
                newEntries = generateLineEntriesFromJavaSound(mixer, lineInfos);
                newOutputDevices.putAll(newEntries);
                newEntries.entrySet().forEach(x -> {
                    Mixer.Info mi = x.getValue().getMixer().getMixerInfo();
                    String name = "OUT:" + mi.getName() + "/" + mi.getVersion() + "/" + mi.getVendor() + "/"
                            + mi.getDescription();
                    OutputDevice outputDevice = new OutputDevice(x.getKey(), name);
                    respOutputDevices.add(outputDevice);
                });
            } catch (LineUnavailableException lue) {
                LOG.error("Unable to get line from mixer", lue);
            }

            // in devices
            try {
                lineInfos = mixer.getTargetLineInfo(new Line.Info(TargetDataLine.class));
                newEntries = generateLineEntriesFromJavaSound(mixer, lineInfos);
                newInputDevices.putAll(newEntries);
                newEntries.entrySet().forEach(x -> {
                    Mixer.Info mi = x.getValue().getMixer().getMixerInfo();
                    String name = "IN:" + mi.getName() + "/" + mi.getVersion() + "/" + mi.getVendor() + "/"
                            + mi.getDescription();
                    InputDevice inputDevice = new InputDevice(x.getKey(), name);
                    respInputDevices.add(inputDevice);
                });
            } catch (LineUnavailableException lue) {
                LOG.error("Unable to get line from mixer", lue);
            }
        }

        inputDevices = newInputDevices;
        outputDevices = newOutputDevices;
        return new LoadDevicesResponse(respOutputDevices, respInputDevices);
    }

    private Object openDevices(Address fromAddress, Address toAddress, OpenDevicesRequest request) {
        if (outputDevices == null || inputDevices == null) {
            return new ErrorResponse("Devices not loaded");
        }

        if (openOutputDevice != null || openInputDevice != null) {
            return new ErrorResponse("Devices already open");
        }

        int outputId = request.getOutputId();
        int inputId = request.getInputId();

        LineEntry outputLineEntry = outputDevices.get(outputId);
        if (outputLineEntry == null) {
            LOG.error("Output device not available: {}", outputId);
            return new ErrorResponse("Output device " + outputId + " not available");
        }

        LineEntry inputLineEntry = inputDevices.get(inputId);
        if (inputLineEntry == null) {
            LOG.error("Input device not available: {}", inputId);
            return new ErrorResponse("Input device " + inputId + " not available");
        }

        // open input device
        try {
            openInputDevice = (TargetDataLine) AudioSystem.getLine(inputLineEntry.getLineInfo());
            openInputDevice.open(EXPECTED_FORMAT);
            openInputDevice.start();
        } catch (Exception e) {
            openInputDevice = null;
            LOG.error("Unable to open input device", e);
            return new ErrorResponse("Unable to open input device");
        }

        // open output device
        try {
            openOutputDevice = (SourceDataLine) AudioSystem.getLine(outputLineEntry.getLineInfo());
            openOutputDevice.open(EXPECTED_FORMAT);
            openOutputDevice.start();
        } catch (Exception e) {
            try {
                openInputDevice.close();
            } catch (Exception innerE) {
                LOG.error("Unable to close input device", innerE);
            }

            openInputDevice = null;
            openOutputDevice = null;
            LOG.error("Unable to open output device", e);
            return new ErrorResponse("Unable to open output device");
        }

        // start input read thread
        InputReadRunnable inputReadRunnable = new InputReadRunnable(openInputDevice, bus, INPUT_BUFFER_SIZE);
        inputReadThread = new Thread(inputReadRunnable);
        inputReadThread.setDaemon(true);
        inputReadThread.setName(getClass().getSimpleName() + "-" + inputReadRunnable.getClass().getSimpleName());
        inputReadThread.start();

        // start input read thread
        outputQueue = new LinkedBlockingQueue<>();
        OutputWriteRunnable outputWriteRunnable = new OutputWriteRunnable(openOutputDevice, outputQueue,
                OUTPUT_BUFFER_SIZE);
        outputWriteThread = new Thread(outputWriteRunnable);
        outputWriteThread.setDaemon(true);
        outputWriteThread
                .setName(getClass().getSimpleName() + "-" + outputWriteRunnable.getClass().getSimpleName());
        outputWriteThread.start();

        // set address to shuttle input PCM blocks to
        openedFromAddress = fromAddress;
        openedToAddress = toAddress;

        return new SuccessResponse();
    }

    private Object closeDevices() {
        if (openOutputDevice != null) {
            try {
                openOutputDevice.close();
            } catch (Exception innerE) {
                LOG.error("Unable to close output device", innerE);
            }

            openOutputDevice = null;
            outputWriteThread.interrupt();
            outputWriteThread = null;
            outputQueue = null;
        }

        if (openInputDevice != null) {
            try {
                openInputDevice.close();
            } catch (Exception innerE) {
                LOG.error("Unable to close input device", innerE);
            }

            openInputDevice = null;
            inputReadThread.interrupt();
        }

        openedToAddress = null;

        return new SuccessResponse();
    }

    private Map<Integer, LineEntry> generateLineEntriesFromJavaSound(Mixer m, Line.Info[] lineInfos)
            throws LineUnavailableException {
        Map<Integer, LineEntry> entries = new HashMap<>();
        for (Line.Info lineInfo : lineInfos) {
            if (lineInfo instanceof DataLine.Info) {
                DataLine.Info dataLineInfo = (DataLine.Info) lineInfo;
                AudioFormat[] forms = dataLineInfo.getFormats();
                for (AudioFormat form : forms) {
                    if (EXPECTED_FORMAT.matches(form)) {
                        entries.put(nextDeviceId, new LineEntry(m, lineInfo));
                        nextDeviceId++;
                    }
                }
            }
        }
        return entries;
    }

    private void sendMessage(Address to, Address from, Object response) {
        String dstPrefix = to.getElement(0);
        Shuttle shuttle = outgoingShuttles.get(dstPrefix);

        if (shuttle != null) {
            shuttle.send(Collections.singleton(new Message(from, to, response)));
        } else {
            LOG.warn("Unable to find shuttle for outgoing response: {}", response);
        }
    }

}