{"version":"V2.0","versionNumber":"V2.4.2","type":"core2","components":[{"name":"page0","type":"lvgl_page","layer":0,"screenId":"builtin","screenName":"","id":"yZNR+zZkaeb_6W%v","createTime":1771835181050,"backgroundColor":"#ffffff","isLVGL":true,"isSelected":true}],"resources":[{"hardware":["hardware_button","hardware_pin_button","imu","speaker","touch","mic","sdcard"]}],"units":[],"hats":[],"caps":[],"chains":[],"bases":[],"plcmodules":[],"i2cs":[],"chainBus":[],"blockly":"truepage0true","screen":[{"simulationName":"Built-in","type":"builtin","width":320,"height":240,"scale":0.77,"screenName":"","blockId":"","screenColorType":0,"rotation":1,"id":"builtin","createTime":1771835181047}],"logicWhenNum":0,"pythonCode":"# Afinador mínimo en Sol (G4)\n# M5Stack Core2 + Mic interno + barra de afinación\n\nimport M5\nfrom M5 import *\nimport time, math\n\nNOTE_G = 392.0 # Sol G4\n\nlabel_note = None\nlabel_freq = None\nlabel_status = None\n\nSAMPLE_RATE = 8000\nBUFFER_LEN = 2048\n\nrec_data = None\n\n# Zona de la barra\nBAR_X = 10\nBAR_Y = 140\nBAR_W = 300\nBAR_H = 40\n\nMAX_CENTS = 50.0 # rango visual +/- 50 cents\nNEUTRAL_CENTS = 10.0 # zona OK (+/- 10)\n\n\ndef zero_cross_from_array(arr, sample_rate):\n n = len(arr)\n if n < 10:\n return 0.0\n\n prev = arr[0]\n zero_cross = 0\n\n for i in range(1, n):\n v = arr[i]\n # ignorar ruido muy pequeño\n if abs(prev) < 1 and abs(v) < 1:\n prev = v\n continue\n if (prev <= 0 and v > 0) or (prev >= 0 and v < 0):\n zero_cross += 1\n prev = v\n\n if zero_cross < 2:\n return 0.0\n\n duration = n / float(sample_rate)\n return (zero_cross / 2.0) / duration\n\n\ndef detect_freq(buf, sample_rate):\n n = len(buf)\n if n < 16:\n return 0.0\n\n mean = sum(buf) / n\n arr = [b - mean for b in buf]\n\n return zero_cross_from_array(arr, sample_rate)\n\n\ndef cents_diff(f_meas, f_ref):\n if f_meas <= 0:\n return 9999.0\n return 1200.0 * math.log(f_meas / f_ref, 2.0)\n\n\ndef update_bar(diff_c):\n \"\"\"\n Dibuja la barra de afinación en la parte inferior.\n diff_c en cents (puede ser None si no hay señal).\n \"\"\"\n # Limpiar zona de barra\n M5.Lcd.fillRect(BAR_X, BAR_Y, BAR_W, BAR_H, 0x000000)\n\n # Marco de la barra\n M5.Lcd.drawRect(BAR_X, BAR_Y, BAR_W, BAR_H, 0x444444)\n\n mid_x = BAR_X + BAR_W // 2\n # Línea central (cents = 0)\n M5.Lcd.drawLine(mid_x, BAR_Y, mid_x, BAR_Y + BAR_H, 0x555555)\n\n # Zona \"OK\" en verde alrededor del centro\n neutral_pix = int((NEUTRAL_CENTS / MAX_CENTS) * (BAR_W // 2))\n if neutral_pix < 5:\n neutral_pix = 5\n M5.Lcd.fillRect(\n mid_x - neutral_pix,\n BAR_Y + BAR_H // 2 - 4,\n neutral_pix * 2,\n 8,\n 0x07E0, # verde\n )\n\n if diff_c is None or diff_c == 9999.0:\n # Sin señal: no dibujamos el indicador\n return\n\n # Limitar rango visual\n if diff_c > MAX_CENTS:\n d = MAX_CENTS\n elif diff_c < -MAX_CENTS:\n d = -MAX_CENTS\n else:\n d = diff_c\n\n # -MAX_CENTS -> izquierda ; +MAX_CENTS -> derecha\n ratio = d / MAX_CENTS # -1..1\n max_offset = (BAR_W // 2) - 6\n pos_x = int(mid_x + ratio * max_offset)\n\n # Color del indicador según si está alto/bajo/ok\n if diff_c > NEUTRAL_CENTS:\n color = 0x001F # azul (BAJA)\n elif diff_c < -NEUTRAL_CENTS:\n color = 0xF800 # rojo (SUBE)\n else:\n color = 0xFFFF # blanco dentro de zona verde\n\n # Rectángulo indicador\n M5.Lcd.fillRect(\n pos_x - 4,\n BAR_Y + 4,\n 8,\n BAR_H - 8,\n color,\n )\n\n\ndef setup():\n global rec_data, label_note, label_freq, label_status\n\n M5.begin()\n Widgets.fillScreen(0x000000)\n\n # Nota fija (blanco, tamaño medio)\n label_note = Widgets.Label(\n \"note\", 10, 15, 1.5,\n 0xFFFFFF, 0x000000, Widgets.FONTS.DejaVu18\n )\n label_note.setText(\"Zanfona Oscar: Sol\")\n\n # Frecuencia medida (amarillo)\n label_freq = Widgets.Label(\n \"freq\", 10, 55, 2.0,\n 0xFFFF00, 0x000000, Widgets.FONTS.DejaVu18\n )\n label_freq.setText(\"f = --- Hz\")\n\n # Estado SUBE/BAJA/OK (grande)\n label_status = Widgets.Label(\n \"status\", 10, 100, 2.2,\n 0xFFFFFF, 0x000000, Widgets.FONTS.DejaVu24\n )\n label_status.setText(\"\")\n\n # Mic\n Mic.begin()\n Mic.setSampleRate(SAMPLE_RATE)\n\n rec_data = bytearray(BUFFER_LEN)\n print(\"BUFFER_LEN:\", BUFFER_LEN)\n\n # Barra inicial sin señal\n update_bar(None)\n\n\ndef loop():\n global rec_data\n\n M5.update()\n\n # Grabamos un fragmento (~0.25 s)\n Mic.record(rec_data, SAMPLE_RATE, False)\n while Mic.isRecording():\n time.sleep_ms(5)\n M5.update()\n\n # Calcular frecuencia\n freq = detect_freq(rec_data, SAMPLE_RATE)\n\n label_freq.setText(\"f = {:.1f} Hz\".format(freq))\n\n if freq <= 0:\n label_status.setText(\"Sin señal \")\n label_status.setColor(0xFFFFFF, 0x000000)\n update_bar(None)\n return\n\n diff_c = cents_diff(freq, NOTE_G)\n\n if diff_c > NEUTRAL_CENTS:\n # Nota demasiado alta → BAJA la tensión\n label_status.setText(\"BAJA \")\n label_status.setColor(0x001F, 0x000000) # azul\n elif diff_c < -NEUTRAL_CENTS:\n # Nota demasiado baja → SUBE la tensión\n label_status.setText(\"SUBE \")\n label_status.setColor(0xF800, 0x000000) # rojo\n else:\n label_status.setText(\"OK \")\n label_status.setColor(0x07E0, 0x000000) # verde\n\n # Actualizar barra con los cents\n update_bar(diff_c)\n\n time.sleep_ms(40)\n\n\nif __name__ == \"__main__\":\n setup()\n while True:\n loop()","customList":[]}