joeylib2/tests/gate/audioGate.c

2228 lines
80 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// audioGate.c - grade the audio the audio gates record, and turn the raw
// PCM their recorders leave into WAV files.
//
// audioGate phases LABEL WAV PHASE [PHASE ...]
// audioGate conformance PORT LOG WAV [--stereo] [--skip NAME ...] [--level-tolerance DB]
// audioGate psp LABEL WAV QUIT_SECONDS
// audioGate wav RAW OUT.WAV RATE [SKIP]
// audioGate onset RAW THRESHOLD
//
// phases grades the AUDIO example through phases of known loudness
// (scripts/verify-{amiga,dos,dvx,sdl3,web,win16,win32}-audio.sh): each
// PHASE is name:startSec:endSec:minRms:maxRms, either bound left empty to
// skip it; exit 0 when every phase is inside its bounds, 1 when one is not
// or the file is unusable, 2 on a usage error. conformance grades a
// recording of AUDPAR against the audio contract
// (scripts/verify-audio-conformance.sh; the comment above confRun says
// how). psp grades what AUDIOPRB played on the PSP
// (scripts/verify-psp-audio.sh; the comment above pspRun says how). wav
// writes raw signed 16-bit stereo PCM, from byte SKIP on and cut to whole
// frames, as a WAV at RATE; onset prints the byte offset of the first
// stereo frame of raw PCM holding a sample louder than THRESHOLD, or
// nothing.
//
// The graders replace tools/gradeAudioPhases.py,
// tools/gradeAudioConformance.py, and scripts/lib/pspAudioGrade.py, and
// measure as they did, so a recording grades as it always has: numpy's
// sums, Hann window, and bin frequencies are computed in numpy's order
// (gateFft), and scipy's Welch estimate in scipy's.
#define _DEFAULT_SOURCE
#include <ctype.h>
#include <errno.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "gateArg.h"
#include "gateFft.h"
#include "gateText.h"
#include "gateWav.h"
#include "toolBuf.h"
#include "toolFile.h"
#define TOOL_NAME "audioGate"
#define FAIL_EXIT 1
#define COMMAND_ARG 1
#define FIRST_ARG 2
// phases: LABEL WAV PHASE..., each phase name:start:end:minRms:maxRms.
#define PHASES_ARGS_MIN 5
#define PHASE_FIELDS 5u
#define PHASE_NAME 0u
#define PHASE_START 1u
#define PHASE_END 2u
#define PHASE_MIN 3u
#define PHASE_MAX 4u
#define RECORDING_MIN_S 1.0
// psp: examples/audio/audio.c's probe timetable and streamed tone.
#define PSP_ARGS 5
#define FRAME_HZ 60
#define PROBE_STREAM_FIRST 30
#define PROBE_STREAM_FRAMES 90
#define STREAM_RATE_HZ 8000
#define STREAM_BLOCK 256
#define STREAM_BLOCKS 48
#define STREAM_START_HZ 330
#define STREAM_DROP_HZ 220
#define STREAM_TAKE_HZ 55
#define TAKES 3
// The music starts when its first sample rises above ONSET_LEVEL.
#define ONSET_LEVEL 200.0
#define MUSIC_WINDOW_START 0.05
#define MUSIC_WINDOW_END 0.40
#define MUSIC_RMS_MIN 1000
#define MUSIC_RMS_MAX 8000
// A take is measured from TAKE_WINDOW_START seconds after it is due: far
// enough in to miss the audio queue's latency, early in its fall.
#define TAKE_WINDOW_START 0.10
#define TAKE_WINDOW_END 0.20
#define TAKE_MIN_RMS 15000
#define TAKE_PITCH_TOLERANCE 0.10
#define END_SLACK 0.5
#define CROSSINGS_PER_CYCLE 2.0
// conformance: gradeAudioConformance.py's constants, by its names.
#define SYNC_HZ 1000.0
#define SYNC_BAND_HZ 60.0
#define FRAME_S 0.010
#define WINDOW_S 0.040
#define SYNC_SHARE 0.5
#define SYNC_MIN_DBFS -45.0
#define SYNC_RUN_FRAMES 10
#define SYNC_LAST_SHARE 0.8
#define SYNC_SCALE_TOLERANCE 0.05
#define MARGIN_S 0.060
#define PITCH_LOW_HZ 40.0
#define PITCH_HIGH_HZ 8000.0
#define PITCH_MIN_SAMPLES 256u
#define PITCH_PAD_SHIFT 2
#define PITCH_TOLERANCE 0.01
#define LOG_FLOOR 1e-12
#define LEVEL_STEP_DB 2.0
#define LEVEL_TOLERANCE 1.0
#define NOISE_LEVEL_TOLERANCE 1.5
#define VOICE_MATCH_DB 3.0
#define TONE_MATCH_DB 4.0
#define VOICE_PITCH_ATTEN_MAX 8
#define VOICE_ATTEN_MAX 14
#define VOICE_OTHER_FIRST 1
#define VOICE_OTHER_LAST 2
#define NOISE_RATE_HZ 125000.0
#define NOISE_TOLERANCE 0.15
#define NOISE_FIT_LIMIT 0.45
#define NULL_SEARCH_LOW 0.6
#define NULL_SEARCH_HIGH 1.6
#define NULL_LEVEL_LOW 0.2
#define NULL_DEPTH_DB 6.0
#define NULL_SMOOTH 5
#define NULL_SEARCH_MIN 3u
#define NOISE_GRADED_HZ 4100.0
#define SILENCE_DBFS -55.0
#define SOUND_DBFS -45.0
#define PAN_APART_DB 12.0
#define PAN_CENTRE_DB 3.0
#define CENTRE_CHECK_HZ 440.0
#define NOISE_LOUDNESS_PITCH 31
#define MOD_END_SLACK_S 0.6
#define MOD_STOP_LOOK_S 0.2
#define MOD_END_MAX_S 11.5
#define MOD_LISTEN_S 1.0
#define SFX_LATENCY_MAX 0.050
#define ONSET_SEARCH_S 1.0
#define PURE_BAND 0.10
#define RAW_PURE_SHARE 0.75
#define ONSET_BAND 0.08
#define ONSET_SHARE 0.5
#define MOD_LEAD_S 0.1
#define TICK_SHARE 0.03
#define TICK_SLACK 0.25
#define TICK_MIN 1.0
#define FULL_SCALE 32768.0
#define RMS_FLOOR 1e-9
#define LEVEL_FRAME_S 0.020
#define SFX_SAMPLES 4000
#define SAMPLE_FULL_SCALE 127.0
#define BALANCE_RATE "8000"
#define MOD_VOICE_DB 0.0
#define MOD_LEVEL_S 2.0
#define DB_PER_DECADE 20.0
#define POWER_DB_PER_DECADE 10.0
#define WELCH_SEGMENT_MAX 2048u
#define MS_PER_SECOND 1000.0
#define PERCENT 100.0
#define PAN_LEFT 0
#define PAN_RIGHT 2
// Where each AUDPAR line's values are: "tone HZ", "sync HZ",
// "voice N HZ ATTEN", "noise PITCH ATTEN", "sfx N RATE HZ PAN [SAMPLES]",
// "stream N RATE HZ PAN RAW [SAMPLES]", "mod start|end|stop|loop [PLAYING]",
// "tick HZ OK CALLS MS".
#define TONE_HZ_AT 0u
#define VOICE_NUMBER_AT 0u
#define VOICE_HZ_AT 1u
#define VOICE_ATTEN_AT 2u
#define NOISE_PITCH_AT 0u
#define NOISE_ATTEN_AT 1u
#define SAMPLE_RATE_AT 1u
#define SAMPLE_HZ_AT 2u
#define SAMPLE_PAN_AT 3u
#define SFX_SAMPLES_AT 4u
#define STREAM_RAW_AT 4u
#define STREAM_SAMPLES_AT 5u
#define MOD_WHAT_AT 0u
#define MOD_PLAYING_AT 1u
#define TICK_HZ_AT 0u
#define TICK_OK_AT 1u
#define TICK_CALLS_AT 2u
#define TICK_MS_AT 3u
#define CHANNEL_MIX (-1)
#define CHANNEL_LEFT 0
#define CHANNEL_RIGHT 1
#define CONF_POSITIONALS 3u
// The lowest rate graded: a FRAME_S hop of at least one sample.
#define MIN_RATE_HZ 100u
#define LOG_TAG "AUDPAR "
#define LIST_FIRST_CAP 16u
// raw PCM: signed 16-bit stereo.
#define RAW_CHANNELS 2u
#define SAMPLE_BYTES 2u
#define RAW_FRAME_BYTES (RAW_CHANNELS * SAMPLE_BYTES)
#define WAV_ARGS_MIN 5
#define WAV_ARGS_MAX 6
#define ONSET_ARGS 4
#define BYTE_BITS 8u
#define TEXT_MAX 40u
// Samples cut out of a recording (malloc'd).
typedef struct SamplesT {
double *x;
size_t n;
} SamplesT;
// An AUDPAR log line: "AUDPAR <ms> <kind> <values>".
typedef struct EventT {
double time;
char *kind;
char **values;
size_t valueCount;
size_t valueCap;
} EventT;
typedef struct EventsT {
EventT *items;
size_t count;
size_t cap;
} EventsT;
// A list of (first, second, value), appended to, or used as a dictionary
// keyed by (first, second) that keeps its keys in the order they came.
typedef struct EntryT {
long first;
long second;
double value;
} EntryT;
typedef struct EntriesT {
EntryT *items;
size_t count;
size_t cap;
} EntriesT;
// A conformance grading: the arguments, the recording, the timeline's map
// onto it, the transforms (the window's, shared by the sync and onset
// searches, and the last of each other length), and what the steps have
// measured so far.
typedef struct ConfT {
const char *port;
const char *logPath;
const char *wavPath;
const char **skips;
size_t skipCount;
size_t skipCap;
double levelTolerance;
bool stereo;
uint32_t passed;
uint32_t graded;
uint32_t failed;
ToolBufT name;
double *data;
double *mono;
size_t frames;
uint32_t channels;
uint32_t rate;
EventsT events;
double t0;
double scale;
size_t window;
size_t hop;
GateFftT windowFft;
GateFftT pitchFft;
GateFftT purityFft;
GateFftT welchFft;
double *taper;
double *freqs;
double *bins;
double *scratch;
double lag;
bool havePlainPure;
double plainPure;
EntriesT toneLevels;
EntriesT voiceLevels;
EntriesT noiseLevels;
EntriesT noiseCentroids;
bool haveBalanceVoice;
double balanceVoice;
EntriesT balanceSfx;
bool haveBalanceMod;
double balanceMod;
} ConfT;
typedef int (*CommandFnT)(int argc, char **argv);
// A subcommand.
typedef struct CommandT {
const char *name;
CommandFnT run;
} CommandT;
static void *arrayRoom(void *items, size_t count, size_t *cap, size_t size);
static void bandOf(const double *freqs, size_t bins, double low, double high, bool inclusive, size_t *lo, size_t *hi);
static void confArgs(int argc, char **argv, ConfT *c);
static void confCentred(ConfT *c, double start, double end, const char *what);
static double confCentroid(ConfT *c, const double *x, size_t n);
static void confCheck(ConfT *c, bool ok, const char *fmt, ...) __attribute__((format(printf, 3, 4)));
static SamplesT confCut(const ConfT *c, double start, double end, int channel, double shift);
static double confDbfs(const double *x, size_t n);
static void confEventLine(const char *line, size_t len, EventsT *events);
static void confEvents(const char *text, size_t len, EventsT *events);
static double confFloat(const ConfT *c, const EventT *e, size_t at);
static void confHann(double *window, size_t n);
static long confInt(const ConfT *c, const EventT *e, size_t at);
static double confLoudest(const ConfT *c, double start, double end, bool steady, double shift);
static double confMeasure(const ConfT *c, double start, double end, int channel, double shift, bool steady);
static double confMedian(const double *values, size_t n);
static void confMod(ConfT *c, double start);
static void confModStop(ConfT *c, double start);
static void confName(ConfT *c, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
static void confNoise(ConfT *c, double start, double end, const EventT *e);
static void confNoiseLevels(ConfT *c);
static bool confNoiseRate(ConfT *c, const double *x, size_t n, double expected, double *got);
static bool confOnset(ConfT *c, double start, double hz, double *found);
static double confPitch(ConfT *c, const double *x, size_t n);
static double confPurity(ConfT *c, const double *x, size_t n, double hz);
static int confRun(int argc, char **argv);
static void confSample(ConfT *c, double start, const EventT *e);
static void confSilence(ConfT *c, double start, double end);
static double confSteady(const ConfT *c, const double *x, size_t n);
static void confSummary(ConfT *c);
static bool *confSyncFlags(ConfT *c, size_t *count);
static bool confSyncOnset(const bool *flags, size_t count, long from, long *at);
static void confTick(ConfT *c, const EventT *e);
static void confTone(ConfT *c, double start, double end, const EventT *e);
static const char *confValue(const ConfT *c, const EventT *e, size_t at);
static void confVoice(ConfT *c, double start, double end, const EventT *e);
static size_t confWelch(ConfT *c, const double *x, size_t n, double **freqs, double **psd);
static void entriesAdd(EntriesT *list, long first, long second, double value);
static EntryT *entriesFind(EntriesT *list, long first, long second);
static void entriesSet(EntriesT *list, long first, long second, double value);
static void fftFor(GateFftT *fft, size_t n);
static double *freqsOf(size_t n, uint32_t rate);
static void lineAdd(ToolBufT *lines, const char *label, const char *fmt, ...) __attribute__((format(printf, 3, 4)));
static void *memoryFor(size_t count, size_t size);
static bool monoRead(const char *path, double **samples, size_t *count, uint32_t *rate, ToolBufT *problem);
static bool monoRms(const double *samples, size_t count, uint32_t rate, double startSec, double endSec, double *level);
static int onsetRun(int argc, char **argv);
static bool optionValue(int argc, char **argv, int *a, const char *name, const char **value);
static int phasesRun(int argc, char **argv);
static double pspCrossings(const double *samples, size_t count, uint32_t rate, double start, double end);
static int pspRun(int argc, char **argv);
static double pspTakeHz(int take, double seconds);
static uint8_t *rawRead(const char *path, size_t *len);
static void spanOf(int64_t a, int64_t b, size_t len, size_t *lo, size_t *hi);
static int64_t truncated(double value);
static void usageFail(const char *fmt, ...) __attribute__((noreturn, format(printf, 1, 2)));
static int wavRun(int argc, char **argv);
// AUDPAR's kSineCycle: its sound effects' sine, a cycle of 8 samples.
static const int32_t kSineCycle[] = { 0, 71, 100, 71, 0, -71, -100, -71 };
// The pans an effect is played at, by number.
static const char *const kPanNames[] = { "left", "centre", "right" };
static const CommandT kCommands[] = {
{ "phases", phasesRun },
{ "conformance", confRun },
{ "psp", pspRun },
{ "wav", wavRun },
{ "onset", onsetRun }
};
static const char kUsage[] = "usage: audioGate phases LABEL WAV PHASE... (PHASE: name:startSec:endSec:minRms:maxRms)\n audioGate conformance PORT LOG WAV [--stereo] [--skip NAME]... [--level-tolerance DB]\n audioGate psp LABEL WAV QUIT_SECONDS\n audioGate wav RAW OUT.WAV RATE [SKIP]\n audioGate onset RAW THRESHOLD\n";
// Room for one more item in a growing array: the array, reallocated when
// it is full.
static void *arrayRoom(void *items, size_t count, size_t *cap, size_t size) {
void *grown;
size_t want;
if (count < *cap) {
return items;
}
want = *cap == 0u ? LIST_FIRST_CAP : *cap * 2u;
grown = realloc(items, want * size);
if (grown == NULL) {
toolFail("out of memory");
}
*cap = want;
return grown;
}
// The bins a numpy mask over ascending frequencies picks, between low and
// high (inclusive or not): [lo, hi).
static void bandOf(const double *freqs, size_t bins, double low, double high, bool inclusive, size_t *lo, size_t *hi) {
size_t k;
bool inside;
bool found;
*lo = 0u;
*hi = 0u;
found = false;
for (k = 0u; k < bins; k++) {
inside = inclusive ? freqs[k] >= low && freqs[k] <= high : freqs[k] > low && freqs[k] < high;
if (inside) {
*lo = found ? *lo : k;
*hi = k + 1u;
found = true;
}
}
}
// PORT LOG WAV, with the options anywhere among them: --stereo, --skip
// NAME (once for each name), --level-tolerance DB (both also as
// --name=VALUE), -h or --help. Anything else is a usage error.
static void confArgs(int argc, char **argv, ConfT *c) {
const char *positionals[CONF_POSITIONALS];
const char *arg;
const char *value;
size_t positionalCount;
int a;
positionalCount = 0u;
c->levelTolerance = LEVEL_TOLERANCE;
for (a = FIRST_ARG; a < argc; a++) {
arg = argv[a];
if (strcmp(arg, "-h") == 0 || strcmp(arg, "--help") == 0) {
fputs(kUsage, stdout);
exit(0);
}
if (strcmp(arg, "--stereo") == 0) {
c->stereo = true;
} else if (optionValue(argc, argv, &a, "--skip", &value)) {
c->skips = (const char **)arrayRoom((void *)c->skips, c->skipCount, &c->skipCap, sizeof(const char *));
c->skips[c->skipCount++] = value;
} else if (optionValue(argc, argv, &a, "--level-tolerance", &value)) {
c->levelTolerance = gateArgDouble("level tolerance", value);
} else if (arg[0] == '-') {
usageFail("unknown option %s", arg);
} else if (positionalCount < CONF_POSITIONALS) {
positionals[positionalCount++] = arg;
} else {
usageFail("one argument too many: %s", arg);
}
}
if (positionalCount < CONF_POSITIONALS) {
usageFail("conformance needs PORT LOG WAV");
}
c->port = positionals[0];
c->logPath = positionals[1];
c->wavPath = positionals[2];
}
// Each channel's level for a step: within PAN_CENTRE_DB of each other.
static void confCentred(ConfT *c, double start, double end, const char *what) {
double left;
double right;
left = confMeasure(c, start, end, CHANNEL_LEFT, 0.0, false);
right = confMeasure(c, start, end, CHANNEL_RIGHT, 0.0, false);
confName(c, "%s centred", what);
confCheck(c, fabs(left - right) <= PAN_CENTRE_DB, "left %.1f dBFS, right %.1f dBFS", left, right);
}
// The spectrum's centroid (Welch).
static double confCentroid(ConfT *c, const double *x, size_t n) {
double *freqs;
double *psd;
double *weighted;
double total;
double centroid;
size_t bins;
size_t k;
bins = confWelch(c, x, n, &freqs, &psd);
weighted = (double *)memoryFor(bins, sizeof(double));
for (k = 0u; k < bins; k++) {
weighted[k] = freqs[k] * psd[k];
}
total = gateSum(psd, bins);
centroid = gateSum(weighted, bins) / (total > LOG_FLOOR ? total : LOG_FLOOR);
free(weighted);
free(freqs);
free(psd);
return centroid;
}
// The check confName named: PASS or FAIL with its detail, or SKIP when the
// port skips it (a --skip NAME its name starts with).
static void confCheck(ConfT *c, bool ok, const char *fmt, ...) {
va_list args;
const char *name;
const char *verdict;
size_t i;
name = gateTextString(&c->name);
verdict = NULL;
for (i = 0u; i < c->skipCount && verdict == NULL; i++) {
if (strncmp(name, c->skips[i], strlen(c->skips[i])) == 0) {
verdict = "SKIP";
}
}
if (verdict == NULL) {
c->graded++;
c->passed += ok ? 1u : 0u;
c->failed += ok ? 0u : 1u;
verdict = ok ? "PASS" : "FAIL";
}
printf("%s %s: ", verdict, name);
va_start(args, fmt);
vprintf(fmt, args);
va_end(args);
putchar('\n');
}
// A step's samples on the recording (the mix, or one channel),
// MARGIN_S in from each end, shifted by shift seconds.
static SamplesT confCut(const ConfT *c, double start, double end, int channel, double shift) {
SamplesT out;
int64_t a;
int64_t b;
size_t lo;
size_t hi;
size_t i;
if (channel != CHANNEL_MIX && (uint32_t)channel >= c->channels) {
toolFail("%s: --stereo, but it has %u channel", c->wavPath, (unsigned)c->channels);
}
a = truncated((c->t0 + start * c->scale + shift + MARGIN_S) * (double)c->rate);
b = truncated((c->t0 + end * c->scale + shift - MARGIN_S) * (double)c->rate);
spanOf(a, b, c->frames, &lo, &hi);
out.n = hi - lo;
out.x = (double *)memoryFor(out.n, sizeof(double));
for (i = 0u; i < out.n; i++) {
out.x[i] = channel == CHANNEL_MIX ? c->mono[lo + i] : c->data[(lo + i) * c->channels + (uint32_t)channel];
}
return out;
}
// A level in dBFS without DC: a DAC resting off centre (the ST's YM) is
// silence.
static double confDbfs(const double *x, size_t n) {
double *squares;
double mean;
double rms;
double d;
size_t i;
rms = 0.0;
if (n > 0u) {
squares = (double *)memoryFor(n, sizeof(double));
mean = gateMean(x, n);
for (i = 0u; i < n; i++) {
d = x[i] - mean;
squares[i] = d * d;
}
rms = sqrt(gateMean(squares, n));
free(squares);
}
return DB_PER_DECADE * log10((rms > RMS_FLOOR ? rms : RMS_FLOOR) / FULL_SCALE);
}
// An AUDPAR line, "AUDPAR <ms> <kind> <values>": the first "AUDPAR " that
// digits, a space, and a word follow; the rest of the line split on white
// space.
static void confEventLine(const char *line, size_t len, EventsT *events) {
EventT *e;
char *digits;
char *value;
size_t tagLen;
size_t at;
size_t numberEnd;
size_t wordEnd;
size_t valueEnd;
tagLen = strlen(LOG_TAG);
for (at = 0u; at + tagLen <= len; at++) {
if (memcmp(line + at, LOG_TAG, tagLen) != 0) {
continue;
}
numberEnd = at + tagLen;
while (numberEnd < len && isdigit((unsigned char)line[numberEnd])) {
numberEnd++;
}
if (numberEnd == at + tagLen || numberEnd >= len || line[numberEnd] != ' ') {
continue;
}
wordEnd = numberEnd + 1u;
while (wordEnd < len && (isalnum((unsigned char)line[wordEnd]) || line[wordEnd] == '_')) {
wordEnd++;
}
if (wordEnd == numberEnd + 1u) {
continue;
}
events->items = (EventT *)arrayRoom(events->items, events->count, &events->cap, sizeof(EventT));
e = &events->items[events->count++];
memset(e, 0, sizeof(*e));
digits = strndup(line + at + tagLen, numberEnd - at - tagLen);
e->kind = strndup(line + numberEnd + 1u, wordEnd - numberEnd - 1u);
if (digits == NULL || e->kind == NULL) {
toolFail("out of memory");
}
e->time = strtod(digits, NULL) / MS_PER_SECOND;
free(digits);
for (at = wordEnd; at < len; at = valueEnd) {
if (isspace((unsigned char)line[at])) {
valueEnd = at + 1u;
continue;
}
valueEnd = at;
while (valueEnd < len && !isspace((unsigned char)line[valueEnd])) {
valueEnd++;
}
value = strndup(line + at, valueEnd - at);
if (value == NULL) {
toolFail("out of memory");
}
e->values = (char **)arrayRoom(e->values, e->valueCount, &e->valueCap, sizeof(char *));
e->values[e->valueCount++] = value;
}
return;
}
}
// Every AUDPAR line of the log.
static void confEvents(const char *text, size_t len, EventsT *events) {
size_t at;
size_t end;
for (at = 0u; at < len; at = end + 1u) {
end = at;
while (end < len && text[end] != '\n') {
end++;
}
confEventLine(text + at, end - at, events);
}
}
// A log value as a number.
static double confFloat(const ConfT *c, const EventT *e, size_t at) {
double value;
if (!gateArgParseDouble(confValue(c, e, at), &value)) {
toolFail("%s: AUDPAR %s at %.3f s: '%s' is not a number", c->logPath, e->kind, e->time, e->values[at]);
}
return value;
}
// scipy's get_window('hann', n): periodic, 0.5 + 0.5 cos over
// linspace(-pi, pi, n + 1) with its last point dropped.
static void confHann(double *window, size_t n) {
double step;
size_t i;
if (n == 1u) {
window[0] = 1.0;
return;
}
step = (M_PI - -M_PI) / (double)n;
for (i = 0u; i < n; i++) {
window[i] = 0.5 + 0.5 * cos((double)i * step + -M_PI);
}
}
// A log value as a whole number.
static long confInt(const ConfT *c, const EventT *e, size_t at) {
long value;
if (!gateArgParseLong(confValue(c, e, at), &value)) {
toolFail("%s: AUDPAR %s at %.3f s: '%s' is not a whole number", c->logPath, e->kind, e->time, e->values[at]);
}
return value;
}
// A step's level in its louder channel on a stereo port, else
// the mix's.
static double confLoudest(const ConfT *c, double start, double end, bool steady, double shift) {
double left;
double right;
if (!c->stereo) {
return confMeasure(c, start, end, CHANNEL_MIX, shift, steady);
}
left = confMeasure(c, start, end, CHANNEL_LEFT, shift, steady);
right = confMeasure(c, start, end, CHANNEL_RIGHT, shift, steady);
return right > left ? right : left;
}
// The level of a cut: dbfs(), or steadyDbfs().
static double confMeasure(const ConfT *c, double start, double end, int channel, double shift, bool steady) {
SamplesT x;
double level;
x = confCut(c, start, end, channel, shift);
level = steady ? confSteady(c, x.x, x.n) : confDbfs(x.x, x.n);
free(x.x);
return level;
}
// numpy.median.
static double confMedian(const double *values, size_t n) {
double *sorted;
double middle;
double swap;
size_t i;
size_t j;
sorted = (double *)memoryFor(n, sizeof(double));
memcpy(sorted, values, n * sizeof(double));
for (i = 1u; i < n; i++) {
for (j = i; j > 0u && sorted[j - 1u] > sorted[j]; j--) {
swap = sorted[j];
sorted[j] = sorted[j - 1u];
sorted[j - 1u] = swap;
}
}
middle = n % 2u != 0u ? sorted[n / 2u] : (sorted[n / 2u - 1u] + sorted[n / 2u]) / 2.0;
free(sorted);
return middle;
}
// A module that does not loop: it sounds, ends, and is reported ended
// within MOD_END_SLACK_S of its sound stopping; its level weighed against
// a voice's.
static void confMod(ConfT *c, double start) {
const EventT *endEvent;
const EventT *e;
SamplesT x;
double endAt;
double until;
double lastLoud;
double soundEnd;
size_t loudCount;
size_t lastIndex;
size_t i;
bool stillPlaying;
bool sounded;
endEvent = NULL;
for (i = 0u; i < c->events.count && endEvent == NULL; i++) {
e = &c->events.items[i];
if (strcmp(e->kind, "mod") == 0 && e->valueCount > MOD_WHAT_AT && strcmp(e->values[MOD_WHAT_AT], "end") == 0) {
endEvent = e;
}
}
if (endEvent == NULL) {
confName(c, "mod");
confCheck(c, false, "no mod end in the log");
return;
}
endAt = endEvent->time;
c->balanceMod = confLoudest(c, start, start + MOD_LEVEL_S < endAt ? start + MOD_LEVEL_S : endAt, true, 0.0);
c->haveBalanceMod = true;
stillPlaying = endEvent->valueCount > MOD_PLAYING_AT && strcmp(endEvent->values[MOD_PLAYING_AT], "0") != 0;
// Listened to a second past the end, or up to whatever sounds next.
until = endAt + MOD_LISTEN_S;
for (i = 0u; i < c->events.count; i++) {
e = &c->events.items[i];
if (e->time > endAt && strcmp(e->kind, "silence") != 0 && e->time < until) {
until = e->time;
}
}
x = confCut(c, start, until, CHANNEL_MIX, 0.0);
loudCount = 0u;
lastIndex = 0u;
sounded = false;
for (i = 0u; i + c->hop < x.n; i += c->hop) {
if (confDbfs(x.x + i, c->hop) > SOUND_DBFS) {
loudCount++;
lastIndex = i / c->hop;
sounded = true;
}
}
free(x.x);
lastLoud = sounded ? (double)(lastIndex + 1u) * FRAME_S : 0.0;
soundEnd = start + (MARGIN_S + lastLoud) / c->scale;
confName(c, "mod sounds");
confCheck(c, sounded, "%.1f s of it above %.0f dBFS", (double)loudCount * FRAME_S, SOUND_DBFS);
confName(c, "mod ends");
confCheck(c, !stillPlaying && endAt - start < MOD_END_MAX_S, "jlAudioIsPlayingMod false %.2f s after the start", endAt - start);
confName(c, "mod end reported");
confCheck(c, -MOD_LEAD_S <= endAt - soundEnd && endAt - soundEnd <= MOD_END_SLACK_S, "its sound stopped at %.2f s, reported at %.2f s", soundEnd - start, endAt - start);
}
// A looping module: it sounds until jlAudioStopMod, and is silent from
// SFX_LATENCY_MAX after.
static void confModStop(ConfT *c, double start) {
double before;
double after;
before = confMeasure(c, start - MOD_STOP_LOOK_S, start, CHANNEL_MIX, 0.0, false);
after = confMeasure(c, start + SFX_LATENCY_MAX, start + SFX_LATENCY_MAX + MOD_STOP_LOOK_S, CHANNEL_MIX, 0.0, true);
confName(c, "mod sounds until its stop");
confCheck(c, before > SOUND_DBFS, "%.1f dBFS before jlAudioStopMod", before);
confName(c, "mod stops");
confCheck(c, after < SILENCE_DBFS, "%.1f dBFS from %.0f ms after jlAudioStopMod", after, SFX_LATENCY_MAX * MS_PER_SECOND);
}
// The name of the next check.
static void confName(ConfT *c, const char *fmt, ...) {
va_list args;
c->name.len = 0u;
va_start(args, fmt);
gateTextAddV(&c->name, fmt, args);
va_end(args);
}
// A noise step: the first at a pitch is heard (its rate read off its
// spectral null where that is graded, its centroid kept for the darkening);
// every one's level kept for the attenuation steps.
static void confNoise(ConfT *c, double start, double end, const EventT *e) {
SamplesT x;
long pitch;
long atten;
double level;
double expected;
double got;
pitch = confInt(c, e, NOISE_PITCH_AT);
atten = confInt(c, e, NOISE_ATTEN_AT);
x = confCut(c, start, end, CHANNEL_MIX, 0.0);
level = confSteady(c, x.x, x.n);
if (atten == 0 && entriesFind(&c->noiseLevels, pitch, 0) == NULL) {
expected = NOISE_RATE_HZ / (double)(pitch > 1 ? pitch : 1);
entriesAdd(&c->noiseCentroids, pitch, 0, confCentroid(c, x.x, x.n));
if (level <= SOUND_DBFS) {
confName(c, "noise %ld sounds", pitch);
confCheck(c, false, "%.1f dBFS: it does not hold for the step", level);
} else if (expected <= NOISE_GRADED_HZ) {
confName(c, "noise %ld rate", pitch);
if (confNoiseRate(c, x.x, x.n, expected, &got)) {
confCheck(c, fabs(got / expected - 1.0) <= NOISE_TOLERANCE, "%.0f Hz, %.0f asked, at %.1f dBFS", got, expected, level);
} else {
confCheck(c, false, "no null near %.0f Hz, at %.1f dBFS", expected, level);
}
} else {
confName(c, "noise %ld sounds", pitch);
confCheck(c, true, "%.1f dBFS (its rate, %.0f Hz, is graded by the darkening below)", level, expected);
}
}
entriesSet(&c->noiseLevels, pitch, atten, level);
free(x.x);
}
// The noise's attenuation steps, at the last pitch that had any: each
// 2 dB a step below attenuation 0, in rising order.
static void confNoiseLevels(ConfT *c) {
EntryT *base;
EntryT *step;
long *attens;
long pitch;
long swap;
size_t count;
size_t i;
size_t j;
bool found;
double drop;
double tolerance;
pitch = 0;
found = false;
for (i = 0u; i < c->noiseLevels.count; i++) {
if (c->noiseLevels.items[i].second > 0) {
pitch = c->noiseLevels.items[i].first;
found = true;
}
}
base = found ? entriesFind(&c->noiseLevels, pitch, 0) : NULL;
if (base == NULL) {
return;
}
attens = (long *)memoryFor(c->noiseLevels.count, sizeof(long));
count = 0u;
for (i = 0u; i < c->noiseLevels.count; i++) {
if (c->noiseLevels.items[i].first == pitch && c->noiseLevels.items[i].second > 0) {
attens[count++] = c->noiseLevels.items[i].second;
}
}
for (i = 1u; i < count; i++) {
for (j = i; j > 0u && attens[j - 1u] > attens[j]; j--) {
swap = attens[j];
attens[j] = attens[j - 1u];
attens[j - 1u] = swap;
}
}
tolerance = NOISE_LEVEL_TOLERANCE > c->levelTolerance ? NOISE_LEVEL_TOLERANCE : c->levelTolerance;
for (i = 0u; i < count; i++) {
step = entriesFind(&c->noiseLevels, pitch, attens[i]);
drop = base->value - step->value;
confName(c, "noise level %ld", attens[i]);
confCheck(c, fabs(drop - LEVEL_STEP_DB * (double)attens[i]) <= tolerance, "%.1f dB below attenuation 0, %.0f asked", drop, LEVEL_STEP_DB * (double)attens[i]);
}
free(attens);
}
// The noise's sample-and-hold rate, read off its first
// spectral null: noise held at R Hz has a sinc^2 spectrum that first falls
// to nothing at R. Looked for between NULL_SEARCH_LOW and NULL_SEARCH_HIGH
// of the rate asked, and only a real dip counts -- NULL_DEPTH_DB below the
// spectrum's level under it -- so a wrong rate shows as a null in the wrong
// place or as none. (Fitting the whole spectrum was thrown by the images a
// chip's own output rate leaves above it: the IIgs's DOC runs at
// 26.3 kHz.) False when there is no such null.
static bool confNoiseRate(ConfT *c, const double *x, size_t n, double expected, double *got) {
double *freqs;
double *psd;
double *level;
double sum;
double highest;
double top;
size_t bins;
size_t searchLo;
size_t searchHi;
size_t belowLo;
size_t belowHi;
size_t dip;
size_t k;
long j;
long at;
bool found;
bins = confWelch(c, x, n, &freqs, &psd);
level = (double *)memoryFor(bins, sizeof(double));
// numpy.convolve(psd, ones(5) / 5, 'same'): each bin the products
// summed in order, the kernel's ends hanging off the edges.
for (k = 0u; k < bins; k++) {
sum = 0.0;
for (j = 0; j < NULL_SMOOTH; j++) {
at = (long)k - NULL_SMOOTH / 2 + j;
if (at >= 0 && (size_t)at < bins) {
sum += psd[at] * (1.0 / (double)NULL_SMOOTH);
}
}
level[k] = POWER_DB_PER_DECADE * log10(sum + LOG_FLOOR);
}
top = NULL_SEARCH_HIGH * expected < NOISE_FIT_LIMIT * (double)c->rate ? NULL_SEARCH_HIGH * expected : NOISE_FIT_LIMIT * (double)c->rate;
bandOf(freqs, bins, NULL_SEARCH_LOW * expected, top, true, &searchLo, &searchHi);
bandOf(freqs, bins, NULL_LEVEL_LOW * expected, NULL_SEARCH_LOW * expected, true, &belowLo, &belowHi);
found = false;
if (searchHi - searchLo >= NULL_SEARCH_MIN && belowHi > belowLo) {
dip = searchLo;
for (k = searchLo + 1u; k < searchHi; k++) {
dip = level[k] < level[dip] ? k : dip;
}
highest = level[belowLo];
for (k = belowLo + 1u; k < belowHi; k++) {
highest = level[k] > highest ? level[k] : highest;
}
if (dip != searchLo && dip != searchHi - 1u && highest - level[dip] >= NULL_DEPTH_DB) {
*got = freqs[dip];
found = true;
}
}
free(level);
free(freqs);
free(psd);
return found;
}
// Seconds from a step's log time to a sound at hz (its band
// holding ONSET_SHARE of a loud window's power); false when none comes in
// ONSET_SEARCH_S. The tail of whatever came before is a different pitch
// and does not count.
static bool confOnset(ConfT *c, double start, double hz, double *found) {
const double *frame;
int64_t a;
int64_t limit;
int64_t i;
double mean;
double total;
size_t lo;
size_t hi;
size_t bins;
size_t k;
bins = c->window / 2u + 1u;
bandOf(c->freqs, bins, hz * (1.0 - ONSET_BAND), hz * (1.0 + ONSET_BAND), false, &lo, &hi);
a = truncated((c->t0 + start * c->scale) * (double)c->rate);
limit = a + truncated(ONSET_SEARCH_S * (double)c->rate);
limit = limit < (int64_t)c->frames ? limit : (int64_t)c->frames;
for (i = a; i < limit - (int64_t)c->window; i += (int64_t)c->hop) {
frame = c->mono + i;
mean = gateMean(frame, c->window);
for (k = 0u; k < c->window; k++) {
c->scratch[k] = (frame[k] - mean) * c->taper[k];
}
gateFftAbs(&c->windowFft, c->scratch, c->window, c->bins);
for (k = 0u; k < bins; k++) {
c->bins[k] *= c->bins[k];
}
total = gateSum(c->bins, bins);
if (confDbfs(frame, c->window) > SOUND_DBFS && gateSum(c->bins + lo, hi - lo) > ONSET_SHARE * total) {
*found = (double)(i - a) / (double)c->rate;
return true;
}
}
return false;
}
// The strongest frequency in PITCH_LOW_HZ..PITCH_HIGH_HZ,
// refined between bins.
static double confPitch(ConfT *c, const double *x, size_t n) {
double *shaped;
double *spectrum;
double *freqs;
double mean;
double a;
double b;
double d;
double offset;
size_t size;
size_t bins;
size_t lo;
size_t hi;
size_t peak;
size_t k;
if (n < PITCH_MIN_SAMPLES) {
return 0.0;
}
size = (size_t)1u << ((int)log2((double)n) + PITCH_PAD_SHIFT);
bins = size / 2u + 1u;
shaped = (double *)memoryFor(n, sizeof(double));
spectrum = (double *)memoryFor(bins, sizeof(double));
freqs = freqsOf(size, c->rate);
gateHanning(shaped, n);
mean = gateMean(x, n);
for (k = 0u; k < n; k++) {
shaped[k] = (x[k] - mean) * shaped[k];
}
fftFor(&c->pitchFft, size);
gateFftAbs(&c->pitchFft, shaped, n, spectrum);
// The rate is at least MIN_RATE_HZ (confRun): bins fall in the band.
bandOf(freqs, bins, PITCH_LOW_HZ, PITCH_HIGH_HZ, true, &lo, &hi);
peak = lo;
for (k = lo + 1u; k < hi; k++) {
peak = spectrum[k] > spectrum[peak] ? k : peak;
}
offset = 0.0;
if (peak > 0u && peak < bins - 1u) {
a = log(spectrum[peak - 1u] + LOG_FLOOR);
b = log(spectrum[peak] + LOG_FLOOR);
d = log(spectrum[peak + 1u] + LOG_FLOOR);
if (a - 2.0 * b + d != 0.0) {
offset = 0.5 * (a - d) / (a - 2.0 * b + d);
}
}
free(shaped);
free(spectrum);
free(freqs);
return ((double)peak + offset) * (double)c->rate / (double)size;
}
// The share of a tone's power within PURE_BAND of its pitch: a
// sine played as it was made keeps nearly all of it; one whose bytes were
// taken in the wrong format (signed for unsigned) spreads it into
// harmonics and a DC step.
static double confPurity(ConfT *c, const double *x, size_t n, double hz) {
double *shaped;
double *spectrum;
double *freqs;
double mean;
double total;
double share;
size_t bins;
size_t lo;
size_t hi;
size_t k;
if (n < PITCH_MIN_SAMPLES) {
return 0.0;
}
bins = n / 2u + 1u;
shaped = (double *)memoryFor(n, sizeof(double));
spectrum = (double *)memoryFor(bins, sizeof(double));
freqs = freqsOf(n, c->rate);
gateHanning(shaped, n);
mean = gateMean(x, n);
for (k = 0u; k < n; k++) {
shaped[k] = (x[k] - mean) * shaped[k];
}
fftFor(&c->purityFft, n);
gateFftAbs(&c->purityFft, shaped, n, spectrum);
for (k = 0u; k < bins; k++) {
spectrum[k] *= spectrum[k];
}
bandOf(freqs, bins, hz * (1.0 - PURE_BAND), hz * (1.0 + PURE_BAND), true, &lo, &hi);
total = gateSum(spectrum, bins);
share = gateSum(spectrum + lo, hi - lo) / (total > LOG_FLOOR ? total : LOG_FLOOR);
free(shaped);
free(spectrum);
free(freqs);
return share;
}
// conformance: AUDPAR plays one timeline and logs when each step started
// ("AUDPAR <ms> <event> <values>"). This finds the timeline in the
// recording by its two sync tones (1 kHz, at the start and the end, so an
// emulator whose clock runs a little fast or slow is mapped straight), cuts
// each step out of it, and measures it:
//
// tone, voice the fundamental within PITCH_TOLERANCE of the asked Hz
// voice levels voice 0's attenuation steps 2 dB apart, LEVEL_TOLERANCE
// voices 1, 2 their pitch, and as loud as voice 0 (VOICE_MATCH_DB)
// noise the generator's rate, 125000 / pitch Hz: read off the
// first spectral null where every machine's output passes
// it (NOISE_GRADED_HZ, within NOISE_TOLERANCE), darker as
// the pitch rises everywhere, and its steps 2 dB apart
// sfx a sine of 8 samples a cycle sounds rate / 8 Hz, starting
// within SFX_LATENCY_MAX of the call; panned left or right
// it is in one channel (--stereo ports); and the mix: a
// full-scale sample peaks where a voice at attenuation 0
// does, so the sine sounds its own level under full scale
// (kSineCycle's) against voice 0 (VOICE_MATCH_DB), each by
// its louder channel on a --stereo port (the Amiga's
// voices play on one side), the sine's level the median
// of the BALANCE_RATE effects (centre, left, right): one
// sound an emulator spoils does not decide it (MAME 0.264
// clocks the X68000's MSM6258 at 4 MHz, not the 8 its CT1
// pin says, until the first write to the OPM's CT1, so the
// first ADPCM sound after boot decodes every other nibble)
// stream the same sine streamed, plain and raw (in the port's raw
// format), likewise; the raw one as clean as the plain one
// (RAW_PURE_SHARE of its power at the pitch)
// mod stop a module that loops sounds until jlAudioStopMod, and is
// silent from SFX_LATENCY_MAX after it
// mod a module that does not loop sounds, then ends, and
// jlAudioIsPlayingMod goes false within MOD_END_SLACK_S of
// its sound stopping; and the mix: AUDPAR's module plays
// one channel of a full-scale square at full volume, as
// loud as voice 0 at attenuation 0 (MOD_VOICE_DB,
// VOICE_MATCH_DB), by the louder channel on a --stereo port
// tick the callbacks that came, against hz * time
// silence every gap is quiet: nothing left sounding
//
// --stereo: the port has stereo output: grade the pans. --skip NAME: a
// check this port cannot pass (a documented hardware limit), by its name's
// start, reported as SKIP; once for each. --level-tolerance DB: how far an
// attenuation step may land from 2 dB a step (a chip whose own levels are
// coarser, the YM2149's, gets its own figure). Prints one line per check, then "AUDPAR
// PORT passed/graded". Exit 0 when every graded check passes, 1 when one
// fails, 2 on a usage or input error.
static int confRun(int argc, char **argv) {
ConfT c;
GateWavT wav;
ToolBufT problem;
const EventT *e;
const EventT *firstSync;
const EventT *lastSync;
uint8_t *logText;
bool *flags;
size_t logLen;
size_t syncCount;
size_t doneCount;
size_t flagCount;
size_t frame;
size_t i;
uint32_t channel;
long first;
long last;
bool haveFirst;
bool haveLast;
double span;
double start;
double end;
memset(&c, 0, sizeof(c));
memset(&problem, 0, sizeof(problem));
confArgs(argc, argv, &c);
if (!gateWavRead(c.wavPath, &wav, &problem) || !gateTextReadFile(c.logPath, &logText, &logLen, &problem)) {
printf("AUDPAR %s: %s\n", c.port, gateTextString(&problem));
return GATE_USAGE_EXIT;
}
confEvents((const char *)logText, logLen, &c.events);
free(logText);
syncCount = 0u;
doneCount = 0u;
firstSync = NULL;
lastSync = NULL;
for (i = 0u; i < c.events.count; i++) {
e = &c.events.items[i];
if (strcmp(e->kind, "sync") == 0) {
if (syncCount == 0u) {
firstSync = e;
} else if (syncCount == 1u) {
lastSync = e;
}
syncCount++;
} else if (strcmp(e->kind, "done") == 0) {
doneCount++;
}
}
if (syncCount != 2u || doneCount == 0u) {
printf("AUDPAR %s: the log does not hold a whole run (syncs %zu, done %zu)\n", c.port, syncCount, doneCount);
return GATE_USAGE_EXIT;
}
c.channels = wav.channels;
c.rate = wav.rate;
c.frames = gateWavFrames(&wav);
c.data = (double *)memoryFor(c.frames * c.channels, sizeof(double));
for (frame = 0u; frame < c.frames; frame++) {
for (channel = 0u; channel < c.channels; channel++) {
c.data[frame * c.channels + channel] = gateWavSample(&wav, frame, channel);
}
}
c.mono = gateWavMono(&wav);
gateWavFree(&wav);
if (c.rate < MIN_RATE_HZ) {
toolFail("%s: %u Hz is too low a rate to grade", c.wavPath, (unsigned)c.rate);
}
c.window = (size_t)truncated(WINDOW_S * (double)c.rate);
c.hop = (size_t)truncated(FRAME_S * (double)c.rate);
c.taper = (double *)memoryFor(c.window, sizeof(double));
c.scratch = (double *)memoryFor(c.window, sizeof(double));
c.bins = (double *)memoryFor(c.window / 2u + 1u, sizeof(double));
c.freqs = freqsOf(c.window, c.rate);
gateHanning(c.taper, c.window);
gateFftInit(&c.windowFft, c.window);
// Align: the two sync onsets map log seconds onto the recording.
flags = confSyncFlags(&c, &flagCount);
haveFirst = confSyncOnset(flags, flagCount, 0, &first);
span = lastSync->time - firstSync->time;
if (span <= 0.0) {
toolFail("%s: the closing sync is logged no later than the opening one", c.logPath);
}
haveLast = haveFirst && confSyncOnset(flags, flagCount, first + (long)truncated(SYNC_LAST_SHARE * span / FRAME_S), &last);
free(flags);
if (!haveFirst || !haveLast) {
confName(&c, "sync");
confCheck(&c, false, "sync tones found: %s%s", haveFirst ? "start" : "none", haveLast ? ", end" : "");
printf("AUDPAR %s %u/%u\n", c.port, (unsigned)c.passed, (unsigned)c.graded);
return FAIL_EXIT;
}
c.t0 = (double)first * FRAME_S;
c.scale = (double)(last - first) * FRAME_S / span;
confName(&c, "sync");
confCheck(&c, fabs(c.scale - 1.0) < SYNC_SCALE_TOLERANCE, "start %.3f s, the timeline runs at %.4f of the program's clock", c.t0, c.scale);
for (i = 0u; i < c.events.count; i++) {
e = &c.events.items[i];
start = e->time;
end = i + 1u < c.events.count ? c.events.items[i + 1u].time : e->time;
if (strcmp(e->kind, "tone") == 0 || strcmp(e->kind, "sync") == 0) {
confTone(&c, start, end, e);
} else if (strcmp(e->kind, "voice") == 0) {
confVoice(&c, start, end, e);
} else if (strcmp(e->kind, "noise") == 0) {
confNoise(&c, start, end, e);
} else if (strcmp(e->kind, "sfx") == 0 || strcmp(e->kind, "stream") == 0) {
confSample(&c, start, e);
} else if (strcmp(e->kind, "silence") == 0) {
confSilence(&c, start, end);
} else if (strcmp(e->kind, "mod") == 0 && e->valueCount > MOD_WHAT_AT && strcmp(e->values[MOD_WHAT_AT], "start") == 0) {
confMod(&c, start);
} else if (strcmp(e->kind, "mod") == 0 && e->valueCount > MOD_WHAT_AT && strcmp(e->values[MOD_WHAT_AT], "stop") == 0) {
confModStop(&c, start);
} else if (strcmp(e->kind, "tick") == 0) {
confTick(&c, e);
}
}
confSummary(&c);
printf("AUDPAR %s %u/%u\n", c.port, (unsigned)c.passed, (unsigned)c.graded);
return c.failed == 0u ? 0 : FAIL_EXIT;
}
// An sfx or stream step: it starts in time, at its pitch, cleanly, at its
// pan; the BALANCE_RATE effects weighed against a voice.
static void confSample(ConfT *c, double start, const EventT *e) {
SamplesT x;
const char *what;
const char *panName;
char panText[TEXT_MAX];
size_t samplesAt;
long pan;
long samples;
double hz;
double length;
double found;
double got;
double level;
double pure;
double left;
double right;
double apart;
bool started;
bool ok;
hz = confFloat(c, e, SAMPLE_HZ_AT);
pan = confInt(c, e, SAMPLE_PAN_AT);
// The sine's samples follow the pan (a stream's after its raw flag),
// when the log gives them.
samplesAt = strcmp(e->kind, "sfx") == 0 ? SFX_SAMPLES_AT : STREAM_SAMPLES_AT;
samples = e->valueCount > samplesAt ? confInt(c, e, samplesAt) : SFX_SAMPLES;
length = (double)samples / confFloat(c, e, SAMPLE_RATE_AT);
panName = panText;
if (pan >= 0 && (size_t)pan < GATE_COUNT_OF(kPanNames)) {
panName = kPanNames[pan];
} else {
snprintf(panText, sizeof(panText), "%ld", pan);
}
// A stream's name says so, and whether it went raw.
what = "sfx";
if (strcmp(e->kind, "sfx") != 0) {
what = e->valueCount > STREAM_RAW_AT && strcmp(e->values[STREAM_RAW_AT], "1") == 0 ? "raw stream" : "stream";
}
started = confOnset(c, start, hz, &found);
confName(c, "%s %s Hz %s starts", what, e->values[SAMPLE_RATE_AT], panName);
if (started) {
confCheck(c, found <= SFX_LATENCY_MAX, "%.0f ms after the call", found * MS_PER_SECOND);
} else {
confCheck(c, false, "never");
}
c->lag = started ? found : 0.0;
x = confCut(c, start, start + length, CHANNEL_MIX, c->lag);
got = confPitch(c, x.x, x.n);
level = confDbfs(x.x, x.n);
pure = confPurity(c, x.x, x.n, hz);
free(x.x);
confName(c, "%s %s Hz %s", what, e->values[SAMPLE_RATE_AT], panName);
confCheck(c, fabs(got / hz - 1.0) <= PITCH_TOLERANCE && level > SOUND_DBFS, "%.1f Hz (%.0f asked) at %.1f dBFS, %.1f%% at its pitch", got, hz, level, pure * PERCENT);
// The raw stream's bytes in the wrong format sound at the same pitch,
// but spread into harmonics: it must be about as clean as the plain
// stream through the same path.
if (strcmp(what, "stream") == 0) {
c->plainPure = pure;
c->havePlainPure = true;
} else if (strcmp(what, "raw stream") == 0 && c->havePlainPure) {
confName(c, "raw stream as clean as the plain one");
confCheck(c, pure >= c->plainPure * RAW_PURE_SHARE, "%.1f%% at its pitch, the plain stream %.1f%%", pure * PERCENT, c->plainPure * PERCENT);
}
if (strcmp(what, "sfx") == 0 && strcmp(e->values[SAMPLE_RATE_AT], BALANCE_RATE) == 0) {
entriesAdd(&c->balanceSfx, 0, 0, confLoudest(c, start, start + length, false, c->lag));
}
if (c->channels >= RAW_CHANNELS) {
left = confMeasure(c, start, start + length, CHANNEL_LEFT, c->lag, false);
right = confMeasure(c, start, start + length, CHANNEL_RIGHT, c->lag, false);
apart = left - right;
if (pan == PAN_LEFT) {
ok = apart >= PAN_APART_DB;
} else if (pan == PAN_RIGHT) {
ok = -apart >= PAN_APART_DB;
} else {
ok = fabs(apart) <= PAN_CENTRE_DB;
}
if (c->stereo) {
confName(c, "pan %s", panName);
confCheck(c, ok, "left %.1f dBFS, right %.1f dBFS", left, right);
}
}
}
// A gap: quiet. After a sample-path sound, its tail is late by the latency
// the sfx check graded: the silence is judged after it.
static void confSilence(ConfT *c, double start, double end) {
double level;
level = confMeasure(c, start, end, CHANNEL_MIX, c->lag, true);
confName(c, "silence at %.2f s", start);
confCheck(c, level < SILENCE_DBFS, "%.1f dBFS", level);
}
// The median level of LEVEL_FRAME_S frames: a step's own
// level, which a glitch of the recording path (a real-time emulator's sound
// server dropping or repeating a block) cannot move the way it moves a mean.
static double confSteady(const ConfT *c, const double *x, size_t n) {
double *levels;
double level;
size_t frame;
size_t count;
size_t i;
frame = (size_t)truncated(LEVEL_FRAME_S * (double)c->rate);
if (n < frame) {
return confDbfs(x, n);
}
count = n / frame;
levels = (double *)memoryFor(count, sizeof(double));
for (i = 0u; i < count; i++) {
levels[i] = confDbfs(x + i * frame, frame);
}
level = confMedian(levels, count);
free(levels);
return level;
}
// The checks across steps: the tones' levels even, voice 0's attenuation
// steps, the voices and the noise as loud as voice 0, the mix (effects and
// module against a voice), the noise's attenuation steps, and its
// darkening as its pitch rises.
static void confSummary(ConfT *c) {
ToolBufT text;
EntryT *base;
EntryT *entry;
EntryT *now;
double *values;
double spread;
double drop;
double apart;
double power;
double sfxVoiceDb;
size_t i;
long atten;
long voice;
bool darker;
memset(&text, 0, sizeof(text));
if (c->toneLevels.count > 0u) {
spread = 0.0;
for (i = 0u; i < c->toneLevels.count; i++) {
entry = &c->toneLevels.items[i];
spread = fabs(entry->value - c->toneLevels.items[0].value) > spread ? fabs(entry->value - c->toneLevels.items[0].value) : spread;
gateTextAdd(&text, "%s%ld Hz %.1f", i > 0u ? ", " : "", entry->first, entry->value);
}
confName(c, "tone levels even");
confCheck(c, spread <= TONE_MATCH_DB, "%s", gateTextString(&text));
}
base = entriesFind(&c->voiceLevels, 0, 0);
if (base != NULL) {
for (atten = 1; atten <= VOICE_ATTEN_MAX; atten++) {
entry = entriesFind(&c->voiceLevels, 0, atten);
if (entry != NULL) {
drop = base->value - entry->value;
confName(c, "voice level %ld", atten);
confCheck(c, fabs(drop - LEVEL_STEP_DB * (double)atten) <= c->levelTolerance, "%.1f dB below attenuation 0, %.0f asked", drop, LEVEL_STEP_DB * (double)atten);
}
}
for (voice = VOICE_OTHER_FIRST; voice <= VOICE_OTHER_LAST; voice++) {
entry = entriesFind(&c->voiceLevels, voice, 0);
if (entry != NULL) {
confName(c, "voice %ld as loud as voice 0", voice);
confCheck(c, fabs(entry->value - base->value) <= VOICE_MATCH_DB, "%.1f against %.1f dBFS", entry->value, base->value);
}
}
// Full-swing noise and a full-swing square carry the same power, as
// on the PSG every port's voices are modelled on.
entry = entriesFind(&c->noiseLevels, NOISE_LOUDNESS_PITCH, 0);
if (entry != NULL) {
confName(c, "noise as loud as a voice");
confCheck(c, fabs(entry->value - base->value) <= VOICE_MATCH_DB, "%.1f against %.1f dBFS", entry->value, base->value);
}
}
if (c->haveBalanceVoice && c->balanceSfx.count > 0u) {
// The canonical mix: AUDPAR's sine, peaking under full scale, sounds
// this far under a full-swing square of the same peak (a voice at
// attenuation 0).
power = 0.0;
for (i = 0u; i < GATE_COUNT_OF(kSineCycle); i++) {
power += (double)(kSineCycle[i] * kSineCycle[i]);
}
sfxVoiceDb = DB_PER_DECADE * log10(sqrt(power / (double)GATE_COUNT_OF(kSineCycle)) / SAMPLE_FULL_SCALE);
values = (double *)memoryFor(c->balanceSfx.count, sizeof(double));
for (i = 0u; i < c->balanceSfx.count; i++) {
values[i] = c->balanceSfx.items[i].value;
}
apart = confMedian(values, c->balanceSfx.count) - c->balanceVoice;
free(values);
confName(c, "sfx as loud as a voice");
confCheck(c, fabs(apart - sfxVoiceDb) <= VOICE_MATCH_DB, "%+.1f dB against voice 0, %+.1f asked (a full-scale sample peaks where the voice does)", apart, sfxVoiceDb);
}
if (c->haveBalanceVoice && c->haveBalanceMod) {
apart = c->balanceMod - c->balanceVoice;
confName(c, "mod as loud as a voice");
confCheck(c, fabs(apart - MOD_VOICE_DB) <= VOICE_MATCH_DB, "%+.1f dB against voice 0, %+.1f asked (a module channel at full volume peaks where the voice does)", apart, MOD_VOICE_DB);
}
confNoiseLevels(c);
// Each pitch darker than the one before, where the later one's rate is
// inside the recording's band: rates past half the sample rate all
// record as near-white noise and cannot be told apart.
if (c->noiseCentroids.count > 1u) {
darker = true;
text.len = 0u;
for (i = 0u; i < c->noiseCentroids.count; i++) {
now = &c->noiseCentroids.items[i];
gateTextAdd(&text, "%s%ld: %.0f Hz", i > 0u ? ", " : "", now->first, now->value);
if (i > 0u && NOISE_RATE_HZ / (double)(now->first > 1 ? now->first : 1) < (double)c->rate / 2.0 && !(now->value < c->noiseCentroids.items[i - 1u].value)) {
darker = false;
}
}
confName(c, "noise darkens");
confCheck(c, darker, "%s", gateTextString(&text));
}
toolBufFree(&text);
}
// Per FRAME_S hop, whether the 1 kHz sync tone dominates (a
// loud window holding SYNC_SHARE of its power in the sync band).
static bool *confSyncFlags(ConfT *c, size_t *count) {
bool *flags;
double total;
size_t bins;
size_t lo;
size_t hi;
size_t start;
size_t k;
size_t i;
bool loud;
bins = c->window / 2u + 1u;
*count = c->frames > c->window ? (c->frames - c->window + c->hop - 1u) / c->hop : 0u;
flags = (bool *)memoryFor(*count, sizeof(bool));
bandOf(c->freqs, bins, SYNC_HZ - SYNC_BAND_HZ, SYNC_HZ + SYNC_BAND_HZ, false, &lo, &hi);
for (i = 0u; i < *count; i++) {
start = i * c->hop;
for (k = 0u; k < c->window; k++) {
c->scratch[k] = c->mono[start + k] * c->taper[k];
}
gateFftAbs(&c->windowFft, c->scratch, c->window, c->bins);
for (k = 0u; k < bins; k++) {
c->bins[k] *= c->bins[k];
}
total = gateSum(c->bins, bins);
loud = confDbfs(c->mono + start, c->window) > SYNC_MIN_DBFS;
flags[i] = loud && total > 0.0 && gateSum(c->bins + lo, hi - lo) / total > SYNC_SHARE;
}
return flags;
}
// The first frame at or after from that starts
// SYNC_RUN_FRAMES of sync; false when none does.
static bool confSyncOnset(const bool *flags, size_t count, long from, long *at) {
long run;
long i;
run = 0;
for (i = from; i < (long)count; i++) {
run = flags[i] ? run + 1 : 0;
if (run == SYNC_RUN_FRAMES) {
*at = i - SYNC_RUN_FRAMES + 1;
return true;
}
}
return false;
}
// A tick step: the callbacks that came, against hz * time.
static void confTick(ConfT *c, const EventT *e) {
long hz;
long calls;
long ms;
double expected;
double slack;
bool registered;
hz = confInt(c, e, TICK_HZ_AT);
registered = strcmp(confValue(c, e, TICK_OK_AT), "1") == 0;
calls = confInt(c, e, TICK_CALLS_AT);
ms = confInt(c, e, TICK_MS_AT);
expected = (double)(hz * ms) / MS_PER_SECOND;
slack = TICK_SHARE * expected > TICK_MIN ? TICK_SHARE * expected : TICK_MIN;
confName(c, "tick %ld Hz", hz);
confCheck(c, registered && fabs((double)calls - expected) <= slack + TICK_SLACK, "registered %s, %ld calls in %ld ms, %.0f expected", registered ? "yes" : "no", calls, ms, expected);
}
// A tone or sync step: at its pitch, and sounding; a tone's level kept for
// the evenness check, the 440 Hz one's pan graded.
static void confTone(ConfT *c, double start, double end, const EventT *e) {
SamplesT x;
double hz;
double got;
double level;
hz = confFloat(c, e, TONE_HZ_AT);
x = confCut(c, start, end, CHANNEL_MIX, 0.0);
got = confPitch(c, x.x, x.n);
level = confDbfs(x.x, x.n);
free(x.x);
confName(c, "%s %lld Hz", e->kind, (long long)truncated(hz));
confCheck(c, fabs(got / hz - 1.0) <= PITCH_TOLERANCE && level > SOUND_DBFS, "%.1f Hz at %.1f dBFS", got, level);
if (strcmp(e->kind, "tone") == 0) {
entriesAdd(&c->toneLevels, (long)truncated(hz), 0, level);
if (c->stereo && hz == CENTRE_CHECK_HZ) {
confCentred(c, start, end, "tone");
}
}
}
// A log value, which must be there.
static const char *confValue(const ConfT *c, const EventT *e, size_t at) {
if (at >= e->valueCount) {
toolFail("%s: AUDPAR %s at %.3f s has %zu values, not %zu", c->logPath, e->kind, e->time, e->valueCount, at + 1u);
}
return e->values[at];
}
// A voice step: its level kept for the attenuation and loudness checks,
// its pan graded at attenuation 0, its pitch while it is loud enough to
// read.
static void confVoice(ConfT *c, double start, double end, const EventT *e) {
SamplesT x;
long voice;
long atten;
double hz;
double level;
double got;
char what[TEXT_MAX];
voice = confInt(c, e, VOICE_NUMBER_AT);
hz = confFloat(c, e, VOICE_HZ_AT);
atten = confInt(c, e, VOICE_ATTEN_AT);
x = confCut(c, start, end, CHANNEL_MIX, 0.0);
level = confSteady(c, x.x, x.n);
if (c->stereo && atten == 0) {
snprintf(what, sizeof(what), "voice %ld", voice);
confCentred(c, start, end, what);
}
entriesSet(&c->voiceLevels, voice, atten, level);
if (voice == 0 && atten == 0) {
c->balanceVoice = confLoudest(c, start, end, true, 0.0);
c->haveBalanceVoice = true;
}
if (atten <= VOICE_PITCH_ATTEN_MAX) {
got = confPitch(c, x.x, x.n);
confName(c, "voice %ld %lld Hz atten %ld", voice, (long long)truncated(hz), atten);
confCheck(c, fabs(got / hz - 1.0) <= PITCH_TOLERANCE, "%.1f Hz at %.1f dBFS", got, level);
}
free(x.x);
}
// scipy.signal.welch(x - mean, rate, nperseg=min(2048, n)): Hann-windowed
// segments half overlapped, each made zero mean, their one-sided power
// densities averaged in turn. The bins' frequencies and densities, in
// malloc'd arrays; how many.
static size_t confWelch(ConfT *c, const double *x, size_t n, double **freqs, double **psd) {
double *centred;
double *window;
double *squares;
double *segment;
double *re;
double *im;
double scale;
double mean;
double power;
size_t perSegment;
size_t step;
size_t segments;
size_t bins;
size_t s;
size_t k;
perSegment = n < WELCH_SEGMENT_MAX ? n : WELCH_SEGMENT_MAX;
if (perSegment == 0u) {
toolFail("%s: a step too short to measure", c->logPath);
}
step = perSegment - perSegment / 2u;
segments = (n - perSegment / 2u) / step;
bins = perSegment / 2u + 1u;
centred = (double *)memoryFor(n, sizeof(double));
window = (double *)memoryFor(perSegment, sizeof(double));
squares = (double *)memoryFor(perSegment, sizeof(double));
segment = (double *)memoryFor(perSegment, sizeof(double));
re = (double *)memoryFor(bins, sizeof(double));
im = (double *)memoryFor(bins, sizeof(double));
*psd = (double *)memoryFor(bins, sizeof(double));
*freqs = freqsOf(perSegment, c->rate);
mean = gateMean(x, n);
for (k = 0u; k < n; k++) {
centred[k] = x[k] - mean;
}
confHann(window, perSegment);
for (k = 0u; k < perSegment; k++) {
squares[k] = window[k] * window[k];
}
scale = 1.0 / ((double)c->rate * gateSum(squares, perSegment));
fftFor(&c->welchFft, perSegment);
for (s = 0u; s < segments; s++) {
mean = gateMean(centred + s * step, perSegment);
for (k = 0u; k < perSegment; k++) {
segment[k] = window[k] * (centred[s * step + k] - mean);
}
gateFftReal(&c->welchFft, segment, perSegment, re, im);
for (k = 0u; k < bins; k++) {
power = (re[k] * re[k] + im[k] * im[k]) * scale;
// One-sided: every bin but DC (and Nyquist, for an even length)
// carries its negative twin too.
if (k > 0u && (perSegment % 2u != 0u || k < bins - 1u)) {
power *= 2.0;
}
(*psd)[k] += power;
}
}
for (k = 0u; k < bins; k++) {
(*psd)[k] /= (double)segments;
}
free(centred);
free(window);
free(squares);
free(segment);
free(re);
free(im);
return bins;
}
static void entriesAdd(EntriesT *list, long first, long second, double value) {
list->items = (EntryT *)arrayRoom(list->items, list->count, &list->cap, sizeof(EntryT));
list->items[list->count].first = first;
list->items[list->count].second = second;
list->items[list->count].value = value;
list->count++;
}
static EntryT *entriesFind(EntriesT *list, long first, long second) {
size_t i;
for (i = 0u; i < list->count; i++) {
if (list->items[i].first == first && list->items[i].second == second) {
return &list->items[i];
}
}
return NULL;
}
// The value at (first, second): replaced where the key is, else added at
// the end.
static void entriesSet(EntriesT *list, long first, long second, double value) {
EntryT *entry;
entry = entriesFind(list, first, second);
if (entry == NULL) {
entriesAdd(list, first, second, value);
return;
}
entry->value = value;
}
// A transform of n points: fft as it is when it is that long already.
static void fftFor(GateFftT *fft, size_t n) {
if (fft->n == n) {
return;
}
if (fft->n != 0u) {
gateFftFree(fft);
}
gateFftInit(fft, n);
}
// numpy.fft.rfftfreq(n, 1.0 / rate), malloc'd.
static double *freqsOf(size_t n, uint32_t rate) {
double *freqs;
size_t k;
freqs = (double *)memoryFor(n / 2u + 1u, sizeof(double));
for (k = 0u; k <= n / 2u; k++) {
freqs[k] = gateRfftFreq(k, n, rate);
}
return freqs;
}
// "label: text\n" appended to lines.
static void lineAdd(ToolBufT *lines, const char *label, const char *fmt, ...) {
va_list args;
gateTextAdd(lines, "%s: ", label);
va_start(args, fmt);
gateTextAddV(lines, fmt, args);
va_end(args);
toolBufByte(lines, '\n');
}
// count items of size bytes, zeroed (at least one, so none is not NULL);
// running out is fatal.
static void *memoryFor(size_t count, size_t size) {
void *memory;
memory = calloc(count > 0u ? count : 1u, size);
if (memory == NULL) {
toolFail("out of memory");
}
return memory;
}
// A 16-bit WAV's samples averaged over its channels; false, with what is
// wrong in problem, when it cannot be read.
static bool monoRead(const char *path, double **samples, size_t *count, uint32_t *rate, ToolBufT *problem) {
GateWavT wav;
if (!gateWavRead(path, &wav, problem)) {
return false;
}
if (wav.rate == 0u) {
gateTextAdd(problem, "%s: a rate of 0 Hz", path);
gateWavFree(&wav);
return false;
}
*samples = gateWavMono(&wav);
*count = gateWavFrames(&wav);
*rate = wav.rate;
gateWavFree(&wav);
return true;
}
// The level from startSec to endSec, summed in order; false when
// the span is empty.
static bool monoRms(const double *samples, size_t count, uint32_t rate, double startSec, double endSec, double *level) {
size_t a;
size_t b;
size_t i;
double total;
spanOf(truncated(startSec * (double)rate), truncated(endSec * (double)rate), count, &a, &b);
if (b <= a) {
return false;
}
total = 0.0;
for (i = a; i < b; i++) {
total += samples[i] * samples[i];
}
*level = sqrt(total / (double)(b - a));
return true;
}
// onset: the byte offset of the first stereo frame of raw PCM holding a
// sample louder than THRESHOLD, or nothing.
static int onsetRun(int argc, char **argv) {
uint8_t *raw;
size_t len;
size_t i;
long threshold;
int16_t sample;
if (argc != ONSET_ARGS) {
usageFail("onset needs RAW THRESHOLD");
}
threshold = gateArgLong("threshold", argv[FIRST_ARG + 1]);
raw = rawRead(argv[FIRST_ARG], &len);
len -= len % RAW_FRAME_BYTES;
for (i = 0u; i < len / SAMPLE_BYTES; i++) {
sample = (int16_t)((uint32_t)raw[i * SAMPLE_BYTES] | ((uint32_t)raw[i * SAMPLE_BYTES + 1u] << BYTE_BITS));
if (labs((long)sample) > threshold) {
printf("%zu\n", (i / RAW_CHANNELS) * RAW_FRAME_BYTES);
break;
}
}
free(raw);
return 0;
}
// Whether argv[*a] is the option name, as "name VALUE" (the next argument
// taken) or "name=VALUE"; its value at *value. A name at the end, without
// its value, is a usage error.
static bool optionValue(int argc, char **argv, int *a, const char *name, const char **value) {
size_t len;
len = strlen(name);
if (strncmp(argv[*a], name, len) != 0) {
return false;
}
if (argv[*a][len] == '=') {
*value = argv[*a] + len + 1u;
return true;
}
if (argv[*a][len] != '\0') {
return false;
}
if (*a + 1 >= argc) {
usageFail("%s needs a value", name);
}
(*a)++;
*value = argv[*a];
return true;
}
// phases: each phase's RMS within its bounds.
static int phasesRun(int argc, char **argv) {
const char *label;
const char *path;
char *fields[PHASE_FIELDS];
char *copy;
char *field;
char *colon;
double *samples;
double duration;
double level;
double bounds[PHASE_FIELDS];
ToolBufT problem;
ToolBufT parts;
ToolBufT fails;
size_t count;
size_t fieldCount;
size_t f;
uint32_t rate;
bool good;
int a;
if (argc < PHASES_ARGS_MIN) {
usageFail("phases needs LABEL WAV PHASE...");
}
label = argv[FIRST_ARG];
path = argv[FIRST_ARG + 1];
memset(&problem, 0, sizeof(problem));
memset(&parts, 0, sizeof(parts));
memset(&fails, 0, sizeof(fails));
if (!monoRead(path, &samples, &count, &rate, &problem)) {
fprintf(stderr, "%s: FAIL - %s\n", label, gateTextString(&problem));
return FAIL_EXIT;
}
duration = (double)count / (double)rate;
if (duration < RECORDING_MIN_S) {
fprintf(stderr, "%s: FAIL - recorded only %.2fs of audio; the capture never attached to the emulator's output\n", label, duration);
return FAIL_EXIT;
}
for (a = FIRST_ARG + 2; a < argc; a++) {
copy = strdup(argv[a]);
if (copy == NULL) {
toolFail("out of memory");
}
// name:start:end:low:high, the times numbers, the bounds numbers or
// empty.
fieldCount = 0u;
for (field = copy; field != NULL; field = colon != NULL ? colon + 1 : NULL) {
colon = strchr(field, ':');
if (colon != NULL) {
*colon = '\0';
}
if (fieldCount < PHASE_FIELDS) {
fields[fieldCount] = field;
}
fieldCount++;
}
good = fieldCount == PHASE_FIELDS;
for (f = PHASE_START; good && f < PHASE_FIELDS; f++) {
good = (f >= PHASE_MIN && fields[f][0] == '\0') || gateArgParseDouble(fields[f], &bounds[f]);
}
if (!good) {
fprintf(stderr, "%s: bad phase spec '%s'\n", label, argv[a]);
return GATE_USAGE_EXIT;
}
if (!monoRms(samples, count, rate, bounds[PHASE_START], bounds[PHASE_END], &level)) {
lineAdd(&fails, label, "phase '%s' (%s-%ss) is past the end of a %.1fs recording", fields[PHASE_NAME], fields[PHASE_START], fields[PHASE_END], duration);
} else {
gateTextAdd(&parts, "%s%s=%.0f", parts.len > 0u ? " " : "", fields[PHASE_NAME], level);
if (fields[PHASE_MIN][0] != '\0' && level < bounds[PHASE_MIN]) {
lineAdd(&fails, label, "phase '%s' RMS %.0f is below %s: expected sound here and got near-silence", fields[PHASE_NAME], level, fields[PHASE_MIN]);
}
if (fields[PHASE_MAX][0] != '\0' && level > bounds[PHASE_MAX]) {
lineAdd(&fails, label, "phase '%s' RMS %.0f is above %s: expected near-silence here and got sound", fields[PHASE_NAME], level, fields[PHASE_MAX]);
}
}
free(copy);
}
free(samples);
printf("%s: %.1fs recorded, RMS %s\n", label, duration, parts.len > 0u ? gateTextString(&parts) : "");
if (fails.len > 0u) {
fwrite(fails.data, 1u, fails.len, stderr);
fprintf(stderr, "%s: FAIL\n", label);
return FAIL_EXIT;
}
printf("%s: PASS\n", label);
return 0;
}
// Zero crossings a second from start to end.
static double pspCrossings(const double *samples, size_t count, uint32_t rate, double start, double end) {
size_t a;
size_t b;
size_t i;
size_t crossings;
spanOf(truncated(start * (double)rate), truncated(end * (double)rate), count, &a, &b);
crossings = 0u;
for (i = a + 1u; i < b; i++) {
if ((samples[i - 1u] < 0.0) != (samples[i] < 0.0)) {
crossings++;
}
}
return (double)crossings / ((double)(b - a) / (double)rate);
}
// psp: grade what AUDIOPRB played on the PSP. AUDIOPRB is examples/audio
// built to press its own keys (make/examples.mk): the tracker song from the
// start, then S every PROBE_STREAM_FRAMES frames from PROBE_STREAM_FIRST
// (and N once, whose noise sweep the streams cover). Each S streams a
// falling square wave, each take STREAM_TAKE_HZ higher than the last.
// scripts/verify-psp-audio.sh records what reached the PSP's audio output
// (PPSSPP's own recording, 44.1 kHz stereo on the emulated clock), and this
// grades it from the moment the music starts:
//
// music the tracker song alone, before the first take: sound, and
// quieter than a take
// take N each of the first three takes, a little way in: loud, and at
// its pitch, from the zero crossings -- so the PSP plays the mix
// at the rate it was made for
// end the recording stops within END_SLACK seconds of the program
// being told to quit: it ran until then, and ended on it
//
// QUIT_SECONDS is when (since the recording began) the gate pressed Select,
// which the PSP port gives as Escape. Prints one line per check and PASS or
// FAIL; exit 1 on a failure.
static int pspRun(int argc, char **argv) {
const char *label;
const char *path;
double *samples;
double quitSeconds;
double duration;
double onset;
double level;
double due;
double start;
double end;
double hz;
double want;
ToolBufT problem;
ToolBufT fails;
size_t count;
size_t i;
uint32_t rate;
int take;
if (argc != PSP_ARGS) {
usageFail("psp needs LABEL WAV QUIT_SECONDS");
}
label = argv[FIRST_ARG];
path = argv[FIRST_ARG + 1];
quitSeconds = gateArgDouble("quit seconds", argv[FIRST_ARG + 2]);
memset(&problem, 0, sizeof(problem));
memset(&fails, 0, sizeof(fails));
if (!monoRead(path, &samples, &count, &rate, &problem)) {
fprintf(stderr, "%s: FAIL - %s\n", label, gateTextString(&problem));
return FAIL_EXIT;
}
duration = (double)count / (double)rate;
i = 0u;
while (i < count && fabs(samples[i]) <= ONSET_LEVEL) {
i++;
}
if (i == count) {
fprintf(stderr, "%s: FAIL - %.1fs recorded and all of it silent\n", label, duration);
return FAIL_EXIT;
}
onset = (double)i / (double)rate;
if (!monoRms(samples, count, rate, onset + MUSIC_WINDOW_START, onset + MUSIC_WINDOW_END, &level)) {
fprintf(stderr, "%s: FAIL - the music starts at %.2fs, too near the end of a %.1fs recording to measure\n", label, onset, duration);
return FAIL_EXIT;
}
printf("%s: music from %.2fs, RMS %.0f\n", label, onset, level);
if (!(MUSIC_RMS_MIN <= level && level <= MUSIC_RMS_MAX)) {
lineAdd(&fails, label, "FAIL - music RMS %.0f, want %d-%d", level, MUSIC_RMS_MIN, MUSIC_RMS_MAX);
}
for (take = 0; take < TAKES; take++) {
due = onset + (double)(PROBE_STREAM_FIRST + PROBE_STREAM_FRAMES * take) / (double)FRAME_HZ;
start = due + TAKE_WINDOW_START;
end = due + TAKE_WINDOW_END;
if (!monoRms(samples, count, rate, start, end, &level)) {
lineAdd(&fails, label, "FAIL - take %d is past the end of the recording", take + 1);
continue;
}
hz = pspCrossings(samples, count, rate, start, end) / CROSSINGS_PER_CYCLE;
want = pspTakeHz(take, (TAKE_WINDOW_START + TAKE_WINDOW_END) / 2.0);
printf("%s: take %d at %.2fs: RMS %.0f, %.0f Hz (want %.0f)\n", label, take + 1, start, level, hz, want);
if (level < TAKE_MIN_RMS) {
lineAdd(&fails, label, "FAIL - take %d RMS %.0f, want at least %d", take + 1, level, TAKE_MIN_RMS);
}
if (fabs(hz - want) > want * TAKE_PITCH_TOLERANCE) {
lineAdd(&fails, label, "FAIL - take %d at %.0f Hz, want %.0f", take + 1, hz, want);
}
}
printf("%s: the recording ends at %.2fs, Select at %.2fs\n", label, duration, quitSeconds);
if (fabs(duration - quitSeconds) > END_SLACK) {
lineAdd(&fails, label, "FAIL - the recording ends at %.2fs, not within %.1fs of Select at %.2fs", duration, END_SLACK, quitSeconds);
}
free(samples);
if (fails.len > 0u) {
fwrite(fails.data, 1u, fails.len, stderr);
return FAIL_EXIT;
}
printf("%s: PASS\n", label);
return 0;
}
// The pitch a take is at, seconds after it starts.
static double pspTakeHz(int take, double seconds) {
long block;
block = (long)truncated(seconds * (double)STREAM_RATE_HZ) / STREAM_BLOCK;
return (double)(STREAM_START_HZ + STREAM_TAKE_HZ * take) - (double)(STREAM_DROP_HZ * block) / (double)STREAM_BLOCKS;
}
// A raw PCM file, whole; it is fatal when it cannot be read.
static uint8_t *rawRead(const char *path, size_t *len) {
ToolBufT problem;
uint8_t *raw;
memset(&problem, 0, sizeof(problem));
if (!gateTextReadFile(path, &raw, len, &problem)) {
toolFail("%s", gateTextString(&problem));
}
return raw;
}
// [a, b) on len items, clamped to them: [lo, hi), empty when b <= a.
static void spanOf(int64_t a, int64_t b, size_t len, size_t *lo, size_t *hi) {
int64_t n;
n = (int64_t)len;
a = a < 0 ? 0 : (a > n ? n : a);
b = b < 0 ? 0 : (b > n ? n : b);
*lo = (size_t)a;
*hi = (size_t)(b > a ? b : a);
}
// value toward zero, as a sample index (clamped to int64_t's range; NaN is
// 0).
static int64_t truncated(double value) {
if (isnan(value)) {
return 0;
}
if (value >= (double)INT64_MAX) {
return INT64_MAX;
}
if (value <= (double)INT64_MIN) {
return INT64_MIN;
}
return (int64_t)value;
}
// A usage error: what is wrong, then the usage; exit 2.
static void usageFail(const char *fmt, ...) {
va_list args;
fprintf(stderr, "%s: ", TOOL_NAME);
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
fputc('\n', stderr);
fputs(kUsage, stderr);
exit(GATE_USAGE_EXIT);
}
// wav: raw signed 16-bit stereo PCM, from byte SKIP on and cut to whole
// frames, as a WAV at RATE.
static int wavRun(int argc, char **argv) {
uint8_t *raw;
size_t len;
size_t from;
long rate;
long skip;
if (argc < WAV_ARGS_MIN || argc > WAV_ARGS_MAX) {
usageFail("wav needs RAW OUT.WAV RATE [SKIP]");
}
rate = gateArgLong("rate", argv[FIRST_ARG + 2]);
skip = argc == WAV_ARGS_MAX ? gateArgLong("skip", argv[FIRST_ARG + 3]) : 0;
if (rate <= 0 || rate > (long)UINT32_MAX || skip < 0) {
usageFail("RATE must be above 0 and SKIP not below it");
}
raw = rawRead(argv[FIRST_ARG], &len);
from = (size_t)skip < len ? (size_t)skip : len;
if (!gateWavWrite(argv[FIRST_ARG + 1], RAW_CHANNELS, (uint32_t)rate, raw + from, (len - from) / RAW_FRAME_BYTES * RAW_FRAME_BYTES)) {
toolFail("cannot write %s: %s", argv[FIRST_ARG + 1], strerror(errno));
}
free(raw);
return 0;
}
int main(int argc, char **argv) {
size_t i;
toolFileName(TOOL_NAME);
if (argc > COMMAND_ARG && (strcmp(argv[COMMAND_ARG], "-h") == 0 || strcmp(argv[COMMAND_ARG], "--help") == 0)) {
fputs(kUsage, stdout);
return 0;
}
for (i = 0u; argc > COMMAND_ARG && i < GATE_COUNT_OF(kCommands); i++) {
if (strcmp(argv[COMMAND_ARG], kCommands[i].name) == 0) {
return kCommands[i].run(argc, argv);
}
}
if (argc > COMMAND_ARG) {
usageFail("unknown command %s", argv[COMMAND_ARG]);
}
usageFail("no command");
}