/* * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. * * 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 2.1 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; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA */ package jace.core; import jace.config.ConfigurableField; import jace.config.DynamicSelection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.sound.sampled.AudioFormat; 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.Mixer.Info; import javax.sound.sampled.SourceDataLine; /** * Manages sound resources used by various audio devices (such as speaker and * mockingboard cards.) The plumbing is managed in this class so that the * consumers do not have to do a lot of work to manage mixer lines or deal with * how to reuse active lines if needed. It is possible that this class might be * used to manage volume in the future, but that remains to be seen. * * @author Brendan Robert (BLuRry) brendan.robert@gmail.com */ public class SoundMixer extends Device { private final Set availableLines = Collections.synchronizedSet(new HashSet()); private final Map activeLines = Collections.synchronizedMap(new HashMap()); /** * Bits per sample */ @ConfigurableField(name = "Bits per sample", shortName = "bits") public static int BITS = 16; /** * Sample playback rate */ @ConfigurableField(name = "Playback Rate", shortName = "freq") public static int RATE = 48000; /** * Sound format used for playback */ private AudioFormat af; /** * Is sound line available for playback at all? */ public boolean lineAvailable; @ConfigurableField(name = "Audio device", description = "Audio output device") public static DynamicSelection preferredMixer = new DynamicSelection(null) { @Override public boolean allowNull() { return false; } @Override public LinkedHashMap getSelections() { Info[] mixerInfo = AudioSystem.getMixerInfo(); LinkedHashMap out = new LinkedHashMap<>(); for (Info i : mixerInfo) { out.put(i.getName(), i.getName()); } return out; } }; private Mixer theMixer; @Override public String getDeviceName() { return "Sound Output"; } @Override public String getShortName() { return "mixer"; } @Override public synchronized void reconfigure() { detach(); try { initMixer(); if (lineAvailable) { initAudio(); System.out.println("Started sound"); } else { System.out.println("Sound not stared: Line not available"); } } catch (LineUnavailableException ex) { System.out.println("Unable to start sound"); Logger.getLogger(SoundMixer.class.getName()).log(Level.SEVERE, null, ex); } attach(); } /** * Obtain sound playback line if available * * @throws javax.sound.sampled.LineUnavailableException If there is no line * available */ private void initAudio() throws LineUnavailableException { af = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, RATE, BITS, 2, BITS / 4, RATE, true); // af = new AudioFormat(RATE, BITS, 2, true, true); DataLine.Info dli = new DataLine.Info(SourceDataLine.class, af); lineAvailable = AudioSystem.isLineSupported(dli); } public synchronized SourceDataLine getLine(Object requester) throws LineUnavailableException { if (activeLines.containsKey(requester)) { return activeLines.get(requester); } SourceDataLine sdl = null; if (availableLines.isEmpty()) { sdl = getNewLine(); } else { sdl = availableLines.iterator().next(); availableLines.remove(sdl); } activeLines.put(requester, sdl); sdl.start(); return sdl; } public void returnLine(Object requester) { if (activeLines.containsKey(requester)) { SourceDataLine sdl = activeLines.remove(requester); // Calling drain on pulse driver can cause it to freeze up (?) // sdl.drain(); sdl.stop(); sdl.flush(); availableLines.add(sdl); } } private SourceDataLine getNewLine() throws LineUnavailableException { SourceDataLine l = null; // Line.Info[] info = theMixer.getSourceLineInfo(); DataLine.Info dli = new DataLine.Info(SourceDataLine.class, af); System.out.println("Maximum output lines: " + theMixer.getMaxLines(dli)); System.out.println("Allocated output lines: " + theMixer.getSourceLines().length); System.out.println("Getting source line from " + theMixer.getMixerInfo().toString() + ": " + af.toString()); try { l = (SourceDataLine) theMixer.getLine(dli); } catch (IllegalArgumentException e) { lineAvailable = false; throw new LineUnavailableException(e.getMessage()); } catch (LineUnavailableException e) { lineAvailable = false; throw e; } if (!(l instanceof SourceDataLine)) { lineAvailable = false; throw new LineUnavailableException("Line is not an output line!"); } final SourceDataLine sdl = (SourceDataLine) l; // sdl.open(af); // if (false) { // return sdl; // } sdl.open(); sdl.start(); // new Thread(new Runnable() { // @Override // public void run() { // System.out.println("Going into an infinite loop!!!"); // try { // while (true) { // sdl.write(new byte[]{randomByte(),randomByte(),randomByte(),randomByte()}, 0, 4); // } // } catch (Throwable t) { // t.printStackTrace(); // } // System.out.println("Thread dying..."); // } // }).start(); return sdl; } public byte randomByte() { return (byte) (Math.random() * 256); } @Override public void tick() { } @Override public void attach() { // if (Motherboard.enableSpeaker) // Motherboard.speaker.attach(); } @Override public void detach() { availableLines.stream().forEach((line) -> { line.close(); }); Set requesters = new HashSet(activeLines.keySet()); requesters.stream().map((o) -> { if (o instanceof Device) { ((Device) o).detach(); } return o; }).filter((o) -> (o instanceof Card)).forEach((o) -> { ((Card) o).reconfigure(); }); if (theMixer != null) { for (Line l : theMixer.getSourceLines()) { l.close(); } } availableLines.clear(); activeLines.clear(); } private void initMixer() { Info selected = null; Info[] mixerInfo = AudioSystem.getMixerInfo(); if (mixerInfo == null || mixerInfo.length == 0) { theMixer = null; lineAvailable = false; System.out.println("No sound mixer is available!"); return; } String mixer = preferredMixer.getValue(); selected = mixerInfo[0]; for (Info i : mixerInfo) { if (i.getName().equalsIgnoreCase(mixer)) { selected = i; break; } } theMixer = AudioSystem.getMixer(selected); for (Line l : theMixer.getSourceLines()) { l.close(); } lineAvailable = true; } }