Modernize EQ

This commit is contained in:
birdbird 2022-08-13 06:51:10 +00:00
parent 8c15b0394a
commit e1741e9a83
5 changed files with 360 additions and 461 deletions

View File

@ -1,186 +0,0 @@
/*
This file is part of Subsonic.
Subsonic 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.
Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2011 (C) Sindre Mehus
*/
package org.moire.ultrasonic.audiofx;
import android.content.Context;
import android.media.MediaPlayer;
import android.media.audiofx.Equalizer;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import timber.log.Timber;
import org.moire.ultrasonic.util.FileUtil;
import java.io.Serializable;
/**
* Backward-compatible wrapper for {@link Equalizer}, which is API Level 9.
*
* @author Sindre Mehus
* @version $Id$
*/
public class EqualizerController
{
private static Boolean available = null;
private static final MutableLiveData<EqualizerController> instance = new MutableLiveData<>();
private Context context;
public Equalizer equalizer;
private int audioSessionId;
/**
* Retrieves the EqualizerController as LiveData
*/
public static LiveData<EqualizerController> get()
{
return instance;
}
/**
* Initializes the EqualizerController instance with a MediaPlayer
*/
public static void create(Context context, MediaPlayer mediaPlayer)
{
if (mediaPlayer == null) return;
if (!isAvailable()) return;
EqualizerController controller = new EqualizerController();
controller.context = context;
try
{
controller.audioSessionId = mediaPlayer.getAudioSessionId();
controller.equalizer = new Equalizer(0, controller.audioSessionId);
controller.loadSettings();
instance.postValue(controller);
}
catch (Throwable x)
{
Timber.w(x, "Failed to create equalizer.");
}
}
/**
* Releases the EqualizerController instance when the underlying MediaPlayer is no longer available
*/
public static void release()
{
EqualizerController controller = instance.getValue();
if (controller == null) return;
controller.equalizer.release();
instance.postValue(null);
}
/**
* Checks if the {@link Equalizer} class is available.
*/
private static boolean isAvailable()
{
if (available != null) return available;
try
{
Class.forName("android.media.audiofx.Equalizer");
available = true;
}
catch (Exception ex)
{
Timber.i(ex, "CheckAvailable received an exception getting class for the Equalizer");
available = false;
}
return available;
}
public void saveSettings()
{
if (!available) return;
try
{
FileUtil.serialize(context, new EqualizerSettings(equalizer), "equalizer.dat");
}
catch (Throwable x)
{
Timber.w(x, "Failed to save equalizer settings.");
}
}
public void loadSettings()
{
if (!available) return;
try
{
EqualizerSettings settings = FileUtil.deserialize(context, "equalizer.dat");
if (settings != null)
{
settings.apply(equalizer);
}
}
catch (Throwable x)
{
Timber.w(x, "Failed to load equalizer settings.");
}
}
private static class EqualizerSettings implements Serializable
{
private static final long serialVersionUID = 626565082425206061L;
private final short[] bandLevels;
private short preset;
private final boolean enabled;
public EqualizerSettings(Equalizer equalizer)
{
enabled = equalizer.getEnabled();
bandLevels = new short[equalizer.getNumberOfBands()];
for (short i = 0; i < equalizer.getNumberOfBands(); i++)
{
bandLevels[i] = equalizer.getBandLevel(i);
}
try
{
preset = equalizer.getCurrentPreset();
}
catch (Exception x)
{
preset = -1;
}
}
public void apply(Equalizer equalizer)
{
for (short i = 0; i < bandLevels.length; i++)
{
equalizer.setBandLevel(i, bandLevels[i]);
}
if (preset >= 0 && preset < equalizer.getNumberOfPresets())
{
equalizer.usePreset(preset);
}
equalizer.setEnabled(enabled);
}
}
}

View File

