From 9ab8b07b070fbd21dc4ee648eec54f0bf17c92ed Mon Sep 17 00:00:00 2001 From: JeffreyCA Date: Sun, 10 Jan 2021 13:19:57 -0500 Subject: [PATCH] fix: volume fade-in and fade-out issues #256 --- __tests__/tests/player.test.js | 57 +++++++++++++-- src/config/volumeFade.js | 5 ++ src/index.js | 127 ++++++++++++++++++++++++++++----- src/utils.js | 46 ++++++++---- 4 files changed, 200 insertions(+), 35 deletions(-) create mode 100644 src/config/volumeFade.js diff --git a/__tests__/tests/player.test.js b/__tests__/tests/player.test.js index 6678caaf..4e876338 100644 --- a/__tests__/tests/player.test.js +++ b/__tests__/tests/player.test.js @@ -1593,11 +1593,12 @@ describe('', () => { ) wrapper.find('.play-btn').simulate('click') + await sleep(300) wrapper.find('.play-btn').simulate('click') expect(wrapper.state().soundValue).toEqual(1) expect(wrapper.state().currentAudioVolume).toEqual(1) - await sleep(200) + await sleep(300) expect(onAudioVolumeChange).not.toHaveBeenCalled() expect(wrapper.state().soundValue).toEqual(1) @@ -1609,8 +1610,16 @@ describe('', () => { volume: 1, } const fn = jest.fn() - adjustVolume(audio, 0, { duration: 0 }).then(fn) + const { fadeInterval, updateIntervalEndVolume } = adjustVolume( + audio, + 1, + 0, + { duration: 0 }, + fn, + ) await sleep(100) + expect(fadeInterval).toBeUndefined() + expect(updateIntervalEndVolume).toBeUndefined() expect(audio.volume).toStrictEqual(0) expect(fn).toHaveBeenCalledTimes(1) }) @@ -1620,8 +1629,16 @@ describe('', () => { volume: 1, } const fn = jest.fn() - adjustVolume(audio, 1, { duration: 100 }).then(fn) + const { fadeInterval, updateIntervalEndVolume } = adjustVolume( + audio, + 1, + 1, + { duration: 100 }, + fn, + ) await sleep(100) + expect(fadeInterval).toBeUndefined() + expect(updateIntervalEndVolume).toBeUndefined() expect(audio.volume).toStrictEqual(1) expect(fn).toHaveBeenCalledTimes(1) }) @@ -1631,7 +1648,15 @@ describe('', () => { volume: 1, } const fn = jest.fn() - adjustVolume(audio, 0, { duration: 200 }).then(fn) + const { fadeInterval, updateIntervalEndVolume } = adjustVolume( + audio, + audio.volume, + 0, + { duration: 200 }, + fn, + ) + expect(fadeInterval).not.toBeUndefined() + expect(updateIntervalEndVolume).not.toBeUndefined() expect(audio.volume).toStrictEqual(1) expect(fn).not.toHaveBeenCalled() @@ -1640,4 +1665,28 @@ describe('', () => { expect(Math.floor(audio.volume)).toStrictEqual(0) expect(fn).toHaveBeenCalledTimes(1) }) + + it('should respond to end volume changes for fade-ins', async () => { + const audio = { + volume: 0, + } + const fn = jest.fn() + const { fadeInterval, updateIntervalEndVolume } = adjustVolume( + audio, + audio.volume, + 1, + { duration: 200 }, + fn, + ) + expect(fadeInterval).not.toBeUndefined() + expect(updateIntervalEndVolume).not.toBeUndefined() + updateIntervalEndVolume(0.5) + expect(audio.volume).toStrictEqual(0) + expect(fn).not.toHaveBeenCalled() + + await sleep(500) + + expect(audio.volume).toStrictEqual(0.5) + expect(fn).toHaveBeenCalledTimes(1) + }) }) diff --git a/src/config/volumeFade.js b/src/config/volumeFade.js new file mode 100644 index 00000000..13ccba27 --- /dev/null +++ b/src/config/volumeFade.js @@ -0,0 +1,5 @@ +export const VOLUME_FADE = { + IN: 'in', + OUT: 'out', + NONE: 'none', +} diff --git a/src/index.js b/src/index.js index 8b6a5658..73476ad8 100644 --- a/src/index.js +++ b/src/index.js @@ -49,6 +49,7 @@ import PLAY_MODE from './config/playMode' import PROP_TYPES from './config/propTypes' import { sliderBaseOptions } from './config/slider' import { THEME } from './config/theme' +import { VOLUME_FADE } from './config/volumeFade' import LOCALE_CONFIG from './locale' import Lyric from './lyric' import { @@ -123,7 +124,9 @@ export default class ReactJkMusicPlayer extends PureComponent { isAutoPlayWhenUserClicked: false, playIndex: this.props.playIndex || this.props.defaultPlayIndex || 0, canPlay: false, - isVolumeFadeChang: false, + currentVolumeFade: VOLUME_FADE.NONE, + currentVolumeFadeInterval: undefined, + updateIntervalEndVolume: undefined, } static defaultProps = { @@ -449,6 +452,20 @@ export default class ReactJkMusicPlayer extends PureComponent { return null } + let actionButtonIcon + + if (playing) { + if (this.state.currentVolumeFade === VOLUME_FADE.OUT) { + actionButtonIcon = this.iconMap.play + } else { + actionButtonIcon = this.iconMap.pause + } + } else if (this.state.currentVolumeFade === VOLUME_FADE.IN) { + actionButtonIcon = this.iconMap.pause + } else { + actionButtonIcon = this.iconMap.play + } + return createPortal(
- {playing ? this.iconMap.pause : this.iconMap.play} + {actionButtonIcon} )} { @@ -1183,21 +1205,92 @@ export default class ReactJkMusicPlayer extends PureComponent { onTogglePlay = () => { if (this.state.audioLists.length >= 1) { const { fadeIn, fadeOut } = this.props.volumeFade || {} - this.setState({ isVolumeFadeChang: fadeIn || fadeOut }) - - if (this.state.playing) { - adjustVolume(this.audio, 0, { - duration: fadeOut, - }).then(() => { - this.audio.pause() + const { currentVolumeFade, currentVolumeFadeInterval } = this.state + const isCurrentlyFading = + currentVolumeFade === VOLUME_FADE.IN || + currentVolumeFade === VOLUME_FADE.OUT + + /** + * Currently in middle of fading in/out, so need to cancel the current interval and do the opposite action. + * E.g. if currently fading out, then we need to cancel the fade-out and do a fade-in starting at current volume. + */ + if (isCurrentlyFading) { + // Clear current fade-in/out + clearInterval(currentVolumeFadeInterval) + this.setState({ + currentVolumeFadeInterval: undefined, + updateIntervalEndVolume: undefined, }) - return } - adjustVolume(this.audio, this.state.currentAudioVolume, { - duration: fadeIn, - }) - this.setState({ isAutoPlayWhenUserClicked: true }, this.loadAndPlayAudio) + // Currently playing track or in the middle of fading in + if ( + (!isCurrentlyFading && this.state.playing) || + currentVolumeFade === VOLUME_FADE.IN + ) { + this.setState({ currentVolumeFade: VOLUME_FADE.OUT }) + // Fade in from current volume to 0 + const { + fadeInterval: fadeOutInterval, + updateIntervalEndVolume, + } = adjustVolume( + this.audio, + this.audio.volume, + 0, + { + duration: fadeOut, + }, + () => { + this.audio.pause() + this.setState({ + currentVolumeFade: VOLUME_FADE.NONE, + currentVolumeFadeInterval: undefined, + updateIntervalEndVolume: undefined, + }) + // It's possible that the volume level in the UI has changed since beginning of fade + this.audio.volume = this.state.soundValue + }, + ) + + this.setState({ + currentVolumeFadeInterval: fadeOutInterval, + updateIntervalEndVolume, + }) + } else { + this.setState({ currentVolumeFade: VOLUME_FADE.IN }) + const startVolume = isCurrentlyFading ? this.audio.volume : 0 + const endVolume = this.state.soundValue + // Always fade in from 0 to current volume + const { + fadeInterval: fadeInInterval, + updateIntervalEndVolume, + } = adjustVolume( + this.audio, + startVolume, + endVolume, + { + // If starting track from beginning, start immediately without fade-in + duration: fadeIn, + }, + () => { + this.setState({ + currentVolumeFade: VOLUME_FADE.NONE, + currentVolumeFadeInterval: undefined, + updateIntervalEndVolume: undefined, + }) + this.audio.volume = this.state.soundValue + }, + ) + + this.setState( + { + currentVolumeFadeInterval: fadeInInterval, + updateIntervalEndVolume, + isAutoPlayWhenUserClicked: true, + }, + this.loadAndPlayAudio, + ) + } } } @@ -1436,8 +1529,8 @@ export default class ReactJkMusicPlayer extends PureComponent { onAudioVolumeChange = () => { const { volume } = this.audio - const { isVolumeFadeChang } = this.state - if (isVolumeFadeChang) { + const { currentVolumeFade } = this.state + if (currentVolumeFade !== VOLUME_FADE.NONE) { return } this.setState({ diff --git a/src/utils.js b/src/utils.js index a9be88bf..dc4f9120 100644 --- a/src/utils.js +++ b/src/utils.js @@ -54,24 +54,42 @@ export function swing(p) { export function adjustVolume( element, - newVolume, + startVolume, + endVolume, { duration = 1000, easing = swing, interval = 13 } = {}, + callback, ) { - const originalVolume = element.volume - const delta = newVolume - originalVolume + let delta = endVolume - startVolume if (!delta || !duration || !easing || !interval) { - element.volume = newVolume - return Promise.resolve() + element.volume = endVolume + callback() + return { fadeInterval: undefined, updateIntervalEndVolume: undefined } } + const ticks = Math.floor(duration / interval) let tick = 1 - return new Promise((resolve) => { - const timer = setInterval(() => { - element.volume = originalVolume + easing(tick / ticks) * delta - if (++tick === ticks) { - clearInterval(timer) - resolve() - } - }, interval) - }) + + const updateIntervalEndVolume = (newVolume) => { + endVolume = newVolume + } + + const timer = setInterval(() => { + // End volume may have changed in middle of fading + const newDelta = endVolume - startVolume + if (newDelta !== delta) { + delta = newDelta + } + + element.volume = startVolume + easing(tick / ticks) * delta + if (++tick >= ticks) { + element.volume = endVolume + clearInterval(timer) + callback() + } + }, interval) + + return { + fadeInterval: timer, + updateIntervalEndVolume, + } }