Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add speedup & down to sound player UI #1530

Merged
merged 10 commits into from
Jan 6, 2024
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.quran.labs.androidquran.dao.audio

import android.os.Parcelable
import com.quran.labs.androidquran.common.audio.model.QariItem
import com.quran.data.model.SuraAyah
import com.quran.labs.androidquran.common.audio.model.QariItem
import kotlinx.parcelize.Parcelize

@Parcelize
Expand All @@ -12,6 +12,7 @@ data class AudioRequest(val start: SuraAyah,
val repeatInfo: Int = 0,
val rangeRepeatInfo: Int = 0,
val enforceBounds: Boolean,
val playbackSpeed: Float = 1f,
val shouldStream: Boolean,
val audioPathInfo: AudioPathInfo) : Parcelable {
fun isGapless() = qari.isGapless
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ constructor(private val quranDisplayData: QuranDisplayData,
verseRepeat: Int,
rangeRepeat: Int,
enforceRange: Boolean,
playbackSpeed: Float,
shouldStream: Boolean) {
val audioPathInfo = getLocalAudioPathInfo(qari)
if (audioPathInfo != null) {
Expand Down Expand Up @@ -62,7 +63,7 @@ constructor(private val quranDisplayData: QuranDisplayData,
}

val audioRequest = AudioRequest(
actualStart, actualEnd, qari, verseRepeat, rangeRepeat, enforceRange, stream, audioPath)
actualStart, actualEnd, qari, verseRepeat, rangeRepeat, enforceRange, playbackSpeed, stream, audioPath)
play(audioRequest)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import android.support.v4.media.session.PlaybackStateCompat
import android.util.SparseIntArray
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils.clamp
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.media.session.MediaButtonReceiver
import com.quran.data.core.QuranInfo
Expand Down Expand Up @@ -398,11 +399,15 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener,
processStopRequest()
} else if (ACTION_REWIND == action) {
processRewindRequest()
} else if (ACTION_UPDATE_REPEAT == action) {
} else if (ACTION_UPDATE_SETTINGS == action) {
val playInfo = intent.getParcelableExtra<AudioRequest>(EXTRA_PLAY_INFO)
val localAudioQueue = audioQueue
if (playInfo != null && localAudioQueue != null) {
audioQueue = localAudioQueue.withUpdatedAudioRequest(playInfo)
if (playInfo.playbackSpeed != audioRequest?.playbackSpeed) {
processUpdatePlaybackSpeed(playInfo.playbackSpeed)
serviceHandler.sendEmptyMessageDelayed(MSG_UPDATE_AUDIO_POS, 200)
}
audioRequest = playInfo
}
} else {
Expand Down Expand Up @@ -567,14 +572,11 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener,
}
notifyAyahChanged()
if (maxAyahs >= updatedAyah + 1) {
var t = gaplessSuraData[updatedAyah + 1] - localPlayer.currentPosition
Timber.d("updateAudioPlayPosition postingDelayed after: %d", t)
if (t < 100) {
t = 100
} else if (t > 10000) {
t = 10000
}
serviceHandler.sendEmptyMessageDelayed(MSG_UPDATE_AUDIO_POS, t.toLong())
val timeDelta = gaplessSuraData[updatedAyah + 1] - localPlayer.currentPosition
val t = clamp(timeDelta, 100, 10000)
val tAccountingForSpeed = t / (audioRequest?.playbackSpeed ?: 1f)
Timber.d("updateAudioPlayPosition after: %d, speed %f", t, tAccountingForSpeed)
serviceHandler.sendEmptyMessageDelayed(MSG_UPDATE_AUDIO_POS, tAccountingForSpeed.toLong())
} else if (maxAyahs == updatedAyah) {
serviceHandler.sendEmptyMessageDelayed(MSG_UPDATE_AUDIO_POS, 150)
}
Expand Down Expand Up @@ -678,6 +680,15 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener,
}
}

private fun processUpdatePlaybackSpeed(speed: Float) {
if (State.Playing === state && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
player?.playbackParams?.let { params ->
params.setSpeed(speed)
player?.playbackParams = params
}
}
}

private fun processSkipRequest() {
if (audioRequest == null) {
return
Expand Down Expand Up @@ -1048,6 +1059,9 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener,
)
player.start()
state = State.Playing
audioRequest?.playbackSpeed?.let { speed ->
processUpdatePlaybackSpeed(speed)
}
serviceHandler.sendEmptyMessageDelayed(MSG_UPDATE_AUDIO_POS, 200)
}

