2228 lines
80 KiB
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_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 <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 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 <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 (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");
|
|
}
|