org.openhab.io.multimedia.actions.Audio.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.io.multimedia.actions.Audio.java

Source

/**
 * Copyright (c) 2010-2015, openHAB.org and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.openhab.io.multimedia.actions;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URL;
import java.net.URLConnection;
import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.Port;
import javax.sound.sampled.UnsupportedAudioFileException;

import javazoom.jl.decoder.JavaLayerException;
import javazoom.jl.player.Player;

import org.apache.commons.collections.Closure;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.scriptengine.action.ActionDoc;
import org.openhab.core.scriptengine.action.ParamDoc;
import org.openhab.io.multimedia.internal.MultimediaActivator;
import org.openhab.io.multimedia.tts.TTSService;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Audio {

    private static final String SOUND_DIR = "sounds";
    private static final Logger logger = LoggerFactory.getLogger(Audio.class);

    private static final Pattern plsStreamPattern = Pattern.compile("^File[0-9]=(.+)$");

    private static Float macVolumeValue = null;

    private static Player streamPlayer = null;

    private static Socket shoutCastSocket = null;

    @ActionDoc(text = "plays a sound from the sounds folder")
    static public void playSound(
            @ParamDoc(name = "filename", text = "the filename with extension") String filename) {
        try {
            InputStream is = new FileInputStream(SOUND_DIR + File.separator + filename);
            if (filename.toLowerCase().endsWith(".mp3")) {
                Player player = new Player(is);
                playInThread(player);
            } else {
                AudioInputStream ais = AudioSystem.getAudioInputStream(is);
                Clip clip = AudioSystem.getClip();
                clip.open(ais);
                playInThread(clip);
            }
        } catch (FileNotFoundException e) {
            logger.error("Cannot play sound '{}': {}", new String[] { filename, e.getMessage() });
        } catch (JavaLayerException e) {
            logger.error("Cannot play sound '{}': {}", new String[] { filename, e.getMessage() });
        } catch (UnsupportedAudioFileException e) {
            logger.error("Format of sound file '{}' is not supported: {}",
                    new String[] { filename, e.getMessage() });
        } catch (IOException e) {
            logger.error("Cannot play sound '{}': {}", new String[] { filename, e.getMessage() });
        } catch (LineUnavailableException e) {
            logger.error("Cannot play sound '{}': {}", new String[] { filename, e.getMessage() });
        }
    }

    @ActionDoc(text = "plays an audio stream from an url")
    static public synchronized void playStream(
            @ParamDoc(name = "url", text = "the url of the audio stream") String url) {
        if (streamPlayer != null) {
            // if we are already playing a stream, stop it first
            streamPlayer.close();
            streamPlayer = null;
        }
        if (url == null) {
            // the call was only for stopping the currently playing stream
            return;
        }
        try {
            if (url.toLowerCase().endsWith(".m3u")) {
                InputStream is = new URL(url).openStream();
                String urls = IOUtils.toString(is);
                for (String line : urls.split("\n")) {
                    if (!line.isEmpty() && !line.startsWith("#")) {
                        url = line;
                        break;
                    }
                }
            } else if (url.toLowerCase().endsWith(".pls")) {
                InputStream is = new URL(url).openStream();
                String urls = IOUtils.toString(is);
                for (String line : urls.split("\n")) {
                    if (!line.isEmpty() && line.startsWith("File")) {
                        Matcher matcher = plsStreamPattern.matcher(line);
                        if (matcher.find()) {
                            url = matcher.group(1);
                            break;
                        }
                    }
                }
            }
            URL streamUrl = new URL(url);
            URLConnection connection = streamUrl.openConnection();
            InputStream is = null;
            if (connection.getContentType().equals("unknown/unknown")) {
                //Java does not parse non-standard headers used by SHOUTCast
                int port = streamUrl.getPort() > 0 ? streamUrl.getPort() : 80;
                // Manipulate User-Agent to receive a stream
                shoutCastSocket = new Socket(streamUrl.getHost(), port);

                OutputStream os = shoutCastSocket.getOutputStream();
                String user_agent = "WinampMPEG/5.09";
                String req = "GET / HTTP/1.0\r\nuser-agent: " + user_agent
                        + "\r\nIcy-MetaData: 1\r\nConnection: keep-alive\r\n\r\n";
                os.write(req.getBytes());
                is = shoutCastSocket.getInputStream();
            } else {
                is = streamUrl.openStream();
            }
            if (is != null) {
                Player player = new Player(is);
                streamPlayer = player;
                playInThread(player);
            }
        } catch (JavaLayerException e) {
            logger.error("Cannot play stream '{}': JavaLayerException - {}", url, e.getMessage());
        } catch (MalformedURLException e) {
            logger.error("Cannot play stream '{}': MalformedURLException - {}", url, e.getMessage());
        } catch (IOException e) {
            logger.error("Cannot play stream '{}': {}", url, e);
        }
    }

    /**
     * Says the given text..
     * 
     * <p>This method checks for registered TTS services. If there is a service
     * available for the current OS, this will be chosen. Otherwise, it
     * will pick a (the first) TTS service that is platform-independent.</p>
     * 
     * @param text the text to speak
     */
    @ActionDoc(text = "says a given text through the default TTS service")
    static public void say(@ParamDoc(name = "text") Object text) {
        say(text.toString(), null);
    }

    /**
     * Text-to-speech with a given voice.
     * 
     * <p>This method checks for registered TTS services. If there is a service
     * available for the current OS, this will be chosen. Otherwise, it
     * will pick a (the first) TTS service that is platform-independent.</p>
     * 
     * @param text the text to speak
     * @param voice the name of the voice to use or null, if the default voice should be used
     */
    @ActionDoc(text = "says a given text through the default TTS service with a given voice")
    static public void say(@ParamDoc(name = "text") Object text, @ParamDoc(name = "voice") String voice) {
        say(text, voice, null);
    }

    /**
     * Text-to-speech with a given voice.
     * 
     * <p>This method checks for registered TTS services. If there is a service
     * available for the current OS, this will be chosen. Otherwise, it
     * will pick a (the first) TTS service that is platform-independent.</p>
     * 
     * @param text the text to speak
     * @param voice the name of the voice to use or null, if the default voice should be used
     * @param device the name of audio device to be used to play the audio or null, if the default output device should be used
     */
    @ActionDoc(text = "says a given text through the default TTS service with a given voice")
    static public void say(@ParamDoc(name = "text") Object text, @ParamDoc(name = "voice") String voice,
            @ParamDoc(name = "device") String device) {
        if (StringUtils.isNotBlank(text.toString())) {
            TTSService ttsService = getTTSService(MultimediaActivator.getContext(), System.getProperty("osgi.os"));
            if (ttsService == null) {
                ttsService = getTTSService(MultimediaActivator.getContext(), "any");
            }
            if (ttsService != null) {
                ttsService.say(text.toString(), voice, device);
            } else {
                logger.error("No TTS service available - tried to say: {}", text);
            }
        }
    }

    @ActionDoc(text = "sets the master volume of the host")
    static public void setMasterVolume(
            @ParamDoc(name = "volume", text = "volume in the range [0,1]") final float volume) throws IOException {
        if (volume < 0 || volume > 1) {
            throw new IllegalArgumentException("Volume value must be in the range [0,1]!");
        }
        if (isMacOSX()) {
            macVolumeValue = volume;
            setMasterVolumeMac(volume * 100f);
        } else {
            setMasterVolumeJavaSound(volume);
        }
    }

    @ActionDoc(text = "sets the master volume of the host")
    static public void setMasterVolume(@ParamDoc(name = "percent") final PercentType percent) throws IOException {
        setMasterVolume(percent.toBigDecimal().floatValue() / 100f);
    }

    private static void setMasterVolumeMac(float volume) throws IOException {
        Runtime.getRuntime().exec(new String[] { "osascript", "-e", "set volume output volume " + volume });
    }

    private static void setMasterVolumeJavaSound(final float volume) {
        runVolumeCommand(new Closure() {
            public void execute(Object input) {
                FloatControl volumeControl = (FloatControl) input;
                volumeControl.setValue(volume);
            }
        });
    }

    @ActionDoc(text = "increases the master volume of the host")
    static public void increaseMasterVolume(@ParamDoc(name = "percent") final float percent) throws IOException {
        if (percent <= 0 || percent > 100) {
            throw new IllegalArgumentException("Percent must be in the range (0,100]!");
        }
        Float volume = getMasterVolume();
        if (volume == 0) {
            // as increasing 0 by x percent will still be 0, we have to set some initial positive value
            volume = 0.001f;
        }
        float newVolume = volume * (1f + percent / 100f);
        if (isMacOSX() && newVolume - volume < .01) {
            // the getMasterVolume() only returns integers, so we have to make sure that we
            // increase the volume level at least by 1%.
            newVolume += .01;
        }
        if (newVolume > 1) {
            newVolume = 1;
        }
        setMasterVolume(newVolume);
    }

    @ActionDoc(text = "decreases the master volume of the host")
    static public void decreaseMasterVolume(@ParamDoc(name = "percent") final float percent) throws IOException {
        if (percent <= 0 || percent > 100) {
            throw new IllegalArgumentException("Percent must be in the range (0,100]!");
        }
        float volume = getMasterVolume();
        float newVolume = volume * (1f - percent / 100f);
        if (isMacOSX() && newVolume > 0 && volume - newVolume < .01) {
            // the getMasterVolume() only returns integers, so we have to make sure that we
            // decrease the volume level at least by 1%.
            newVolume -= .01;
        }
        if (newVolume < 0) {
            newVolume = 0;
        }
        setMasterVolume(newVolume);
    }

    @ActionDoc(text = "gets the master volume of the host", returns = "volume as a float in the range [0,1]")
    static public float getMasterVolume() throws IOException {
        if (isMacOSX()) {
            return getMasterVolumeMac();
        } else {
            return getMasterVolumeJavaSound();
        }
    }

    private static synchronized float getMasterVolumeMac() throws IOException {
        // we use a cache of the value as the script execution is pretty slow
        if (macVolumeValue == null) {
            Process p = Runtime.getRuntime()
                    .exec(new String[] { "osascript", "-e", "output volume of (get volume settings)" });
            String value = IOUtils.toString(p.getInputStream()).trim();
            macVolumeValue = Float.valueOf(value) / 100f;
        }
        return macVolumeValue;
    }

    private static float getMasterVolumeJavaSound() throws IOException {
        final Float[] volumes = new Float[1];
        runVolumeCommand(new Closure() {
            public void execute(Object input) {
                FloatControl volumeControl = (FloatControl) input;
                volumes[0] = volumeControl.getValue();
            }
        });
        if (volumes[0] != null) {
            return volumes[0];
        } else {
            throw new IOException("Cannot determine master volume level");
        }
    }

    private static void runVolumeCommand(Closure closure) {
        Mixer.Info[] infos = AudioSystem.getMixerInfo();
        for (Mixer.Info info : infos) {
            Mixer mixer = AudioSystem.getMixer(info);
            if (mixer.isLineSupported(Port.Info.SPEAKER)) {
                Port port;
                try {
                    port = (Port) mixer.getLine(Port.Info.SPEAKER);
                    port.open();
                    if (port.isControlSupported(FloatControl.Type.VOLUME)) {
                        FloatControl volume = (FloatControl) port.getControl(FloatControl.Type.VOLUME);
                        closure.execute(volume);
                    }
                    port.close();
                } catch (LineUnavailableException e) {
                    logger.error("Cannot access master volume control", e);
                }
            }
        }
    }

    private static void playInThread(final Clip clip) {
        // run in new thread
        new Thread() {
            public void run() {
                try {
                    clip.start();
                    while (clip.isActive()) {
                        sleep(1000L);
                    }
                    clip.close();
                } catch (Exception e) {
                    throw new RuntimeException(e.getMessage());
                }
            }
        }.start();
    }

    static private void playInThread(final Player player) {
        // run in new thread
        new Thread() {
            public void run() {
                try {
                    player.play();
                } catch (Exception e) {
                    throw new RuntimeException(e.getMessage());
                } finally {
                    if (shoutCastSocket != null) {
                        try {
                            shoutCastSocket.close();
                        } catch (IOException e) {
                        }
                    }
                }
            }
        }.start();
    }

    /**
     * Queries the OSGi service registry for a service that provides a TTS implementation
     * for a given platform.
     * 
     * @param context the bundle context to access the OSGi service registry
     * @param os a valid osgi.os string value or "any" if service should be platform-independent
     * @return a service instance or null, if none could be found
     */
    static private TTSService getTTSService(BundleContext context, String os) {
        if (context != null) {
            String filter = os != null ? "(os=" + os + ")" : null;
            try {
                Collection<ServiceReference<TTSService>> refs = context.getServiceReferences(TTSService.class,
                        filter);
                if (refs != null && refs.size() > 0) {
                    return (TTSService) context.getService(refs.iterator().next());
                } else {
                    return null;
                }
            } catch (InvalidSyntaxException e) {
                // this should never happen
            }
        }
        return null;
    }

    private static boolean isMacOSX() {
        return System.getProperty("osgi.os").equals("macosx");
    }

}