/* * EVP RECORDER - Basic Version * OpenParanormal Project * * Records audio to SD card with auto-detection and manual control * Displays waveform on OLED screen * * Hardware: * - ESP32 DevKit * - MAX4466 Microphone (GPIO 34) * - SSD1306 OLED 128x64 I2C (SDA=21, SCL=22) * - SD Card Module SPI (CS=5, MOSI=23, MISO=19, SCK=18) * - Record Button (GPIO 13) * - Mode Button (GPIO 12) * - Power LED (GPIO 26) * - Recording LED (GPIO 25) */ #include #include #include #include #include // Pin Definitions #define MIC_PIN 34 #define SD_CS 5 #define BUTTON_RECORD 13 #define BUTTON_MODE 12 #define LED_POWER 26 #define LED_RECORD 25 // Display #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); // Recording State bool isRecording = false; bool autoDetect = true; int threshold = 2200; // Adjust based on your environment unsigned long recordingStart = 0; File audioFile; String currentFilename = ""; // Audio Buffer #define BUFFER_SIZE 512 uint16_t audioBuffer[BUFFER_SIZE]; int bufferIndex = 0; // Waveform Display #define WAVEFORM_POINTS 128 int waveformData[WAVEFORM_POINTS]; int waveformIndex = 0; // Button Debouncing unsigned long lastRecordPress = 0; unsigned long lastModePress = 0; #define DEBOUNCE_DELAY 200 // Display Mode enum DisplayMode { MODE_WAVEFORM, MODE_VU_METER, MODE_SETTINGS }; DisplayMode currentMode = MODE_WAVEFORM; // Statistics unsigned long samplesRecorded = 0; int peakLevel = 0; int avgLevel = 0; void setup() { Serial.begin(115200); Serial.println("EVP Recorder Starting..."); // Pin modes pinMode(MIC_PIN, INPUT); pinMode(BUTTON_RECORD, INPUT_PULLUP); pinMode(BUTTON_MODE, INPUT_PULLUP); pinMode(LED_POWER, OUTPUT); pinMode(LED_RECORD, OUTPUT); // Power LED on digitalWrite(LED_POWER, HIGH); digitalWrite(LED_RECORD, LOW); // Initialize Display if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println("SSD1306 allocation failed"); while(1) { // Blink power LED to indicate error digitalWrite(LED_POWER, !digitalRead(LED_POWER)); delay(500); } } display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(0, 0); display.println("EVP Recorder"); display.println("Initializing..."); display.display(); delay(1000); // Initialize SD Card display.println("SD Card..."); display.display(); if(!SD.begin(SD_CS)) { display.println("SD FAILED!"); display.display(); Serial.println("SD Card initialization failed!"); while(1) { digitalWrite(LED_POWER, !digitalRead(LED_POWER)); digitalWrite(LED_RECORD, !digitalRead(LED_RECORD)); delay(250); } } display.println("SD OK"); display.display(); // Create EVP directory if doesn't exist if(!SD.exists("/EVP")) { SD.mkdir("/EVP"); } // Ready display.println(""); display.println("READY!"); display.println(""); display.println("REC: Record"); display.println("MODE: Change view"); display.display(); Serial.println("EVP Recorder Ready!"); delay(2000); // Initialize waveform data for(int i = 0; i < WAVEFORM_POINTS; i++) { waveformData[i] = SCREEN_HEIGHT / 2; } } void loop() { handleButtons(); readAudio(); updateDisplay(); // Auto-detect feature if(!isRecording && autoDetect) { checkAutoDetect(); } delay(10); } void handleButtons() { // Record button if(digitalRead(BUTTON_RECORD) == LOW) { if(millis() - lastRecordPress > DEBOUNCE_DELAY) { lastRecordPress = millis(); toggleRecording(); } } // Mode button if(digitalRead(BUTTON_MODE) == LOW) { if(millis() - lastModePress > DEBOUNCE_DELAY) { lastModePress = millis(); cycleMode(); } } } void toggleRecording() { if(isRecording) { stopRecording(); } else { startRecording(); } } void startRecording() { // Generate filename with timestamp // Format: /EVP/REC_NNNN.wav (simple counter for basic version) int fileNum = 1; while(true) { currentFilename = "/EVP/REC_" + String(fileNum) + ".raw"; if(!SD.exists(currentFilename)) break; fileNum++; } audioFile = SD.open(currentFilename, FILE_WRITE); if(!audioFile) { Serial.println("Failed to create file!"); return; } isRecording = true; recordingStart = millis(); samplesRecorded = 0; digitalWrite(LED_RECORD, HIGH); Serial.println("Recording started: " + currentFilename); } void stopRecording() { if(audioFile) { // Flush any remaining buffer if(bufferIndex > 0) { audioFile.write((uint8_t*)audioBuffer, bufferIndex * 2); } audioFile.close(); } isRecording = false; digitalWrite(LED_RECORD, LOW); bufferIndex = 0; unsigned long duration = (millis() - recordingStart) / 1000; Serial.println("Recording stopped. Duration: " + String(duration) + "s"); Serial.println("Samples: " + String(samplesRecorded)); } void readAudio() { int micValue = analogRead(MIC_PIN); // Update waveform display waveformData[waveformIndex] = map(micValue, 0, 4095, SCREEN_HEIGHT - 1, 0); waveformIndex = (waveformIndex + 1) % WAVEFORM_POINTS; // Calculate statistics if(micValue > peakLevel) peakLevel = micValue; avgLevel = (avgLevel * 9 + micValue) / 10; // Running average // Record if active if(isRecording) { audioBuffer[bufferIndex++] = micValue; samplesRecorded++; // Write buffer when full if(bufferIndex >= BUFFER_SIZE) { audioFile.write((uint8_t*)audioBuffer, BUFFER_SIZE * 2); bufferIndex = 0; } } } void checkAutoDetect() { // Start recording if sound exceeds threshold if(avgLevel > threshold) { Serial.println("Auto-detect triggered! Level: " + String(avgLevel)); startRecording(); } } void cycleMode() { currentMode = (DisplayMode)((currentMode + 1) % 3); Serial.print("Mode: "); switch(currentMode) { case MODE_WAVEFORM: Serial.println("Waveform"); break; case MODE_VU_METER: Serial.println("VU Meter"); break; case MODE_SETTINGS: Serial.println("Settings"); break; } } void updateDisplay() { display.clearDisplay(); switch(currentMode) { case MODE_WAVEFORM: drawWaveform(); break; case MODE_VU_METER: drawVUMeter(); break; case MODE_SETTINGS: drawSettings(); break; } // Status bar (always shown) drawStatusBar(); display.display(); } void drawWaveform() { // Draw waveform for(int i = 1; i < WAVEFORM_POINTS; i++) { int idx = (waveformIndex + i) % WAVEFORM_POINTS; int prevIdx = (waveformIndex + i - 1) % WAVEFORM_POINTS; display.drawLine(i-1, waveformData[prevIdx], i, waveformData[idx], SSD1306_WHITE); } // Center line display.drawLine(0, SCREEN_HEIGHT / 2, SCREEN_WIDTH - 1, SCREEN_HEIGHT / 2, SSD1306_WHITE); } void drawVUMeter() { // Draw VU meter bars int barHeight = 40; int barY = 10; // Peak level bar int peakWidth = map(peakLevel, 0, 4095, 0, SCREEN_WIDTH - 20); display.fillRect(10, barY, peakWidth, 8, SSD1306_WHITE); display.drawRect(10, barY, SCREEN_WIDTH - 20, 8, SSD1306_WHITE); // Average level bar int avgWidth = map(avgLevel, 0, 4095, 0, SCREEN_WIDTH - 20); display.fillRect(10, barY + 15, avgWidth, 8, SSD1306_WHITE); display.drawRect(10, barY + 15, SCREEN_WIDTH - 20, 8, SSD1306_WHITE); // Labels display.setTextSize(1); display.setCursor(0, barY); display.print("P"); display.setCursor(0, barY + 15); display.print("A"); // Values display.setCursor(0, barY + 30); display.print("Peak: "); display.println(peakLevel); display.print("Avg: "); display.println(avgLevel); // Reset peak after display peakLevel = avgLevel; } void drawSettings() { display.setTextSize(1); display.setCursor(0, 10); display.println("SETTINGS"); display.println(""); display.print("Threshold: "); display.println(threshold); display.println(""); display.print("Auto-detect: "); display.println(autoDetect ? "ON" : "OFF"); display.println(""); display.print("Samples: "); display.println(samplesRecorded); } void drawStatusBar() { // Top status bar display.drawLine(0, 8, SCREEN_WIDTH - 1, 8, SSD1306_WHITE); display.setTextSize(1); display.setCursor(0, 0); if(isRecording) { unsigned long duration = (millis() - recordingStart) / 1000; display.print("REC "); display.print(duration); display.print("s"); } else { display.print("EVP READY"); } // Battery indicator (placeholder - requires voltage divider) display.setCursor(SCREEN_WIDTH - 24, 0); display.print("BAT"); } // Helper: Convert raw data to WAV file // NOTE: This basic version saves raw audio data // Use the Python converter script to convert to WAV: // See /software/convert_raw_to_wav.py