-- The microphone and the voice pieces (PLAN.md section 62), with no microphone: SDL's disk audio -- driver reads its "microphone" from a file, which this script writes first -- two seconds of -- silence, three of a 220 Hz tone at half scale, three of 440 Hz at quarter scale, and silence after. -- Run it headless from a scratch directory holding this folder: -- -- SDL_AUDIO_DRIVER=disk SDL_AUDIO_DISK_INPUT_FILE=micIn.raw SDL_AUDIO_DISK_OUTPUT_FILE=micOut.raw \ -- SDL_AUDIO_FORMAT=F32 SDL_AUDIO_FREQUENCY=48000 SDL_AUDIO_CHANNELS=1 SDL_VIDEODRIVER=offscreen \ -- Singe -w micVoice/micVoice.singe -- -- It prints one line: MICTEST pass=N fail=0. Every failure prints what it wanted first. What it -- cannot prove here, and says so: that a real microphone sounds right, monitoring's delay, and a -- calibration that succeeds -- nothing plays the disk driver's speaker into its microphone, so the -- calibration must report that it never heard its clicks. dofile("Singe/Framework.singe") local RATE = 48000 local FRAME = 960 local INPUT = os.getenv("SDL_AUDIO_DISK_INPUT_FILE") local pass = 0 local fail = 0 local function check(what, got, want) if got == want then pass = pass + 1 else fail = fail + 1 debugPrint(string.format("FAIL %s: got %s, wanted %s", what, tostring(got), tostring(want))) end end local function near(what, got, want, within) check(what .. string.format(" (%.3f, wanted %.3f within %.3f)", got, want, within), math.abs(got - want) <= within, true) end -- 16-bit samples in a string, as micRead and opusDecode give them, and their RMS from 0 to 1. local function rmsOf(samples) local sum = 0 local count = #samples // 2 for i = 1, count do local value = string.unpack("= 1, true) check("micOpen", micOpen(), true) check("micIsOpen", micIsOpen(), true) local started = singeGetTicks() local steps = {} local function at(seconds, fn) steps[#steps + 1] = { at = seconds * 1000, fn = fn, done = false } end -- Silence first. at(1.0, function() local hz = micPitch() local rms = micLevel() check("no pitch in silence", hz, 0) check("silence is quiet", rms < 0.01, true) end) -- The 220 Hz stretch: a recording of two seconds, a read of one, and the pitch and level. at(2.5, function() check("micRecordStart", micRecordStart("micRecording.wav"), true) micRead() end) at(3.5, function() local samples = micRead() local hz, clarity = micPitch() local rms = micLevel() near("one second read", #samples / 2, RATE, RATE * 0.1) near("the read's level", rmsOf(samples), 0.5 / math.sqrt(2), 0.03) near("220 Hz found", hz, 220, 1.0) check("220 Hz is clear", clarity > 0.9, true) near("220 Hz level", rms, 0.5 / math.sqrt(2), 0.03) -- Opus round trip on a frame of it, and the decoded frame's level against the original's. local frame = samples:sub(1, FRAME * 2) local encoder = opusEncoderNew(RATE, 1, "audio") local decoder = opusDecoderNew(RATE, 1) local decoded = nil opusEncoderSet(encoder, 64000, 10) for _ = 1, 5 do decoded = opusDecode(decoder, opusEncode(encoder, frame)) end check("a decoded frame is a frame", #decoded, FRAME * 2) near("a decoded frame keeps its level", rmsOf(decoded), rmsOf(frame), rmsOf(frame) * 0.25) check("a lost packet is concealed to a frame", #opusDecode(decoder, nil), FRAME * 2) voiceFree(encoder) voiceFree(decoder) -- The voice processor on a run of frames of it. local processor = voiceProcessorNew(FRAME, RATE, 100) local cleaned = nil local voiced = nil for i = 0, 40 do local piece = samples:sub((i % 40) * FRAME * 2 + 1, (i % 40 + 1) * FRAME * 2) cleaned, voiced = voiceProcess(processor, piece) end check("a processed frame is a frame", #cleaned, FRAME * 2) check("a processed tone is not silenced", rmsOf(cleaned) > 0, true) check("voiced is a boolean", type(voiced), "boolean") voiceFree(processor) end) at(4.5, function() micRecordStop() local file = io.open(singeGetDataPath() .. "files/micRecording.wav", "rb") or io.open("micRecording.wav", "rb") local data = file and file:read("a") or "" if file then file:close() end check("the recording is a WAV", data:sub(1, 4) .. data:sub(9, 12), "RIFFWAVE") check("the recording is 48 kHz", #data >= 28 and string.unpack("= step.at then step.done = true step.fn() end end return OVERLAY_UPDATED end