Expand Down Expand Up @@ -1351,18 +1365,15 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener,
}

companion object {
// These are the Intent actions that we are prepared to handle. Notice that
// the fact these constants exist in our class is a mere convenience: what
// really defines the actions our service can handle are the <action> tags
// in the <intent-filters> tag for our service in AndroidManifest.xml.
// These are the Intent actions that we are prepared to handle.
const val ACTION_PLAYBACK = "com.quran.labs.androidquran.action.PLAYBACK"
const val ACTION_PLAY = "com.quran.labs.androidquran.action.PLAY"
const val ACTION_PAUSE = "com.quran.labs.androidquran.action.PAUSE"
const val ACTION_STOP = "com.quran.labs.androidquran.action.STOP"
const val ACTION_SKIP = "com.quran.labs.androidquran.action.SKIP"
const val ACTION_REWIND = "com.quran.labs.androidquran.action.REWIND"
const val ACTION_CONNECT = "com.quran.labs.androidquran.action.CONNECT"
const val ACTION_UPDATE_REPEAT = "com.quran.labs.androidquran.action.UPDATE_REPEAT"
const val ACTION_UPDATE_SETTINGS = "com.quran.labs.androidquran.action.UPDATE_SETTINGS"

// pending notification request codes
private const val REQUEST_CODE_MAIN = 0
Expand All @@ -1378,6 +1389,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener,

// so user can pass in a serializable LegacyAudioRequest to the intent
const val EXTRA_PLAY_INFO = "com.quran.labs.androidquran.PLAY_INFO"
const val EXTRA_PLAY_SPEED = "com.quran.labs.androidquran.PLAY_SPEED"
private const val NOTIFICATION_CHANNEL_ID = Constants.AUDIO_CHANNEL
private const val MSG_INCOMING = 1
private const val MSG_START_AUDIO = 2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1496,15 +1496,16 @@ private void playFromAyah(int startSura, int startAyah) {
final SuraAyah end = getSelectionEnd();
// handle the case of multiple ayat being selected and play them as a range if so
final SuraAyah ending = (end == null || start.equals(end) || start.after(end))? null : end;
playFromAyah(start, ending, page, 0, 0, ending != null);
playFromAyah(start, ending, page, 0, 0, ending != null, 1.0f);
}

public void playFromAyah(SuraAyah start,
SuraAyah end,
int page,
int verseRepeat,
int rangeRepeat,
boolean enforceRange) {
boolean enforceRange,
float playbackSpeed) {
final SuraAyah ending = end != null ? end :
audioUtils.getLastAyahToPlay(start, page,
quranSettings.getPreferredDownloadAmount(), isDualPageVisible());
Expand All @@ -1516,7 +1517,7 @@ public void playFromAyah(SuraAyah start,
final QariItem item = audioStatusBar.getAudioInfo();
final boolean shouldStream = quranSettings.shouldStream();
audioPresenter.play(
start, ending, item, verseRepeat, rangeRepeat, enforceRange, shouldStream);
start, ending, item, verseRepeat, rangeRepeat, enforceRange, playbackSpeed, shouldStream);
}
}

Expand Down Expand Up @@ -1569,6 +1570,7 @@ public void handlePlayback(AudioRequest request) {
intent.putExtra(AudioService.EXTRA_PLAY_INFO, request);
lastAudioRequest = request;
audioStatusBar.setRepeatCount(request.getRepeatInfo());
audioStatusBar.setSpeed(request.getPlaybackSpeed());
audioStatusBar.switchMode(AudioStatusBar.LOADING_MODE);
}

Expand All @@ -1583,6 +1585,27 @@ public void onPausePressed() {
audioStatusBar.switchMode(AudioStatusBar.PAUSED_MODE);
}

@Override
public void setPlaybackSpeed(float speed) {
if (lastAudioRequest != null) {
final AudioRequest updatedAudioRequest = new AudioRequest(lastAudioRequest.getStart(),
lastAudioRequest.getEnd(),
lastAudioRequest.getQari(),
lastAudioRequest.getRepeatInfo(),
lastAudioRequest.getRangeRepeatInfo(),
lastAudioRequest.getEnforceBounds(),
speed,
lastAudioRequest.getShouldStream(),
lastAudioRequest.getAudioPathInfo());

Intent i = new Intent(this, AudioService.class);
i.setAction(AudioService.ACTION_UPDATE_SETTINGS);
i.putExtra(AudioService.EXTRA_PLAY_INFO, updatedAudioRequest);
startService(i);
lastAudioRequest = updatedAudioRequest;
}
}