@ -1,275 +0,0 @@
package org.moire.ultrasonic.fragment;
import android.media.audiofx.Equalizer;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import org.jetbrains.annotations.NotNull;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.audiofx.EqualizerController;
import org.moire.ultrasonic.util.Util;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import timber.log.Timber;
/**
* Displays the Equalizer
*/
public class EqualizerFragment extends Fragment {
private static final int MENU_GROUP_PRESET = 100;
private final Map<Short, SeekBar> bars = new HashMap<>();
private EqualizerController equalizerController;
private Equalizer equalizer;
private LinearLayout equalizerLayout;
private View presetButton;
private CheckBox enabledCheckBox;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.equalizer, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
FragmentTitle.Companion.setTitle(this, R.string.equalizer_label);
equalizerLayout = view.findViewById(R.id.equalizer_layout);
presetButton = view.findViewById(R.id.equalizer_preset);
enabledCheckBox = view.findViewById(R.id.equalizer_enabled);
EqualizerController.get().observe(getViewLifecycleOwner(), new Observer<EqualizerController>() {
@Override
public void onChanged(EqualizerController controller) {
if (controller != null) {
Timber.d("EqualizerController Observer.onChanged received controller");
equalizerController = controller;
equalizer = controller.equalizer;
setup();
} else {
Timber.d("EqualizerController Observer.onChanged has no controller");
equalizerController = null;
equalizer = null;
}
}
});
}
@Override
public void onPause()
{
super.onPause();
if (equalizerController == null) return;
equalizerController.saveSettings();
}
@Override
public void onCreateContextMenu(@NotNull ContextMenu menu, @NotNull View view, ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
if (equalizer == null) return;
short currentPreset;
try
{
currentPreset = equalizer.getCurrentPreset();
}
catch (Exception x)
{
currentPreset = -1;
}
for (short preset = 0; preset < equalizer.getNumberOfPresets(); preset++)
{
MenuItem menuItem = menu.add(MENU_GROUP_PRESET, preset, preset, equalizer.getPresetName(preset));
if (preset == currentPreset)
{
menuItem.setChecked(true);
}
}
menu.setGroupCheckable(MENU_GROUP_PRESET, true, true);
}
@Override
public boolean onContextItemSelected(@NotNull MenuItem menuItem)
{
if (equalizer == null) return true;
try
{
short preset = (short) menuItem.getItemId();
equalizer.usePreset(preset);
updateBars();
}
catch (Exception ex)
{
//TODO: Show a dialog?
Timber.i(ex, "An exception has occurred in EqualizerFragment onContextItemSelected");
}
return true;
}
private void setup()
{
initEqualizer();
registerForContextMenu(presetButton);
presetButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
presetButton.showContextMenu();
}
});
enabledCheckBox.setChecked(equalizer.getEnabled());
enabledCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener()
{
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b)
{
setEqualizerEnabled(b);
}
});
}
private void setEqualizerEnabled(boolean enabled)
{
if (equalizer == null) return;
equalizer.setEnabled(enabled);
updateBars();
}
private void updateBars()
{
if (equalizer == null) return;
try
{
for (Map.Entry<Short, SeekBar> entry : bars.entrySet())
{
short band = entry.getKey();
SeekBar bar = entry.getValue();
bar.setEnabled(equalizer.getEnabled());
short minEQLevel = equalizer.getBandLevelRange()[0];
bar.setProgress(equalizer.getBandLevel(band) - minEQLevel);
}
}
catch (Exception ex)
{
//TODO: Show a dialog?
Timber.i(ex, "An exception has occurred in EqualizerFragment updateBars");
}
}
private void initEqualizer()
{
if (equalizer == null) return;
try
{
short[] bandLevelRange = equalizer.getBandLevelRange();
short numberOfBands = equalizer.getNumberOfBands();
final short minEQLevel = bandLevelRange[0];
final short maxEQLevel = bandLevelRange[1];
for (short i = 0; i < numberOfBands; i++)
{
final short band = i;
View bandBar = LayoutInflater.from(getContext()).inflate(R.layout.equalizer_bar, equalizerLayout, false);
TextView freqTextView;
if (bandBar != null)
{
freqTextView = (TextView) bandBar.findViewById(R.id.equalizer_frequency);
final TextView levelTextView = (TextView) bandBar.findViewById(R.id.equalizer_level);
SeekBar bar = (SeekBar) bandBar.findViewById(R.id.equalizer_bar);
freqTextView.setText(String.format(Locale.getDefault(), "%d Hz", equalizer.getCenterFreq(band) / 1000));
bars.put(band, bar);
bar.setMax(maxEQLevel - minEQLevel);
short level = equalizer.getBandLevel(band);
bar.setProgress(level - minEQLevel);
bar.setEnabled(equalizer.getEnabled());
updateLevelText(levelTextView, level);
bar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener()
{
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)
{
short level = (short) (progress + minEQLevel);
if (fromUser)
{
try
{
equalizer.setBandLevel(band, level);
}
catch (Exception ex)
{
//TODO: Show a dialog?
Timber.i(ex, "An exception has occurred in Equalizer onProgressChanged");
}
}
updateLevelText(levelTextView, level);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar)
{
}
@Override
public void onStopTrackingTouch(SeekBar seekBar)
{
}
});
equalizerLayout.addView(bandBar);
}
}
}
catch (Exception ex)
{
//TODO: Show a dialog?
Timber.i(ex, "An exception has occurred while initializing Equalizer");
}
}
private static void updateLevelText(TextView levelTextView, short level)
{
if (levelTextView != null)
{
levelTextView.setText(String.format(Locale.getDefault(), "%s%d dB", level > 0 ? "+" : "", level / 100));
}
}
}

