Skip to content

Commit

Permalink
Change playback speed exponentially (#840)
Browse files Browse the repository at this point in the history
Co-authored-by: Mikael Finstad <[email protected]>
  • Loading branch information
mklaber and mifi authored Nov 14, 2021
1 parent 9ea7dcd commit 6cc18f1
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 4 deletions.
11 changes: 7 additions & 4 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import Mousetrap from 'mousetrap';
import JSON5 from 'json5';

import fromPairs from 'lodash/fromPairs';
import clamp from 'lodash/clamp';
import sortBy from 'lodash/sortBy';
import flatMap from 'lodash/flatMap';
import isEqual from 'lodash/isEqual';
Expand Down Expand Up @@ -62,6 +61,7 @@ import {
hasDuplicates, havePermissionToReadFile, isMac, resolvePathIfNeeded, pathExists, html5ifiedPrefix, html5dummySuffix, findExistingHtml5FriendlyFile,
} from './util';
import { formatDuration } from './util/duration';
import { adjustRate } from './util/rate-calculator';
import { askForOutDir, askForImportChapters, createNumSegments, createFixedDurationSegments, promptTimeOffset, askForHtml5ifySpeed, askForFileOpenAction, confirmExtractAllStreamsDialog, cleanupFilesDialog, showDiskFull, showCutFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, showMultipleFilesDialog, showOpenAndMergeDialog, openAbout, showEditableJsonDialog } from './dialogs';
import { openSendReportDialog } from './reporting';
import { fallbackLng } from './i18n';
Expand Down Expand Up @@ -1232,7 +1232,7 @@ const App = memo(() => {
}
}, [filePath, captureFormat, customOutDir, previewFilePath, outputDir, enableTransferTimestamps, hideAllNotifications]);