@Override
public void onNextPressed() {
startService(audioUtils.getAudioIntent(this,
Expand Down Expand Up @@ -1621,23 +1644,27 @@ public void onShowQariList() {
}

public boolean updatePlayOptions(int rangeRepeat,
int verseRepeat, boolean enforceRange) {
int verseRepeat,
boolean enforceRange,
float playbackSpeed) {
if (lastAudioRequest != null) {
final AudioRequest updatedAudioRequest = new AudioRequest(lastAudioRequest.getStart(),
lastAudioRequest.getEnd(),
lastAudioRequest.getQari(),
verseRepeat,
rangeRepeat,
enforceRange,
playbackSpeed,
lastAudioRequest.getShouldStream(),
lastAudioRequest.getAudioPathInfo());
Intent i = new Intent(this, AudioService.class);
i.setAction(AudioService.ACTION_UPDATE_REPEAT);
i.setAction(AudioService.ACTION_UPDATE_SETTINGS);
i.putExtra(AudioService.EXTRA_PLAY_INFO, updatedAudioRequest);
startService(i);

lastAudioRequest = updatedAudioRequest;
audioStatusBar.setRepeatCount(verseRepeat);
audioStatusBar.setSpeed(playbackSpeed);
return true;
} else {
return false;
Expand All @@ -1653,11 +1680,12 @@ public void setRepeatCount(int repeatCount) {
repeatCount,
lastAudioRequest.getRangeRepeatInfo(),
lastAudioRequest.getEnforceBounds(),
lastAudioRequest.getPlaybackSpeed(),
lastAudioRequest.getShouldStream(),
lastAudioRequest.getAudioPathInfo());

Intent i = new Intent(this, AudioService.class);
i.setAction(AudioService.ACTION_UPDATE_REPEAT);
i.setAction(AudioService.ACTION_UPDATE_SETTINGS);
i.putExtra(AudioService.EXTRA_PLAY_INFO, updatedAudioRequest);
startService(i);
lastAudioRequest = updatedAudioRequest;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class AyahPlaybackFragment : AyahActionFragment() {
private var shouldEnforce = false
private var rangeRepeatCount = 0
private var verseRepeatCount = 0
private var currentSpeed = 1.0f

private lateinit var applyButton: Button
private lateinit var startSuraSpinner: QuranSpinner
Expand All @@ -43,6 +44,7 @@ class AyahPlaybackFragment : AyahActionFragment() {
private lateinit var endingAyahSpinner: QuranSpinner
private lateinit var repeatVersePicker: NumberPicker
private lateinit var repeatRangePicker: NumberPicker
private lateinit var playbackSpeedPicker: NumberPicker
private lateinit var restrictToRange: CheckBox

private lateinit var startAyahAdapter: ArrayAdapter<CharSequence>
Expand Down Expand Up @@ -76,6 +78,7 @@ class AyahPlaybackFragment : AyahActionFragment() {
applyButton.setOnClickListener(onClickListener)
repeatVersePicker = view.findViewById(R.id.repeat_verse_picker)
repeatRangePicker = view.findViewById(R.id.repeat_range_picker)
playbackSpeedPicker = view.findViewById(R.id.playback_speed_picker)

val context = requireContext()
val isArabicNames = QuranSettings.getInstance(context).isArabicNames
Expand All @@ -91,18 +94,15 @@ class AyahPlaybackFragment : AyahActionFragment() {
}
values[MAX_REPEATS] = getString(R.string.infinity)
if (isArabicNames) {
repeatVersePicker.formatter = NumberPicker.Formatter { value: Int -> arFormat(value) }
repeatRangePicker.formatter = NumberPicker.Formatter { value: Int -> arFormat(value) }
val typeface = TypefaceManager.getHeaderFooterTypeface(context)
repeatVersePicker.typeface = typeface
repeatVersePicker.setSelectedTypeface(typeface)
repeatRangePicker.typeface = typeface
repeatRangePicker.setSelectedTypeface(typeface)
// Use larger text size since KFGQPC font is small
repeatVersePicker.setSelectedTextSize(R.dimen.arabic_number_picker_selected_text_size)
repeatRangePicker.setSelectedTextSize(R.dimen.arabic_number_picker_selected_text_size)
repeatVersePicker.setTextSize(R.dimen.arabic_number_picker_text_size)
repeatRangePicker.setTextSize(R.dimen.arabic_number_picker_text_size)
listOf(repeatVersePicker, repeatRangePicker, playbackSpeedPicker).forEach {
it.formatter = NumberPicker.Formatter { value: Int -> arFormat(value) }
val typeface = TypefaceManager.getHeaderFooterTypeface(context)
it.typeface = typeface
it.setSelectedTypeface(typeface)
// Use larger text size since KFGQPC font is small
it.setSelectedTextSize(R.dimen.arabic_number_picker_selected_text_size)
it.setTextSize(R.dimen.arabic_number_picker_text_size)
}
}
repeatVersePicker.minValue = 1
repeatVersePicker.maxValue = MAX_REPEATS + 1
Expand All @@ -112,6 +112,10 @@ class AyahPlaybackFragment : AyahActionFragment() {
repeatRangePicker.displayedValues = values
repeatRangePicker.value = defaultRangeRepeat
repeatVersePicker.value = defaultVerseRepeat
playbackSpeedPicker.minValue = 1
playbackSpeedPicker.maxValue = SPEEDS.size
playbackSpeedPicker.displayedValues = SPEEDS.map { numberFormat.format(it) }.toTypedArray()
playbackSpeedPicker.value = DEFAULT_SPEED_INDEX + 1
repeatRangePicker.setOnValueChangedListener { _: NumberPicker?, _: Int, newVal: Int ->
if (newVal > 1) {
// whenever we want to repeat the range, we have to enable restrictToRange
Expand Down Expand Up @@ -189,20 +193,21 @@ class AyahPlaybackFragment : AyahActionFragment() {
val enforceRange = restrictToRange.isChecked
var updatedRange = false

val speed = SPEEDS[playbackSpeedPicker.value - 1]
if (currentStart != decidedStart || currentEnding != decidedEnd) {
// different range or not playing, so make a new request
updatedRange = true
context.playFromAyah(
currentStart, currentEnding, page, verseRepeat,
rangeRepeat, enforceRange
rangeRepeat, enforceRange, speed
)
} else if (shouldEnforce != enforceRange || rangeRepeatCount != rangeRepeat || verseRepeatCount != verseRepeat) {
} else if (shouldEnforce != enforceRange || rangeRepeatCount != rangeRepeat || verseRepeatCount != verseRepeat || currentSpeed != speed) {
// can just update repeat settings
if (!context.updatePlayOptions(rangeRepeat, verseRepeat, enforceRange)
if (!context.updatePlayOptions(rangeRepeat, verseRepeat, enforceRange, speed)
) {
// audio stopped in the process, let's start it
context.playFromAyah(
currentStart, currentEnding, page, verseRepeat, rangeRepeat, enforceRange
currentStart, currentEnding, page, verseRepeat, rangeRepeat, enforceRange, speed
)
}
}
Expand Down Expand Up @@ -303,6 +308,7 @@ class AyahPlaybackFragment : AyahActionFragment() {
if (lastRequest != lastSeenAudioRequest) {
verseRepeatCount = lastRequest.repeatInfo
rangeRepeatCount = lastRequest.rangeRepeatInfo
currentSpeed = lastRequest.playbackSpeed
shouldEnforce = lastRequest.enforceBounds
} else {
shouldReset = false
Expand All @@ -324,6 +330,7 @@ class AyahPlaybackFragment : AyahActionFragment() {
}
rangeRepeatCount = 0
verseRepeatCount = 0
currentSpeed = 1.0f
decidedStart = null
decidedEnd = null
applyButton.setText(R.string.play_apply_and_play)
Expand All @@ -347,6 +354,7 @@ class AyahPlaybackFragment : AyahActionFragment() {
restrictToRange.isChecked = shouldEnforce
repeatRangePicker.value = rangeRepeatCount + 1
repeatVersePicker.value = verseRepeatCount + 1
playbackSpeedPicker.value = SPEEDS.indexOf(currentSpeed) + 1
}
}
}
Expand All @@ -355,5 +363,7 @@ class AyahPlaybackFragment : AyahActionFragment() {
private val ITEM_LAYOUT = R.layout.sherlock_spinner_item
private val ITEM_DROPDOWN_LAYOUT = R.layout.sherlock_spinner_dropdown_item
private const val MAX_REPEATS = 25
private val SPEEDS = listOf(0.5f, 0.75f, 1.0f, 1.25f, 1.5f)
private const val DEFAULT_SPEED_INDEX = 2
}
}
Loading