View File

@ -0,0 +1,124 @@
/*
* EqualizerController.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.audiofx
import android.media.audiofx.Equalizer
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import java.io.Serializable
import java.lang.Exception
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.util.FileUtil.deserialize
import org.moire.ultrasonic.util.FileUtil.serialize
import timber.log.Timber
/**
* Wrapper for [Equalizer] with automatic restoration of presets and settings.
*
* TODO: Maybe store the settings in the DB?
*/
class EqualizerController {
@JvmField
var equalizer: Equalizer? = null
private var audioSessionId = 0
fun saveSettings() {
if (equalizer == null) return
try {
serialize(UApp.applicationContext(), EqualizerSettings(equalizer!!), "equalizer.dat")
} catch (all: Throwable) {
Timber.w(all, "Failed to save equalizer settings.")
}
}
fun loadSettings() {
if (equalizer == null) return
try {
val settings = deserialize<EqualizerSettings>(
UApp.applicationContext(), "equalizer.dat"
)
settings?.apply(equalizer!!)
} catch (all: Throwable) {
Timber.w(all, "Failed to load equalizer settings.")
}
}
private class EqualizerSettings(equalizer: Equalizer) : Serializable {
private val bandLevels: ShortArray
private var preset: Short = 0
private val enabled: Boolean
fun apply(equalizer: Equalizer) {
for (i in bandLevels.indices) {
equalizer.setBandLevel(i.toShort(), bandLevels[i])
}
if (preset >= 0 && preset < equalizer.numberOfPresets) {
equalizer.usePreset(preset)
}
equalizer.enabled = enabled
}
init {
enabled = equalizer.enabled
bandLevels = ShortArray(equalizer.numberOfBands.toInt())
for (i in 0 until equalizer.numberOfBands) {
bandLevels[i] = equalizer.getBandLevel(i.toShort())
}
preset = try {
equalizer.currentPreset
} catch (ignored: Exception) {
-1
}
}
companion object {
private const val serialVersionUID = 6269873247206061L
}
}
companion object {
private val instance = MutableLiveData<EqualizerController?>()
/**
* Retrieves the EqualizerController as LiveData
*/
@JvmStatic
fun get(): LiveData<EqualizerController?> {
return instance
}
/**
* Initializes the EqualizerController instance with a Session
*
* @param sessionId
* @return the new controller
*/
fun create(sessionId: Int): EqualizerController? {
val controller = EqualizerController()
return try {
controller.audioSessionId = sessionId
controller.equalizer = Equalizer(0, controller.audioSessionId)
controller.loadSettings()
instance.postValue(controller)
controller
} catch (all: Throwable) {
Timber.w(all, "Failed to create equalizer.")
null
}
}
/**
* Releases the EqualizerController instance when the underlying MediaPlayer is no longer available
*/
fun release() {
val controller = instance.value ?: return
controller.equalizer!!.release()
instance.postValue(null)
}
}
}

