You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
169 lines
7.9 KiB
169 lines
7.9 KiB
// Настоящий SoundTouch: сверяем PCM с картой источников и проверяем обе границы переключения. |
|
#include "call/call_delay.h" |
|
#include "call/call_timeline.h" |
|
#include "debug_config.h" |
|
#include <algorithm> |
|
#include <cmath> |
|
#include <cstdlib> |
|
#include <deque> |
|
#include <vector> |
|
|
|
#define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "delay test line=%d: %s", __LINE__, #x); std::exit(1); } } while (0) |
|
|
|
static int16_t signal(uint64_t index) { |
|
// Непериодическая последовательность различает соседние сэмплы и ошибки на границе. |
|
uint64_t v = index * 6364136223846793005ULL + 1442695040888963407ULL; |
|
return (int16_t)((int)(v >> 48) - 32768); |
|
} |
|
|
|
struct Trial { |
|
CallTimeline timeline; |
|
CallDelay delay; |
|
uint64_t available = 0, supplied = 0, generated = 0, device = 0, pulls = 0; |
|
uint64_t boundaries = 0, previousEnd = 0, lastEntered = 0, lastExited = 0; |
|
soundtouch::SourceKind previousKind = soundtouch::SourceKind::Copy; |
|
std::deque<int16_t> expected; |
|
int64_t previousPts = -1; |
|
|
|
Trial() : delay(read, this, source, this) { timeline.reset(1); CHECK(delay.valid()); } |
|
|
|
static unsigned read(void* context, int16_t* pcm, unsigned capacity) { |
|
auto& t = *static_cast<Trial*>(context); |
|
unsigned n = (unsigned)std::min<uint64_t>(t.available, capacity); |
|
if (!n) return 0; |
|
t.timeline.feed(1000000 + (int64_t)t.supplied * 1000000 / 48000, n, 9); |
|
for (unsigned i = 0; i < n; ++i) pcm[i] = signal(t.supplied + i); |
|
t.supplied += n; t.available -= n; |
|
return n; |
|
} |
|
|
|
static void source(void* context, const soundtouch::SourceSpan& s) { |
|
using soundtouch::SourceKind; |
|
auto& t = *static_cast<Trial*>(context); |
|
CHECK(s.kind == SourceKind::Copy || s.kind == SourceKind::Mix); |
|
CHECK(s.output == t.generated && s.count > 0); |
|
CHECK(s.input0 + s.count <= t.supplied); |
|
if (s.kind == SourceKind::Mix) CHECK(s.input1 + s.count <= t.supplied); |
|
auto state = t.delay.stats(); |
|
if (state.entered != t.lastEntered || state.exited != t.lastExited) { |
|
CHECK(t.previousKind == SourceKind::Copy && s.kind == SourceKind::Copy); |
|
CHECK(s.input0 == t.previousEnd); // Нет пропуска или повторного PCM именно на переходе. |
|
++t.boundaries; |
|
t.lastEntered = state.entered; t.lastExited = state.exited; |
|
} |
|
for (unsigned i = 0; i < s.count; ++i) { |
|
double value = signal(s.input0 + i); |
|
if (s.kind == SourceKind::Mix) value += (signal(s.input1 + i) - value) * i / s.count; |
|
t.expected.push_back((int16_t)std::lround(value)); |
|
} |
|
t.previousKind = s.kind; t.previousEnd = s.input0 + s.count; |
|
t.generated += s.count; |
|
t.timeline.source(s); |
|
} |
|
|
|
unsigned pull(unsigned count, unsigned reserve = 960, bool full = true) { |
|
std::vector<int16_t> pcm(count); |
|
unsigned n = delay.pull(pcm.data(), count, available, reserve); |
|
CHECK(delay.valid()); |
|
if (full) CHECK(n == count); |
|
for (unsigned i = 0; i < n; ++i) { |
|
CHECK(!expected.empty()); |
|
CHECK(std::abs((int)pcm[i] - expected.front()) <= 1); |
|
expected.pop_front(); |
|
} |
|
timeline.read(n); device += n; ++pulls; |
|
// Прогресс реального устройства запаздывает; карта обязана пережить смену DSP до доигрывания его PCM. |
|
int64_t now = (int64_t)device * 1000000 / 48000; |
|
timeline.progress(1, device, now + 20000, now, true); |
|
auto clock = timeline.clock(now); |
|
if (device > 1920) { |
|
CHECK(clock.valid && clock.epoch == 1 && clock.source_epoch == 9); |
|
CHECK(clock.pts_us >= previousPts); previousPts = clock.pts_us; |
|
} |
|
return n; |
|
} |
|
}; |
|
|
|
static void steady(unsigned block, unsigned phase) { |
|
Trial t; |
|
t.available = 60 * 960; |
|
uint64_t next = phase; |
|
for (uint64_t time = 0; time < 60 * 48000; time += block) { |
|
while (next <= time) { t.available += 960; next += 960; } |
|
t.pull(block); |
|
} |
|
auto s = t.delay.stats(); |
|
CHECK(s.mode == CallDelay::Mode::Direct && s.entered && s.exited && t.boundaries >= 2); |
|
CHECK(s.phase == 0 && s.tempo == 1); |
|
CHECK(t.available + s.pending < 960 * 4); |
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS delay steady block=%u phase=%u boundaries=%llu cuts=%llu pending=%.2fms aligned=%u", |
|
block, phase, (unsigned long long)t.boundaries, (unsigned long long)s.cuts, (t.available + s.pending) / 48, s.phase); |
|
} |
|
|
|
static void switching() { |
|
Trial t; |
|
for (unsigned cycle = 0; cycle < 100; ++cycle) { |
|
t.available += 20 * 960; |
|
// Входной избыток включает SoundTouch; рост сетевого резерва вызывает выход даже посреди блока. |
|
for (unsigned tick = 0; tick < 80; ++tick) { |
|
if (!(tick % 2)) t.available += 960; |
|
unsigned reserve = tick < 40 ? 960 : (unsigned)(t.available + t.delay.stats().pending + 960); |
|
t.pull(480, reserve); |
|
} |
|
CHECK(t.delay.stats().mode == CallDelay::Mode::Direct); |
|
} |
|
CHECK(t.boundaries >= 200); |
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS delay repeated switching boundaries=%llu", (unsigned long long)t.boundaries); |
|
} |
|
|
|
static void tail_and_reset() { |
|
Trial t; t.available = 100 * 960; |
|
while (t.pull(777, 960, false)) {} |
|
CHECK(t.expected.empty()); |
|
CHECK(t.previousKind == soundtouch::SourceKind::Copy && t.previousEnd == t.supplied); |
|
CHECK(t.delay.stats().pending == 0); |
|
t.delay.reset(); |
|
CHECK(t.delay.stats().pending == 0 && t.delay.stats().cuts == 0 && t.delay.stats().phase == 0); |
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS delay final tail preserved without flush, reset clears phase/history"); |
|
} |
|
|
|
static void phase_after_silence() { |
|
for (unsigned block : {240, 480, 777, 960}) for (unsigned silence : {1, 239, 480, 777, 959}) { |
|
Trial t; |
|
t.timeline.silence(silence); t.delay.silence(silence); t.device += silence; |
|
t.available = 5 * 960; // Прямой режим без SoundTouch; стартовая фаза произвольна. |
|
uint64_t next = 960; |
|
for (uint64_t time = 0; time < 20 * 48000; time += block) { |
|
while (next <= time) { t.available += 960; next += 960; } |
|
t.pull(block); |
|
} |
|
auto s = t.delay.stats(); |
|
CHECK(!s.entered && !s.exited && s.cuts > 0 && !s.phase && s.tempo == 1); |
|
} |
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS delay phase: 20 arbitrary initial silence/block combinations converge using cuts only"); |
|
} |
|
|
|
static void changing_blocks() { |
|
Trial t; |
|
const unsigned blocks[] = {1, 191, 192, 193, 575, 576, 577, 768, 960, 1920, 8192}; |
|
uint64_t next = 0, time = 0; |
|
t.available = 48000; |
|
for (unsigned tick = 0; tick < 3000; ++tick) { |
|
unsigned block = blocks[tick % (sizeof(blocks) / sizeof(blocks[0]))]; |
|
while (next <= time) { t.available += 960; next += 960; } |
|
// Пауза устройства на каждый новый цикл создаёт избыток, затем резерв заставляет выйти из DSP. |
|
if (!(tick % 50)) t.available += 48000; |
|
unsigned reserve = tick % 50 < 25 ? 960 : (unsigned)(t.available + t.delay.stats().pending + 960); |
|
t.pull(block, reserve); |
|
time += block; |
|
} |
|
CHECK(t.boundaries >= 120); |
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS delay changing blocks 1..8192 samples, boundaries=%llu", (unsigned long long)t.boundaries); |
|
} |
|
|
|
int main() { |
|
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); |
|
for (unsigned block : {240, 480, 777, 960}) for (unsigned phase : {0, 239, 479, 719, 959}) steady(block, phase); |
|
switching(); tail_and_reset(); phase_after_silence(); changing_blocks(); |
|
return 0; |
|
}
|
|
|