PPM control for NDF fractional control

This commit is contained in:
Chris Frankland-Wright 2025-10-28 16:24:44 +00:00
parent 2e8bc9ac5e
commit 2295a29d75
11 changed files with 532 additions and 20 deletions

View file

@ -0,0 +1,264 @@
/* Linear Timecode for Audio Library for Teensy 3.x / 4.x
Copyright (c) 2019, Frank Bösing, f.boesing (at) gmx.de
Development of this audio library was funded by PJRC.COM, LLC by sales of
Teensy and Audio Adaptor boards. Please support PJRC's efforts to develop
open source software by purchasing Teensy or other PJRC products.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice, development funding notice, and this permission
notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
/*
https://forum.pjrc.com/threads/41584-Audio-Library-for-Linear-Timecode-(LTC)
LTC example audio at: https://www.youtube.com/watch?v=uzje8fDyrgg
Forked by Chris Frankland-Wright 2025 for Teensy Audio Shield Input with autodetect FPS for the Fetch | Haichi
*/
#include <Arduino.h>
#include <Audio.h>
#include "analyze_ltc.h"
// —— Configuration ——
const float FORCE_FPS = 0.0f; // 0 → autodetect
const int FRAME_OFFSET = 4; // compensation in frames
const unsigned long LOSS_TIMEOUT = 1000UL; // ms before we go into LOST
// BLINK_PERIOD is now the half-period (on or off time)
const unsigned long BLINK_PERIOD[3] = {100, 500, 100}; // ACTIVE, LOST, NO_LTC (base)
AudioInputI2S i2s1;
AudioAnalyzeLTC ltc1;
AudioControlSGTL5000 sgtl5000;
AudioConnection patchCord(i2s1, 0, ltc1, 0);
enum State { NO_LTC=0, LTC_ACTIVE, LTC_LOST };
State ltcState = NO_LTC;
bool ledOn = false;
unsigned long lastDecode = 0;
unsigned long lastBlink = 0;
// Variables for NO_LTC double-blink pattern
int noLtcBlinkCount = 0;
unsigned long noLtcPauseTime = 600;
// FPS detection
float currentFps = 25.0f;
float periodMs = 0;
const float SMOOTH_ALPHA = 0.05f; // Much slower smoothing to reduce noise
unsigned long lastDetectTs = 0;
// Simple consecutive counter approach
float candidateFps = 25.0f;
int consecutiveCount = 0;
const int REQUIRED_CONSECUTIVE = 5; // Need 5 consecutive same readings
// freerun
long freeAbsFrame = 0;
unsigned long lastFreeRun = 0;
void setup() {
Serial.begin(115200);
AudioMemory(12);
sgtl5000.enable();
sgtl5000.inputSelect(AUDIO_INPUT_LINEIN);
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
unsigned long now = millis();
// compute framePeriod from currentFps
unsigned long framePeriod = (unsigned long)(1000.0f / currentFps + 0.5f);
// 1) If in ACTIVE and we've gone > LOSS_TIMEOUT w/o decode, enter LOST
if (ltcState == LTC_ACTIVE && (now - lastDecode) >= LOSS_TIMEOUT) {
ltcState = LTC_LOST;
// bump freeAbsFrame by 1second worth of frames:
int nominal = (currentFps>29.5f) ? 30 : int(currentFps+0.5f);
long dayFrames= 24L*3600L*nominal;
freeAbsFrame = (freeAbsFrame + nominal) % dayFrames;
// reset freerun timer so we start next tick fresh
lastFreeRun = now;
}
// 2) Handle incoming LTC frame
if (ltc1.available()) {
ltcframe_t frame = ltc1.read();
int h = ltc1.hour(&frame),
m = ltc1.minute(&frame),
s = ltc1.second(&frame),
f = ltc1.frame(&frame);
bool isDF = ltc1.bit10(&frame);
// — FPS detect or force —
if (FORCE_FPS > 0.0f) {
currentFps = FORCE_FPS;
} else {
if (isDF) {
// Drop-frame flag is only used for 29.97fps.
if (currentFps != 29.97f) {
currentFps = 29.97f;
consecutiveCount = 0;
}
} else {
if (lastDetectTs) {
float dt = now - lastDetectTs;
// Use an IIR filter to smooth the measured period
periodMs = (periodMs == 0) ? dt : (SMOOTH_ALPHA * dt + (1.0f - SMOOTH_ALPHA) * periodMs);
float measFps = 1000.0f / periodMs;
// More comprehensive list of standard frame rates
const float choices[] = {23.98f, 24.0f, 25.0f, 29.97f, 30.0f};
float bestFit = 25.0f;
float minDiff = 1e6;
for (auto rate : choices) {
float diff = fabsf(measFps - rate);
if (diff < minDiff) {
minDiff = diff;
bestFit = rate;
}
}
// Use wider thresholds and hysteresis based on current frame rate
float newFps = bestFit;
// For 23.98/24fps discrimination with hysteresis
if (fabsf(bestFit - 24.0f) < 0.1f || fabsf(bestFit - 23.98f) < 0.1f) {
if (currentFps == 24.0f) {
// Currently 24fps - need much stronger evidence of 23.98 to switch
newFps = (measFps < 23.96f) ? 23.98f : 24.0f;
} else if (currentFps == 23.98f) {
// Currently 23.98fps - need evidence of 24 to switch back
newFps = (measFps > 23.99f) ? 24.0f : 23.98f;
} else {
// First detection - strongly favor 24fps unless very clearly 23.98
newFps = (measFps < 23.965f) ? 23.98f : 24.0f;
}
// Debug output to see what's being measured
//Serial.printf("[DEBUG] measFps=%.3f, newFps=%.2f, currentFps=%.2f\r\n",
// measFps, newFps, currentFps);
}
// For 29.97/30fps discrimination with hysteresis
else if (fabsf(bestFit - 30.0f) < 0.1f || fabsf(bestFit - 29.97f) < 0.1f) {
if (currentFps == 30.0f) {
// Currently 30fps - need strong evidence of 29.97 to switch
newFps = (measFps < 29.98f) ? 29.97f : 30.0f;
} else if (currentFps == 29.97f) {
// Currently 29.97fps - need evidence of 30 to switch back
newFps = (measFps > 29.99f) ? 30.0f : 29.97f;
} else {
// First detection - strongly favor 30fps unless very clearly 29.97
newFps = (measFps < 29.98f) ? 29.97f : 30.0f;
}
}
// Require consecutive readings before changing frame rate
if (newFps == candidateFps) {
consecutiveCount++;
if (consecutiveCount >= REQUIRED_CONSECUTIVE && newFps != currentFps) {
currentFps = newFps;
consecutiveCount = 0;
}
} else {
candidateFps = newFps;
consecutiveCount = 1;
}
}
}
lastDetectTs = now;
}
// — pack + offset + wrap —
int nominal = (currentFps>29.5f) ? 30 : int(currentFps+0.5f);
long dayFrames = 24L*3600L*nominal;
long absF = ((long)h*3600 + m*60 + s)*nominal + f + FRAME_OFFSET;
absF = (absF % dayFrames + dayFrames) % dayFrames;
// — reset anchors & state —
freeAbsFrame = absF;
lastFreeRun = now;
lastDecode = now;
ltcState = LTC_ACTIVE;
// — print LOCK —
long totSec = absF/nominal;
int outF = absF % nominal;
int outS = totSec % 60;
long totMin = totSec/60;
int outM = totMin % 60;
int outH = (totMin/60) % 24;
char sep = isDF?';':':';
Serial.printf("[LOCK] %02d:%02d:%02d%c%02d | %.2ffps\r\n",
outH,outM,outS,sep,outF,currentFps);
// — LED → ACTIVE immediately —
lastBlink = now;
ledOn = true;
digitalWrite(LED_BUILTIN, HIGH);
noLtcBlinkCount = 0; // Reset pattern when LTC becomes active
}
// 3) If in LOST, do freerun printing
else if (ltcState == LTC_LOST) {
if ((now - lastFreeRun) >= framePeriod) {
freeAbsFrame = (freeAbsFrame + 1) % (24L*3600L*((int)(currentFps+0.5f)));
lastFreeRun += framePeriod;
// — print FREE —
int nominal = (currentFps>29.5f) ? 30 : int(currentFps+0.5f);
long totSec = freeAbsFrame/nominal;
int outF = freeAbsFrame % nominal;
int outS = totSec % 60;
long totMin = totSec/60;
int outM = totMin % 60;
int outH = (totMin/60)%24;
Serial.printf("[FREE] %02d:%02d:%02d:%02d | %.2ffps\r\n",
outH,outM,outS,outF,currentFps);
}
noLtcBlinkCount = 0; // Reset pattern when LTC is lost
}
// 4) LED heartbeat
unsigned long now_led = millis();
if (ltcState == NO_LTC) {
unsigned long blinkInterval = BLINK_PERIOD[NO_LTC];
if (noLtcBlinkCount < 4) { // First two blinks (on-off-on-off)
if (now_led - lastBlink >= blinkInterval) {
ledOn = !ledOn;
digitalWrite(LED_BUILTIN, ledOn);
lastBlink = now_led;
noLtcBlinkCount++;
}
} else { // Pause
if (now_led - lastBlink >= noLtcPauseTime) {
noLtcBlinkCount = 0; // Reset for next double-blink
lastBlink = now_led;
}
}
} else { // LTC_ACTIVE or LTC_LOST
// LTC_ACTIVE: 5Hz flash (100ms on/off). Period = 200ms.
// LTC_LOST: 1Hz flash (500ms on/off). Period = 1000ms.
unsigned long blinkInterval = (ltcState == LTC_ACTIVE) ? 100 : BLINK_PERIOD[LTC_LOST];
if (now_led - lastBlink >= blinkInterval) {
ledOn = !ledOn;
digitalWrite(LED_BUILTIN, ledOn);
lastBlink = now_led;
}
}
}