@@ -124,6 +124,7 @@ auto EnableWideStereo = false;
124124auto EnableUhj = false ;
125125auto EnableSuperStereo = false ;
126126auto DisableVideo = false ;
127+ auto TimeStretchFactor = 1 .0f ;
127128
128129constexpr auto AVNoSyncThreshold = seconds{10 };
129130
@@ -417,6 +418,8 @@ struct AudioState {
417418 ALenum mFormat {AL_NONE };
418419 ALuint mFrameSize {0 };
419420
421+ ALuint mPShiftSlot {};
422+
420423 std::mutex mSrcMutex ;
421424 std::condition_variable mSrcCond ;
422425 std::atomic_flag mConnected ;
@@ -432,6 +435,8 @@ struct AudioState {
432435 alDeleteSources (1 , &mSource );
433436 if (mBuffers [0 ])
434437 alDeleteBuffers (gsl::narrow_cast<ALsizei>(mBuffers .size ()), mBuffers .data ());
438+ if (mPShiftSlot )
439+ alDeleteAuxiliaryEffectSlots (1 , &mPShiftSlot );
435440
436441 av_freep (static_cast <void *>(mSamples .data ()));
437442 }
@@ -563,7 +568,11 @@ auto AudioState::getClockNoLock() const -> nanoseconds
563568 * keep going.
564569 */
565570 if (mEndTime > nanoseconds::min ())
566- return std::chrono::steady_clock::now ().time_since_epoch () - mEndTime + mCurrentPts ;
571+ {
572+ auto const rtdiff = std::chrono::steady_clock::now ().time_since_epoch () - mEndTime ;
573+ auto const diff = duration_cast<seconds_d64>(rtdiff) * TimeStretchFactor;
574+ return duration_cast<nanoseconds>(diff) + mCurrentPts ;
575+ }
567576
568577 /* This more safely converts fixed32 to nanoseconds, avoiding overflow
569578 * unlike a normal duration_cast call.
@@ -1322,10 +1331,65 @@ void AudioState::handler()
13221331 if (ambi_order > 1 )
13231332 {
13241333 std::ranges::for_each (mBuffers , [ambi_order](const ALuint bufid)
1325- { alBufferi (bufid, AL_UNPACK_AMBISONIC_ORDER_SOFT , gsl::narrow_cast <int >(ambi_order)); });
1334+ { alBufferi (bufid, AL_UNPACK_AMBISONIC_ORDER_SOFT , static_cast <int >(ambi_order)); });
13261335 }
13271336 if (EnableSuperStereo)
13281337 alSourcei (mSource , AL_STEREO_MODE_SOFT , AL_SUPER_STEREO_SOFT );
1338+ if (TimeStretchFactor != 1 .0f )
1339+ {
1340+ /* AL_PITCH alone will speed up or slow down playback and alter the
1341+ * pitch (e.g. 2.0 will play twice as fast, 12 semitones higher).
1342+ * AL_EFFECT_PITCH_SHIFTER can be used to adjust the sound back to its
1343+ * original pitch while maintaining the speed change.
1344+ */
1345+
1346+ /* Calculate the tuning (in semitones and cents) to counteract the
1347+ * pitch change from AL_PITCH.
1348+ */
1349+ auto const tune = static_cast <int >(std::lround (std::log2 (TimeStretchFactor) * -1200 .0f ));
1350+ auto const [course_tune, fine_tune] = std::invoke ([tune]() -> std::pair<int , int >
1351+ {
1352+ auto const course = tune / 100 ;
1353+ auto const fine = tune % 100 ;
1354+ /* Fine-tuning is limited to -50...+50, so adjust values as needed
1355+ * (e.g. tune=-590: course=-5, fine=-90 -> course=-6, fine=10).
1356+ */
1357+ if (fine > 50 )
1358+ return {course+1 , fine-100 };
1359+ if (fine < -50 )
1360+ return {course-1 , fine+100 };
1361+ return {course, fine};
1362+ });
1363+ fmt::println (" Time stretch: {} (course tuning: {}, fine tuning: {})" , TimeStretchFactor,
1364+ course_tune, fine_tune);
1365+
1366+ /* Filter to mute the dry (un-corrected) path. */
1367+ auto mutefilter = ALuint{};
1368+ alGenFilters (1 , &mutefilter);
1369+ alFilteri (mutefilter, AL_FILTER_TYPE , AL_FILTER_LOWPASS );
1370+ alFilterf (mutefilter, AL_LOWPASS_GAIN , 0 .0f );
1371+
1372+ /* Pitch shifter effect to correct the pitch after AL_PITCH adjust,emt. */
1373+ auto effect = ALuint{};
1374+ alGenEffects (1 , &effect);
1375+ alEffecti (effect, AL_EFFECT_TYPE , AL_EFFECT_PITCH_SHIFTER );
1376+ alEffecti (effect, AL_PITCH_SHIFTER_COARSE_TUNE , course_tune);
1377+ alEffecti (effect, AL_PITCH_SHIFTER_FINE_TUNE , fine_tune);
1378+ if (auto const err = alGetError (); err != AL_NO_ERROR )
1379+ fmt::print (" alGetError: {}\n " , err);
1380+ else
1381+ {
1382+ alGenAuxiliaryEffectSlots (1 , &mPShiftSlot );
1383+ alAuxiliaryEffectSloti (mPShiftSlot , AL_EFFECTSLOT_EFFECT , static_cast <ALint>(effect));
1384+
1385+ alSourcef (mSource , AL_PITCH , TimeStretchFactor);
1386+ alSourcei (mSource , AL_DIRECT_FILTER , static_cast <ALint>(mutefilter));
1387+ alSource3i (mSource , AL_AUXILIARY_SEND_FILTER , static_cast <ALint>(mPShiftSlot ), 0 ,
1388+ AL_FILTER_NULL );
1389+ }
1390+ alDeleteEffects (1 , &effect);
1391+ alDeleteFilters (1 , &mutefilter);
1392+ }
13291393
13301394 if (alGetError () != AL_NO_ERROR )
13311395 return ;
@@ -1388,7 +1452,7 @@ void AudioState::handler()
13881452 * buffered. Prerolling would be better here, but short of that, this
13891453 * will do.
13901454 */
1391- const auto start_delay = round<seconds>(AudioBufferTotalTime/2
1455+ const auto start_delay = round<seconds>(AudioBufferTotalTime/2 * TimeStretchFactor
13921456 * mCodecCtx ->sample_rate ).count ();
13931457 alSourcei (mSource , AL_SAMPLE_OFFSET , -gsl::narrow_cast<int >(start_delay));
13941458 }
@@ -1495,8 +1559,8 @@ auto VideoState::getClock() -> nanoseconds
14951559 auto displock = std::lock_guard{mDispPtsMutex };
14961560 if (mDisplayPtsTime == microseconds::min ())
14971561 return nanoseconds::zero ();
1498- auto delta = get_avtime () - mDisplayPtsTime ;
1499- return mDisplayPts + delta;
1562+ auto const delta = duration_cast<seconds_d64>( get_avtime () - mDisplayPtsTime ) ;
1563+ return duration_cast<nanoseconds>( mDisplayPts + delta*TimeStretchFactor) ;
15001564}
15011565
15021566/* Called by VideoState::updateVideo to display the next video frame. */
@@ -1952,7 +2016,8 @@ auto MovieState::getClock() const -> nanoseconds
19522016{
19532017 if (mClockBase == microseconds::min ())
19542018 return nanoseconds::zero ();
1955- return get_avtime () - mClockBase ;
2019+ auto const diff = duration_cast<seconds_d64>(get_avtime () - mClockBase );
2020+ return duration_cast<nanoseconds>(diff * TimeStretchFactor);
19562021}
19572022
19582023auto MovieState::getMasterClock () -> nanoseconds
@@ -2121,6 +2186,7 @@ auto main(std::span<std::string_view> args) -> int
21212186 fmt::println (std::cerr, " \n Options:\n "
21222187 " -gain <g> Set audio playback gain (prepend +/- or append \" dB\" to \n "
21232188 " indicate decibels, otherwise it's linear amplitude)\n "
2189+ " -tstretch <s> Set playback speed factor (with pitch correction)"
21242190 " -novideo Disable video playback\n "
21252191 " -direct Play audio directly on the output, bypassing virtualization\n "
21262192 " -superstereo Apply Super Stereo processing to stereo tracks\n "
@@ -2266,6 +2332,45 @@ auto main(std::span<std::string_view> args) -> int
22662332 }
22672333 continue ;
22682334 }
2335+ if (argval == " -tstretch" )
2336+ {
2337+ if (curarg+1 == args_end)
2338+ fmt::println (std::cerr, " Missing argument for -tstretch" );
2339+ else
2340+ {
2341+ const auto optarg = *++curarg;
2342+
2343+ auto endpos = size_t {};
2344+ const auto stretchval = std::invoke ([optarg, &endpos]
2345+ {
2346+ try { return std::stof (std::string{optarg}, &endpos); }
2347+ catch (std::exception &e) {
2348+ fmt::println (std::cerr, " Exception reading tstretch value: {}" , e.what ());
2349+ }
2350+ return std::numeric_limits<float >::quiet_NaN ();
2351+ });
2352+ /* Time stretching is limited to [0.5, 2.0] because the pitch
2353+ * shifter effect is limited to [-12, +12] semitones.
2354+ */
2355+ if (!(stretchval >= 0 .5f && stretchval <= 2 .0f ) || endpos != optarg.size ())
2356+ fmt::println (std::cerr, " Invalid tstretch value: {}" , optarg);
2357+ else if (!alcIsExtensionPresent (almgr.mDevice , " ALC_EXT_EFX" ))
2358+ fmt::println (std::cerr, " EFX not supported for time stretching" );
2359+ else
2360+ {
2361+ /* Make sure the pitch shifter effect works. */
2362+ auto effect = ALuint{};
2363+ alGenEffects (1 , &effect);
2364+ alEffecti (effect, AL_EFFECT_TYPE , AL_EFFECT_PITCH_SHIFTER );
2365+ if (auto const err = alGetError (); err != AL_NO_ERROR )
2366+ fmt::println (std::cerr, " Pitch Shifter not supported for time stretching" );
2367+ else
2368+ TimeStretchFactor = stretchval;
2369+ alDeleteEffects (1 , &effect);
2370+ }
2371+ }
2372+ continue ;
2373+ }
22692374 break ;
22702375 }
22712376
0 commit comments