package com.bel.android.dspmanager.service; import android.app.Service; import android.bluetooth.BluetoothA2dp; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.media.AudioManager; import android.media.audiofx.AudioEffect; import android.media.audiofx.BassBoost; import android.media.audiofx.Equalizer; import android.media.audiofx.Virtualizer; import android.os.Binder; import android.os.IBinder; import android.util.Log; import com.bel.android.dspmanager.activity.DSPManager; import java.util.HashMap; import java.util.Map; import java.util.UUID; /** * <p>This calls listen to events that affect DSP function and responds to them.</p> * <ol> * <li>new audio session declarations</li> * <li>headset plug / unplug events</li> * <li>preference update events.</li> * </ol> * * @author alankila */ public class HeadsetService extends Service { /** * Helper class representing the full complement of effects attached to one * audio session. * * @author alankila */ protected static class EffectSet { private static final UUID EFFECT_TYPE_VOLUME = UUID.fromString("09e8ede0-ddde-11db-b4f6-0002a5d5c51b"); /** Session-specific dynamic range compressor */ public final AudioEffect mCompression; /** Session-specific equalizer */ private final Equalizer mEqualizer; /** Session-specific bassboost */ private final BassBoost mBassBoost; /** Session-specific virtualizer */ private final Virtualizer mVirtualizer; protected EffectSet(int sessionId) { try { mCompression = new AudioEffect(EFFECT_TYPE_VOLUME, AudioEffect.EFFECT_TYPE_NULL, 0, sessionId); } catch (IllegalArgumentException e) { throw new RuntimeException(e); } catch (UnsupportedOperationException e) { throw new RuntimeException(e); } mEqualizer = new Equalizer(0, sessionId); mBassBoost = new BassBoost(0, sessionId); mVirtualizer = new Virtualizer(0, sessionId); } protected void release() { mCompression.release(); mEqualizer.release(); mBassBoost.release(); mVirtualizer.release(); } } protected static final String TAG = HeadsetService.class.getSimpleName(); public class LocalBinder extends Binder { public HeadsetService getService() { return HeadsetService.this; } } private final LocalBinder mBinder = new LocalBinder(); /** Known audio sessions and their associated audioeffect suites. */ protected final Map<Integer, EffectSet> mAudioSessions = new HashMap<Integer, EffectSet>(); /** Is a wired headset plugged in? */ protected boolean mUseHeadset; /** Is bluetooth headset plugged in? */ protected boolean mUseBluetooth; /** Is a dock or USB audio device plugged in? */ protected boolean mUseUSB; /** Has DSPManager assumed control of equalizer levels? */ private float[] mOverriddenEqualizerLevels; /** * Receive new broadcast intents for adding DSP to session */ private final BroadcastReceiver mAudioSessionReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); int sessionId = intent.getIntExtra(AudioEffect.EXTRA_AUDIO_SESSION, 0); if (action.equals(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)) { Log.i(TAG, String.format("New audio session: %d", sessionId)); if (!mAudioSessions.containsKey(sessionId)) { mAudioSessions.put(sessionId, new EffectSet(sessionId)); } } if (action.equals(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)) { Log.i(TAG, String.format("Audio session removed: %d", sessionId)); EffectSet gone = mAudioSessions.remove(sessionId); if (gone != null) { gone.release(); } } updateDsp(); } }; /** * Update audio parameters when preferences have been updated. */ private final BroadcastReceiver mPreferenceUpdateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Log.i(TAG, "Preferences updated."); updateDsp(); } }; /** * This code listens for changes in bluetooth and headset events. It is * adapted from google's own MusicFX application, so it's presumably the * most correct design there is for this problem. */ private final BroadcastReceiver mRoutingReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { final String action = intent.getAction(); final boolean prevUseHeadset = mUseHeadset; final boolean prevUseBluetooth = mUseBluetooth; final boolean prevUseUSB = mUseUSB; if (action.equals(Intent.ACTION_HEADSET_PLUG)) { mUseHeadset = intent.getIntExtra("state", 0) == 1; } else if (action.equals(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) { int state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, BluetoothA2dp.STATE_DISCONNECTED); mUseBluetooth = state == BluetoothA2dp.STATE_CONNECTED; } else if (action.equals(Intent.ACTION_ANALOG_AUDIO_DOCK_PLUG)) { mUseUSB = intent.getIntExtra("state", 0) == 1; } Log.i(TAG, "Headset=" + mUseHeadset + "; Bluetooth=" + mUseBluetooth + " ; USB=" + mUseUSB); if (prevUseHeadset != mUseHeadset || prevUseBluetooth != mUseBluetooth || prevUseUSB != mUseUSB) { updateDsp(); } } }; @Override public void onCreate() { super.onCreate(); Log.i(TAG, "Starting service."); IntentFilter audioFilter = new IntentFilter(); audioFilter.addAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); audioFilter.addAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); registerReceiver(mAudioSessionReceiver, audioFilter); final IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); intentFilter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); intentFilter.addAction(Intent.ACTION_ANALOG_AUDIO_DOCK_PLUG); intentFilter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY); registerReceiver(mRoutingReceiver, intentFilter); registerReceiver(mPreferenceUpdateReceiver, new IntentFilter(DSPManager.ACTION_UPDATE_PREFERENCES)); } @Override public void onDestroy() { super.onDestroy(); Log.i(TAG, "Stopping service."); unregisterReceiver(mAudioSessionReceiver); unregisterReceiver(mRoutingReceiver); unregisterReceiver(mPreferenceUpdateReceiver); } @Override public IBinder onBind(Intent intent) { return mBinder; } /** * Gain temporary control over the global equalizer. * Used by DSPManager when testing a new equalizer setting. * * @param levels */ public void setEqualizerLevels(float[] levels) { mOverriddenEqualizerLevels = levels; updateDsp(); } /** * There appears to be no way to find out what the current actual audio routing is. * For instance, if a wired headset is plugged in, the following objects/classes are involved: * </p> * <ol> * <li>wiredaccessoryobserver</li> * <li>audioservice</li> * <li>audiosystem</li> * <li>audiopolicyservice</li> * <li>audiopolicymanager</li> * </ol> * <p>Once the decision of new routing has been made by the policy manager, it is relayed to * audiopolicyservice, which waits for some time to let application buffers drain, and then * informs it to hardware. The full chain is:</p> * <ol> * <li>audiopolicymanager</li> * <li>audiopolicyservice</li> * <li>audiosystem</li> * <li>audioflinger</li> * <li>audioeffect (if any)</li> * </ol> * <p>However, the decision does not appear to be relayed to java layer, so we must * make a guess about what the audio output routing is.</p> * * @return string token that identifies configuration to use */ public String getAudioOutputRouting() { if (mUseHeadset) { return "headset"; } if (mUseBluetooth) { return "bluetooth"; } if (mUseUSB) { return "usb"; } return "speaker"; } /** * Push new configuration to audio stack. */ protected void updateDsp() { final String mode = getAudioOutputRouting(); SharedPreferences preferences = getSharedPreferences( DSPManager.SHARED_PREFERENCES_BASENAME + "." + mode, 0); Log.i(TAG, "Selected configuration: " + mode); for (Integer sessionId : mAudioSessions.keySet()) { updateDsp(preferences, mAudioSessions.get(sessionId)); } } private void updateDsp(SharedPreferences prefs, EffectSet session) { session.mCompression.setEnabled(prefs.getBoolean("dsp.compression.enable", false)); session.mCompression.setParameter(session.mCompression.intToByteArray(0), session.mCompression.shortToByteArray( Short.valueOf(prefs.getString("dsp.compression.mode", "0")))); session.mBassBoost.setEnabled(prefs.getBoolean("dsp.bass.enable", false)); session.mBassBoost.setStrength(Short.valueOf(prefs.getString("dsp.bass.mode", "0"))); session.mEqualizer.setEnabled(prefs.getBoolean("dsp.tone.enable", false)); float[] equalizerLevels; if (mOverriddenEqualizerLevels != null) { equalizerLevels = mOverriddenEqualizerLevels; } else { /* Equalizer state is in a single string preference with all values separated by ; */ String[] levels = prefs.getString("dsp.tone.eq.custom", "0;0;0;0;0").split(";"); equalizerLevels = new float[levels.length]; for (int i = 0; i < levels.length; i++) { equalizerLevels[i] = Float.valueOf(levels[i]); } } for (short i = 0; i < equalizerLevels.length; i ++) { session.mEqualizer.setBandLevel(i, (short) Math.round(equalizerLevels[i] * 100)); } session.mEqualizer.setParameter(session.mEqualizer.intToByteArray(1000), session.mEqualizer.shortToByteArray( Short.valueOf(prefs.getString("dsp.tone.loudness", "10000")))); session.mVirtualizer.setEnabled(prefs.getBoolean("dsp.headphone.enable", false)); session.mVirtualizer.setStrength( Short.valueOf(prefs.getString("dsp.headphone.mode", "0"))); } }