diff --git a/radio/src/CMakeLists.txt b/radio/src/CMakeLists.txt index 8be195d85..dd2cbc30e 100644 --- a/radio/src/CMakeLists.txt +++ b/radio/src/CMakeLists.txt @@ -464,6 +464,7 @@ set(SRC serial.cpp audio.cpp model_audio.cpp + pdm_wav_recorder.cpp sbus.cpp input_mapping.cpp inactivity_timer.cpp diff --git a/radio/src/boards/rm-h750/board.h b/radio/src/boards/rm-h750/board.h index aed9be1aa..46f4ef02f 100644 --- a/radio/src/boards/rm-h750/board.h +++ b/radio/src/boards/rm-h750/board.h @@ -208,6 +208,10 @@ bool isBacklightEnabled(); int audioInit(); void audioConsumeCurrentBuffer(); +#if defined(PDM_CLOCK) +#include "pdm_software_driver.h" +#endif + // Telemetry driver #define INTMODULE_FIFO_SIZE 512 #define TELEMETRY_FIFO_SIZE 512 diff --git a/radio/src/boards/rm-h750/pdm_software_driver.cpp b/radio/src/boards/rm-h750/pdm_software_driver.cpp new file mode 100644 index 000000000..53373de4d --- /dev/null +++ b/radio/src/boards/rm-h750/pdm_software_driver.cpp @@ -0,0 +1,289 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include "board.h" + +#include "hal/gpio.h" +#include "stm32_gpio.h" +#include "stm32_dma.h" +#include "stm32_hal_ll.h" + +#if defined(PDM_CAPTURE_DMA) + +static constexpr uint32_t PDM_SAI_MCKDIV = + PDM_SAI_KER_FREQ / PDM_CLOCK_FREQ; + +static_assert(PDM_SAI_MCKDIV >= 1 && PDM_SAI_MCKDIV <= 63, + "PDM_SAI MCKDIV out of range for SAI_xCR1_MCKDIV (6 bits)"); + +// Burst size: ~6 ms of PDM at 1.6 MHz (multiple of 32 required for word packing). +static constexpr uint32_t PDM_BURST_BITS = 10080; +static constexpr uint32_t PDM_BURST_WORDS = (PDM_BURST_BITS + 31) / 32; + +static uint32_t pdmBurstBuf[PDM_BURST_WORDS]; +static uint8_t lastSoundLevel = 0; + +// 40000 bytes at 1.6 MHz = ~25 ms ring; gives ~21 ms slack for audio-task stalls. +static constexpr uint32_t PDM_RING_BYTES = 40000; +static uint8_t pdmRingBuf[PDM_RING_BYTES] __DMA_NO_CACHE; +static uint32_t pdmRingReadPos = 0; + +// CIC integrator + comb state (used by pdmConvertToPCM, defined further down). +// Declared here so pdmStart() can zero them at session boundaries — without a +// reset the leftover integrator state from the previous session would emit a +// DC transient on the first decoded burst. +static int32_t cic_i1 = 0, cic_i2 = 0, cic_i3 = 0; +static int32_t cic_p1 = 0, cic_p2 = 0, cic_p3 = 0; +static uint32_t cic_bitsSeen = 0; + +static bool pdmRunning = false; + +void pdmStart() +{ + if (pdmRunning) return; + + // Per-session state reset. Must happen before the DMA starts producing data + // so the audio-task path never sees a half-reset filter. + cic_i1 = cic_i2 = cic_i3 = 0; + cic_p1 = cic_p2 = cic_p3 = 0; + cic_bitsSeen = 0; + pdmRingReadPos = 0; + lastSoundLevel = 0; + + gpio_init_af(PDM_CLOCK, PDM_CLOCK_GPIO_AF, GPIO_PIN_SPEED_VERY_HIGH); + gpio_init(PDM_DATA, GPIO_IN, GPIO_PIN_SPEED_VERY_HIGH); + + SET_BIT(RCC->APB2ENR, RCC_APB2ENR_SAI1EN); + (void)READ_BIT(RCC->APB2ENR, RCC_APB2ENR_SAI1EN); + + SAI_Block_TypeDef* block = PDM_SAI_BLOCK; + + CLEAR_BIT(block->CR1, SAI_xCR1_SAIEN); + while (READ_BIT(block->CR1, SAI_xCR1_SAIEN)) {} + + block->CR1 = (0U << SAI_xCR1_MODE_Pos) // Master TX + | (0U << SAI_xCR1_PRTCFG_Pos) // Free protocol + | (4U << SAI_xCR1_DS_Pos) // 16-bit data + | SAI_xCR1_NODIV // BCLK = ker_ck / MCKDIV + | (PDM_SAI_MCKDIV << SAI_xCR1_MCKDIV_Pos); + + block->CR2 = (1U << SAI_xCR2_FTH_Pos); // FIFO threshold = 1/4 full + + block->FRCR = (15U << 0) // FRL = 15 (16-bit frame) + | (7U << 8); // FSALL = 7 + block->SLOTR = (0U << 8) // NBSLOT = 0 -> 1 slot + | (1U << 16); // SLOTEN slot 0 + + // Prime the FIFO so the block starts clocking immediately (no underrun). + for (int i = 0; i < 4; ++i) block->DR = 0U; + + SET_BIT(block->CR1, SAI_xCR1_SAIEN); + + // TIM15 is free because FLYSKY_GIMBAL is OFF. + SET_BIT(RCC->APB2ENR, RCC_APB2ENR_TIM15EN); + (void)READ_BIT(RCC->APB2ENR, RCC_APB2ENR_TIM15EN); + + stm32_dma_enable_clock(PDM_CAPTURE_DMA); + + PDM_CAPTURE_TIMER->CR1 = 0; + PDM_CAPTURE_TIMER->PSC = 0; + PDM_CAPTURE_TIMER->ARR = (PDM_CAPTURE_TIMER_FREQ / PDM_CLOCK_FREQ) - 1U; + PDM_CAPTURE_TIMER->CNT = 0; + PDM_CAPTURE_TIMER->EGR = TIM_EGR_UG; // load PSC/ARR + PDM_CAPTURE_TIMER->SR = 0; // clear update flag + PDM_CAPTURE_TIMER->DIER = TIM_DIER_UDE; // fire DMA request on every UEV + + LL_DMA_DeInit(PDM_CAPTURE_DMA, PDM_CAPTURE_DMA_STREAM); + + LL_DMA_InitTypeDef dmaInit; + LL_DMA_StructInit(&dmaInit); + dmaInit.PeriphRequest = PDM_CAPTURE_DMA_REQUEST; + dmaInit.Mode = LL_DMA_MODE_CIRCULAR; + dmaInit.Direction = LL_DMA_DIRECTION_PERIPH_TO_MEMORY; + dmaInit.PeriphOrM2MSrcAddress = (uintptr_t)&PDM_DATA_GPIO_PORT->IDR; + dmaInit.PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_NOINCREMENT; + dmaInit.PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_BYTE; + dmaInit.MemoryOrM2MDstAddress = (uintptr_t)pdmRingBuf; + dmaInit.MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT; + dmaInit.MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_BYTE; + dmaInit.NbData = PDM_RING_BYTES; + dmaInit.Priority = LL_DMA_PRIORITY_HIGH; + LL_DMA_Init(PDM_CAPTURE_DMA, PDM_CAPTURE_DMA_STREAM, &dmaInit); + + LL_DMA_EnableStream(PDM_CAPTURE_DMA, PDM_CAPTURE_DMA_STREAM); + + PDM_CAPTURE_TIMER->CR1 |= TIM_CR1_CEN; + + pdmRunning = true; +} + +void pdmStop() +{ + if (!pdmRunning) return; + + // Stop DMA pacing first so no further requests fire. + PDM_CAPTURE_TIMER->CR1 &= ~TIM_CR1_CEN; + PDM_CAPTURE_TIMER->DIER = 0; + + // Disable the DMA stream and wait for it to actually stop. A subsequent + // pdmStart() calls LL_DMA_DeInit which expects the stream idle. + LL_DMA_DisableStream(PDM_CAPTURE_DMA, PDM_CAPTURE_DMA_STREAM); + while (LL_DMA_IsEnabledStream(PDM_CAPTURE_DMA, PDM_CAPTURE_DMA_STREAM)) {} + + // Disable the SAI block — this stops the PDM clock to the mic, putting it + // into low-power mode. + SAI_Block_TypeDef* block = PDM_SAI_BLOCK; + CLEAR_BIT(block->CR1, SAI_xCR1_SAIEN); + while (READ_BIT(block->CR1, SAI_xCR1_SAIEN)) {} + + pdmRunning = false; +} + +static uint32_t pdmRingAvailable() +{ + const uint32_t ndtr = + LL_DMA_GetDataLength(PDM_CAPTURE_DMA, PDM_CAPTURE_DMA_STREAM); + const uint32_t writePos = PDM_RING_BYTES - ndtr; + if (writePos >= pdmRingReadPos) return writePos - pdmRingReadPos; + return PDM_RING_BYTES - pdmRingReadPos + writePos; +} + +bool pdmCapture() +{ + uint32_t avail = pdmRingAvailable(); + if (avail < PDM_BURST_BITS) return false; + + // Skip ahead if DMA is about to overwrite unread data. + if (avail > PDM_RING_BYTES - PDM_BURST_BITS) { + const uint32_t target_avail = PDM_BURST_BITS + PDM_BURST_BITS / 2; + const uint32_t skip = avail - target_avail; + pdmRingReadPos = (pdmRingReadPos + skip) % PDM_RING_BYTES; + } + + static_assert(PDM_BURST_BITS % 32U == 0U, + "PDM_BURST_BITS must be a multiple of 32"); + + uint32_t src = pdmRingReadPos; + for (uint32_t w = 0; w < PDM_BURST_WORDS; ++w) { + uint32_t acc = 0; + for (uint32_t b = 0; b < 32; ++b) { + const uint8_t s = pdmRingBuf[src]; + if (++src >= PDM_RING_BYTES) src = 0; + acc = (acc << 1) | ((s >> PDM_DATA_GPIO_PIN) & 1U); + } + pdmBurstBuf[w] = acc; + } + + pdmRingReadPos = src; + return true; +} + +bool pdmUpdateSoundLevel() +{ + if (!pdmCapture()) return false; + + // Ones-density of the PDM bitstream approximates signal amplitude. + uint32_t ones = 0; + for (uint32_t w = 0; w < PDM_BURST_WORDS; ++w) { + ones += __builtin_popcount(pdmBurstBuf[w]); + } + + const uint32_t total = PDM_BURST_WORDS * 32U; + const int32_t mid = (int32_t)(total / 2U); + int32_t dev = (int32_t)ones - mid; + if (dev < 0) dev = -dev; + + uint32_t level = (uint32_t)dev * 200U / total; + if (level > 100U) level = 100U; + lastSoundLevel = (uint8_t)level; + return true; +} + +uint8_t pdmGetSoundLevel() +{ + return lastSoundLevel; +} + +// --------------------------------------------------------------------------- +// PDM -> PCM conversion: 3rd-order CIC decimator (R = PDM_PCM_DECIMATION). +// +// Structure (Hogenauer): +// integrator ×3 at 1 MHz -> down-sample by R -> comb ×3 at 1 MHz / R +// +// Integrators run on 1-bit input (0 or 1), so for R=64 and N=3 the worst-case +// magnitude that has to be representable is R^N = 262 144 — fits easily in +// int32_t. 2's-complement wrap-around through the integrators is intentional +// and cancels in the comb stage. +// +// DC gain of the filter is R^N. Silence (50/50 bitstream) therefore sits at +// R^N / 2, and the useful signal swing is ±R^N / 2. We subtract the DC offset +// and scale so that full swing maps to the int16 range. +// --------------------------------------------------------------------------- + +// PDM_POST_GAIN_SHIFT is defined in pdm_software_driver.h so trimSilence +// can scale its threshold from the same value. + +// CIC integrator + comb state is declared near the top of the file (so +// pdmStart() can reset it on session boundaries). + +uint32_t pdmConvertToPCM(int16_t* pcm, uint32_t max) +{ + if (!pcm || max == 0) return 0; + + constexpr uint32_t R = PDM_PCM_DECIMATION; + constexpr int32_t G = (int32_t)(R * R * R); + constexpr int32_t DC = G / 2; + + // floor_log2(DC) - 14: ensures (c3 - DC) >> SCALE_SHIFT fits in int16. + // Works for any R, not just powers of two. + constexpr int SCALE_SHIFT = (31 - __builtin_clz((unsigned)(DC))) - 14; + static_assert(SCALE_SHIFT >= 0, "Decimation too small to fit PCM range"); + + uint32_t produced = 0; + const uint32_t totalBits = PDM_BURST_WORDS * 32U; + + for (uint32_t idx = 0; idx < totalBits && produced < max; ++idx) { + const uint32_t word = pdmBurstBuf[idx >> 5]; + const int32_t bit = (int32_t)((word >> (31U - (idx & 31U))) & 1U); + + cic_i1 += bit; + cic_i2 += cic_i1; + cic_i3 += cic_i2; + + if (++cic_bitsSeen == R) { + cic_bitsSeen = 0; + + const int32_t c1 = cic_i3 - cic_p1; cic_p1 = cic_i3; + const int32_t c2 = c1 - cic_p2; cic_p2 = c1; + const int32_t c3 = c2 - cic_p3; cic_p3 = c2; + + int32_t s = (c3 - DC) >> SCALE_SHIFT; + s <<= PDM_POST_GAIN_SHIFT; + if (s > 32767) s = 32767; + if (s < -32768) s = -32768; + pcm[produced++] = (int16_t)s; + } + } + + return produced; +} + +#endif // PDM_CAPTURE_DMA diff --git a/radio/src/boards/rm-h750/pdm_software_driver.h b/radio/src/boards/rm-h750/pdm_software_driver.h new file mode 100644 index 000000000..d7833470d --- /dev/null +++ b/radio/src/boards/rm-h750/pdm_software_driver.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#pragma once + +#include + +// Decimation factor: output rate = PDM_CLOCK_FREQ / PDM_PCM_DECIMATION. +// R=100, PDM_CLOCK=1.6 MHz → SRC_RATE=16000 Hz exactly (no resampler needed). +#define PDM_PCM_DECIMATION 100 + +// Post-CIC gain (left shift before int16 saturation; +6 dB per step). +// Default calibrated at 4. Override per target in hal.h before this header +// is reached. trimSilence() in pdm_wav_recorder.cpp scales its threshold +// from this same value, so changing it keeps trim in sync automatically. +#ifndef PDM_POST_GAIN_SHIFT +#define PDM_POST_GAIN_SHIFT 5 +#endif + +// Bring up SAI clock + TIM15 + DMA capture and reset filter state. +// Idempotent. Call when entering a context that needs the mic. +void pdmStart(); +// Stop the DMA pacer, disable SAI block (the PDM clock to the mic stops). +void pdmStop(); +bool pdmUpdateSoundLevel(); +uint8_t pdmGetSoundLevel(); +bool pdmCapture(); +uint32_t pdmConvertToPCM(int16_t* pcm, uint32_t max); diff --git a/radio/src/cli.cpp b/radio/src/cli.cpp index 2d18dde8d..38ac8267a 100644 --- a/radio/src/cli.cpp +++ b/radio/src/cli.cpp @@ -50,8 +50,10 @@ #include #include #include +#include #include "lua/lua_states.h" +#include "pdm_wav_recorder.h" #define CLI_COMMAND_MAX_ARGS 8 #define CLI_COMMAND_MAX_LEN 256 @@ -262,6 +264,69 @@ int cliPlay(const char ** argv) return 0; } +#if defined(PDM_CLOCK) +// Record from the PDM microphone to a 16-bit mono WAV file. The actual +// capture + resampling + WAV I/O lives in PdmWavRecorder so the same +// pipeline is reused by the GUI recorder tool. +int cliRecord(const char ** argv) +{ + if (!argv[1] || !argv[1][0]) { + cliSerialPrint("%s: missing filename", argv[0]); + return 0; + } + + int seconds = 5; + if (argv[2] && argv[2][0]) { + if (toInt(argv, 2, &seconds) == 0 || seconds <= 0 || seconds > 600) { + cliSerialPrint("%s: Invalid duration \"%s\"", argv[0], argv[2]); + return 0; + } + } + + char pathBuf[64]; + const char* path = argv[1]; + const size_t nameLen = strlen(argv[1]); + const bool hasExt = + nameLen >= 4 && strcasecmp(argv[1] + nameLen - 4, ".wav") == 0; + if (!hasExt) { + if (nameLen + 5 > sizeof(pathBuf)) { + cliSerialPrint("%s: path too long", argv[0]); + return 0; + } + memcpy(pathBuf, argv[1], nameLen); + memcpy(pathBuf + nameLen, ".wav", 5); + path = pathBuf; + } + + const uint32_t totalSamples = PdmWavRecorder::DST_RATE * (uint32_t)seconds; + // Capture only runs while needed — bring the hardware up for this command. + pdmStart(); + PdmWavRecorder rec; + FRESULT res = rec.start(path, (uint32_t)seconds); + if (res != FR_OK) { + pdmStop(); + cliSerialPrint("%s: cannot open \"%s\" (err %u)", argv[0], path, + (unsigned)res); + return 0; + } + + cliSerialPrint("Recording %d s (%u samples @ %u Hz) to %s", + seconds, (unsigned)totalSamples, + (unsigned)PdmWavRecorder::DST_RATE, path); + + while (rec.isRecording() && rec.getSamplesWritten() < totalSamples) { + sleep_ms(50); + } + + rec.stop(); + pdmStop(); + cliSerialPrint("Recorded %u samples (%u bytes)", + (unsigned)rec.getSamplesWritten(), + (unsigned)rec.getBytesWritten()); + return 0; +} +#endif // PDM_CLOCK + int cliLs(const char ** argv) { FILINFO fno; @@ -1787,6 +1852,9 @@ const CliCommand cliCommands[] = { { "readsd", cliReadSD, " " }, { "testsd", cliTestSD, "" }, { "play", cliPlay, "" }, +#if defined(PDM_CLOCK) + { "rec", cliRecord, " []" }, +#endif { "reboot", cliReboot, "[wdt]" }, { "set", cliSet, " " }, #if defined(ENABLE_SERIAL_PASSTHROUGH) diff --git a/radio/src/gui/colorlcd/CMakeLists.txt b/radio/src/gui/colorlcd/CMakeLists.txt index 7943a0530..a6c1970aa 100644 --- a/radio/src/gui/colorlcd/CMakeLists.txt +++ b/radio/src/gui/colorlcd/CMakeLists.txt @@ -98,6 +98,7 @@ set(GUI_SRC radio/radio_trainer.cpp radio/radio_version.cpp radio/radio_gps_tool.cpp + radio/radio_mic_recorder.cpp ) macro(add_gui_src src) diff --git a/radio/src/gui/colorlcd/radio/radio_mic_recorder.cpp b/radio/src/gui/colorlcd/radio/radio_mic_recorder.cpp new file mode 100644 index 000000000..296c6b4c3 --- /dev/null +++ b/radio/src/gui/colorlcd/radio/radio_mic_recorder.cpp @@ -0,0 +1,282 @@ +/* +* Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include "radio_mic_recorder.h" + +#if defined(PDM_CLOCK) + +#include + +#include "button.h" +#include "dialog.h" +#include "edgetx.h" +#include "ff.h" +#include "sdcard.h" +#include "static.h" +#include "timers_driver.h" + +static constexpr coord_t MIC_BTN_W = 260; +static constexpr coord_t MIC_BTN_H = EdgeTxStyles::UI_ELEMENT_HEIGHT * 2 + PAD_LARGE; + +RadioMicRecorder::RadioMicRecorder() : + Page(ICON_RADIO_TOOLS) +{ + // Bring the PDM hardware up only while this tool is open. Capture is off + // outside of this scope so the mic clock and DMA stream are idle. + pdmStart(); + buildHeader(header); + buildBody(body); + enterIdle(); +} + +RadioMicRecorder::~RadioMicRecorder() +{ + // An async call scheduled from the LabelDialog confirm path may still be + // pending when the page is closed (e.g. user long-presses EXIT while the + // dialog is being dismissed). Cancelling ensures the callback never fires + // on a destroyed instance. + lv_async_call_cancel(&RadioMicRecorder::asyncProcessPendingRename, this); + if (recorder.isRecording()) recorder.stop(); + pdmStop(); +} + +void RadioMicRecorder::asyncProcessPendingRename(void* ctx) +{ + static_cast(ctx)->processPendingRename(); +} + +void RadioMicRecorder::buildHeader(Window* window) +{ + header->setTitle(STR_MENUTOOLS); + header->setTitle2(STR_MIC_RECORDER); +} + +void RadioMicRecorder::buildBody(Window* window) +{ + window->padAll(PAD_ZERO); + + const coord_t w = window->width(); + const coord_t h = window->height(); + + bigLabel = new StaticText( + window, + {0, h / 3 - 20, w, 40}, + "", COLOR_THEME_PRIMARY1_INDEX, FONT(XL)); + lv_obj_set_style_text_align(bigLabel->getLvObj(), LV_TEXT_ALIGN_CENTER, 0); + + infoLabel = new StaticText( + window, + {0, h / 3 + 30, w, 24}, + "", COLOR_THEME_SECONDARY1_INDEX, FONT(STD)); + lv_obj_set_style_text_align(infoLabel->getLvObj(), LV_TEXT_ALIGN_CENTER, 0); + + actionButton = new TextButton( + window, + {(w - MIC_BTN_W) / 2, + h - MIC_BTN_H - PAD_LARGE * 2, + MIC_BTN_W, MIC_BTN_H}, + "", [this]() { + onActionPressed(); + return 0; + }); + actionButton->setFont(FONT_XL_INDEX); +} + +void RadioMicRecorder::onEvent(event_t event) +{ + if (event == EVT_KEY_LONG(KEY_EXIT)) { + killEvents(event); + if (recorder.isRecording()) recorder.stop(); + onCancel(); + } +} + +void RadioMicRecorder::onActionPressed() +{ + switch (state) { + case State::IDLE: enterCountdown(); break; + case State::COUNTDOWN: enterIdle(); break; // cancel + case State::RECORDING: stopRecording(); break; + } +} + +void RadioMicRecorder::enterIdle() +{ + if (recorder.isRecording()) recorder.stop(); + state = State::IDLE; + stateStart = get_tmr10ms(); + refreshUI(); +} + +void RadioMicRecorder::enterCountdown() +{ + state = State::COUNTDOWN; + stateStart = get_tmr10ms(); + refreshUI(); +} + +void RadioMicRecorder::enterRecording() +{ + pickNextFilename(); + FRESULT res = recorder.start(filename, 0); + if (res != FR_OK) { + char msg[40]; + snprintf(msg, sizeof(msg), "%s %u", STR_OPEN_ERROR, (unsigned)res); + bigLabel->setText(msg); + state = State::IDLE; + stateStart = get_tmr10ms(); + actionButton->setText(STR_RECORD); + return; + } + state = State::RECORDING; + stateStart = get_tmr10ms(); + refreshUI(); +} + +void RadioMicRecorder::stopRecording() +{ + recorder.stop(); + PdmWavRecorder::trimSilence(filename); + state = State::IDLE; + stateStart = get_tmr10ms(); + refreshUI(); + + const char* base = strrchr(filename, '/'); + base = base ? base + 1 : filename; + + char baseName[PATH_MAX_LEN] = {}; + strncpy(baseName, base, sizeof(baseName) - 1); + char* dot = strrchr(baseName, '.'); + if (dot) *dot = '\0'; + + new LabelDialog(baseName, LEN_FUNCTION_NAME, STR_SAVE_AS, [this](std::string newName) { + if (newName.empty()) return; + char dir[sizeof(SOUNDS_PATH) + 1]; + strcpy(dir, SOUNDS_PATH "/"); + strncpy(dir + SOUNDS_PATH_LNG_OFS, currentLanguagePack->id, 2); + snprintf(pendingRename, sizeof(pendingRename), "%s%s.wav", dir, newName.c_str()); + if (strcmp(pendingRename, filename) == 0) { refreshUI(); return; } + + // Defer the overwrite check until LabelDialog has finished closing — + // creating a modal now would leave us stacked on top of LabelDialog, + // which corrupts the lv_group chain when LabelDialog deletes itself. + // The page dtor cancels this async call if the user bails out before + // it fires (see ~RadioMicRecorder). + lv_async_call(&RadioMicRecorder::asyncProcessPendingRename, this); + }); +} + +void RadioMicRecorder::processPendingRename() +{ + FILINFO info; + if (f_stat(pendingRename, &info) == FR_OK) { + new ConfirmDialog(STR_FILE_EXISTS, STR_ASK_OVERWRITE, + [this]() { applyRename(); }); + } else { + applyRename(); + } +} + +void RadioMicRecorder::applyRename() +{ + f_unlink(pendingRename); + f_rename(filename, pendingRename); + strncpy(filename, pendingRename, sizeof(filename) - 1); + filename[sizeof(filename) - 1] = '\0'; + refreshUI(); +} + +void RadioMicRecorder::refreshUI() +{ + char buf[64]; + switch (state) { + case State::IDLE: + bigLabel->setText(STR_PUSH_TO_RECORD); + if (recorder.getSamplesWritten() > 0) { + snprintf(buf, sizeof(buf), "%s %s (%us)", STR_SAVED, + filename, (unsigned)recorder.getElapsedSeconds()); + infoLabel->setText(buf); + } else { + infoLabel->setText(""); + } + actionButton->setText(STR_RECORD); + break; + + case State::COUNTDOWN: { + const uint32_t elapsed10 = (uint32_t)(get_tmr10ms() - stateStart); + int remaining = (int)COUNTDOWN_SECONDS - (int)(elapsed10 / 100U); + if (remaining < 0) remaining = 0; + snprintf(buf, sizeof(buf), "%s %d", STR_STARTING_IN, remaining); + bigLabel->setText(buf); + infoLabel->setText(STR_GET_READY); + actionButton->setText(STR_CANCEL); + break; + } + + case State::RECORDING: { + const uint32_t s = recorder.getElapsedSeconds(); + snprintf(buf, sizeof(buf), "%s %02u:%02u", STR_REC, + (unsigned)(s / 60U), (unsigned)(s % 60U)); + bigLabel->setText(buf); + snprintf(buf, sizeof(buf), "%s %u KB", + filename, (unsigned)(recorder.getBytesWritten() / 1024U)); + infoLabel->setText(buf); + actionButton->setText(STR_STOP); + break; + } + } +} + +void RadioMicRecorder::checkEvents() +{ + Page::checkEvents(); + + if (state == State::COUNTDOWN) { + const uint32_t elapsed10 = (uint32_t)(get_tmr10ms() - stateStart); + if (elapsed10 >= COUNTDOWN_SECONDS * 100U) { + enterRecording(); + } else { + refreshUI(); + } + } else if (state == State::RECORDING) { + if (!recorder.isRecording()) { + state = State::IDLE; + stateStart = get_tmr10ms(); + } + refreshUI(); + } +} + +void RadioMicRecorder::pickNextFilename() +{ + char dir[sizeof(SOUNDS_PATH) + 1]; + strcpy(dir, SOUNDS_PATH "/"); + strncpy(dir + SOUNDS_PATH_LNG_OFS, currentLanguagePack->id, 2); + + FILINFO info; + for (int i = 0; i < 100; i++) { + snprintf(filename, sizeof(filename), "%srec_%02d.wav", dir, i); + if (f_stat(filename, &info) != FR_OK) return; + } + snprintf(filename, sizeof(filename), "%srec.wav", dir); +} + +#endif // PDM_CLOCK diff --git a/radio/src/gui/colorlcd/radio/radio_mic_recorder.h b/radio/src/gui/colorlcd/radio/radio_mic_recorder.h new file mode 100644 index 000000000..53f870407 --- /dev/null +++ b/radio/src/gui/colorlcd/radio/radio_mic_recorder.h @@ -0,0 +1,75 @@ +/* +* Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#pragma once + +#include "page.h" +#include "pdm_wav_recorder.h" +#include "sdcard.h" + +#if defined(PDM_CLOCK) + +class TextButton; +class StaticText; + +class RadioMicRecorder : public Page +{ + public: + RadioMicRecorder(); + ~RadioMicRecorder() override; + + protected: + enum class State : uint8_t { IDLE, COUNTDOWN, RECORDING }; + + static constexpr uint32_t COUNTDOWN_SECONDS = 5; + static constexpr int PATH_MAX_LEN = sizeof(SOUNDS_PATH) + 14; // "/lang/rec_00.wav\0" + + State state = State::IDLE; + tmr10ms_t stateStart = 0; + char filename[PATH_MAX_LEN] = {0}; + char pendingRename[PATH_MAX_LEN] = {0}; + PdmWavRecorder recorder; + + StaticText* bigLabel = nullptr; + StaticText* infoLabel = nullptr; + TextButton* actionButton = nullptr; + + void buildHeader(Window* window); + void buildBody(Window* window); + void checkEvents() override; + void onEvent(event_t event) override; + + void onActionPressed(); + void enterIdle(); + void enterCountdown(); + void enterRecording(); + void stopRecording(); + void processPendingRename(); + void applyRename(); + void refreshUI(); + void pickNextFilename(); + + // Stable callback address so the dtor can cancel a pending lv_async call + // scheduled from the LabelDialog confirm path. + static void asyncProcessPendingRename(void* ctx); +}; + +#endif // PDM_CLOCK diff --git a/radio/src/gui/colorlcd/radio/radio_tools.cpp b/radio/src/gui/colorlcd/radio/radio_tools.cpp index 710a4702c..782b6b43e 100644 --- a/radio/src/gui/colorlcd/radio/radio_tools.cpp +++ b/radio/src/gui/colorlcd/radio/radio_tools.cpp @@ -29,6 +29,7 @@ #include "radio_ghost_module_config.h" #include "radio_spectrum_analyser.h" #include "radio_gps_tool.h" +#include "radio_mic_recorder.h" #include "standalone_lua.h" #include "etx_lv_theme.h" #include "lib_file.h" @@ -211,6 +212,13 @@ static void run_ghost_config(const std::string&) } #endif +#if defined(PDM_CLOCK) +static void run_mic_recorder(const std::string&) +{ + new RadioMicRecorder(); +} +#endif + //----------------------------------------------------------------------------- struct ToolButton : public TextButton { @@ -349,6 +357,10 @@ void RadioToolsPage::rebuild(Window* window) } #endif +#if defined(PDM_CLOCK) + tools.emplace_back(ToolEntry{STR_MIC_RECORDER, "", run_mic_recorder}); +#endif + #if defined(LUA) scanLuaTools(tools); #endif diff --git a/radio/src/pdm_wav_recorder.cpp b/radio/src/pdm_wav_recorder.cpp new file mode 100644 index 000000000..8b0d646a2 --- /dev/null +++ b/radio/src/pdm_wav_recorder.cpp @@ -0,0 +1,243 @@ +/* +* Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include "pdm_wav_recorder.h" + +#if defined(PDM_CLOCK) + +#include + +#include "os/task.h" + +// Guards against concurrent access from the audio task and the owner thread. +static PdmWavRecorder* s_active = nullptr; +static mutex_handle_t s_mutex; +static bool s_mutexInited = false; + +static void ensureMutex() +{ + if (!s_mutexInited) { + mutex_create(&s_mutex); + s_mutexInited = true; + } +} + +static void writeLE16(uint8_t* p, uint16_t v) +{ + p[0] = v & 0xFF; + p[1] = (v >> 8) & 0xFF; +} + +static void writeLE32(uint8_t* p, uint32_t v) +{ + p[0] = v & 0xFF; + p[1] = (v >> 8) & 0xFF; + p[2] = (v >> 16) & 0xFF; + p[3] = (v >> 24) & 0xFF; +} + +FRESULT PdmWavRecorder::start(const char* path, uint32_t expectedSeconds) +{ + ensureMutex(); + MutexLock lock = MutexLock::MakeInstance(&s_mutex); + + if (s_active != nullptr) return FR_LOCKED; + + samplesWritten = 0; + maxSamples = expectedSeconds * DST_RATE; + recording = false; + + FRESULT res = f_open(&file, path, FA_CREATE_ALWAYS | FA_WRITE); + if (res != FR_OK) return res; + + const uint32_t dataBytes = maxSamples * 2U; + uint8_t header[44]; + memcpy(&header[0], "RIFF", 4); + writeLE32(&header[4], 36U + dataBytes); + memcpy(&header[8], "WAVE", 4); + memcpy(&header[12], "fmt ", 4); + writeLE32(&header[16], 16); + writeLE16(&header[20], 1); // PCM + writeLE16(&header[22], 1); // mono + writeLE32(&header[24], DST_RATE); + writeLE32(&header[28], DST_RATE * 2U); // byte rate + writeLE16(&header[32], 2); // block align + writeLE16(&header[34], 16); // bits per sample + memcpy(&header[36], "data", 4); + writeLE32(&header[40], dataBytes); + + UINT written = 0; + res = f_write(&file, header, sizeof(header), &written); + if (res != FR_OK || written != sizeof(header)) { + f_close(&file); + return res != FR_OK ? res : FR_DISK_ERR; + } + + recording = true; + s_active = this; + return FR_OK; +} + +bool PdmWavRecorder::tickLocked() +{ + if (!recording) return false; + if (!pdmCapture()) return false; + + uint32_t n = pdmConvertToPCM(pcm, PCM_MAX); + if (n == 0) return false; + + if (maxSamples != 0) { + const uint32_t remaining = maxSamples - samplesWritten; + if (n > remaining) n = remaining; + } + + UINT w = 0; + FRESULT res = f_write(&file, pcm, n * sizeof(int16_t), &w); + if (res != FR_OK || w != n * sizeof(int16_t)) { + recording = false; + return false; + } + samplesWritten += n; + if (maxSamples != 0 && samplesWritten >= maxSamples) { + recording = false; + } + return true; +} + +void PdmWavRecorder::audioTick() +{ + if (s_active == nullptr) return; + ensureMutex(); + MutexLock lock = MutexLock::MakeInstance(&s_mutex); + if (s_active != nullptr) s_active->tickLocked(); +} + +FRESULT PdmWavRecorder::stop() +{ + ensureMutex(); + MutexLock lock = MutexLock::MakeInstance(&s_mutex); + + if (s_active != this) return FR_OK; + + s_active = nullptr; + recording = false; + + const uint32_t dataBytes = samplesWritten * 2U; + uint8_t buf[4]; + UINT written = 0; + + writeLE32(buf, 36U + dataBytes); + f_lseek(&file, 4); + f_write(&file, buf, 4, &written); + + writeLE32(buf, dataBytes); + f_lseek(&file, 40); + f_write(&file, buf, 4, &written); + + return f_close(&file); +} + +FRESULT PdmWavRecorder::trimSilence(const char* path) +{ + // Threshold tracks the post-CIC gain so trim stays calibrated when + // PDM_POST_GAIN_SHIFT is changed. Baseline 4000 was tuned at shift=4. + static constexpr int32_t _silenceRaw = + ((int32_t)4000 * (1 << PDM_POST_GAIN_SHIFT)) / 16; + static constexpr int16_t SILENCE_THRESHOLD = + _silenceRaw > 32767 ? (int16_t)32767 : (int16_t)_silenceRaw; + static constexpr uint32_t PAD_SAMPLES = DST_RATE / 100; // 10 ms + static constexpr uint32_t CHUNK = 256; + + FIL f; + FRESULT res = f_open(&f, path, FA_READ | FA_WRITE); + if (res != FR_OK) return res; + + uint8_t hdr[44]; + UINT br = 0; + res = f_read(&f, hdr, sizeof(hdr), &br); + if (res != FR_OK || br != sizeof(hdr)) { f_close(&f); return res; } + + const uint32_t dataBytes = + (uint32_t)hdr[40] | ((uint32_t)hdr[41] << 8) | + ((uint32_t)hdr[42] << 16) | ((uint32_t)hdr[43] << 24); + const uint32_t total = dataBytes / 2; + if (total == 0) { f_close(&f); return FR_OK; } + + int16_t buf[CHUNK]; + uint32_t firstActive = UINT32_MAX; + uint32_t lastActive = 0; + + for (uint32_t base = 0; base < total; base += CHUNK) { + const uint32_t n = (base + CHUNK <= total) ? CHUNK : total - base; + f_lseek(&f, 44 + base * 2); + res = f_read(&f, buf, n * 2, &br); + if (res != FR_OK) { f_close(&f); return res; } + const uint32_t got = br / 2; + for (uint32_t i = 0; i < got; i++) { + const int16_t abs = buf[i] < 0 ? (int16_t)-buf[i] : buf[i]; + if (abs > SILENCE_THRESHOLD) { + if (firstActive == UINT32_MAX) firstActive = base + i; + lastActive = base + i; + } + } + } + + if (firstActive == UINT32_MAX) { f_close(&f); return FR_OK; } // all silent + + const uint32_t trimStart = firstActive > PAD_SAMPLES ? firstActive - PAD_SAMPLES : 0; + const uint32_t trimEnd = (lastActive + PAD_SAMPLES < total) ? lastActive + PAD_SAMPLES : total - 1; + const uint32_t newTotal = trimEnd - trimStart + 1; + + if (trimStart == 0 && trimEnd == total - 1) { f_close(&f); return FR_OK; } + + if (trimStart > 0) { + uint32_t src = trimStart, dst = 0; + while (src <= trimEnd) { + const uint32_t n = (src + CHUNK - 1 <= trimEnd) ? CHUNK : trimEnd - src + 1; + f_lseek(&f, 44 + src * 2); + res = f_read(&f, buf, n * 2, &br); + if (res != FR_OK) { f_close(&f); return res; } + UINT bw = 0; + f_lseek(&f, 44 + dst * 2); + res = f_write(&f, buf, br, &bw); + if (res != FR_OK) { f_close(&f); return res; } + src += n; + dst += n; + } + } + + f_lseek(&f, 44 + newTotal * 2); + f_truncate(&f); + + const uint32_t newDataBytes = newTotal * 2; + uint8_t tmp[4]; + UINT bw = 0; + writeLE32(tmp, 36 + newDataBytes); + f_lseek(&f, 4); + f_write(&f, tmp, 4, &bw); + writeLE32(tmp, newDataBytes); + f_lseek(&f, 40); + f_write(&f, tmp, 4, &bw); + + return f_close(&f); +} + +#endif // PDM_CLOCK diff --git a/radio/src/pdm_wav_recorder.h b/radio/src/pdm_wav_recorder.h new file mode 100644 index 000000000..5eec2ece5 --- /dev/null +++ b/radio/src/pdm_wav_recorder.h @@ -0,0 +1,67 @@ +/* +* Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#pragma once + +#include "edgetx.h" + +#if defined(PDM_CLOCK) + +#include +#include "ff.h" + +// 16 kHz: PDM_CLOCK_FREQ(1.6 MHz)/PDM_PCM_DECIMATION(100) = 16000 Hz directly. +// Must also divide AUDIO_SAMPLE_RATE(32 kHz) evenly for the WAV player. +class PdmWavRecorder +{ + public: + static constexpr uint32_t DST_RATE = 16000; + + // expectedSeconds == 0 means open-ended; header is patched on stop(). + // At most one recorder active at a time; concurrent start() returns FR_LOCKED. + FRESULT start(const char* path, uint32_t expectedSeconds); + FRESULT stop(); + + // Called from the audio task every ~4 ms. + static void audioTick(); + + // Trim leading/trailing silence in-place and patch the WAV header. + static FRESULT trimSilence(const char* path); + + bool isRecording() const { return recording; } + uint32_t getSamplesWritten() const { return samplesWritten; } + uint32_t getBytesWritten() const { return samplesWritten * 2U; } + uint32_t getElapsedSeconds() const { return samplesWritten / DST_RATE; } + + private: + static constexpr uint32_t PCM_MAX = 256; + + bool tickLocked(); + + FIL file; + int16_t pcm[PCM_MAX]; + // 32-bit aligned reads on Cortex-M7 are atomic — safe to read without mutex. + volatile uint32_t samplesWritten = 0; + uint32_t maxSamples = 0; // 0 = open-ended + volatile bool recording = false; +}; + +#endif // PDM_CLOCK diff --git a/radio/src/targets/simu/simufatfs.cpp b/radio/src/targets/simu/simufatfs.cpp index 17886b0bf..3fcd5a3a2 100644 --- a/radio/src/targets/simu/simufatfs.cpp +++ b/radio/src/targets/simu/simufatfs.cpp @@ -611,6 +611,21 @@ FRESULT f_getcwd(TCHAR *path, UINT sz_path) return FR_OK; } +FRESULT f_truncate(FIL* fil) +{ + if (fil && fil->obj.fs) { + _simu_FIL* sf = reinterpret_cast<_simu_FIL*>(fil->obj.fs); + if (sf->stream && sf->stream->is_open()) { + sf->stream->flush(); + if (!sf->stream->good()) return FR_DISK_ERR; + std::error_code ec; + fs::resize_file(sf->filepath, fil->fptr, ec); + if (ec) return FR_DISK_ERR; + } + } + return FR_OK; +} + FRESULT f_getfree(const TCHAR* path, DWORD* nclst, FATFS** fatfs) { // just fake that we always have some clusters free diff --git a/radio/src/targets/simu/simulib.cpp b/radio/src/targets/simu/simulib.cpp index 8e2a05616..d5ae5f222 100644 --- a/radio/src/targets/simu/simulib.cpp +++ b/radio/src/targets/simu/simulib.cpp @@ -880,3 +880,13 @@ uint32_t simuGetCustomSwitchColor(uint8_t idx) #endif return 0; } + +#if defined(PDM_CLOCK) +#include "boards/rm-h750/pdm_software_driver.h" +void pdmStart() {} +void pdmStop() {} +bool pdmUpdateSoundLevel() { return false; } +uint8_t pdmGetSoundLevel() { return 0; } +bool pdmCapture() { return false; } +uint32_t pdmConvertToPCM(int16_t*, uint32_t) { return 0; } +#endif diff --git a/radio/src/targets/tx15/CMakeLists.txt b/radio/src/targets/tx15/CMakeLists.txt index 2f320e96e..5daa2d75c 100644 --- a/radio/src/targets/tx15/CMakeLists.txt +++ b/radio/src/targets/tx15/CMakeLists.txt @@ -183,6 +183,7 @@ add_library(board OBJECT EXCLUDE_FROM_ALL ${BOARD_COMMON_SRC} ${BOARD_SRC_DIR}/tp_gt911.cpp ${BOARD_SRC_DIR}/audio_driver.cpp + ${BOARD_SRC_DIR}/pdm_software_driver.cpp drivers/tas2505.cpp targets/common/arm/stm32/heartbeat_driver.cpp targets/common/arm/stm32/mixer_scheduler_driver.cpp diff --git a/radio/src/targets/tx15/hal.h b/radio/src/targets/tx15/hal.h index 6dccf1624..53d8d38e0 100644 --- a/radio/src/targets/tx15/hal.h +++ b/radio/src/targets/tx15/hal.h @@ -29,7 +29,7 @@ SDMMC uses own DMA controler DMA1 Stream0: LED_STRIP_TIMER_DMA_STREAM Stream1: INTMODULE_DMA_STREAM -Stream2: FLYSKY_HALL_DMA_Stream_RX +Stream2: FLYSKY_HALL_DMA_Stream_RX (FLYSKY_GIMBAL) / PDM_CAPTURE_DMA_STREAM (!FLYSKY_GIMBAL) Stream3: TELEMETRY_DMA_Stream_RX Stream4: I2S_DMA_Stream Stream5: INTMODULE_RX_DMA_STREAM @@ -55,9 +55,11 @@ TIM7: TIM8: TRAINER_TIMER TIM12: MIXER_SCHEDULER_TIMER TIM14: MS_TIMER -TIM15: PDM +TIM15: TIM16: TIM17: ROTARY_ENCODER_TIMER + +SAI1_Block_A: PDM_CLOCK (1 MHz bit-clock output on SAI1_CK1 / PE5, !FLYSKY_GIMBAL only) */ #ifndef _HAL_H_ @@ -201,6 +203,24 @@ TIM17: ROTARY_ENCODER_TIMER #define I2S_DMA_Stream_IRQn DMA1_Stream4_IRQn #define I2S_DMA_Stream_IRQHandler DMA1_Stream4_IRQHandler +// MEMS PDM microphone — not available when FLYSKY_GIMBAL is enabled +#if !defined(FLYSKY_GIMBAL) + #define PDM_CLOCK GPIO_PIN(GPIOE, 5) + #define PDM_CLOCK_GPIO_AF LL_GPIO_AF_6 + #define PDM_SAI_BLOCK SAI1_Block_A + #define PDM_SAI_KER_FREQ 48000000 // PLL1Q (see system_clock.c) + #define PDM_CLOCK_FREQ 1600000 // 1.6 MHz → MCKDIV=30, R=100 → 16 kHz PCM direct + #define PDM_DATA GPIO_PIN(GPIOE, 4) + #define PDM_DATA_GPIO_PORT GPIOE + #define PDM_DATA_GPIO_PIN 4 + #define PDM_DATA_IDR_MASK (1U << PDM_DATA_GPIO_PIN) + #define PDM_CAPTURE_DMA DMA1 + #define PDM_CAPTURE_DMA_STREAM LL_DMA_STREAM_2 + #define PDM_CAPTURE_DMA_REQUEST LL_DMAMUX1_REQ_TIM15_UP + #define PDM_CAPTURE_TIMER TIM15 + #define PDM_CAPTURE_TIMER_FREQ (PERI2_FREQUENCY * TIMER_MULT_APB2) +#endif + // I2C Bus #define I2C_B1 I2C4 diff --git a/radio/src/targets/tx16smk3/CMakeLists.txt b/radio/src/targets/tx16smk3/CMakeLists.txt index 95aaa396e..f0656828a 100644 --- a/radio/src/targets/tx16smk3/CMakeLists.txt +++ b/radio/src/targets/tx16smk3/CMakeLists.txt @@ -179,6 +179,7 @@ add_library(board OBJECT EXCLUDE_FROM_ALL ${BOARD_COMMON_SRC} ${BOARD_SRC_DIR}/tp_gt911.cpp ${BOARD_SRC_DIR}/audio_driver.cpp + ${BOARD_SRC_DIR}/pdm_software_driver.cpp ${BOARD_SRC_DIR}/luminosity_sensor.cpp drivers/tas2505.cpp drivers/icm42607C.cpp diff --git a/radio/src/targets/tx16smk3/hal.h b/radio/src/targets/tx16smk3/hal.h index 76a7c0bb0..a1d84bdeb 100644 --- a/radio/src/targets/tx16smk3/hal.h +++ b/radio/src/targets/tx16smk3/hal.h @@ -29,7 +29,7 @@ SDMMC uses own DMA controler DMA1 Stream0: LED_STRIP_TIMER_DMA_STREAM Stream1: INTMODULE_DMA_STREAM -Stream2: FLYSKY_HALL_DMA_Stream_RX +Stream2: PDM_CAPTURE_DMA_STREAM Stream3: TELEMETRY_DMA_Stream_RX Stream4: I2S_DMA_Stream Stream5: INTMODULE_RX_DMA_STREAM @@ -55,10 +55,12 @@ TIM7: TIM8: TRAINER_TIMER TIM12: MIXER_SCHEDULER_TIMER TIM14: MS_TIMER -TIM15: PDM +TIM15: PDM_CAPTURE_TIMER TIM16: TIM17: ROTARY_ENCODER_TIMER +SAI1_Block_A: PDM_CLOCK (1 MHz bit-clock output on SAI1_CK1 / PE5) + USART1: TELEMETRY_USART USART2: EXTMODULE_USART USART3: AUX2_SERIAL_USART @@ -405,6 +407,27 @@ USART6: INTMODULE_USART #define I2S_DMA_Stream_IRQn DMA1_Stream4_IRQn #define I2S_DMA_Stream_IRQHandler DMA1_Stream4_IRQHandler +//MEMS — PDM microphone: +// clock output driven by SAI1 block A on SAI1_CK1 / PE5 (AF6, 1 MHz) +// data input captured by DMA from PE4 @ 1 MHz. +#define PDM_CLOCK GPIO_PIN(GPIOE, 5) +#define PDM_CLOCK_GPIO_AF LL_GPIO_AF_6 +#define PDM_SAI_BLOCK SAI1_Block_A +#define PDM_SAI_KER_FREQ 48000000 // PLL1Q (see system_clock.c) +#define PDM_CLOCK_FREQ 1600000 // 1.6 MHz → MCKDIV=30, R=100 → 16 kHz PCM direct +#define PDM_DATA GPIO_PIN(GPIOE, 4) +#define PDM_DATA_GPIO_PORT GPIOE +#define PDM_DATA_GPIO_PIN 4 +#define PDM_DATA_IDR_MASK (1U << PDM_DATA_GPIO_PIN) + +// capture PE4 via GPIO->memory DMA paced by TIM15 updates -> hardware-precise, +// non-blocking PDM sampling. +#define PDM_CAPTURE_DMA DMA1 +#define PDM_CAPTURE_DMA_STREAM LL_DMA_STREAM_2 +#define PDM_CAPTURE_DMA_REQUEST LL_DMAMUX1_REQ_TIM15_UP +#define PDM_CAPTURE_TIMER TIM15 +#define PDM_CAPTURE_TIMER_FREQ (PERI2_FREQUENCY * TIMER_MULT_APB2) + // I2C Bus #define I2C_B1 I2C4 #define I2C_B1_SDA_GPIO GPIO_PIN(GPIOD, 13) // PD.13 diff --git a/radio/src/tasks.cpp b/radio/src/tasks.cpp index f5b5896d4..653d78b44 100644 --- a/radio/src/tasks.cpp +++ b/radio/src/tasks.cpp @@ -28,6 +28,7 @@ #include "timers_driver.h" #include "hal/abnormal_reboot.h" #include "hal/watchdog_driver.h" +#include "pdm_wav_recorder.h" #include "tasks.h" #include "tasks/mixer_task.h" @@ -115,6 +116,12 @@ static void audioTask() DEBUG_TIMER_SAMPLE(debugTimerAudioIterval); DEBUG_TIMER_START(debugTimerAudioDuration); audioQueue.wakeup(); +#if defined(PDM_CLOCK) + // Drive microphone recording (if any) at the audio-task cadence. + // Much shorter than the UI tick interval, so the PDM ring buffer + // never fills enough to trigger skip-ahead / sample drops. + PdmWavRecorder::audioTick(); +#endif DEBUG_TIMER_STOP(debugTimerAudioDuration); sleep_until(&next_tick, 4); } diff --git a/radio/src/translations/i18n/cn.h b/radio/src/translations/i18n/cn.h index 38a577bac..73f816be2 100644 --- a/radio/src/translations/i18n/cn.h +++ b/radio/src/translations/i18n/cn.h @@ -966,6 +966,16 @@ #define TR_POWERMETER_ATTN "衰减器" #define TR_POWERMETER_FREQ "频率" #define TR_MENUTOOLS "扩展工具" +#define TR_MIC_RECORDER "麦克风录音" +#define TR_PUSH_TO_RECORD "按下录音" +#define TR_RECORD "录音" +#define TR_STOP "停止" +#define TR_REC "REC" +#define TR_STARTING_IN "倒计时" +#define TR_GET_READY "准备好..." +#define TR_SAVED "已保存:" +#define TR_SAVE_AS "另存为" +#define TR_OPEN_ERROR "打开失败" #define TR_TURN_OFF_RECEIVER "请关闭接收机" #define TR_STOPPING "正在停止..." #define TR_MENU_SPECTRUM_ANALYSER "频谱仪" diff --git a/radio/src/translations/i18n/cz.h b/radio/src/translations/i18n/cz.h index e90c96e66..ec1405eb4 100644 --- a/radio/src/translations/i18n/cz.h +++ b/radio/src/translations/i18n/cz.h @@ -964,6 +964,16 @@ #define TR_POWERMETER_ATTN "Útlum" #define TR_POWERMETER_FREQ "Frek." #define TR_MENUTOOLS "NÁSTROJE" +#define TR_MIC_RECORDER "Záznam mikrofonu" +#define TR_PUSH_TO_RECORD "Stiskni pro záznam" +#define TR_RECORD "Nahrát" +#define TR_STOP "Zastavit" +#define TR_REC "REC" +#define TR_STARTING_IN "Začne za" +#define TR_GET_READY "Připravit se..." +#define TR_SAVED "Uloženo:" +#define TR_SAVE_AS "Uložit jako" +#define TR_OPEN_ERROR "Chyba otevření" #define TR_TURN_OFF_RECEIVER "Vypněte přijímač" #define TR_STOPPING "Zastavuji..." #define TR_MENU_SPECTRUM_ANALYSER "SPEKTRÁLNÍ ANALYZÉR" diff --git a/radio/src/translations/i18n/da.h b/radio/src/translations/i18n/da.h index 8a84e5e24..1d5ddff01 100644 --- a/radio/src/translations/i18n/da.h +++ b/radio/src/translations/i18n/da.h @@ -971,6 +971,16 @@ #define TR_POWERMETER_ATTN "Attn" #define TR_POWERMETER_FREQ "Freq." #define TR_MENUTOOLS "VÆRKTØJ" +#define TR_MIC_RECORDER "Mikrofon optager" +#define TR_PUSH_TO_RECORD "Tryk for at optage" +#define TR_RECORD "Optag" +#define TR_STOP "Stop" +#define TR_REC "REC" +#define TR_STARTING_IN "Starter om" +#define TR_GET_READY "Gør dig klar..." +#define TR_SAVED "Gemt:" +#define TR_SAVE_AS "Gem som" +#define TR_OPEN_ERROR "Åbn fejl" #define TR_TURN_OFF_RECEIVER "Sluk modtager" #define TR_STOPPING "Standser..." #define TR_MENU_SPECTRUM_ANALYSER "SPECTRUM ANALYSE" diff --git a/radio/src/translations/i18n/de.h b/radio/src/translations/i18n/de.h index dffbdae34..ace346fae 100644 --- a/radio/src/translations/i18n/de.h +++ b/radio/src/translations/i18n/de.h @@ -965,6 +965,16 @@ #define TR_POWERMETER_ATTN "Dämpf." #define TR_POWERMETER_FREQ "Freq." #define TR_MENUTOOLS TR_BW_COL("TOOLS", "Tools") +#define TR_MIC_RECORDER TR_BW_COL("Mikrofon Rek.", "Mikrofon Rekorder") +#define TR_PUSH_TO_RECORD TR_BW_COL("Drücken=Aufnahme", "Drücken zum Aufnehmen") +#define TR_RECORD "Aufnahme" +#define TR_STOP "Stop" +#define TR_REC "REC" +#define TR_STARTING_IN "Start in" +#define TR_GET_READY "Bereit machen..." +#define TR_SAVED "Gespeichert:" +#define TR_SAVE_AS "Speichern als" +#define TR_OPEN_ERROR "Öffnungsfehler" #define TR_TURN_OFF_RECEIVER "Empf. ausschalten" #define TR_STOPPING "Stoppe..." #define TR_MENU_SPECTRUM_ANALYSER TR_BW_COL("SPEKTRUM ANALYSATOR", "Spektrum Analysator") diff --git a/radio/src/translations/i18n/en.h b/radio/src/translations/i18n/en.h index 6eca516e3..8660c4206 100644 --- a/radio/src/translations/i18n/en.h +++ b/radio/src/translations/i18n/en.h @@ -967,6 +967,16 @@ #define TR_POWERMETER_ATTN "Attn" #define TR_POWERMETER_FREQ "Freq." #define TR_MENUTOOLS "TOOLS" +#define TR_MIC_RECORDER "Mic recorder" +#define TR_PUSH_TO_RECORD "Push to record" +#define TR_RECORD "Record" +#define TR_STOP "Stop" +#define TR_REC "REC" +#define TR_STARTING_IN "Starting in" +#define TR_GET_READY "Get ready..." +#define TR_SAVED "Saved:" +#define TR_SAVE_AS "Save as" +#define TR_OPEN_ERROR "Open error" #define TR_TURN_OFF_RECEIVER "Turn off receiver" #define TR_STOPPING "Stopping..." #define TR_MENU_SPECTRUM_ANALYSER "SPECTRUM ANALYSER" diff --git a/radio/src/translations/i18n/es.h b/radio/src/translations/i18n/es.h index 7eba825d7..47a4680aa 100644 --- a/radio/src/translations/i18n/es.h +++ b/radio/src/translations/i18n/es.h @@ -960,6 +960,16 @@ #define TR_POWERMETER_ATTN "Attn" #define TR_POWERMETER_FREQ "Frec." #define TR_MENUTOOLS "UTILS" +#define TR_MIC_RECORDER "Grabador de mic" +#define TR_PUSH_TO_RECORD "Pulsar para grabar" +#define TR_RECORD "Grabar" +#define TR_STOP "Parar" +#define TR_REC "REC" +#define TR_STARTING_IN "Empieza en" +#define TR_GET_READY "Prepárate..." +#define TR_SAVED "Guardado:" +#define TR_SAVE_AS "Guardar como" +#define TR_OPEN_ERROR "Error al abrir" #define TR_TURN_OFF_RECEIVER "Apaga el receptor" #define TR_STOPPING "Parando..." #define TR_MENU_SPECTRUM_ANALYSER "ANALIZADOR DE ESPECTRO" diff --git a/radio/src/translations/i18n/fi.h b/radio/src/translations/i18n/fi.h index c021622c0..8383da195 100644 --- a/radio/src/translations/i18n/fi.h +++ b/radio/src/translations/i18n/fi.h @@ -960,6 +960,16 @@ #define TR_POWERMETER_ATTN "Attn" #define TR_POWERMETER_FREQ "Taajuus" #define TR_MENUTOOLS "TOOLS" +#define TR_MIC_RECORDER "Mikrofoni tallennin" +#define TR_PUSH_TO_RECORD "Paina tallentaaksesi" +#define TR_RECORD "Tallenna" +#define TR_STOP "Lopeta" +#define TR_REC "REC" +#define TR_STARTING_IN "Alkaa" +#define TR_GET_READY "Valmistaudu..." +#define TR_SAVED "Tallennettu:" +#define TR_SAVE_AS "Tallenna nimellä" +#define TR_OPEN_ERROR "Avausvirhe" #define TR_TURN_OFF_RECEIVER "Turn off receiver" #define TR_STOPPING "Stopping..." #define TR_MENU_SPECTRUM_ANALYSER "SPEKTRIANALYSERI" diff --git a/radio/src/translations/i18n/fr.h b/radio/src/translations/i18n/fr.h index 80886dcce..d5391fe0d 100644 --- a/radio/src/translations/i18n/fr.h +++ b/radio/src/translations/i18n/fr.h @@ -964,6 +964,16 @@ #define TR_POWERMETER_ATTN "Attn" #define TR_POWERMETER_FREQ "Fréq." #define TR_MENUTOOLS "OUTILS" +#define TR_MIC_RECORDER "Enregistreur micro" +#define TR_PUSH_TO_RECORD "Appuyer pour enregistrer" +#define TR_RECORD "Enregistrer" +#define TR_STOP "Arrêter" +#define TR_REC "REC" +#define TR_STARTING_IN "Démarrage dans" +#define TR_GET_READY "Tenez-vous prêt..." +#define TR_SAVED "Enregistré:" +#define TR_SAVE_AS "Enregistrer sous" +#define TR_OPEN_ERROR "Erreur ouverture" #define TR_TURN_OFF_RECEIVER "Éteindre Récepteur." #define TR_STOPPING "Arrêt..." #define TR_MENU_SPECTRUM_ANALYSER "ANALYSEUR SPECTRE" diff --git a/radio/src/translations/i18n/he.h b/radio/src/translations/i18n/he.h index 3d406638d..9687f57f6 100644 --- a/radio/src/translations/i18n/he.h +++ b/radio/src/translations/i18n/he.h @@ -969,6 +969,16 @@ #define TR_POWERMETER_ATTN "Attn" #define TR_POWERMETER_FREQ "Freq." #define TR_MENUTOOLS "כלים וסקריפטים" +#define TR_MIC_RECORDER "מקליט מיקרופון" +#define TR_PUSH_TO_RECORD "לחץ להקלטה" +#define TR_RECORD "הקלט" +#define TR_STOP "עצור" +#define TR_REC "REC" +#define TR_STARTING_IN "מתחיל בעוד" +#define TR_GET_READY "...היה מוכן" +#define TR_SAVED ":נשמר" +#define TR_SAVE_AS "שמור בשם" +#define TR_OPEN_ERROR "שגיאת פתיחה" #define TR_TURN_OFF_RECEIVER "כיבוי מקלט" #define TR_STOPPING "...עוצר" #define TR_MENU_SPECTRUM_ANALYSER "SPECTRUM ANALYSER" diff --git a/radio/src/translations/i18n/it.h b/radio/src/translations/i18n/it.h index 923e49144..4c8ab8e4a 100644 --- a/radio/src/translations/i18n/it.h +++ b/radio/src/translations/i18n/it.h @@ -966,6 +966,16 @@ #define TR_POWERMETER_ATTN "Att." #define TR_POWERMETER_FREQ "Freq." #define TR_MENUTOOLS "TOOLS" +#define TR_MIC_RECORDER "Registr. microfono" +#define TR_PUSH_TO_RECORD "Premi per registrare" +#define TR_RECORD "Registra" +#define TR_STOP "Stop" +#define TR_REC "REC" +#define TR_STARTING_IN "Inizia tra" +#define TR_GET_READY "Preparati..." +#define TR_SAVED "Salvato:" +#define TR_SAVE_AS "Salva come" +#define TR_OPEN_ERROR "Errore apertura" #define TR_TURN_OFF_RECEIVER "Spegni la RX" #define TR_STOPPING "Fermando..." #define TR_MENU_SPECTRUM_ANALYSER "ANALIZZATORE SPETTRO" diff --git a/radio/src/translations/i18n/jp.h b/radio/src/translations/i18n/jp.h index 4cb5738f7..c3d1f79e2 100644 --- a/radio/src/translations/i18n/jp.h +++ b/radio/src/translations/i18n/jp.h @@ -965,6 +965,16 @@ #define TR_POWERMETER_ATTN "Attn" #define TR_POWERMETER_FREQ "周波数" #define TR_MENUTOOLS "ツール" +#define TR_MIC_RECORDER "Mic recorder" +#define TR_PUSH_TO_RECORD "Push to record" +#define TR_RECORD "Record" +#define TR_STOP "Stop" +#define TR_REC "REC" +#define TR_STARTING_IN "Starting in" +#define TR_GET_READY "Get ready..." +#define TR_SAVED "Saved:" +#define TR_SAVE_AS "Save as" +#define TR_OPEN_ERROR "Open error" #define TR_TURN_OFF_RECEIVER "受信機 電源OFF" #define TR_STOPPING "停止中..." #define TR_MENU_SPECTRUM_ANALYSER "スペクトラムアナライザー" diff --git a/radio/src/translations/i18n/ko.h b/radio/src/translations/i18n/ko.h index 2f5137502..53fe63529 100644 --- a/radio/src/translations/i18n/ko.h +++ b/radio/src/translations/i18n/ko.h @@ -1008,6 +1008,16 @@ #define TR_POWERMETER_ATTN "감쇠" #define TR_POWERMETER_FREQ "주파수" #define TR_MENUTOOLS "도구" +#define TR_MIC_RECORDER "마이크 녹음기" +#define TR_PUSH_TO_RECORD "눌러서 녹음" +#define TR_RECORD "녹음" +#define TR_STOP "중지" +#define TR_REC "REC" +#define TR_STARTING_IN "시작까지" +#define TR_GET_READY "준비하세요..." +#define TR_SAVED "저장됨:" +#define TR_SAVE_AS "다른 이름으로 저장" +#define TR_OPEN_ERROR "열기 오류" #define TR_TURN_OFF_RECEIVER "수신기 전원 끄기" #define TR_STOPPING "중지 중..." diff --git a/radio/src/translations/i18n/nl.h b/radio/src/translations/i18n/nl.h index e4510519d..b8714dfb8 100644 --- a/radio/src/translations/i18n/nl.h +++ b/radio/src/translations/i18n/nl.h @@ -962,6 +962,16 @@ #define TR_POWERMETER_ATTN "Attn" #define TR_POWERMETER_FREQ "Freq." #define TR_MENUTOOLS "TOOLS" +#define TR_MIC_RECORDER "Microfoon recorder" +#define TR_PUSH_TO_RECORD "Druk om op te nemen" +#define TR_RECORD "Opnemen" +#define TR_STOP "Stop" +#define TR_REC "REC" +#define TR_STARTING_IN "Start over" +#define TR_GET_READY "Maak je klaar..." +#define TR_SAVED "Opgeslagen:" +#define TR_SAVE_AS "Opslaan als" +#define TR_OPEN_ERROR "Fout bij openen" #define TR_TURN_OFF_RECEIVER "Turn off receiver" #define TR_STOPPING "Stopping..." #define TR_MENU_SPECTRUM_ANALYSER "SPECTRUM ANALYSER" diff --git a/radio/src/translations/i18n/pl.h b/radio/src/translations/i18n/pl.h index 8834f06a1..6868d6cf0 100644 --- a/radio/src/translations/i18n/pl.h +++ b/radio/src/translations/i18n/pl.h @@ -959,6 +959,16 @@ #define TR_POWERMETER_ATTN "Attn" #define TR_POWERMETER_FREQ "Freq." #define TR_MENUTOOLS "NARZĘDZIA" +#define TR_MIC_RECORDER "Nagrywarka mikrofonu" +#define TR_PUSH_TO_RECORD "Naciśnij aby nagrać" +#define TR_RECORD "Nagraj" +#define TR_STOP "Stop" +#define TR_REC "REC" +#define TR_STARTING_IN "Start za" +#define TR_GET_READY "Przygotuj się..." +#define TR_SAVED "Zapisano:" +#define TR_SAVE_AS "Zapisz jako" +#define TR_OPEN_ERROR "Błąd otwarcia" #define TR_TURN_OFF_RECEIVER "Wyłącz odbiornik" #define TR_STOPPING "Stopping..." #define TR_MENU_SPECTRUM_ANALYSER "SPECTRUM ANALYSER" diff --git a/radio/src/translations/i18n/pt.h b/radio/src/translations/i18n/pt.h index 3166d7c01..77b6bf1ee 100644 --- a/radio/src/translations/i18n/pt.h +++ b/radio/src/translations/i18n/pt.h @@ -967,6 +967,16 @@ #define TR_POWERMETER_ATTN "Attn" #define TR_POWERMETER_FREQ "Freq." #define TR_MENUTOOLS "FERRAMENTAS" +#define TR_MIC_RECORDER "Gravador de microfone" +#define TR_PUSH_TO_RECORD "Pressionar p/ gravar" +#define TR_RECORD "Gravar" +#define TR_STOP "Parar" +#define TR_REC "REC" +#define TR_STARTING_IN "Começa em" +#define TR_GET_READY "Prepare-se..." +#define TR_SAVED "Guardado:" +#define TR_SAVE_AS "Guardar como" +#define TR_OPEN_ERROR "Erro ao abrir" #define TR_TURN_OFF_RECEIVER "Turn off receiver" #define TR_STOPPING "Stopping..." #define TR_MENU_SPECTRUM_ANALYSER "ANALISADOR ESPECTRO" diff --git a/radio/src/translations/i18n/ru.h b/radio/src/translations/i18n/ru.h index e2a7484f6..a5cb6e624 100644 --- a/radio/src/translations/i18n/ru.h +++ b/radio/src/translations/i18n/ru.h @@ -968,6 +968,16 @@ #define TR_POWERMETER_ATTN "Аттен" #define TR_POWERMETER_FREQ "Частота" #define TR_MENUTOOLS "ИНСТРУМЕНТЫ" +#define TR_MIC_RECORDER "Запись микрофона" +#define TR_PUSH_TO_RECORD "Нажмите для записи" +#define TR_RECORD "Запись" +#define TR_STOP "Стоп" +#define TR_REC "REC" +#define TR_STARTING_IN "Начало через" +#define TR_GET_READY "Приготовьтесь..." +#define TR_SAVED "Сохранено:" +#define TR_SAVE_AS "Сохранить как" +#define TR_OPEN_ERROR "Ошибка открытия" #define TR_TURN_OFF_RECEIVER "Выкл приемник" #define TR_STOPPING "Остановка..." #define TR_MENU_SPECTRUM_ANALYSER "АНАЛИЗАТОР СПЕКТРА" diff --git a/radio/src/translations/i18n/se.h b/radio/src/translations/i18n/se.h index d707aedb2..5ce85caa9 100644 --- a/radio/src/translations/i18n/se.h +++ b/radio/src/translations/i18n/se.h @@ -967,6 +967,16 @@ #define TR_POWERMETER_ATTN "Attn" #define TR_POWERMETER_FREQ "Freq." #define TR_MENUTOOLS "VERKTYG" +#define TR_MIC_RECORDER "Mikrofoninspelning" +#define TR_PUSH_TO_RECORD "Tryck för att spela in" +#define TR_RECORD "Spela in" +#define TR_STOP "Stoppa" +#define TR_REC "REC" +#define TR_STARTING_IN "Startar om" +#define TR_GET_READY "Gör dig redo..." +#define TR_SAVED "Sparad:" +#define TR_SAVE_AS "Spara som" +#define TR_OPEN_ERROR "Öppningsfel" #define TR_TURN_OFF_RECEIVER "Stäng av mottagare" #define TR_STOPPING "Stoppar..." #define TR_MENU_SPECTRUM_ANALYSER "SPEKTRUMANALYSATOR" diff --git a/radio/src/translations/i18n/tw.h b/radio/src/translations/i18n/tw.h index fc974fd59..16216abd0 100644 --- a/radio/src/translations/i18n/tw.h +++ b/radio/src/translations/i18n/tw.h @@ -963,6 +963,16 @@ #define TR_POWERMETER_ATTN "衰減器" #define TR_POWERMETER_FREQ "頻率" #define TR_MENUTOOLS "擴展工具" +#define TR_MIC_RECORDER "麥克風錄音" +#define TR_PUSH_TO_RECORD "按下錄音" +#define TR_RECORD "錄音" +#define TR_STOP "停止" +#define TR_REC "REC" +#define TR_STARTING_IN "倒計時" +#define TR_GET_READY "準備好..." +#define TR_SAVED "已保存:" +#define TR_SAVE_AS "另存為" +#define TR_OPEN_ERROR "打開失敗" #define TR_TURN_OFF_RECEIVER "請關閉接收機" #define TR_STOPPING "正在停止..." #define TR_MENU_SPECTRUM_ANALYSER "頻譜儀" diff --git a/radio/src/translations/i18n/ua.h b/radio/src/translations/i18n/ua.h index 03329c26f..90e1465c6 100644 --- a/radio/src/translations/i18n/ua.h +++ b/radio/src/translations/i18n/ua.h @@ -967,6 +967,16 @@ #define TR_POWERMETER_ATTN "Атт" #define TR_POWERMETER_FREQ "Част." #define TR_MENUTOOLS "ІНСТРУМЕНТИ" +#define TR_MIC_RECORDER "Запис мікрофона" +#define TR_PUSH_TO_RECORD "Натисніть для запису" +#define TR_RECORD "Записати" +#define TR_STOP "Зупинити" +#define TR_REC "REC" +#define TR_STARTING_IN "Початок через" +#define TR_GET_READY "Готуйтесь..." +#define TR_SAVED "Збережено:" +#define TR_SAVE_AS "Зберегти як" +#define TR_OPEN_ERROR "Помилка відкриття" #define TR_TURN_OFF_RECEIVER "Вимкнути приймач" #define TR_STOPPING "Зупинка..." #define TR_MENU_SPECTRUM_ANALYSER "АНАЛІЗАТОР СПЕКТРУ" diff --git a/radio/src/translations/sim_string_list.h b/radio/src/translations/sim_string_list.h index 87a5dacb8..5fdac83b0 100644 --- a/radio/src/translations/sim_string_list.h +++ b/radio/src/translations/sim_string_list.h @@ -243,6 +243,19 @@ #define STR_MAIN_VIEW_9 currentLangStrings->STR_MAIN_VIEW_9 // Main Menu +#if defined(PDM_CLOCK) +#define STR_MIC_RECORDER currentLangStrings->STR_MIC_RECORDER +#define STR_PUSH_TO_RECORD currentLangStrings->STR_PUSH_TO_RECORD +#define STR_RECORD currentLangStrings->STR_RECORD +#define STR_STOP currentLangStrings->STR_STOP +#define STR_REC currentLangStrings->STR_REC +#define STR_STARTING_IN currentLangStrings->STR_STARTING_IN +#define STR_GET_READY currentLangStrings->STR_GET_READY +#define STR_SAVED currentLangStrings->STR_SAVED +#define STR_SAVE_AS currentLangStrings->STR_SAVE_AS +#define STR_OPEN_ERROR currentLangStrings->STR_OPEN_ERROR +#endif + #else // !COLORLCD #if LCD_W >= 212 diff --git a/radio/src/translations/string_list.h b/radio/src/translations/string_list.h index d618ecc47..146fcf847 100644 --- a/radio/src/translations/string_list.h +++ b/radio/src/translations/string_list.h @@ -242,6 +242,19 @@ STR(MAIN_VIEW_8) STR(MAIN_VIEW_9) // Main Menu +#if defined(PDM_CLOCK) +STR(MIC_RECORDER) +STR(PUSH_TO_RECORD) +STR(RECORD) +STR(STOP) +STR(REC) +STR(STARTING_IN) +STR(GET_READY) +STR(SAVED) +STR(SAVE_AS) +STR(OPEN_ERROR) +#endif + #else // !COLORLCD #if LCD_W >= 212