const changePlaybackRate = useCallback((dir) => {
const changePlaybackRate = useCallback((dir, rateMultiplier) => {
if (canvasPlayerEnabled) {
toast.fire({ title: i18n.t('Unable to change playback rate right now'), timer: 1000 });
return;
Expand All @@ -1242,8 +1242,7 @@ const App = memo(() => {
if (!playing) {
video.play();
} else {
// https://github.com/mifi/lossless-cut/issues/447#issuecomment-766339083
const newRate = clamp(Math.round((video.playbackRate + (dir * 0.15)) * 100) / 100, 0.1, 16);
const newRate = adjustRate(video.playbackRate, dir, rateMultiplier);
toast.fire({ title: `${i18n.t('Playback rate:')} ${Math.round(newRate * 100)}%`, timer: 1000 });
video.playbackRate = newRate;
}
Expand Down Expand Up @@ -1674,7 +1673,9 @@ const App = memo(() => {
const togglePlayNoReset = () => togglePlay();
const togglePlayReset = () => togglePlay(true);
const reducePlaybackRate = () => changePlaybackRate(-1);
const reducePlaybackRateMore = () => changePlaybackRate(-1, 2.0);
const increasePlaybackRate = () => changePlaybackRate(1);
const increasePlaybackRateMore = () => changePlaybackRate(1, 2.0);
function seekBackwards() {
seekRel(keyboardNormalSeekSpeed * seekAccelerationRef.current * -1);
seekAccelerationRef.current *= keyboardSeekAccFactor;
Expand Down Expand Up @@ -1707,7 +1708,9 @@ const App = memo(() => {
mousetrap.bind('space', () => togglePlayReset());
mousetrap.bind('k', () => togglePlayNoReset());
mousetrap.bind('j', () => reducePlaybackRate());
mousetrap.bind('shift+j', () => reducePlaybackRateMore());
mousetrap.bind('l', () => increasePlaybackRate());
mousetrap.bind('shift+l', () => increasePlaybackRateMore());
mousetrap.bind('z', () => toggleComfortZoom());
mousetrap.bind(',', () => seekBackwardsShort());
mousetrap.bind('.', () => seekForwardsShort());
Expand Down
2 changes: 2 additions & 0 deletions src/HelpSheet.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ const HelpSheet = memo(({ visible, onTogglePress, ffmpegCommandLog, currentCutSe
<div><kbd>SPACE</kbd>, <kbd>k</kbd> {t('Play/pause')}</div>
<div><kbd>J</kbd> {t('Slow down playback')}</div>
<div><kbd>L</kbd> {t('Speed up playback')}</div>
<div><kbd>SHIFT</kbd> + <kbd>J</kbd> {t('Slow down playback more')}</div>
<div><kbd>SHIFT</kbd> + <kbd>L</kbd> {t('Speed up playback more')}</div>

<h2>{t('Seeking')}</h2>

Expand Down
37 changes: 37 additions & 0 deletions src/util/rate-calculator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import clamp from 'lodash/clamp';

/**
* @constant {number}
* @default
* The default playback rate multiplier is used to adjust the current playback
* rate when no additional modifiers are applied. This is set to ∛2 so that striking
* the fast forward key (`l`) three times speeds playback up to twice the speed.
*/
export const DEFAULT_PLAYBACK_RATE = (2 ** (1 / 3));

/**
* Adjusts the current playback rate up or down
* @param {number} playbackRate current playback rate
* @param {number} direction positive for forward, negative for reverse
* @param {number} [multiplier] rate multiplier, defaults to {@link DEFAULT_PLAYBACK_RATE}
* @returns a new playback rate
*/
export function adjustRate(playbackRate, direction, multiplier) {
const m = multiplier || DEFAULT_PLAYBACK_RATE;
const factor = direction > 0 ? m : (1 / m);
let newRate = playbackRate * factor;
// If the multiplier causes us to go faster than real time or slower than real time,
// stop along the way at 1.0. This could happen if the current playbackRate was reached
// using a different multiplier (e.g., holding the shift key).
// https://github.com/mifi/lossless-cut/issues/447#issuecomment-766339083
if ((newRate > 1.0 && playbackRate < 1.0) || (newRate < 1.0 && playbackRate > 1.0)) {
newRate = 1.0;
}
// And, clean up any rounding errors that get us to almost 1.0 (e.g., treat 1.00001 as 1)
if ((newRate > (m ** (-1 / 2))) && (newRate < (m ** (1 / 2)))) {
newRate = 1.0;
}
return clamp(newRate, 0.1, 16);
}

export default adjustRate;
44 changes: 44 additions & 0 deletions src/util/rate-calculator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { adjustRate, DEFAULT_PLAYBACK_RATE } from './rate-calculator';

it('inverts for reverse direction', () => {
const r = adjustRate(1, -1, 2);
expect(r).toBeLessThan(1);
});

it('uses default rate', () => {
const r = adjustRate(1, 1);
expect(r).toBe(1 * DEFAULT_PLAYBACK_RATE);
});

it('allows multiplier override', () => {
const r = adjustRate(1, 1, Math.PI);
expect(r).toBe(1 * Math.PI);
});

describe('speeding up', () => {
it('sets rate to 1 if close to 1', () => {
expect(adjustRate(1 / DEFAULT_PLAYBACK_RATE + 0.01, 1)).toBe(1);
});

it('sets rate to 1 if passing 1 ', () => {
expect(adjustRate(0.5, 1, 2)).toBe(1);
});

it('will not play faster than 16', () => {
expect(adjustRate(15.999999, 1, 2)).toBe(16);
});
});

describe('slowing down', () => {
it('sets rate to 1 if close to 1', () => {
expect(adjustRate(DEFAULT_PLAYBACK_RATE + 0.01, -1)).toBe(1);
});

it('sets rate to 1 if passing 1', () => {
expect(adjustRate(1.1, -1, 2)).toBe(1);
});

it('will not play slower than 0.1', () => {
expect(adjustRate(0.1111, -1, 2)).toBe(0.1);
});
});

0 comments on commit 6cc18f1

Please sign in to comment.