feat(mic): add start/end trim, auto trim and clip last bit of noise when tapping or clicking stop (#7706)
This commit is contained in:
@@ -30,6 +30,8 @@
|
||||
#include "dialog.h"
|
||||
#include "edgetx.h"
|
||||
#include "ff.h"
|
||||
#include "form.h"
|
||||
#include "hal/rotary_encoder.h"
|
||||
#include "sdcard.h"
|
||||
#include "static.h"
|
||||
#include "timers_driver.h"
|
||||
@@ -38,17 +40,46 @@
|
||||
static constexpr coord_t MIC_BTN_W = 260;
|
||||
static constexpr coord_t MIC_BTN_H = EdgeTxStyles::UI_ELEMENT_HEIGHT * 2 + PAD_LARGE;
|
||||
|
||||
// Review needs two rows in the space the single record button occupies.
|
||||
static constexpr coord_t REV_BTN_H = EdgeTxStyles::UI_ELEMENT_HEIGHT + PAD_MEDIUM;
|
||||
|
||||
// Mirrored peak envelope, drawn as two polylines around a centre line. Fed one
|
||||
// column at a time while recording, then re-filled from the finished file.
|
||||
class WaveformView : public Window
|
||||
// It doubles as the trim editor: a marker line dragged straight over the trace,
|
||||
// with the part that would be cut greyed out.
|
||||
class WaveformView : public FormField
|
||||
{
|
||||
public:
|
||||
static constexpr uint16_t MAX_COLS = 400;
|
||||
static constexpr coord_t KNOB_W = PAD_LARGE * 2;
|
||||
static constexpr coord_t KNOB_H = PAD_MEDIUM;
|
||||
|
||||
WaveformView(Window* parent, const rect_t& rect) : Window(parent, rect)
|
||||
WaveformView(Window* parent, const rect_t& rect,
|
||||
std::function<void(int)> onMove, std::function<void()> onConfirm,
|
||||
std::function<void()> onDismiss) :
|
||||
FormField(parent, rect),
|
||||
moveHandler(std::move(onMove)),
|
||||
confirmHandler(std::move(onConfirm)),
|
||||
dismissHandler(std::move(onDismiss))
|
||||
{
|
||||
padAll(PAD_ZERO);
|
||||
|
||||
// No scrolling: a horizontal drag has to reach the marker, not pan the view.
|
||||
lv_obj_clear_flag(lvobj, LV_OBJ_FLAG_SCROLLABLE);
|
||||
// No click focus either: LVGL clears the group's edit mode on every focus
|
||||
// call, so a touch would otherwise leave the encoder stuck in navigation.
|
||||
lv_obj_clear_flag(lvobj, LV_OBJ_FLAG_CLICK_FOCUSABLE);
|
||||
lv_obj_add_flag(lvobj, LV_OBJ_FLAG_ENCODER_ACCEL);
|
||||
lv_obj_add_event_cb(lvobj, WaveformView::on_pressing, LV_EVENT_PRESSING, this);
|
||||
lv_obj_add_event_cb(lvobj, WaveformView::on_released, LV_EVENT_RELEASED, this);
|
||||
lv_obj_add_event_cb(lvobj, WaveformView::on_key, LV_EVENT_KEY, this);
|
||||
etx_obj_add_style(lvobj, styles->outline, LV_PART_MAIN | LV_STATE_EDITED);
|
||||
etx_obj_add_style(lvobj, styles->outline_color_edit,
|
||||
LV_PART_MAIN | LV_STATE_EDITED);
|
||||
setFocusHandler([this](bool focus) {
|
||||
if (!focus && isEditMode()) setEditMode(false);
|
||||
});
|
||||
|
||||
// Widen the columns rather than leaving the trace short of the edge:
|
||||
// 800 px screens need more than MAX_COLS columns at 2 px.
|
||||
colStep = (coord_t)((width() + MAX_COLS - 1) / MAX_COLS);
|
||||
@@ -80,6 +111,32 @@ class WaveformView : public Window
|
||||
LV_PART_MAIN);
|
||||
lv_obj_add_flag(cursorLine, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
// Created last so they sit on top of the trace.
|
||||
cutShade = lv_obj_create(lvobj);
|
||||
lv_obj_clear_flag(cutShade, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_clear_flag(cutShade, LV_OBJ_FLAG_SCROLLABLE);
|
||||
etx_bg_color(cutShade, COLOR_THEME_DISABLED_INDEX, LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_opa(cutShade, LV_OPA_60, LV_PART_MAIN);
|
||||
lv_obj_set_style_border_width(cutShade, 0, LV_PART_MAIN);
|
||||
lv_obj_set_style_radius(cutShade, 0, LV_PART_MAIN);
|
||||
lv_obj_add_flag(cutShade, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
trimLine = lv_line_create(lvobj);
|
||||
etx_obj_add_style(trimLine, styles->graph_line, LV_PART_MAIN);
|
||||
etx_obj_add_style(trimLine, styles->line_color[COLOR_THEME_EDIT_INDEX],
|
||||
LV_PART_MAIN);
|
||||
lv_obj_set_style_line_width(trimLine, PAD_THREE, LV_PART_MAIN);
|
||||
lv_obj_add_flag(trimLine, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
trimKnob = lv_obj_create(lvobj);
|
||||
lv_obj_clear_flag(trimKnob, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_clear_flag(trimKnob, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(trimKnob, KNOB_W, KNOB_H);
|
||||
etx_solid_bg(trimKnob, COLOR_THEME_EDIT_INDEX);
|
||||
lv_obj_set_style_border_width(trimKnob, 0, LV_PART_MAIN);
|
||||
etx_obj_add_style(trimKnob, styles->rounded, LV_PART_MAIN);
|
||||
lv_obj_add_flag(trimKnob, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
clear();
|
||||
}
|
||||
|
||||
@@ -115,6 +172,31 @@ class WaveformView : public Window
|
||||
cursorAt = permille;
|
||||
}
|
||||
|
||||
// Trim editing: the marker takes the focus so the encoder drags it directly,
|
||||
// and a touch anywhere on the trace drops it at that point.
|
||||
void showTrim(int32_t permille, bool cutBefore)
|
||||
{
|
||||
trimShown = true;
|
||||
cutLeft = cutBefore;
|
||||
lv_obj_clear_flag(trimLine, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_clear_flag(trimKnob, LV_OBJ_FLAG_HIDDEN);
|
||||
setTrim(permille);
|
||||
lv_group_add_obj((lv_group_t*)lv_group_get_default(), lvobj);
|
||||
lv_group_focus_obj(lvobj);
|
||||
setEditMode(true);
|
||||
}
|
||||
|
||||
void hideTrim()
|
||||
{
|
||||
if (!trimShown) return;
|
||||
trimShown = false;
|
||||
if (isEditMode()) setEditMode(false);
|
||||
lv_group_remove_obj(lvobj);
|
||||
lv_obj_add_flag(trimLine, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(trimKnob, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(cutShade, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
// Live feed. Once the view is full, columns are merged pairwise so the whole
|
||||
// take stays visible at half the time resolution.
|
||||
void push(uint8_t level)
|
||||
@@ -150,11 +232,122 @@ class WaveformView : public Window
|
||||
lv_point_t botPts[MAX_COLS];
|
||||
lv_point_t basePts[2];
|
||||
lv_point_t cursorPts[2];
|
||||
lv_point_t trimPts[2];
|
||||
int32_t cursorAt = -1;
|
||||
lv_obj_t* cursorLine = nullptr;
|
||||
lv_obj_t* topLine = nullptr;
|
||||
lv_obj_t* botLine = nullptr;
|
||||
lv_obj_t* baseLine = nullptr;
|
||||
lv_obj_t* trimLine = nullptr;
|
||||
lv_obj_t* trimKnob = nullptr;
|
||||
lv_obj_t* cutShade = nullptr;
|
||||
|
||||
std::function<void(int)> moveHandler;
|
||||
std::function<void()> confirmHandler;
|
||||
std::function<void()> dismissHandler;
|
||||
bool trimShown = false;
|
||||
bool cutLeft = false;
|
||||
int32_t trimAt = -1;
|
||||
|
||||
void setTrim(int32_t permille)
|
||||
{
|
||||
trimAt = permille;
|
||||
|
||||
const lv_coord_t x = (lv_coord_t)((permille * traceW) / 1000);
|
||||
trimPts[0] = {x, 0};
|
||||
trimPts[1] = {x, (lv_coord_t)(height() - 1)};
|
||||
lv_line_set_points(trimLine, trimPts, 2);
|
||||
lv_coord_t knobX = x - KNOB_W / 2;
|
||||
if (knobX < 0) knobX = 0;
|
||||
if (knobX > traceW - KNOB_W) knobX = traceW - KNOB_W;
|
||||
lv_obj_set_pos(trimKnob, knobX, 0);
|
||||
|
||||
const lv_coord_t w = cutLeft ? x : (lv_coord_t)(traceW - x);
|
||||
if (w > 0) {
|
||||
lv_obj_set_pos(cutShade, cutLeft ? 0 : x, 0);
|
||||
lv_obj_set_size(cutShade, w, height());
|
||||
lv_obj_clear_flag(cutShade, LV_OBJ_FLAG_HIDDEN);
|
||||
} else {
|
||||
lv_obj_add_flag(cutShade, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
void moveTo(int32_t permille)
|
||||
{
|
||||
if (permille < 0) permille = 0;
|
||||
if (permille > 1000) permille = 1000;
|
||||
if (permille == trimAt) return;
|
||||
setTrim(permille);
|
||||
if (moveHandler) moveHandler(permille);
|
||||
}
|
||||
|
||||
static void on_pressing(lv_event_t* e)
|
||||
{
|
||||
auto wv = (WaveformView*)lv_event_get_user_data(e);
|
||||
if (!wv || wv->deleted() || !wv->trimShown || wv->traceW <= 0) return;
|
||||
|
||||
auto indev = (lv_indev_t*)lv_event_get_param(e);
|
||||
if (!indev || lv_indev_get_type(indev) != LV_INDEV_TYPE_POINTER) return;
|
||||
|
||||
lv_area_t coords;
|
||||
lv_obj_get_coords(lv_event_get_target(e), &coords);
|
||||
lv_point_t point;
|
||||
lv_indev_get_point(indev, &point);
|
||||
|
||||
const int32_t x = point.x - coords.x1;
|
||||
wv->moveTo((x * 1000) / wv->traceW);
|
||||
wv->rearmEdit();
|
||||
}
|
||||
|
||||
static void on_released(lv_event_t* e)
|
||||
{
|
||||
auto wv = (WaveformView*)lv_event_get_user_data(e);
|
||||
if (wv && !wv->deleted()) wv->rearmEdit();
|
||||
}
|
||||
|
||||
// Anything that focuses an object drops edit mode group wide, so take it back
|
||||
// after a touch: the encoder has to keep driving the marker.
|
||||
void rearmEdit()
|
||||
{
|
||||
if (!trimShown || isEditMode()) return;
|
||||
auto g = (lv_group_t*)lv_obj_get_group(lvobj);
|
||||
if (g && lv_group_get_focused(g) == lvobj) setEditMode(true);
|
||||
}
|
||||
|
||||
static void on_key(lv_event_t* e)
|
||||
{
|
||||
auto wv = (WaveformView*)lv_event_get_user_data(e);
|
||||
if (!wv || wv->deleted() || !wv->trimShown) return;
|
||||
|
||||
const uint32_t key = lv_event_get_key(e);
|
||||
if (key != LV_KEY_LEFT && key != LV_KEY_RIGHT) return;
|
||||
|
||||
// One detent moves the marker by one column, more when spun fast.
|
||||
int32_t step = wv->maxCols ? 1000 / wv->maxCols : 1;
|
||||
if (step < 1) step = 1;
|
||||
step += rotaryEncoderGetAccel() / 4;
|
||||
wv->moveTo(wv->trimAt + (key == LV_KEY_RIGHT ? step : -step));
|
||||
}
|
||||
|
||||
// A touch only drops the marker; ENTER is what confirms the trim.
|
||||
void onClicked() override
|
||||
{
|
||||
if (!trimShown) {
|
||||
FormField::onClicked();
|
||||
return;
|
||||
}
|
||||
if (lv_indev_get_type(lv_indev_get_act()) == LV_INDEV_TYPE_POINTER) return;
|
||||
if (confirmHandler) confirmHandler();
|
||||
}
|
||||
|
||||
void onCancel() override
|
||||
{
|
||||
if (trimShown && dismissHandler) {
|
||||
dismissHandler();
|
||||
return;
|
||||
}
|
||||
FormField::onCancel();
|
||||
}
|
||||
|
||||
void compact()
|
||||
{
|
||||
@@ -234,9 +427,11 @@ void RadioMicRecorder::buildBody(Window* window)
|
||||
const coord_t h = window->height();
|
||||
|
||||
const coord_t btnY = h - MIC_BTN_H - PAD_LARGE * 2;
|
||||
const coord_t rowY2 = h - REV_BTN_H - PAD_LARGE; // play / save / record
|
||||
const coord_t rowY1 = rowY2 - REV_BTN_H - PAD_SMALL; // the trim actions
|
||||
const coord_t bigH = EdgeTxStyles::UI_ELEMENT_HEIGHT + PAD_MEDIUM;
|
||||
const coord_t infoH = EdgeTxStyles::STD_FONT_HEIGHT;
|
||||
const coord_t infoY = btnY - infoH - PAD_SMALL;
|
||||
const coord_t infoY = rowY1 - infoH - PAD_SMALL;
|
||||
const coord_t waveY = PAD_SMALL + bigH + PAD_SMALL;
|
||||
|
||||
bigLabel = new StaticText(
|
||||
@@ -246,7 +441,9 @@ void RadioMicRecorder::buildBody(Window* window)
|
||||
lv_obj_set_style_text_align(bigLabel->getLvObj(), LV_TEXT_ALIGN_CENTER, 0);
|
||||
|
||||
waveform = new WaveformView(
|
||||
window, {PAD_LARGE, waveY, w - PAD_LARGE * 2, infoY - waveY - PAD_SMALL});
|
||||
window, {PAD_LARGE, waveY, w - PAD_LARGE * 2, infoY - waveY - PAD_SMALL},
|
||||
[this](int v) { onTrimMoved(v); }, [this]() { onApplyTrim(); },
|
||||
[this]() { exitTrim(); });
|
||||
waveform->hide();
|
||||
|
||||
infoLabel = new StaticText(
|
||||
@@ -265,44 +462,159 @@ void RadioMicRecorder::buildBody(Window* window)
|
||||
actionButton->setFont(FONT_XL_INDEX);
|
||||
|
||||
const coord_t revW = (w - PAD_LARGE * 4) / 3;
|
||||
const coord_t revX[3] = {PAD_LARGE, (coord_t)(PAD_LARGE * 2 + revW),
|
||||
(coord_t)(PAD_LARGE * 3 + revW * 2)};
|
||||
|
||||
autoTrimButton = new TextButton(
|
||||
window, {revX[0], rowY1, revW, REV_BTN_H}, STR_AUTO_TRIM, [this]() {
|
||||
onAutoTrim();
|
||||
return 0;
|
||||
});
|
||||
|
||||
trimStartButton = new TextButton(
|
||||
window, {revX[1], rowY1, revW, REV_BTN_H}, STR_TRIM_START, [this]() {
|
||||
enterTrim(TrimMode::START);
|
||||
return 0;
|
||||
});
|
||||
|
||||
trimEndButton = new TextButton(
|
||||
window, {revX[2], rowY1, revW, REV_BTN_H}, STR_TRIM_END, [this]() {
|
||||
enterTrim(TrimMode::END);
|
||||
return 0;
|
||||
});
|
||||
|
||||
playButton = new TextButton(
|
||||
window, {PAD_LARGE, btnY, revW, MIC_BTN_H}, STR_PLAY_FILE, [this]() {
|
||||
window, {revX[0], rowY2, revW, REV_BTN_H}, STR_PLAY_FILE, [this]() {
|
||||
onPlayPressed();
|
||||
return 0;
|
||||
});
|
||||
playButton->setWrap();
|
||||
|
||||
saveButton = new TextButton(
|
||||
window, {PAD_LARGE * 2 + revW, btnY, revW, MIC_BTN_H}, STR_SAVE_AS,
|
||||
[this]() {
|
||||
window, {revX[1], rowY2, revW, REV_BTN_H}, STR_SAVE_AS, [this]() {
|
||||
askSaveAs();
|
||||
return 0;
|
||||
});
|
||||
saveButton->setWrap();
|
||||
|
||||
redoButton = new TextButton(
|
||||
window, {PAD_LARGE * 3 + revW * 2, btnY, revW, MIC_BTN_H}, STR_RECORD,
|
||||
[this]() {
|
||||
window, {revX[2], rowY2, revW, REV_BTN_H}, STR_RECORD, [this]() {
|
||||
discardTake();
|
||||
enterCountdown();
|
||||
return 0;
|
||||
});
|
||||
redoButton->setWrap();
|
||||
|
||||
playButton->hide();
|
||||
saveButton->hide();
|
||||
redoButton->hide();
|
||||
applyButton = new TextButton(
|
||||
window, {revX[0], rowY2, revW, REV_BTN_H}, STR_OK, [this]() {
|
||||
onApplyTrim();
|
||||
return 0;
|
||||
});
|
||||
|
||||
cancelButton = new TextButton(
|
||||
window, {revX[1], rowY2, revW, REV_BTN_H}, STR_CANCEL, [this]() {
|
||||
exitTrim();
|
||||
return 0;
|
||||
});
|
||||
|
||||
updateButtons();
|
||||
built = true;
|
||||
}
|
||||
|
||||
void RadioMicRecorder::showReviewButtons(bool reviewing)
|
||||
// The marker runs 0..1000 over the take so its range never has to change as
|
||||
// the take gets shorter.
|
||||
uint32_t RadioMicRecorder::trimSample() const
|
||||
{
|
||||
actionButton->show(!reviewing);
|
||||
playButton->show(reviewing);
|
||||
saveButton->show(reviewing);
|
||||
redoButton->show(reviewing);
|
||||
lv_group_focus_obj(reviewing ? playButton->getLvObj()
|
||||
: actionButton->getLvObj());
|
||||
if (takeSamples == 0) return 0;
|
||||
return (uint32_t)(((uint64_t)trimPermille * (takeSamples - 1)) / 1000U);
|
||||
}
|
||||
|
||||
void RadioMicRecorder::enterTrim(TrimMode mode)
|
||||
{
|
||||
if (isPlayingTake()) audioQueue.stopAll();
|
||||
if (takeSamples < 2) return;
|
||||
|
||||
trimMode = mode;
|
||||
trimPermille = (mode == TrimMode::START) ? 0 : 1000;
|
||||
waveform->setCursor(-1);
|
||||
waveform->showTrim(trimPermille, mode == TrimMode::START);
|
||||
updateButtons();
|
||||
refreshUI();
|
||||
}
|
||||
|
||||
void RadioMicRecorder::exitTrim()
|
||||
{
|
||||
trimMode = TrimMode::NONE;
|
||||
waveform->hideTrim();
|
||||
waveform->setCursor(-1);
|
||||
updateButtons();
|
||||
refreshUI();
|
||||
}
|
||||
|
||||
void RadioMicRecorder::onTrimMoved(int value)
|
||||
{
|
||||
trimPermille = value;
|
||||
refreshUI();
|
||||
}
|
||||
|
||||
void RadioMicRecorder::onApplyTrim()
|
||||
{
|
||||
const uint32_t cut = trimSample();
|
||||
const TrimMode mode = trimMode;
|
||||
|
||||
exitTrim();
|
||||
|
||||
if (mode == TrimMode::START) {
|
||||
applyTrim(cut, takeSamples - 1);
|
||||
} else {
|
||||
applyTrim(0, cut);
|
||||
}
|
||||
}
|
||||
|
||||
// Trims are sample surgery on the finished file: cut, refade the new edge,
|
||||
// refresh the trace. Playback stops because the length just changed.
|
||||
void RadioMicRecorder::applyTrim(uint32_t from, uint32_t to)
|
||||
{
|
||||
if (isPlayingTake()) audioQueue.stopAll();
|
||||
waveform->hideTrim();
|
||||
waveform->setCursor(-1);
|
||||
|
||||
const uint16_t cols = waveform->columns();
|
||||
uint8_t env[WaveformView::MAX_COLS];
|
||||
if (PdmWavRecorder::cut(filename, from, to, env, cols, &takeSamples) == FR_OK)
|
||||
waveform->setEnvelope(env, cols);
|
||||
|
||||
playingShown = false;
|
||||
refreshUI();
|
||||
}
|
||||
|
||||
void RadioMicRecorder::onAutoTrim()
|
||||
{
|
||||
uint32_t from = 0, to = 0;
|
||||
if (PdmWavRecorder::silenceBounds(filename, &from, &to) == FR_OK)
|
||||
applyTrim(from, to);
|
||||
}
|
||||
|
||||
void RadioMicRecorder::updateButtons()
|
||||
{
|
||||
const bool review = (state == State::REVIEW);
|
||||
const bool trimming = review && trimMode != TrimMode::NONE;
|
||||
|
||||
actionButton->show(!review);
|
||||
autoTrimButton->show(review && !trimming);
|
||||
trimStartButton->show(review && !trimming);
|
||||
trimEndButton->show(review && !trimming);
|
||||
playButton->show(review && !trimming);
|
||||
saveButton->show(review && !trimming);
|
||||
redoButton->show(review && !trimming);
|
||||
applyButton->show(trimming);
|
||||
cancelButton->show(trimming);
|
||||
|
||||
if (!built) return;
|
||||
// While trimming the marker itself holds the focus.
|
||||
if (trimming)
|
||||
return;
|
||||
else if (review)
|
||||
lv_group_focus_obj(playButton->getLvObj());
|
||||
else
|
||||
lv_group_focus_obj(actionButton->getLvObj());
|
||||
}
|
||||
|
||||
bool RadioMicRecorder::isPlayingTake() const
|
||||
@@ -354,9 +666,11 @@ void RadioMicRecorder::onActionPressed()
|
||||
void RadioMicRecorder::enterIdle()
|
||||
{
|
||||
if (recorder.isRecording()) recorder.stop();
|
||||
showReviewButtons(false);
|
||||
waveform->hide();
|
||||
state = State::IDLE;
|
||||
trimMode = TrimMode::NONE;
|
||||
waveform->hideTrim();
|
||||
updateButtons();
|
||||
waveform->hide();
|
||||
stateStart = get_tmr10ms();
|
||||
refreshUI();
|
||||
}
|
||||
@@ -364,9 +678,11 @@ void RadioMicRecorder::enterIdle()
|
||||
void RadioMicRecorder::enterCountdown()
|
||||
{
|
||||
if (isPlayingTake()) audioQueue.stopAll();
|
||||
showReviewButtons(false);
|
||||
waveform->hide();
|
||||
state = State::COUNTDOWN;
|
||||
trimMode = TrimMode::NONE;
|
||||
waveform->hideTrim();
|
||||
updateButtons();
|
||||
waveform->hide();
|
||||
stateStart = get_tmr10ms();
|
||||
refreshUI();
|
||||
}
|
||||
@@ -402,7 +718,9 @@ void RadioMicRecorder::stopRecording()
|
||||
uint32_t clipped = 0;
|
||||
const uint16_t cols = waveform->columns();
|
||||
uint8_t env[WaveformView::MAX_COLS];
|
||||
if (PdmWavRecorder::finalise(filename, env, cols, &takeSamples, &clipped) == FR_OK)
|
||||
// Silence is left in place; Auto-trim is the deliberate action.
|
||||
if (PdmWavRecorder::finalise(filename, env, cols, false, &takeSamples,
|
||||
&clipped) == FR_OK)
|
||||
waveform->setEnvelope(env, cols);
|
||||
TRACE("mic: %u/1000 samples clipped at capture", (unsigned)clipped);
|
||||
|
||||
@@ -412,9 +730,11 @@ void RadioMicRecorder::stopRecording()
|
||||
void RadioMicRecorder::enterReview()
|
||||
{
|
||||
playingShown = false;
|
||||
showReviewButtons(true);
|
||||
waveform->show();
|
||||
state = State::REVIEW;
|
||||
trimMode = TrimMode::NONE;
|
||||
waveform->hideTrim();
|
||||
updateButtons();
|
||||
waveform->show();
|
||||
stateStart = get_tmr10ms();
|
||||
refreshUI();
|
||||
}
|
||||
@@ -509,6 +829,21 @@ void RadioMicRecorder::refreshUI()
|
||||
}
|
||||
|
||||
case State::REVIEW: {
|
||||
if (trimMode != TrimMode::NONE) {
|
||||
// Show what would be left, so the marker reads as a result not a cut.
|
||||
const uint32_t cut = trimSample();
|
||||
const uint32_t kept = (trimMode == TrimMode::START)
|
||||
? takeSamples - cut
|
||||
: cut + 1;
|
||||
const uint32_t k = kept / PdmWavRecorder::DST_RATE;
|
||||
snprintf(buf, sizeof(buf), "%02u:%02u", (unsigned)(k / 60U),
|
||||
(unsigned)(k % 60U));
|
||||
bigLabel->setText(buf);
|
||||
infoLabel->setText(trimMode == TrimMode::START ? STR_TRIM_START
|
||||
: STR_TRIM_END);
|
||||
break;
|
||||
}
|
||||
|
||||
const uint32_t s = takeSamples / PdmWavRecorder::DST_RATE;
|
||||
snprintf(buf, sizeof(buf), "%02u:%02u", (unsigned)(s / 60U),
|
||||
(unsigned)(s % 60U));
|
||||
|
||||
@@ -39,6 +39,7 @@ class RadioMicRecorder : public Page
|
||||
|
||||
protected:
|
||||
enum class State : uint8_t { IDLE, COUNTDOWN, RECORDING, REVIEW };
|
||||
enum class TrimMode : uint8_t { NONE, START, END };
|
||||
|
||||
static constexpr uint32_t COUNTDOWN_SECONDS = 5;
|
||||
static constexpr int PATH_MAX_LEN = sizeof(SOUNDS_PATH) + 14; // "/lang/rec_00.wav\0"
|
||||
@@ -61,6 +62,15 @@ class RadioMicRecorder : public Page
|
||||
TextButton* playButton = nullptr;
|
||||
TextButton* saveButton = nullptr;
|
||||
TextButton* redoButton = nullptr;
|
||||
TextButton* autoTrimButton = nullptr;
|
||||
TextButton* trimStartButton = nullptr;
|
||||
TextButton* trimEndButton = nullptr;
|
||||
TextButton* applyButton = nullptr;
|
||||
TextButton* cancelButton = nullptr;
|
||||
bool built = false;
|
||||
|
||||
TrimMode trimMode = TrimMode::NONE;
|
||||
int trimPermille = 0;
|
||||
WaveformView* waveform = nullptr;
|
||||
|
||||
void buildHeader(Window* window);
|
||||
@@ -70,6 +80,13 @@ class RadioMicRecorder : public Page
|
||||
|
||||
void onActionPressed();
|
||||
void onPlayPressed();
|
||||
void onAutoTrim();
|
||||
void enterTrim(TrimMode mode);
|
||||
void exitTrim();
|
||||
void onTrimMoved(int value);
|
||||
void onApplyTrim();
|
||||
void applyTrim(uint32_t from, uint32_t to);
|
||||
uint32_t trimSample() const;
|
||||
void enterIdle();
|
||||
void enterCountdown();
|
||||
void enterRecording();
|
||||
@@ -77,7 +94,7 @@ class RadioMicRecorder : public Page
|
||||
void stopRecording();
|
||||
void discardTake();
|
||||
void askSaveAs();
|
||||
void showReviewButtons(bool reviewing);
|
||||
void updateButtons();
|
||||
bool isPlayingTake() const;
|
||||
void processPendingRename();
|
||||
void applyRename();
|
||||
|
||||
Reference in New Issue
Block a user