forked from arendst/Tasmota
-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathloudness_gauge.tc
More file actions
319 lines (302 loc) · 15.6 KB
/
Copy pathloudness_gauge.tc
File metadata and controls
319 lines (302 loc) · 15.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
// loudness_gauge.tc — plugin-INDEPENDENT sound I/O demo for the Waveshare ESP32-P4 10.1".
//
// OUTPUT: on boot, init the ES8311 DAC over I2C and play an intro .wav (i2sBegin TX).
// INPUT : then init the ES7210 4-mic ADC over I2C and show a live loudness gauge
// (arc dial + % + peak + VU bar) on the LVGL display (i2sMicBegin RX).
//
// No audio plugin needed — both codecs are driven entirely from this script via the
// i2c* syscalls. Board: I2S MCLK=13 BCLK=12 WS=10 DOUT=9(spkr) DIN=11(mic) ; ES8311
// @0x18, ES7210 @0x40 ; display 800x1280. Needs USE_TINYC_LVGL firmware, ABI >= 7.
//
// The intro .wav MUST be 16 kHz mono 16-bit PCM (the ES8311 here is clocked for 16 kHz).
// ffmpeg -i in.wav -ar 16000 -ac 1 -c:a pcm_s16le /intro.wav
//
// ── TWO BUILD TARGETS ───────────────────────────────────────────────────────
// DEFAULT (no define): plugin-INDEPENDENT — this script drives ES8311 + ES7210
// itself via i2c*/i2sMic. Use on any board WITHOUT the audio BinPlugin.
// -DPLUGIN_MIC: the audio BinPlugin (xdrv_42_i2s) already owns the full-duplex
// I2S — its native i2sMic would conflict, so instead read the mic level via
// pluginQuery(42,0,10) and play the intro via audioPlay. Use on the P4 display
// where the plugin is loaded + inited (iniz). The ES7210/ES8311 I2C init and
// the WAV streamer are compiled OUT — the plugin does all of that.
// Enable by compiling with -DPLUGIN_MIC, or uncomment the next line:
// // @defines: -DPLUGIN_MIC
#define INTRO_WAV "/doorbell16.wav" // 16 kHz → no upsampling (8 kHz Startup.wav's upsample
// loop blows the 200k-instruction/call limit in main)
#define MIC_MCLK 13
#define MIC_BCLK 12
#define MIC_WS 10
#define MIC_DIN 11
#define DAC_DOUT 9
#define MIC_RATE 16000
#define PA_EN 53 // speaker power-amp enable (Waveshare P4: GPIO53)
#define OUTPUT 0x03
#define ES7210_ADDR 0x40
#define ES8311_ADDR 0x18
#define ES_BUS 0 // I2C bus the codecs are on
#define AL_TOP_MID 2
#define COL_BG 0x0e1016
#define COL_GREEN 0x3ddc84
#define COL_YELLOW 0xffcc44
#define COL_RED 0xff5555
#define COL_DIM 0x7a8aa0
// (PLUGIN_MIC only) the plugin returns a RAW peak |sample| and its analog mic gain is
// modest, so the signal sits low (e.g. ~40 quiet, a few hundred on speech). Map a
// noise-floor..full-scale window onto 0..100% so the dial actually moves. Tune to taste:
// lower MIC_FS = more sensitive; raise MIC_FLOOR if a silent room still shows a few %.
#define MIC_FLOOR 40 // raw level treated as 0%
#define MIC_FS 2500 // DEFAULT raw level for 100% (lower = more sensitive).
// Tune LIVE with no rebuild: send "MIC <n>" (e.g. MIC 4000).
int micok = 0;
int level = 0;
int peak = 0;
int g_pct = 0; // last gauge percent (echoed in the MIC debug line)
int g_fs = MIC_FS; // (PLUGIN_MIC) runtime full-scale; set live via "MIC <n>"
int arc; int lblPct; int lblPeak; int lblTitle; int bar;
#ifdef PLUGIN_MIC
char qbuf[16]; // pluginQuery dst for the mic peak string
#else
int wav_channels = 1; int wav_rate = 16000; int wav_bits = 16; int wav_data_size = 0;
int pcm[512]; // WAV streaming buffer (native intro only)
#endif
int dbg_fk = -9; int dbg_pr = -9; int dbg_rate = 0; int dbg_bits = 0; int dbg_ib = -9; int dbg_fr = 0; int dbg_path = -1; int dbg_stage = -1;
#ifndef PLUGIN_MIC // ── native codec drivers + WAV streamer (plugin build drops all of this) ──
// ───────── ES7210 mic ADC over I2C (port of pes7210_codec_init) ─────────
void aw(int reg, int val) { i2cWrite8(ES7210_ADDR, reg, val, ES_BUS); }
int ar_(int reg) { return i2cRead8(ES7210_ADDR, reg, ES_BUS); }
void aupd(int reg, int mask, int val) { int v = ar_(reg); v = (v & (255 - mask)) | (mask & val); aw(reg, v); }
void a_mic_all() {
int i;
for (i = 0; i < 4; i = i + 1) { aupd(0x43 + i, 0x10, 0x00); }
aw(0x4B, 0xff); aw(0x4C, 0xff);
aupd(0x01, 0x0b, 0x00); aw(0x4B, 0x00); aupd(0x43, 0x10, 0x10);
aupd(0x01, 0x0b, 0x00); aw(0x4B, 0x00); aupd(0x44, 0x10, 0x10);
aupd(0x01, 0x15, 0x00); aw(0x4C, 0x00); aupd(0x45, 0x10, 0x10);
aupd(0x01, 0x15, 0x00); aw(0x4C, 0x00); aupd(0x46, 0x10, 0x10);
}
void es7210_init() {
aw(0x00, 0xff); aw(0x00, 0x41); aw(0x01, 0x1f);
aw(0x09, 0x30); aw(0x0A, 0x30); aw(0x40, 0xC3);
aw(0x41, 0x70); aw(0x42, 0x70); aw(0x07, 0x20);
aw(0x02, 0xC1); aw(0x07, 0x20); aw(0x04, 0x01); aw(0x05, 0x00);
a_mic_all();
int iface = ar_(0x11) & 0x1f; aw(0x11, iface | 0x60);
iface = ar_(0x11) & 0xfc; aw(0x11, iface);
aw(0x12, 0x00);
aupd(0x43, 0x0f, 14); aupd(0x44, 0x0f, 14);
aupd(0x45, 0x0f, 0); aupd(0x46, 0x0f, 0);
int regv = ar_(0x01); aw(0x01, regv);
aw(0x06, 0x00);
aw(0x47, 0x00); aw(0x48, 0x00); aw(0x49, 0x00); aw(0x4A, 0x00);
a_mic_all();
}
// ───────── ES8311 DAC over I2C (port of pes8311_codec_init, slave 16-bit 16 kHz) ─────────
void dw(int reg, int val) { i2cWrite8(ES8311_ADDR, reg, val, ES_BUS); }
int dr(int reg) { return i2cRead8(ES8311_ADDR, reg, ES_BUS); }
void es8311_init() {
int v;
dw(0x01, 0x30); dw(0x02, 0x00); dw(0x03, 0x10); dw(0x16, 0x24);
dw(0x04, 0x10); dw(0x05, 0x00); dw(0x0B, 0x00); dw(0x0C, 0x00);
dw(0x10, 0x1F); dw(0x11, 0x7F); dw(0x00, 0x80);
v = dr(0x00) & 0xBF; dw(0x00, v); // slave
dw(0x01, 0x3F);
v = dr(0x01) & 0x7F; dw(0x01, v); // mclk from pin
v = dr(0x02) & 0x07; dw(0x02, v); // pre_div/multi = 1
dw(0x05, 0x00); // adc/dac div = 1
v = (dr(0x03) & 0x80) | 0x10; dw(0x03, v); // adc_osr
v = (dr(0x04) & 0x80) | 0x10; dw(0x04, v); // dac_osr
v = dr(0x07) & 0xC0; dw(0x07, v); // lrck_h
dw(0x08, 0xff); // lrck_l
v = (dr(0x06) & 0xE0) | 3; dw(0x06, v); // bclk_div = 4
v = dr(0x01) & (255 - 0x40); dw(0x01, v); // mclk not inverted
v = dr(0x06) & (255 - 0x20); dw(0x06, v); // sclk not inverted
dw(0x13, 0x10); dw(0x1B, 0x0A); dw(0x1C, 0x6A);
v = dr(0x09) | 0x0c; dw(0x09, v); // 16-bit
v = dr(0x0A) | 0x0c; dw(0x0A, v);
v = dr(0x09) & 0xFC; dw(0x09, v); // I2S normal
v = dr(0x0A) & 0xFC; dw(0x0A, v);
v = dr(0x09) & 0xBF; v = v & (255 - 0x40); dw(0x09, v); // enable DAC serial in
v = dr(0x0A) & 0xBF; v = v & (255 - 0x40); dw(0x0A, v);
dw(0x17, 0xBF); dw(0x0E, 0x02); dw(0x12, 0x00); dw(0x14, 0x1A);
v = dr(0x14) & (255 - 0x40); dw(0x14, v); // IS_DMIC = 0
dw(0x0D, 0x01); dw(0x15, 0x40); dw(0x37, 0x48); dw(0x45, 0x00);
v = dr(0x31) & 0x9f; dw(0x31, v); // unmute
dw(0x12, 0x00);
}
void es8311_volume(int vol) { // 0..100, perceptual
if (vol <= 0) { dw(0x32, 0); return; }
if (vol > 100) { vol = 100; }
// reg 0x32 is ~0.5 dB/step (log) → linear-in-reg = perceptually even. Use a usable band
// (reg ~110..195 ≈ -40..+2 dB); a raw 0-255 map left the bottom half inaudible.
dw(0x32, 110 + (vol * 85) / 100);
}
// ───────── WAV parse + play (16-bit, walks chunks; from wav_player) ─────────
int parse_wav(int f) {
char hdr[12]; int n = fileRead(f, hdr, 12);
if (n < 12) { return -1; }
if (hdr[0] != 'R' || hdr[1] != 'I' || hdr[2] != 'F' || hdr[3] != 'F') { return -1; }
int got_fmt = 0; int got_data = 0;
char ck[8]; char fmt[16]; char skip[64];
while (got_fmt == 0 || got_data == 0) {
n = fileRead(f, ck, 8);
if (n < 8) { return -1; }
int csz = (ck[4]&255) | ((ck[5]&255)<<8) | ((ck[6]&255)<<16) | ((ck[7]&255)<<24);
if (ck[0]=='f' && ck[1]=='m' && ck[2]=='t' && ck[3]==' ') {
n = fileRead(f, fmt, 16);
if (n < 16) { return -1; }
wav_channels = (fmt[2]&255) | ((fmt[3]&255)<<8);
wav_rate = (fmt[4]&255) | ((fmt[5]&255)<<8) | ((fmt[6]&255)<<16) | ((fmt[7]&255)<<24);
wav_bits = (fmt[14]&255) | ((fmt[15]&255)<<8);
got_fmt = 1;
int rem = csz - 16;
while (rem > 0) { int rd = 64; if (rd > rem) { rd = rem; } fileRead(f, skip, rd); rem = rem - rd; }
} else if (ck[0]=='d' && ck[1]=='a' && ck[2]=='t' && ck[3]=='a') {
wav_data_size = csz; got_data = 1;
} else {
int rem = csz;
while (rem > 0) { int rd = 64; if (rd > rem) { rd = rem; } fileRead(f, skip, rd); rem = rem - rd; }
}
}
return 0;
}
void play_wav(char path[]) {
char p[96];
int f = fileOpen(path, "r"); dbg_path = 0; // default route (ufsp)
if (f < 0) { sprintf(p, "/ffs%s", path); f = fileOpen(p, "r"); dbg_path = 1; } // flash FS
if (f < 0) { sprintf(p, "/sdfs%s", path); f = fileOpen(p, "r"); dbg_path = 2; } // SD FS (explicit)
dbg_fk = f;
if (f < 0) { addLog("intro: %s not found (all FS)", path); return; }
int pr = parse_wav(f); dbg_pr = pr; dbg_rate = wav_rate; dbg_bits = wav_bits;
if (pr < 0 || wav_bits != 16) { addLog("intro: bad/!16-bit wav"); fileClose(f); return; }
// ES8311 is clocked at MIC_RATE; upsample (sample-repeat) lower-rate WAVs so
// they play at the right pitch (e.g. 8 kHz -> ratio 2).
int ratio = MIC_RATE / wav_rate;
if (ratio < 1) { ratio = 1; }
if (ratio > 4) { ratio = 4; }
dbg_ib = i2sBegin(MIC_MCLK, MIC_BCLK, MIC_WS, DAC_DOUT, MIC_RATE);
if (dbg_ib < 0) { addLog("intro: i2sBegin failed"); fileClose(f); return; }
if (pinFree(PA_EN)) { pinMode(PA_EN, OUTPUT); }
digitalWrite(PA_EN, 1); // enable the speaker power amp for the intro
int remaining = wav_data_size / (wav_channels * 2);
int read_chunk = 128 / ratio; // small chunks keep the P4 I2S DMA fed (512/256 scrambled)
if (read_chunk < 1) { read_chunk = 1; }
while (remaining > 0) {
int chunk = read_chunk;
if (chunk > remaining) { chunk = remaining; }
int frames = fileReadPCM16(f, pcm, chunk, wav_channels);
if (frames <= 0) { break; }
if (ratio > 1) { // expand in place (backwards)
int i;
for (i = frames - 1; i >= 0; i = i - 1) {
int j;
for (j = 0; j < ratio; j = j + 1) { pcm[i * ratio + j] = pcm[i]; }
}
frames = frames * ratio;
}
int w = i2sWrite(pcm, frames); dbg_fr = dbg_fr + w;
if (w < frames) { break; } // TX stalled/timeout — stop, don't loop on noise
remaining = remaining - chunk;
}
fileClose(f);
delay(400); // drain the last DMA buffer before stopping
i2sStop();
digitalWrite(PA_EN, 0); // amp off after the intro (mic gauge doesn't need it)
addLog("intro: played");
}
#endif // !PLUGIN_MIC (native codec + WAV block)
// ───────── loudness meter ─────────
int isqrt(int n) {
if (n <= 0) { return 0; }
int x = n; int y = (x + 1) / 2;
while (y < x) { x = y; y = (x + n / x) / 2; }
return x;
}
int pct_of(int rms) { int p = (isqrt(rms) * 100) / 181; if (p > 100) { p = 100; } return p; }
void Every100ms() {
if (!micok) { return; }
#ifdef PLUGIN_MIC
int n = pluginQuery(qbuf, 42, 0, 10); // 10 = I2S_Q_MICLEVEL: peak |sample| 0..32767
int v = (n > 0) ? atoi(qbuf) : -1;
#else
int v = i2sMicLevel(); // native RMS-ish level 0..32767
#endif
if (v < 0) { return; }
level = v;
if (v > peak) { peak = v; } else { peak = (peak * 9) / 10; }
#ifdef PLUGIN_MIC
int p = (level > MIC_FLOOR ? (level - MIC_FLOOR) * 100 / g_fs : 0); if (p > 100) { p = 100; }
int pk = (peak > MIC_FLOOR ? (peak - MIC_FLOOR) * 100 / g_fs : 0); if (pk > 100) { pk = 100; }
#else
int p = pct_of(level); // native i2sMicLevel = RMS, perceptual sqrt curve
int pk = pct_of(peak);
#endif
g_pct = p;
lvglSetValue(arc, p, 0);
lvglSetValue(bar, p, 0);
char b[24];
sprintf(b, "%d%%", p); lvglSetText(lblPct, b);
sprintf(b, "peak %d%%", pk); lvglSetText(lblPeak, b);
int col = COL_GREEN;
if (p >= 85) { col = COL_RED; } else if (p >= 60) { col = COL_YELLOW; }
lvglSetTextColor(lblPct, col);
}
void Command(char cmd[]) {
#ifdef PLUGIN_MIC
int f = atoi(cmd); // "MIC <n>" sets full-scale live (atoi skips spaces)
if (f >= 50 && f <= 32000) { g_fs = f; }
#endif
char resp[120];
sprintf(resp, "stage=%d rms=%d pct=%d%% fs=%d cmd=[%s]",
dbg_stage, level, g_pct, g_fs, cmd);
responseCmnd(resp);
}
void build_gauge() {
lvglInit(); lvglClean(0); lvglSetBgColor(0, COL_BG);
lblTitle = lvglLabel(0); lvglSetText(lblTitle, "AUDIO LEVEL");
lvglSetFont(lblTitle, 28); lvglSetTextColor(lblTitle, COL_DIM); lvglAlign(lblTitle, AL_TOP_MID, 0, 150);
arc = lvglArc(0); lvglSetSize(arc, 600, 600); lvglSetRange(arc, 0, 100); lvglSetValue(arc, 0, 0);
lvglAlign(arc, AL_TOP_MID, 0, 300);
lblPct = lvglLabel(0); lvglSetText(lblPct, "0%"); lvglSetFont(lblPct, 28);
lvglSetTextColor(lblPct, COL_GREEN); lvglAlign(lblPct, AL_TOP_MID, 0, 580);
lblPeak = lvglLabel(0); lvglSetText(lblPeak, "peak 0%"); lvglSetTextColor(lblPeak, COL_DIM);
lvglAlign(lblPeak, AL_TOP_MID, 0, 940);
bar = lvglBar(0); lvglSetSize(bar, 640, 44); lvglSetRange(bar, 0, 100); lvglSetValue(bar, 0, 0);
lvglAlign(bar, AL_TOP_MID, 0, 1030);
}
int main() {
addCommand("MIC"); // register FIRST so MIC is queryable even if main aborts
dbg_stage = 0;
#ifndef PLUGIN_MIC
i2sMicStop(); // release any I2S RX/TX held by a previous run
i2sStop(); // (handles are global → the intro TX needs the pins free)
#endif
// Intro startup sound FIRST — before build_gauge. The LVGL init consumes the (4) global
// file handles, which made play_wav's fileOpen fail (fk=-1) when the gauge was built first;
// playing the intro before any LVGL work keeps the handles free.
#ifdef PLUGIN_MIC
// No intro by default on the plugin build: the plugin owns ONE full-duplex I2S channel,
// and its I2S_Play (TX) stops the shared clock when it ends, which wedges the always-on
// mic RX → the gauge then reads silence (the native path below can play first because it
// starts the mic AFTER the intro). Opt in with -DPLUGIN_INTRO if you accept that risk.
#ifdef PLUGIN_INTRO
audioPlay(INTRO_WAV);
#endif
#else
es8311_init(); // DAC for the intro
es8311_volume(60); // moderate
play_wav(INTRO_WAV); // native: 128-frame chunks (P4 DMA-safe), finite TX timeout
#endif
dbg_stage = 1; // intro done
build_gauge(); // then show the gauge
dbg_stage = 2; // gauge built
#ifdef PLUGIN_MIC
micok = 1; // the audio plugin already owns + inited the ES7210 mic;
// a quiet plugin just yields n=0 → level read skips (no crash)
#else
es7210_init(); // mic for the live gauge
micok = (i2sMicBegin(MIC_MCLK, MIC_BCLK, MIC_WS, MIC_DIN, MIC_RATE) == 0);
if (!micok) { addLog("loudness_gauge: mic FAILED"); }
#endif
dbg_stage = 3; // complete
return 0;
}