mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-05-18 08:16:36 +03:00
Modernize EQ
This commit is contained in:
parent
8c15b0394a
commit
e1741e9a83
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,6 +25,7 @@ import okhttp3.OkHttpClient
|
|||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.moire.ultrasonic.activity.NavigationActivity
|
import org.moire.ultrasonic.activity.NavigationActivity
|
||||||
import org.moire.ultrasonic.app.UApp
|
import org.moire.ultrasonic.app.UApp
|
||||||
|
import org.moire.ultrasonic.audiofx.EqualizerController
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||||
import org.moire.ultrasonic.service.RxBus
|
import org.moire.ultrasonic.service.RxBus
|
||||||
@ -36,6 +37,7 @@ import timber.log.Timber
|
|||||||
class PlaybackService : MediaLibraryService(), KoinComponent {
|
class PlaybackService : MediaLibraryService(), KoinComponent {
|
||||||
private lateinit var player: ExoPlayer
|
private lateinit var player: ExoPlayer
|
||||||
private lateinit var mediaLibrarySession: MediaLibrarySession
|
private lateinit var mediaLibrarySession: MediaLibrarySession
|
||||||
|
private var equalizer: EqualizerController? = null
|
||||||
|
|
||||||
private lateinit var librarySessionCallback: MediaLibrarySession.Callback
|
private lateinit var librarySessionCallback: MediaLibrarySession.Callback
|
||||||
|
|
||||||
@ -128,6 +130,8 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
|||||||
.setSeekForwardIncrementMs(Settings.seekInterval.toLong())
|
.setSeekForwardIncrementMs(Settings.seekInterval.toLong())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
equalizer = EqualizerController.create(player.audioSessionId)
|
||||||
|
|
||||||
// Enable audio offload
|
// Enable audio offload
|
||||||
if (Settings.useHwOffload)
|
if (Settings.useHwOffload)
|
||||||
player.experimentalSetOffloadSchedulingEnabled(true)
|
player.experimentalSetOffloadSchedulingEnabled(true)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user