Skip to content

Commit

Permalink
Merge pull request #1530 from MahmoudMabrok/add_sound_up_down
Browse files Browse the repository at this point in the history
Add the ability to speed up and slow down recitations
  • Loading branch information
ahmedre authored Jan 6, 2024
2 parents ceb98ab + 49d805e commit 77223ad
Show file tree
Hide file tree
Showing 46 changed files with 221 additions and 44 deletions.
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

0 comments on commit 77223ad

Please sign in to comment.