summaryrefslogtreecommitdiffstats
path: root/src/org/cyanogenmod/audiofx/audiofx/service/SessionManager.java
blob: c74468648f4c5cc792450a012b5cb8f33b9e94a8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
package com.cyngn.audiofx.service;

import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_BASS_ENABLE;
import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_BASS_STRENGTH;
import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_EQ_PRESET_LEVELS;
import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_GLOBAL_ENABLE;
import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_MAXXVOLUME_ENABLE;
import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_REVERB_PRESET;
import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_TREBLE_ENABLE;
import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_TREBLE_STRENGTH;
import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_VIRTUALIZER_ENABLE;
import static com.cyngn.audiofx.Constants.DEVICE_AUDIOFX_VIRTUALIZER_STRENGTH;
import static com.cyngn.audiofx.Constants.DEVICE_DEFAULT_GLOBAL_ENABLE;
import static com.cyngn.audiofx.activity.MasterConfigControl.getDeviceIdentifierString;
import static com.cyngn.audiofx.service.AudioFxService.ALL_CHANGED;
import static com.cyngn.audiofx.service.AudioFxService.BASS_BOOST_CHANGED;
import static com.cyngn.audiofx.service.AudioFxService.ENABLE_REVERB;
import static com.cyngn.audiofx.service.AudioFxService.EQ_CHANGED;
import static com.cyngn.audiofx.service.AudioFxService.REVERB_CHANGED;
import static com.cyngn.audiofx.service.AudioFxService.TREBLE_BOOST_CHANGED;
import static com.cyngn.audiofx.service.AudioFxService.VIRTUALIZER_CHANGED;
import static com.cyngn.audiofx.service.AudioFxService.VOLUME_BOOST_CHANGED;

import android.content.Context;
import android.content.SharedPreferences;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.AudioSystem;
import android.media.audiofx.PresetReverb;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.util.SparseArray;

import com.cyngn.audiofx.backends.EffectSet;
import com.cyngn.audiofx.backends.EffectsFactory;
import com.cyngn.audiofx.eq.EqUtils;

import cyanogenmod.media.AudioSessionInfo;
import cyanogenmod.media.CMAudioManager;

class SessionManager implements AudioOutputChangeListener.AudioOutputChangedCallback {

    private static final String TAG = AudioFxService.TAG;
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    private final Context mContext;
    private final Handler mHandler;
    private final DevicePreferenceManager mDevicePrefs;
    private final CMAudioManager mCMAudio;

    /**
     * All fields ending with L should be locked on {@link #mAudioSessionsL}
     */
    private final SparseArray<EffectSet> mAudioSessionsL = new SparseArray<EffectSet>();


    private AudioDeviceInfo mCurrentDevice = null;

    // audio priority handler messages
    private static final int MSG_UPDATE_DSP = 100;
    private static final int MSG_ADD_SESSION = 101;
    private static final int MSG_REMOVE_SESSION = 102;
    private static final int MSG_UPDATE_FOR_SESSION = 103;
    private static final int MSG_UPDATE_EQ_OVERRIDE = 104;

    public SessionManager(Context context, Handler handler, DevicePreferenceManager devicePrefs,
            AudioDeviceInfo outputDevice) {
        mContext = context;
        mCMAudio = CMAudioManager.getInstance(context);
        mDevicePrefs = devicePrefs;
        mCurrentDevice = outputDevice;
        mHandler = new Handler(handler.getLooper(), new AudioServiceHandler());
    }

    public void onDestroy() {
        synchronized (mAudioSessionsL) {
            mHandler.removeCallbacksAndMessages(null);
            mHandler.getLooper().quit();
        }
    }

    public void update(int flags) {
        if (mHandler == null) {
            return;
        }
        synchronized (mAudioSessionsL) {
            mHandler.obtainMessage(MSG_UPDATE_DSP, flags, 0).sendToTarget();
        }
    }

    public void setOverrideLevels(short band, float level) {
        synchronized (mAudioSessionsL) {
            mHandler.obtainMessage(MSG_UPDATE_EQ_OVERRIDE, band, 0, level).sendToTarget();
        }
    }

