// 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 #include #include #include #include #include #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_CENTER_DB 3.0 #define CENTER_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 ". 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 confCentered(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", "center", "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_CENTER_DB of each other. static void confCentered(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 centered", what); confCheck(c, fabs(left - right) <= PAN_CENTER_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 center (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 ": 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 "). 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 (center, 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_CENTER_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 modeled 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 == CENTER_CHECK_HZ) { confCentered(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); confCentered(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 *centered; 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; centered = (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++) { centered[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(centered + s * step, perSegment); for (k = 0u; k < perSegment; k++) { segment[k] = window[k] * (centered[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(centered); 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"); }