Preserve exact source EOF across tempo changes and expose DSP lookahead
This commit is contained in:
1 parent
e8ee608ba9
commit
5d9da0c6b1
3 files changed
+67
-7
No files matched your search
@@ -42,9 +42,9 @@ struct stream_playback_audio {
|
||||
unsigned inputHead = 0, inputCount = 0, outputHead = 0, outputCount = 0;
|
||||
unsigned bufferFrames = 0;
|
||||
int tempo = 1000;
|
||||
int64_t startPts = 0, expectedPts = -1;
|
||||
int64_t startPts = 0, expectedPts = -1, endPts = 0; // endPts публикуется через preparedEnd.
|
||||
double expectedOutput = 0, producedOutput = 0, lastPts = 0;
|
||||
uint64_t written = 0;
|
||||
uint64_t written = 0, lastAudioFrame = 0; // Consumer: граница последнего PCM до возможной тишины.
|
||||
std::atomic<bool> finished{false}, failed{false}, preparedEnd{false};
|
||||
int16_t pcm[framesPerPacket * 2];
|
||||
float samples[maxFrames * 2];
|
||||
@@ -113,8 +113,9 @@ struct stream_playback_audio* stream_playback_audio_create(int64_t start, unsign
|
||||
a->stretch.setSetting(SETTING_USE_QUICKSEEK, 1);
|
||||
a->stretch.setSetting(SETTING_SEQUENCE_MS, 20); a->stretch.setSetting(SETTING_SEEKWINDOW_MS, 10);
|
||||
a->stretch.setSetting(SETTING_OVERLAP_MS, 4); a->stretch.setSetting(SETTING_USE_AA_FILTER, 0);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "playback audio created start=%lld stereo=1 tempo=1.0..1.6 PCM_buffer=%ums/%u_frames output=SPSC",
|
||||
(long long)start, bufferMs, a->bufferFrames);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_STREAM,
|
||||
"playback audio created start=%lld stereo=1 tempo=1.0..1.6 PCM_buffer=%ums/%u_frames DSP_latency_us=%lld output=SPSC",
|
||||
(long long)start, bufferMs, a->bufferFrames, (long long)stream_playback_audio_latency_us(a));
|
||||
return a;
|
||||
}
|
||||
|
||||
@@ -168,6 +169,10 @@ unsigned stream_playback_audio_buffered(const struct stream_playback_audio* a) {
|
||||
return (unsigned)(prepared > consumed ? prepared - consumed : 0);
|
||||
}
|
||||
|
||||
int64_t stream_playback_audio_latency_us(struct stream_playback_audio* a) {
|
||||
return a ? ((int64_t)a->stretch.getSetting(SETTING_INITIAL_LATENCY) * 1000000 + rate - 1) / rate : 0;
|
||||
}
|
||||
|
||||
/* DSP принадлежит producer. Каждый слот содержит PCM и его исходные PTS, включая смены темпа. */
|
||||
int stream_playback_audio_prepare(struct stream_playback_audio* a) {
|
||||
if (!a || a->failed) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "playback PCM prepare invalid state"); return -1; }
|
||||
@@ -193,6 +198,7 @@ int stream_playback_audio_prepare(struct stream_playback_audio* a) {
|
||||
a->failed = true; return -1;
|
||||
}
|
||||
double firstPts = mapped(span, position), lastPts = mapped(span, next);
|
||||
if (a->finished && i + 1 == a->inputCount && next == std::round(a->expectedOutput)) lastPts = span.ptsLast;
|
||||
slot.spans[slot.count++] = {position, next, firstPts, lastPts};
|
||||
a->preparedGain += std::max(0.0, lastPts - firstPts - (next - position) * 1000000.0 / rate);
|
||||
position = next;
|
||||
@@ -243,10 +249,12 @@ int stream_playback_audio_pull(struct stream_playback_audio* a, int16_t* pcm, un
|
||||
a->lastPts = lastPts; position = next;
|
||||
}
|
||||
a->producedOutput = end; a->written += got; done += got; a->readyOffset += got;
|
||||
a->lastAudioFrame = a->written;
|
||||
if (a->readyOffset == slot.frames) { a->readyOffset = 0; a->readyRead.store(++read, std::memory_order_release); }
|
||||
}
|
||||
a->consumedGain.store(consumedGain, std::memory_order_release);
|
||||
a->consumedFrames.store((uint64_t)a->producedOutput, std::memory_order_release);
|
||||
if (stream_playback_audio_empty(a)) a->lastPts = (double)a->endPts;
|
||||
return (int)done;
|
||||
}
|
||||
|
||||
@@ -262,12 +270,15 @@ int64_t stream_playback_audio_position(struct stream_playback_audio* a, uint64_t
|
||||
while (a->outputCount > 1 && a->output[a->outputHead].last <= played) {
|
||||
a->outputHead = (a->outputHead + 1) % mapCapacity; a->outputCount--;
|
||||
}
|
||||
// Округлённый PCM мог быть опубликован до finish; точный EOF передаётся отдельной immutable границей.
|
||||
if (stream_playback_audio_empty(a) && played >= a->lastAudioFrame) return a->endPts;
|
||||
if (!a->outputCount) return a->startPts;
|
||||
return (int64_t)std::llround(mapped(a->output[a->outputHead], (double)played));
|
||||
}
|
||||
|
||||
int64_t stream_playback_audio_pending_gain(struct stream_playback_audio* a, uint64_t played) {
|
||||
if (!a) return 0;
|
||||
if (stream_playback_audio_empty(a) && played >= a->lastAudioFrame) return 0;
|
||||
double gain = std::max(0.0, a->preparedGain - a->consumedGain.load(std::memory_order_acquire));
|
||||
for (unsigned i = 0; i < a->outputCount; i++) {
|
||||
const auto& span = a->output[(a->outputHead + i) % mapCapacity];
|
||||
@@ -275,7 +286,7 @@ int64_t stream_playback_audio_pending_gain(struct stream_playback_audio* a, uint
|
||||
if (span.last <= begin) continue;
|
||||
gain += std::max(0.0, span.ptsLast - mapped(span, begin) - (span.last - begin) * 1000000.0 / rate);
|
||||
}
|
||||
for (unsigned i = 0; i < a->inputCount; i++) {
|
||||
for (unsigned i = 0; !a->preparedEnd.load(std::memory_order_relaxed) && i < a->inputCount; i++) {
|
||||
const auto& span = a->input[(a->inputHead + i) % mapCapacity];
|
||||
double begin = std::max(span.first, a->preparedOutput);
|
||||
if (span.last <= begin) continue;
|
||||
@@ -299,6 +310,9 @@ int stream_playback_audio_starved(const struct stream_playback_audio* a, uint64_
|
||||
|
||||
void stream_playback_audio_finish(struct stream_playback_audio* a) {
|
||||
if (!a || a->finished) return;
|
||||
a->endPts = std::max(a->startPts, a->expectedPts);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "playback audio finish pts=%lld expected_frames=%.6f prepared_frames=%.0f",
|
||||
(long long)a->endPts, a->expectedOutput, a->preparedOutput);
|
||||
a->stretch.flush(); a->finished = true; stream_playback_audio_prepare(a);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ int stream_playback_audio_feed(struct stream_playback_audio *audio, int64_t pts_
|
||||
int stream_playback_audio_prepare(struct stream_playback_audio *audio);
|
||||
/* Потокобезопасный снимок готового PCM, в stereo frames (48000 frames/s). */
|
||||
unsigned stream_playback_audio_buffered(const struct stream_playback_audio *audio);
|
||||
/* Producer: внутренний lookahead DSP при текущем темпе; добавляется к guard устройства, без PCM target. */
|
||||
int64_t stream_playback_audio_latency_us(struct stream_playback_audio *audio);
|
||||
void stream_playback_audio_set_tempo(struct stream_playback_audio *audio, int tempo_milli);
|
||||
int stream_playback_audio_needs_input(const struct stream_playback_audio *audio);
|
||||
/* frames <=8192; limit_pts_us ограничивает исходный PTS, INT64_MAX разрешает весь подготовленный звук.
|
||||
|
||||
@@ -112,7 +112,8 @@ static void test_pcm_buffer() {
|
||||
CHECK(played == 9600);
|
||||
}
|
||||
CHECK(stream_playback_audio_needs_input(audio));
|
||||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "[PASS] PCM buffer=%ums worker_gap=40ms played=%u/1920 frames", bufferMs, played);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "[PASS] PCM buffer=%ums worker_gap=%ums played=%u frames",
|
||||
bufferMs, bufferMs == 250 ? 200u : 40u, played);
|
||||
stream_playback_audio_destroy(audio);
|
||||
}
|
||||
opus_codec_encoder_destroy(encoder);
|
||||
@@ -146,6 +147,49 @@ static void test_pcm_concurrent() {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "[PASS] independent PCM producer/consumer: stereo x1.6, 777-frame callbacks, exact EOF/PTS");
|
||||
}
|
||||
|
||||
/* Новый темп не меняет PTS уже приготовленного PCM; дробный конец не теряется при сменах темпа. */
|
||||
static void test_prepared_tempo_changes() {
|
||||
auto* encoder = opus_codec_encoder_create(48000, 2); CHECK(encoder);
|
||||
auto* audio = stream_playback_audio_create(0, 250); CHECK(audio);
|
||||
int16_t pcm[1920] = {}, output[1554]; uint8_t packet[1275];
|
||||
int length = opus_codec_encode(encoder, pcm, 960, packet, sizeof(packet)); CHECK(length > 0);
|
||||
stream_playback_audio_set_tempo(audio, 1600);
|
||||
unsigned packets = 0; double expected = 0;
|
||||
while (stream_playback_audio_needs_input(audio)) {
|
||||
CHECK(stream_playback_audio_feed(audio, packets++ * 20000, packet, length) == 0); expected += 600;
|
||||
}
|
||||
int64_t pending = stream_playback_audio_pending_gain(audio, 0);
|
||||
CHECK(pending > 140000);
|
||||
stream_playback_audio_set_tempo(audio, 1000);
|
||||
CHECK(stream_playback_audio_pending_gain(audio, 0) == pending);
|
||||
CHECK(stream_playback_audio_pull(audio, output, 480, INT64_MAX) == 480);
|
||||
CHECK(stream_playback_audio_position(audio, 480) == 16000);
|
||||
uint64_t heard = 480;
|
||||
int64_t previous = 16000;
|
||||
const int tempos[] = {1000, 1001, 1100, 1250, 1457, 1555, 1600};
|
||||
for (unsigned turn = 0; packets < 400 || !stream_playback_audio_empty(audio); ++turn) {
|
||||
CHECK(turn < 10000);
|
||||
if (packets < 400 && stream_playback_audio_needs_input(audio)) {
|
||||
int tempo = tempos[packets % 7]; stream_playback_audio_set_tempo(audio, tempo);
|
||||
CHECK(stream_playback_audio_feed(audio, packets++ * 20000, packet, length) == 0);
|
||||
expected += 960000.0 / tempo;
|
||||
if (packets == 400) stream_playback_audio_finish(audio);
|
||||
}
|
||||
CHECK(stream_playback_audio_prepare(audio) == 0);
|
||||
int count = stream_playback_audio_pull(audio, output, 777, INT64_MAX); CHECK(count >= 0);
|
||||
heard += count;
|
||||
int64_t position = stream_playback_audio_position(audio, heard);
|
||||
CHECK(position >= previous && position <= 8000000); previous = position;
|
||||
CHECK(stream_playback_audio_pending_gain(audio, heard) >= 0);
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "variable tempo EOF frames=%llu expected=%.6f source_pts=%lld pending_gain=%lld",
|
||||
(unsigned long long)heard, expected, (long long)previous, (long long)stream_playback_audio_pending_gain(audio, heard));
|
||||
CHECK(heard == (uint64_t)std::llround(expected) && previous == 8000000);
|
||||
CHECK(stream_playback_audio_pending_gain(audio, heard) <= 1);
|
||||
stream_playback_audio_destroy(audio); opus_codec_encoder_destroy(encoder);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "[PASS] prepared PCM preserves old tempo; variable x1.0..1.6 has exact duration/EOF and monotonic PTS");
|
||||
}
|
||||
|
||||
/* PTS из Windows→Android лога: звук начинается на 428ms позже KEY, audio clock ещё неподвижен. */
|
||||
static void test_late_audio_start() {
|
||||
constexpr int64_t videoPts = 20669261, audioPts = 21097059;
|
||||
@@ -200,7 +244,7 @@ static void test_late_audio_start() {
|
||||
int main() {
|
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_STREAM, DEBUG_LEVEL_INFO);
|
||||
size_t allocations = u_get_allocated_count();
|
||||
test_stereo_tempo(); test_silence_clock(); test_pcm_buffer(); test_pcm_concurrent(); test_late_audio_start();
|
||||
test_stereo_tempo(); test_silence_clock(); test_pcm_buffer(); test_pcm_concurrent(); test_prepared_tempo_changes(); test_late_audio_start();
|
||||
CHECK(u_get_allocated_count() == allocations);
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user