/*
This file is part of leafdigital leafChat.
leafChat 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.
leafChat 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 leafChat. If not, see <http://www.gnu.org/licenses/>.
Copyright 2012 Samuel Marshall.
*/
package com.leafdigital.audio;
import java.io.*;
import java.util.*;
import javax.sound.sampled.*;
import org.tritonus.sampled.convert.jorbis.JorbisFormatConversionProvider;
import org.tritonus.sampled.file.jorbis.JorbisAudioFileReader;
import util.*;
import util.xml.XML;
import com.leafdigital.audio.api.*;
import com.leafdigital.irc.api.*;
import com.leafdigital.prefsui.api.PreferencesUI;
import leafchat.core.api.*;
/**
* Plugin for audio playback.
*/
public class AudioPlugin implements Plugin, Audio
{
private final static String SOUNDS_FOLDER = "sounds";
private int threads = 0;
private Object threadSynch = new Object();
private boolean close = false;
@Override
public synchronized void init(
PluginContext context, PluginLoadReporter reporter) throws GeneralException
{
// Become a singleton
context.registerSingleton(Audio.class, this);
// Listen for /sound command
context.requestMessages(UserCommandMsg.class, this, Msg.PRIORITY_NORMAL);
context.requestMessages(UserCommandListMsg.class, this, Msg.PRIORITY_NORMAL);
// Register prefs page
PreferencesUI preferencesUI =
context.getSingle(PreferencesUI.class);
preferencesUI.registerPage(this,(new SoundsPage(context)).getPage());
}
@Override
public String toString()
{
// Used to display in system log etc.
return "Audio plugin";
}
@Override
public void close() throws GeneralException
{
synchronized(threadSynch)
{
close = true;
while(threads > 0)
{
try
{
threadSynch.wait();
}
catch(InterruptedException e)
{
throw new GeneralException("Close interrupted");
}
}
}
}
/**
* Message: User types command.
* @param msg Message
* @throws GeneralException Any error
*/
public void msg(UserCommandMsg msg) throws GeneralException
{
if(msg.isHandled())
{
return;
}
String command = msg.getCommand();
if("sound".equals(command))
{
sound(msg);
}
}
/**
* Message: Listing available commands.
* @param msg Message
*/
public void msg(UserCommandListMsg msg)
{
msg.addCommand(false, "sound", UserCommandListMsg.FREQ_OBSCURE,
"/sound <name>", "<key>Scripting:</key> Plays the named system sound (see Options/Sounds for list)");
}
private void sound(UserCommandMsg msg) throws GeneralException
{
msg.markHandled();
// Get sound name
String sound = msg.getParams().trim();
if(sound.length() == 0)
{
msg.getMessageDisplay().showError(
"Syntax: /sound <sound name or full file path>");
return;
}
File file;
// Check if they're referring to a full path
if(sound.contains("/") || sound.contains("\\"))
{
if(!sound.endsWith(".ogg"))
{
msg.getMessageDisplay().showError(
"Sound file <key>" + XML.esc(sound) + "</key> must have .ogg "
+ "extension and be in Ogg Vorbis format");
return;
}
file = new File(sound);
if(!file.exists())
{
msg.getMessageDisplay().showError(
"Sound file <key>" + XML.esc(sound) + "</key> not found");
return;
}
}
else
{
// Not a full path, let's see if we can find it in the sounds folder
file = findFile(sound);
if(file == null)
{
msg.getMessageDisplay().showError(
"Sound <key>" + XML.esc(sound) + "</key> not found; "
+ "use the filename within leafChat's sound folder, without a "
+ "path and without the .ogg extension");
return;
}
}
try
{
play(file);
}
catch(AudioSetupException e)
{
msg.getMessageDisplay().showError(
"Sounds cannot be played on this system at the moment (see Options/Sounds).");
}
}
/**
* Gets file in either system or user folder.
* @param name Name of file not including extension
* @return File or null if not found
*/
private File findFile(String name)
{
File possible = new File(getSoundsFolder(false), name + ".ogg");
if(possible.exists())
{
return possible;
}
possible = new File(getSoundsFolder(true), name + ".ogg");
if(possible.exists())
{
return possible;
}
return null;
}
/**
* @param system If true, returns system sounds folder, otherwise user folder
* @return Sounds folder
*/
public File getSoundsFolder(boolean system)
{
if(system)
{
return new File(SOUNDS_FOLDER);
}
else
{
File userSounds = new File(PlatformUtils.getUserFolder(), SOUNDS_FOLDER);
if(!userSounds.exists())
{
userSounds.mkdirs();
}
return userSounds;
}
}
@Override
public void play(String oggName) throws AudioSetupException, GeneralException
{
File file = findFile(oggName);
if(file == null)
{
throw new GeneralException("Unknown audio file " + oggName);
}
play(file);
}
@Override
public void play(File ogg) throws AudioSetupException, GeneralException
{
try
{
play(new FileInputStream(ogg));
}
catch(IOException e)
{
throw new GeneralException("Failed to play audio file " + ogg, e);
}
}
@Override
public void play(InputStream oggStream) throws AudioSetupException, GeneralException
{
try
{
// NOTE: References to Jorbis libraries are hard-coded because the
// service interface for some reason does not work in the leafChat
// classloader architecture, so I can't just rely on the SPI existing.
// Get input as audio stream and get format details
JorbisAudioFileReader reader = new JorbisAudioFileReader();
AudioInputStream audioStream = reader.getAudioInputStream(oggStream);
AudioFormat format = audioStream.getFormat();
// Convert to signed 16-bit little-endian PCM at the same sample rate
AudioFormat targetFormat = new AudioFormat(
AudioFormat.Encoding.PCM_SIGNED,
format.getSampleRate(), 16, format.getChannels(),
format.getChannels() * 2, format.getSampleRate(), false);
// Get converting stream
JorbisFormatConversionProvider provider = new JorbisFormatConversionProvider();
AudioInputStream pcmStream = provider.getAudioInputStream(targetFormat, audioStream);
// Get source data line, open and start it
DataLine.Info lineInfo = new DataLine.Info(
SourceDataLine.class, targetFormat, 8192);
SourceDataLine line;
// The following two exceptions occur if the user's system doesn't have
// sound playback capability for some reason
try
{
line = (SourceDataLine)AudioSystem.getLine(lineInfo);
line.open(targetFormat, 8192);
}
catch(IllegalArgumentException e)
{
throw new AudioSetupException(e);
}
catch(LineUnavailableException e)
{
throw new AudioSetupException(e);
}
line.start();
// Now start a thread for playback
synchronized(threadSynch)
{
new AudioPlayerThread(line, pcmStream);
threads++;
}
}
catch(IOException e)
{
throw new GeneralException("Error reading audio stream", e);
}
catch(UnsupportedAudioFileException e)
{
throw new GeneralException("Audio format not supported", e);
}
}
/**
* Thread that handles audio playback.
*/
private class AudioPlayerThread extends Thread
{
private SourceDataLine line;
private AudioInputStream stream;
private AudioPlayerThread(SourceDataLine line, AudioInputStream stream)
{
super("Audio player thread");
this.line = line;
this.stream = stream;
start();
}
@Override
public void run()
{
try
{
byte[] buffer = new byte[1024];
while(true)
{
int read = stream.read(buffer);
if(read == -1)
{
break;
}
synchronized(threadSynch)
{
if(close)
{
return;
}
}
line.write(buffer, 0, read);
synchronized(threadSynch)
{
if(close)
{
return;
}
}
}
line.drain();
}
catch(Throwable e)
{
ErrorMsg.report("Error playing audio stream", e);
}
finally
{
synchronized(threadSynch)
{
threads--;
threadSynch.notifyAll();
}
line.close();
}
}
}
@Override
public String[] getSounds() throws GeneralException
{
LinkedList<String> sounds = new LinkedList<String>();
fillSoundList(sounds, true);
fillSoundList(sounds, false);
Collections.sort(sounds);
return sounds.toArray(new String[sounds.size()]);
}
/**
* @param sounds List to fill with sounds
* @param system True to use system folder
*/
private void fillSoundList(LinkedList<String> sounds, boolean system)
{
File[] files = IOUtils.listFiles(getSoundsFolder(system));
for(int i=0; i<files.length; i++)
{
String name = files[i].getName();
if(files[i].isFile() && name.endsWith(".ogg"))
{
sounds.add(name.substring(0, name.length()-4));
}
}
}
@Override
public boolean soundExists(String name)
{
return findFile(name) != null;
}
}