package audio; /******************************************************************************* * SDR Trunk * Copyright (C) 2014,2015 Dennis Sheirer * * This program 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. * * This program 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 this program. If not, see ******************************************************************************/ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.Mixer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Broadcaster; import sample.Listener; import source.mixer.MixerChannel; import source.mixer.MixerChannelConfiguration; import audio.AudioEvent.Type; import audio.output.AudioOutput; import audio.output.MonoAudioOutput; import audio.output.StereoAudioOutput; import controller.ThreadPoolManager; import controller.ThreadPoolManager.ThreadType; public class AudioManager implements Listener, IAudioController { private static final Logger mLog = LoggerFactory.getLogger( AudioManager.class ); public static final int AUDIO_TIMEOUT = 2000; //2 seconds public static final int NO_SOURCE = 0; public static final AudioEvent CONFIGURATION_CHANGE_STARTED = new AudioEvent( Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null ); public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE = new AudioEvent( Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null ); private LinkedTransferQueue mAudioPacketQueue = new LinkedTransferQueue<>(); private Map mChannelAssignments = new HashMap<>(); private int mLowestPrioritySource; private List mAvailableAudioOutputs = new ArrayList<>(); private Map mAudioOutputMap = new HashMap<>(); private List mChannelNames = new ArrayList<>(); private Broadcaster mControllerBroadcaster = new Broadcaster<>(); private ThreadPoolManager mThreadPoolManager; private ScheduledFuture mProcessingTask; private MixerChannelConfiguration mMixerChannel; /** * Processes all audio produced by the decoding channels and routes audio * packets to any combination of outputs based on any alias audio routing * options specified by the user. */ public AudioManager( ThreadPoolManager manager ) { mThreadPoolManager = manager; //TODO: save audio system preferences to the settings manager /* Use the system default mixer and mono channel as default startup */ Mixer mixer = AudioSystem.getMixer( null ); if( mixer != null ) { mMixerChannel = new MixerChannelConfiguration( mixer, MixerChannel.MONO ); try { setMixerChannelConfiguration( mMixerChannel ); } catch ( AudioException e ) { mLog.error( "Couldn't create an audio output device", e ); } } } public void dispose() { if( mThreadPoolManager != null && mProcessingTask != null ) { mThreadPoolManager.cancel( mProcessingTask ); } mAudioPacketQueue.clear(); mThreadPoolManager = null; mProcessingTask = null; } @Override public synchronized void receive( AudioPacket packet ) { mAudioPacketQueue.add( packet ); } public class AudioPacketProcessor implements Runnable { @Override public void run() { removeCompletedChannelAssignments(); if( mAudioPacketQueue != null ) { List packets = new ArrayList(); mAudioPacketQueue.drainTo( packets ); for( AudioPacket packet: packets ) { /* Don't process any packet's marked as do not monitor */ if( !packet.getAudioMetadata().isDoNotMonitor() ) { int source = packet.getAudioMetadata().getSource(); if( packet.getType() != AudioPacket.Type.END ) { /* Existing channel assignment */ if( mChannelAssignments.containsKey( source ) ) { mChannelAssignments.get( source ).getAudioOutput() .receive( packet ); } /* Start a new channel assignment */ else if( !mAvailableAudioOutputs.isEmpty() ) { AudioOutput channel = mAvailableAudioOutputs.remove( 0 ); mChannelAssignments.put( packet.getAudioMetadata().getSource(), new AudioChannelAssignment( channel, packet.getAudioMetadata().getPriority() ) ); updateLowestPriorityAssignment(); channel.receive( packet ); } /* Override the lowest priority assignment */ else { if( mLowestPrioritySource > -1 ) { int priority = packet.getAudioMetadata().getPriority(); if( packet.getAudioMetadata().isSelected() || priority < mChannelAssignments .get( mLowestPrioritySource ).getPriority() ) { AudioChannelAssignment assignment = mChannelAssignments.remove( mLowestPrioritySource ); if( assignment != null ) { assignment.setPriority( priority ); int overrideSource = packet.getAudioMetadata().getSource(); mChannelAssignments.put( overrideSource, assignment ); mLowestPrioritySource = overrideSource; assignment.getAudioOutput().receive( packet ); } else { mLowestPrioritySource = -1; } } } } } } } } } } /** * Checks each audio channel assignment and removes any completed channels. */ private void removeCompletedChannelAssignments() { if( !mChannelAssignments.isEmpty() ) { Iterator> it = mChannelAssignments.entrySet().iterator(); Entry entry; boolean removed = false; while( it.hasNext() ) { entry = it.next(); if( entry.getValue().isComplete() ) { mAvailableAudioOutputs.add( entry.getValue().getAudioOutput() ); entry.getValue().dispose(); it.remove(); removed = true; } } if( removed ) { updateLowestPriorityAssignment(); } } } /** * Identifies the lowest priority channel assignment ... the one that can * be overridden with a higher priority call */ private void updateLowestPriorityAssignment() { int lowestPriority = -1; mLowestPrioritySource = -1; for( int source: mChannelAssignments.keySet() ) { AudioChannelAssignment assignment = mChannelAssignments.get( source ); if( assignment.getPriority() > lowestPriority ) { lowestPriority = assignment.getPriority(); mLowestPrioritySource = source; } } } public void endAllAudioAssignments() { for( AudioOutput output: mAudioOutputMap.values() ) { output.receive( new AudioPacket( AudioPacket.Type.END, null ) ); } } public class AudioChannelAssignment implements Listener { private static final long CALL_END_DELAY = 2000; //2 seconds private AudioOutput mAudioOutput; private int mPriority; private long mAudioStoppedTime = System.currentTimeMillis(); public AudioChannelAssignment( AudioOutput audioOutput, int priority ) { mAudioOutput = audioOutput; mPriority = priority; mAudioOutput.addAudioEventListener( this ); } public boolean isComplete() { return mAudioStoppedTime != Long.MAX_VALUE && mAudioStoppedTime + CALL_END_DELAY < System.currentTimeMillis(); } public void dispose() { mAudioOutput.removeAudioEventListener( this ); mAudioOutput.reset(); mAudioOutput = null; } public AudioOutput getAudioOutput() { return mAudioOutput; } public int getPriority() { return mPriority; } public void setPriority( int priority ) { mPriority = priority; } @Override public void receive( AudioEvent event ) { switch( event.getType() ) { case AUDIO_STARTED: case AUDIO_STOPPED: case AUDIO_CONTINUATION: mAudioStoppedTime = System.currentTimeMillis(); break; default: break; } } } @Override public void setMixerChannelConfiguration( MixerChannelConfiguration entry ) throws AudioException { if( entry != null && ( entry.getMixerChannel() == MixerChannel.MONO || entry.getMixerChannel() == MixerChannel.STEREO ) ) { mControllerBroadcaster.broadcast( CONFIGURATION_CHANGE_STARTED ); if( mThreadPoolManager != null && mProcessingTask != null ) { mThreadPoolManager.cancel( mProcessingTask ); } endAllAudioAssignments(); for( Entry e: mChannelAssignments.entrySet() ) { e.getValue().getAudioOutput().dispose(); } mChannelAssignments.clear(); for( AudioOutput output: mAvailableAudioOutputs ) { output.dispose(); } mAvailableAudioOutputs.clear(); mChannelNames.clear(); mAudioOutputMap.clear(); switch( entry.getMixerChannel() ) { case MONO: AudioOutput mono = new MonoAudioOutput( entry.getMixer() ); mAvailableAudioOutputs.add( mono ); mChannelNames.add( mono.getChannelName() ); mAudioOutputMap.put( mono.getChannelName(), mono ); break; case STEREO: AudioOutput left = new StereoAudioOutput( entry.getMixer(), MixerChannel.LEFT ); mAvailableAudioOutputs.add( left ); mChannelNames.add( left.getChannelName() ); mAudioOutputMap.put( left.getChannelName(), left ); AudioOutput right = new StereoAudioOutput( entry.getMixer(), MixerChannel.RIGHT ); mAvailableAudioOutputs.add( right ); mChannelNames.add( right.getChannelName() ); mAudioOutputMap.put( right.getChannelName(), right ); break; default: throw new AudioException( "Unsupported mixer channel " + "configuration: " + entry.getMixerChannel() ); } mProcessingTask = mThreadPoolManager.scheduleFixedRate( ThreadType.AUDIO_PROCESSING, new AudioPacketProcessor(), 15, TimeUnit.MILLISECONDS ); mControllerBroadcaster.broadcast( CONFIGURATION_CHANGE_COMPLETE ); } } @Override public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException { return mMixerChannel; } @Override public List getAudioOutputs() { List outputs = new ArrayList<>( mAudioOutputMap.values() ); Collections.sort( outputs, new Comparator() { @Override public int compare( AudioOutput first, AudioOutput second ) { return first.getChannelName().compareTo( second.getChannelName() ); } } ); return outputs; } @Override public void addControllerListener( Listener listener ) { mControllerBroadcaster.addListener( listener ); } @Override public void removeControllerListener( Listener listener ) { mControllerBroadcaster.removeListener( listener ); } }