Skip to content

Commit 0a3870f

Browse files
committed
Add a time stretch option (-tstretch) to alffplay
1 parent 94d9f3b commit 0a3870f

1 file changed

Lines changed: 111 additions & 6 deletions

File tree

examples/alffplay.cpp

Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ auto EnableWideStereo = false;
124124
auto EnableUhj = false;
125125
auto EnableSuperStereo = false;
126126
auto DisableVideo = false;
127+
auto TimeStretchFactor = 1.0f;
127128

128129
constexpr 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

19582023
auto 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

Comments
 (0)