feat: MEMS microphone software handling for TX15 and TX16SMK3 (#7328)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
13bba22a4d
commit
a7d730fded
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 <stdint.h>
|
||||
|
||||
// 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);
|
||||
@@ -50,8 +50,10 @@
|
||||
#include <malloc.h>
|
||||
#include <new>
|
||||
#include <stdarg.h>
|
||||
#include <string.h>
|
||||
|
||||
#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, "<start sector> <sectors count> <read buffer size (sectors)>" },
|
||||
{ "testsd", cliTestSD, "" },
|
||||
{ "play", cliPlay, "<filename>" },
|
||||
#if defined(PDM_CLOCK)
|
||||
{ "rec", cliRecord, "<filename> [<seconds>]" },
|
||||
#endif
|
||||
{ "reboot", cliReboot, "[wdt]" },
|
||||
{ "set", cliSet, "<what> <value>" },
|
||||
#if defined(ENABLE_SERIAL_PASSTHROUGH)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <stdio.h>
|
||||
|
||||
#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<RadioMicRecorder*>(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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 <string.h>
|
||||
|
||||
#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
|
||||
@@ -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 <stdint.h>
|
||||
#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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 "频谱仪"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 "スペクトラムアナライザー"
|
||||
|
||||
@@ -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 "중지 중..."
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 "АНАЛИЗАТОР СПЕКТРА"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 "頻譜儀"
|
||||
|
||||
@@ -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 "АНАЛІЗАТОР СПЕКТРУ"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user