feat: adopt IINA-style playback window

This commit is contained in:
2026-10-10 01:35:49 +02:00
parent df2456f559
commit 462cb52eb2
8 changed files with 654 additions and 76 deletions
+41 -3
View File
@@ -244,6 +244,12 @@ struct Output::Impl {
AudioUnit unit = nullptr;
std::array<float, capacity * 2> samples{};
std::atomic<uint64_t> read{0}, written{0};
static_assert(std::atomic<float>::is_always_lock_free);
std::atomic<float> targetGain{1};
// Callback-owned while running; start initializes gain before callbacks begin.
double currentGain = 1, gainStep = 0;
float rampTarget = 1;
size_t rampRemaining = 0;
double origin = 0;
double latency = 0;
std::atomic<uint64_t> clockVersion{0};
@@ -260,9 +266,31 @@ struct Output::Impl {
uint64_t w = self.written.load(std::memory_order_acquire);
size_t frames = std::min<uint64_t>(count, w - r);
float *destination = static_cast<float *>(buffers->mBuffers[0].mData);
size_t first = std::min(frames, capacity - size_t(r % capacity));
std::memcpy(destination, self.samples.data() + (r % capacity) * 2, first * 2 * sizeof(float));
std::memcpy(destination + first * 2, self.samples.data(), (frames - first) * 2 * sizeof(float));
float target = self.targetGain.load(std::memory_order_relaxed);
if (target != self.rampTarget) {
self.rampTarget = target;
self.rampRemaining = 240;
self.gainStep = (target - self.currentGain) / 240;
}
// Stable mute consumes the ring normally, leaving the zeroed output alone.
if (self.currentGain != 0 || target != 0) {
size_t first = std::min(frames, capacity - size_t(r % capacity));
std::memcpy(destination, self.samples.data() + (r % capacity) * 2, first * 2 * sizeof(float));
std::memcpy(destination + first * 2, self.samples.data(), (frames - first) * 2 * sizeof(float));
size_t frame = 0;
while (frame < frames && self.rampRemaining) {
self.currentGain += self.gainStep;
if (--self.rampRemaining == 0) self.currentGain = target;
float gain = static_cast<float>(self.currentGain);
destination[frame * 2] *= gain;
destination[frame * 2 + 1] *= gain;
++frame;
}
// Unity is bit-identical to the decoded PCM; do not scale underrun silence.
if (self.currentGain != 1)
for (size_t sample = frame * 2; sample < frames * 2; ++sample)
destination[sample] *= static_cast<float>(self.currentGain);
}
#ifdef PLAYER_AUDIO_CHECK
player_check_audio(destination, count);
#endif
@@ -345,6 +373,10 @@ bool Output::configure_audio(std::string &error) {
bool Output::start(std::string &error) {
if (!impl->unit) return true; // Video-only playback does not open an audio device.
if (impl->running) return true;
impl->rampTarget = impl->targetGain.load(std::memory_order_relaxed);
impl->currentGain = impl->rampTarget;
impl->rampRemaining = 0;
impl->gainStep = 0;
OSStatus status = AudioOutputUnitStart(impl->unit);
if (status != noErr) { error = "Could not start Core Audio (" + std::to_string(status) + ")"; return false; }
impl->running = true;
@@ -357,6 +389,12 @@ void Output::pause() {
impl->running = false;
impl->frozen.store(position, std::memory_order_release);
}
void Output::set_volume(float normalized0to1) noexcept {
impl->targetGain.store(normalized0to1, std::memory_order_relaxed);
}
float Output::volume() const noexcept {
return impl->targetGain.load(std::memory_order_relaxed);
}
void Output::reset() {
impl->read.store(0); impl->written.store(0);
impl->origin = 0;