diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/audiofx/EqualizerController.java b/ultrasonic/src/main/java/org/moire/ultrasonic/audiofx/EqualizerController.java deleted file mode 100644 index 1f844646..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/audiofx/EqualizerController.java +++ /dev/null @@ -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 . - - 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 instance = new MutableLiveData<>(); - - private Context context; - public Equalizer equalizer; - private int audioSessionId; - - /** - * Retrieves the EqualizerController as LiveData - */ - public static LiveData 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); - } - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/EqualizerFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/EqualizerFragment.java deleted file mode 100644 index bdf6bf33..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/EqualizerFragment.java +++ /dev/null @@ -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 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() { - @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 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)); - } - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/audiofx/EqualizerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/audiofx/EqualizerController.kt new file mode 100644 index 00000000..5c9630d5 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/audiofx/EqualizerController.kt @@ -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( + 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() + + /** + * Retrieves the EqualizerController as LiveData + */ + @JvmStatic + fun get(): LiveData { + 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) + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EqualizerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EqualizerFragment.kt new file mode 100644 index 00000000..e1d48e79 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EqualizerFragment.kt @@ -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 = 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(R.id.equalizer_frequency) as TextView + val levelTextView = bandBar.findViewById(R.id.equalizer_level) as TextView + val bar = bandBar.findViewById(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 + ) + } + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index 0a200060..675bb39e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -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)