    /**
     * Callback which listens for session updates from AudioPolicyManager. This is a
     * feature added by CM which notifies when sessions are created or
     * destroyed on a particular stream. This is independent of the standard control
     * intents and should not conflict with them. This feature may not be available on
     * all devices.
     *
     * Default logic is to do our best to only attach to music streams. We never attach
     * to low-latency streams automatically, and we don't attach to mono streams by default
     * either since these are usually notifications/ringtones/etc.
     */
    public boolean shouldHandleSession(AudioSessionInfo info) {
        final boolean music = info.getStream() == AudioManager.STREAM_MUSIC;
        final boolean offloaded = (info.getFlags() < 0)
                || (info.getFlags() & AudioFxService.AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD) > 0
                || (info.getFlags() & AudioFxService.AUDIO_OUTPUT_FLAG_DEEP_BUFFER) > 0;
        final boolean stereo = info.getChannelMask() < 0 || info.getChannelMask() > 1;

        return music && offloaded && stereo && info.getSessionId() > 0;
    }

    public void addSession(AudioSessionInfo info) {
        synchronized (mAudioSessionsL) {
            // Never auto-attach is someone is recording! We don't want to interfere
            // with any sort of loopback mechanisms.
            final boolean recording = AudioSystem.isSourceActive(0) || AudioSystem.isSourceActive(6);
            if (recording) {
                Log.w(TAG, "Recording in progress, not performing auto-attach!");
                return;
            }
            if (shouldHandleSession(info) &&
                    !mHandler.hasMessages(MSG_ADD_SESSION, info.getSessionId())) {
                mHandler.removeMessages(MSG_REMOVE_SESSION, info.getSessionId());
                mHandler.obtainMessage(MSG_ADD_SESSION, info.getSessionId()).sendToTarget();
                if (DEBUG) Log.i(TAG, "New audio session: " + info.toString());
            }
        }
    }

    public void removeSession(AudioSessionInfo info) {
        synchronized (mAudioSessionsL) {
            if (shouldHandleSession(info) &&
                    !mHandler.hasMessages(MSG_REMOVE_SESSION, info.getSessionId())) {
                int sid = info.getSessionId();
                final EffectSet effects = mAudioSessionsL.get(sid);
                if (effects != null) {
                    effects.setMarkedForDeath(true);
                    mHandler.sendMessageDelayed(
                            mHandler.obtainMessage(MSG_REMOVE_SESSION, sid),
                            effects.getReleaseDelay());
                    if (DEBUG) Log.i(TAG, "Audio session queued for removal: " + info.toString());
                }
            }
        }
    }

    public String getCurrentDeviceIdentifier() {
        return getDeviceIdentifierString(mCurrentDevice);
    }

    public boolean hasActiveSessions() {
        synchronized (mAudioSessionsL) {
            return mAudioSessionsL.size() > 0;
        }
    }

    EffectSet getEffectForSession(int sessionId) {
        synchronized (mAudioSessionsL) {
            return mAudioSessionsL.get(sessionId);
        }
    }