View File

@ -0,0 +1,232 @@
/*
* EqualizerFragment.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.fragment
import android.media.audiofx.Equalizer
import android.os.Bundle
import android.view.ContextMenu
import android.view.ContextMenu.ContextMenuInfo
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.LinearLayout
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import android.widget.TextView
import androidx.fragment.app.Fragment
import java.lang.Exception
import java.util.HashMap
import java.util.Locale
import org.moire.ultrasonic.R
import org.moire.ultrasonic.audiofx.EqualizerController
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.util.Util.applyTheme
import timber.log.Timber
/**
* Displays the Equalizer
*/
class EqualizerFragment : Fragment() {
private val bars: MutableMap<Short, SeekBar> = HashMap()
private var equalizerController: EqualizerController? = null
private var equalizer: Equalizer? = null
private var equalizerLayout: LinearLayout? = null
private var presetButton: View? = null
private var enabledCheckBox: CheckBox? = null
override fun onCreate(savedInstanceState: Bundle?) {
applyTheme(this.context)
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.equalizer, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setTitle(this, R.string.equalizer_label)
equalizerLayout = view.findViewById(R.id.equalizer_layout)
presetButton = view.findViewById(R.id.equalizer_preset)
enabledCheckBox = view.findViewById(R.id.equalizer_enabled)
// Subscribe to changes in the active controller
EqualizerController.get().observe(viewLifecycleOwner) { controller ->
if (controller != null) {
Timber.d("EqualizerController Observer.onChanged received controller")
equalizerController = controller
equalizer = controller.equalizer
setup()
} else {
Timber.d("EqualizerController Observer.onChanged has no controller")
equalizerController = null
equalizer = null
}
}
}
override fun onPause() {
super.onPause()
equalizerController?.saveSettings()
}
override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) {
super.onCreateContextMenu(menu, view, menuInfo)
if (equalizer == null) return
val currentPreset: Short = try {
equalizer!!.currentPreset
} catch (ignored: Exception) {
-1
}
for (preset in 0 until equalizer!!.numberOfPresets) {
val menuItem = menu.add(
MENU_GROUP_PRESET, preset, preset,
equalizer!!.getPresetName(
preset.toShort()
)
)
if (preset == currentPreset.toInt()) {
menuItem.isChecked = true
}
}
menu.setGroupCheckable(MENU_GROUP_PRESET, true, true)
}
override fun onContextItemSelected(menuItem: MenuItem): Boolean {
if (equalizer == null) return true
try {
val preset = menuItem.itemId.toShort()
equalizer!!.usePreset(preset)
updateBars()
} catch (all: Exception) {
// TODO: Show a dialog?
Timber.i(all, "An exception has occurred in EqualizerFragment onContextItemSelected")
}
return true
}
private fun setup() {
initEqualizer()
registerForContextMenu(presetButton!!)
presetButton!!.setOnClickListener { presetButton!!.showContextMenu() }
enabledCheckBox!!.isChecked = equalizer!!.enabled
enabledCheckBox!!.setOnCheckedChangeListener { _, b -> setEqualizerEnabled(b) }
}
private fun setEqualizerEnabled(enabled: Boolean) {
equalizer?.enabled = enabled
updateBars()
}
private fun updateBars() {
if (equalizer == null) return
try {
for ((band, bar) in bars) {
bar.isEnabled = equalizer!!.enabled
val minEQLevel = equalizer!!.bandLevelRange[0]
bar.progress = equalizer!!.getBandLevel(band) - minEQLevel
}
} catch (all: Exception) {
// TODO: Show a dialog?
Timber.i(all, "An exception has occurred in EqualizerFragment updateBars")
}
}
private fun initEqualizer() {
if (equalizer == null) return
try {
val bandLevelRange = equalizer!!.bandLevelRange
val numberOfBands = equalizer!!.numberOfBands
val minEQLevel = bandLevelRange[0]
val maxEQLevel = bandLevelRange[1]
for (i in 0 until numberOfBands) {
val bandBar = createSeekBarForBand(i, maxEQLevel, minEQLevel)
equalizerLayout!!.addView(bandBar)
}
} catch (all: Exception) {
// TODO: Show a dialog?
Timber.i(all, "An exception has occurred while initializing Equalizer")
}
}
private fun createSeekBarForBand(index: Int, maxEQLevel: Short, minEQLevel: Short): View {
val band = index.toShort()
val bandBar = LayoutInflater.from(context)
.inflate(R.layout.equalizer_bar, equalizerLayout, false)
val freqTextView: TextView =
bandBar.findViewById<View>(R.id.equalizer_frequency) as TextView
val levelTextView = bandBar.findViewById<View>(R.id.equalizer_level) as TextView
val bar = bandBar.findViewById<View>(R.id.equalizer_bar) as SeekBar
val range = equalizer!!.getBandFreqRange(band)
freqTextView.text = String.format(
Locale.getDefault(),
"%d - %d Hz",
range[0] / 1000, range[1] / 1000
)
bars[band] = bar
bar.max = maxEQLevel - minEQLevel
val bandLevel = equalizer!!.getBandLevel(band)
bar.progress = bandLevel - minEQLevel
bar.isEnabled = equalizer!!.enabled
updateLevelText(levelTextView, bandLevel)
bar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
override fun onProgressChanged(
seekBar: SeekBar,
progress: Int,
fromUser: Boolean
) {
val level = (progress + minEQLevel).toShort()
if (fromUser) {
try {
equalizer!!.setBandLevel(band, level)
} catch (all: Exception) {
// TODO: Show a dialog?
Timber.i(
all,
"An exception has occurred in Equalizer onProgressChanged"
)
}
}
updateLevelText(levelTextView, level)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
return bandBar
}
companion object {
private const val MENU_GROUP_PRESET = 100
private fun updateLevelText(levelTextView: TextView?, level: Short) {
if (levelTextView != null) {
levelTextView.text = String.format(
Locale.getDefault(),
"%s%d dB",
if (level > 0) "+" else "",
level / 100
)
}
}
}
}

View File

@ -25,6 +25,7 @@ import okhttp3.OkHttpClient
import org.koin.core.component.KoinComponent
import org.moire.ultrasonic.activity.NavigationActivity
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.audiofx.EqualizerController
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.service.RxBus
@ -36,6 +37,7 @@ import timber.log.Timber
class PlaybackService : MediaLibraryService(), KoinComponent {
private lateinit var player: ExoPlayer
private lateinit var mediaLibrarySession: MediaLibrarySession
private var equalizer: EqualizerController? = null
private lateinit var librarySessionCallback: MediaLibrarySession.Callback
@ -128,6 +130,8 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
.setSeekForwardIncrementMs(Settings.seekInterval.toLong())
.build()
equalizer = EqualizerController.create(player.audioSessionId)
// Enable audio offload
if (Settings.useHwOffload)
player.experimentalSetOffloadSchedulingEnabled(true)