    /**
     * Update the backend with our changed preferences.
     *
     * This must only be called from the HandlerThread!
     */
    private void updateBackendLocked(int flags, EffectSet session) {
        if (Looper.getMainLooper().equals(Looper.myLooper())) {
            throw new IllegalStateException("updateBackend must not be called on the UI thread!");
        }

        final SharedPreferences prefs = mDevicePrefs.getCurrentDevicePrefs();

        if (DEBUG) {
            Log.i(TAG, "+++ updateBackend() called with flags=[" + flags + "], session=[" + session + "]");
        }

        if (session == null) {
            return;
        }

        final boolean globalEnabled = prefs.getBoolean(DEVICE_AUDIOFX_GLOBAL_ENABLE,
                DEVICE_DEFAULT_GLOBAL_ENABLE);

        if ((flags & ALL_CHANGED) > 0) {
            // global bypass toggle
            session.setGlobalEnabled(globalEnabled);
        }

        if (globalEnabled) {
            // tell the backend it's time to party
            if (!session.beginUpdate()) {
                Log.e(TAG, "session " + session + " failed to beginUpdate()");
                return;
            }

            // equalizer
            try {
                if ((flags & EQ_CHANGED) > 0) {
                    // equalizer is always on unless bypassed
                    session.enableEqualizer(true);
                    String savedPreset = prefs.getString(DEVICE_AUDIOFX_EQ_PRESET_LEVELS, null);
                    if (savedPreset != null) {
                        session.setEqualizerLevelsDecibels(EqUtils.stringBandsToFloats(savedPreset));
                    }
                }
            } catch (Exception e) {
                Log.e(TAG, "Error enabling equalizer!", e);
            }

            // bass
            try {
                if ((flags & BASS_BOOST_CHANGED) > 0 && session.hasBassBoost()) {
                    boolean enable = prefs.getBoolean(DEVICE_AUDIOFX_BASS_ENABLE, false);
                    session.enableBassBoost(enable);
                    session.setBassBoostStrength(Short.valueOf(prefs
                            .getString(DEVICE_AUDIOFX_BASS_STRENGTH, "0")));
                }
            } catch (Exception e) {
                Log.e(TAG, "Error enabling bass boost!", e);
            }

            // reverb
            if (ENABLE_REVERB) {
                try {
                    if ((flags & REVERB_CHANGED) > 0 && session.hasReverb()) {
                        short preset = Short.decode(prefs.getString(DEVICE_AUDIOFX_REVERB_PRESET,
                                String.valueOf(PresetReverb.PRESET_NONE)));
                        session.enableReverb(preset > 0);
                        session.setReverbPreset(preset);
                    }
                } catch (Exception e) {
                    Log.e(TAG, "Error enabling reverb preset", e);
                }
            }

            // virtualizer
            try {
                if ((flags & VIRTUALIZER_CHANGED) > 0 && session.hasVirtualizer()) {
                    boolean enable = prefs.getBoolean(DEVICE_AUDIOFX_VIRTUALIZER_ENABLE, false);
                    session.enableVirtualizer(enable);
                    session.setVirtualizerStrength(Short.valueOf(prefs.getString(
                            DEVICE_AUDIOFX_VIRTUALIZER_STRENGTH, "0")));
                }
            } catch (Exception e) {
                Log.e(TAG, "Error enabling virtualizer!");
            }

            // extended audio effects
            try {
                if ((flags & TREBLE_BOOST_CHANGED) > 0 && session.hasTrebleBoost()) {
                    // treble
                    boolean enable = prefs.getBoolean(DEVICE_AUDIOFX_TREBLE_ENABLE, false);
                    session.enableTrebleBoost(enable);
                    session.setTrebleBoostStrength(Short.valueOf(
                            prefs.getString(DEVICE_AUDIOFX_TREBLE_STRENGTH, "0")));
                }
            } catch (Exception e) {
                Log.e(TAG, "Error enabling treble boost!", e);
            }

            try {
                if ((flags & VOLUME_BOOST_CHANGED) > 0 && session.hasVolumeBoost()) {
                    // maxx volume
                    session.enableVolumeBoost(prefs.getBoolean(DEVICE_AUDIOFX_MAXXVOLUME_ENABLE, false));
                }
            } catch (Exception e) {
                Log.e(TAG, "Error enabling volume boost!", e);
            }

            // mic drop
            if (!session.commitUpdate()) {
                Log.e(TAG, "session " + session + " failed to commitUpdate()");
            }
        }
        if (DEBUG) {
            Log.i(TAG, "--- updateBackend() called with flags=[" + flags + "], session=[" + session + "]");
        }
    }

    private class AudioServiceHandler implements Handler.Callback {

        @Override
        public boolean handleMessage(Message msg) {
            synchronized (mAudioSessionsL) {
                EffectSet session = null;
                Integer sessionId = 0;
                int flags = 0;

                switch (msg.what) {
                    case MSG_ADD_SESSION:
                        /**
                         * msg.obj = sessionId
                         */
                        sessionId = (Integer) msg.obj;
                        if (sessionId == null || sessionId <= 0) {
                            break;
                        }

                        session = mAudioSessionsL.get(sessionId);
                        if (session == null) {
                            try {
                                session = new EffectsFactory()
                                        .createEffectSet(mContext, sessionId, mCurrentDevice);
                            } catch (Exception e) {
                                Log.e(TAG, "couldn't create effects for session id: " + sessionId, e);
                                break;
                            }
                            mAudioSessionsL.put(sessionId, session);
                            if (DEBUG) Log.w(TAG, "added new EffectSet for sessionId=" + sessionId);
                            updateBackendLocked(ALL_CHANGED, session);
                        } else {
                            session.setMarkedForDeath(false);
                        }
                        break;

                    case MSG_REMOVE_SESSION:
                        /**
                         * msg.obj = sessionId
                         */
                        sessionId = (Integer) msg.obj;
                        if (sessionId == null || sessionId <= 0) {
                            break;
                        }

                        session = mAudioSessionsL.get(sessionId);
                        if (session != null && session.isMarkedForDeath()) {
                            mHandler.removeMessages(MSG_UPDATE_FOR_SESSION, sessionId);
                            session.release();
                            mAudioSessionsL.remove(sessionId);
                            if (DEBUG) Log.w(TAG, "removed and released sessionId=" + sessionId);
                        }

                        break;

                    case MSG_UPDATE_DSP:
                        /**
                         * msg.arg1 = update what flags
                         */
                        flags = msg.arg1;

                        final String mode = getCurrentDeviceIdentifier();
                        if (DEBUG) Log.i(TAG, "Updating to configuration: " + mode);

                        final int N = mAudioSessionsL.size();
                        for (int i = 0; i < N; i++) {
                            sessionId = mAudioSessionsL.keyAt(i);
                            mHandler.obtainMessage(MSG_UPDATE_FOR_SESSION, flags, 0, sessionId).sendToTarget();
                        }
                        break;

                    case MSG_UPDATE_FOR_SESSION:
                        /**
                         * msg.arg1 = update what flags
                         * msg.arg2 = unused
                         * msg.obj = session id integer (for consistency)
                         */
                        sessionId = (Integer) msg.obj;
                        flags = msg.arg1;

                        if (sessionId == null || sessionId <= 0) {
                            break;
                        }

                        String device = getCurrentDeviceIdentifier();
                        if (DEBUG) Log.i(TAG, "updating DSP for sessionId=" + sessionId +
                                ", device=" + device + " flags=" + flags);

                        session = mAudioSessionsL.get(sessionId);
                        if (session != null) {
                            updateBackendLocked(flags, session);
                        }
                        break;

                    case MSG_UPDATE_EQ_OVERRIDE:
                        for (int i = 0; i < mAudioSessionsL.size(); i++) {
                            sessionId = mAudioSessionsL.keyAt(i);
                            session = mAudioSessionsL.get(sessionId);
                            if (session != null) {
                                session.setEqualizerBandLevel((short) msg.arg1, (float) msg.obj);
                            }
                        }
                        break;
                }
                return true;
            }
        }
    }

    /**
     * Updates the backend and notifies the frontend when the output device has changed
     */
    @Override
    public void onAudioOutputChanged(boolean firstChange, AudioDeviceInfo outputDevice) {
        synchronized (mAudioSessionsL) {
            if (mCurrentDevice == null ||
                    (outputDevice != null && mCurrentDevice.getId() != outputDevice.getId())) {
                mCurrentDevice = outputDevice;
            }

            EffectSet session = null;

            // Update all the sessions for this output which are moving
            final int N = mAudioSessionsL.size();
            for (int i = 0; i < N; i++) {
                session = mAudioSessionsL.valueAt(i);

                session.setDevice(mCurrentDevice);
                updateBackendLocked(ALL_CHANGED, session);
            }
        }
    }
}