feat(transport): set project tempo by tapping a pulse (phase 97)
This commit is contained in:
parent
7ad85b6144
commit
b7a4a1acd4
9 changed files with 92 additions and 5 deletions
2
Makefile
2
Makefile
|
|
@ -27,7 +27,7 @@ CXXFLAGS ?= -O2 -g
|
|||
CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude
|
||||
LDFLAGS ?=
|
||||
|
||||
CORE_SOURCES := src/project/chain_tools.c src/project/chain_preset.c src/mixer/track_fx.c src/audio/sample.c src/persistence/collect.c src/audio/normalize.c src/project/sections.c src/project/editing.c src/audio/export.c src/plugin/builtin.c src/settings/settings.c src/control/model.c src/audio/waveform.c src/plugin/ladspa.c src/plugin/host.c src/plugin/modern.c src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
|
||||
CORE_SOURCES := src/control/tempo.c src/project/chain_tools.c src/project/chain_preset.c src/mixer/track_fx.c src/audio/sample.c src/persistence/collect.c src/audio/normalize.c src/project/sections.c src/project/editing.c src/audio/export.c src/plugin/builtin.c src/settings/settings.c src/control/model.c src/audio/waveform.c src/plugin/ladspa.c src/plugin/host.c src/plugin/modern.c src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
|
||||
JACK_AVAILABLE := $(shell $(PKG_CONFIG) --exists jack 2>/dev/null && echo yes)
|
||||
ifeq ($(JACK_AVAILABLE),yes)
|
||||
JACK_CFLAGS := $(shell $(PKG_CONFIG) --cflags jack)
|
||||
|
|
|
|||
|
|
@ -61,3 +61,13 @@ fader and before its bus, including the effects' output. Green bars release at
|
|||
it. Stopped, offline or missing-track readings show the -90 dBFS floor. Changing
|
||||
tracks clears the indicator. These are sample peaks, not true-peak or loudness
|
||||
measurements. The display shares the mixer's existing backend snapshot read.
|
||||
|
||||
## Tap tempo
|
||||
|
||||
Tap the transport **Tap** button at a quarter-note pulse. The estimator averages
|
||||
the last four valid intervals and accepts 20–300 BPM. The first tap primes it;
|
||||
a pause longer than three seconds restarts it. Too-fast or backward timestamps
|
||||
are ignored. Each accepted tempo is an undoable edit and stops playback before
|
||||
preparation; tapping is unavailable while recording. New/Open resets tap history.
|
||||
The estimator uses monotonic control-thread time, with no audio-callback clock
|
||||
access or allocation. Tap timing is not a beat detector or MIDI clock sync.
|
||||
|
|
|
|||
|
|
@ -2163,12 +2163,12 @@ Implemented: UI-only stereo peak display in the insert workspace shares the exis
|
|||
Provide a transport Tap button backed by a bounded C99 rolling interval estimator, with timeout/reset, sensible tempo limits and undoable project tempo adoption.
|
||||
|
||||
### Tasks and acceptance criteria
|
||||
- [ ] Implement the workflow with bounded validation, safe preparation and documented limits.
|
||||
- [ ] Pass meaningful core and/or GUI checks, including error and undo behavior.
|
||||
- [ ] Document results and commit this phase separately.
|
||||
- [x] Implement the workflow with bounded validation, safe preparation and documented limits.
|
||||
- [x] Pass meaningful core and/or GUI checks, including error and undo behavior.
|
||||
- [x] Document results and commit this phase separately.
|
||||
|
||||
### Implementation status
|
||||
Planned.
|
||||
Implemented: transport Tap uses a bounded C99 four-interval estimator, 20–300 BPM, monotonic timestamps and a three-second restart gap. Invalid/fast/backward taps preserve estimator/output; accepted tempo edits use normal prepared project adoption and undo. Core rolling-average/timeout/invalid tests and GUI tempo/undo/New reset checks passed with a warning-enabled build.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
18
include/xtc/xtc_tempo.h
Normal file
18
include/xtc/xtc_tempo.h
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#ifndef XTC_TEMPO_H
|
||||
#define XTC_TEMPO_H
|
||||
#include "xtc_types.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
/* Control-thread estimator, four rolling intervals, 20..300 BPM. Zero initialize
|
||||
* or reset before use. Caller supplies monotonic seconds; no clock or I/O here. */
|
||||
typedef struct {double last,intervals[4];unsigned int count,head,started;} xtc_tap_tempo;
|
||||
void xtc_tap_tempo_reset(xtc_tap_tempo *state);
|
||||
/* First tap or gap >3s primes/restarts and returns NOT_FOUND without changing
|
||||
* out. Invalid arguments/too-fast/backward timestamps preserve state and out.
|
||||
* Success returns rounded milli-BPM and updates state. */
|
||||
xtc_result xtc_tap_tempo_push(xtc_tap_tempo *state,double seconds,unsigned int *out);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
23
src/control/tempo.c
Normal file
23
src/control/tempo.c
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#include "xtc/xtc_tempo.h"
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
void xtc_tap_tempo_reset(xtc_tap_tempo *s){if(s)memset(s,0,sizeof(*s));}
|
||||
xtc_result xtc_tap_tempo_push(xtc_tap_tempo *s,double seconds,unsigned int *out)
|
||||
{
|
||||
xtc_tap_tempo copy;double interval,sum=0;unsigned int i,tempo;
|
||||
if(!s || !out || !isfinite(seconds) || seconds<0 || s->started>1 || s->count>4 || s->head>=4 ||
|
||||
(s->count<4 && s->head!=s->count) || (!s->started && s->count) ||
|
||||
(s->started && (!isfinite(s->last) || s->last<0)))return XTC_ERR_INVALID_ARGUMENT;
|
||||
for(i=0;i<s->count;++i)if(!isfinite(s->intervals[i]) || s->intervals[i]<.2 || s->intervals[i]>3)return XTC_ERR_INVALID_ARGUMENT;
|
||||
if(!s->started || seconds-s->last>3) {
|
||||
xtc_tap_tempo_reset(s);s->started=1;s->last=seconds;return XTC_ERR_NOT_FOUND;
|
||||
}
|
||||
interval=seconds-s->last;
|
||||
if(interval<.2)return XTC_ERR_INVALID_ARGUMENT;
|
||||
copy=*s;copy.last=seconds;copy.intervals[copy.head]=interval;copy.head=(copy.head+1)%4;
|
||||
if(copy.count<4)++copy.count;
|
||||
for(i=0;i<copy.count;++i)sum+=copy.intervals[i];
|
||||
tempo=(unsigned int)lround(60000.0*copy.count/sum);
|
||||
if(tempo<20000 || tempo>300000)return XTC_ERR_INVALID_ARGUMENT;
|
||||
*s=copy;*out=tempo;return XTC_OK;
|
||||
}
|
||||
|
|
@ -36,6 +36,7 @@
|
|||
#include <cmath>
|
||||
#include <cerrno>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
|
||||
namespace {
|
||||
class PanelWindow : public Fl_Double_Window {
|
||||
|
|
@ -266,6 +267,9 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
|
|||
auto *fit = new Fl_Button(790, 82, 100, 28, "Fit song");
|
||||
fit->tooltip("Choose the largest supported zoom that fits the song (Ctrl+0)");
|
||||
fit->callback([](Fl_Widget *, void *c) { static_cast<MainWindow *>(c)->fit_song(); }, this);
|
||||
auto *tap=new Fl_Button(905,82,80,28,"Tap");
|
||||
tap->tooltip("Tap tempo: averages up to four intervals, 20–300 BPM; pauses over three seconds restart.");
|
||||
tap->callback([](Fl_Widget *,void *c){auto *w=static_cast<MainWindow *>(c);double now=std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch()).count();auto r=w->tap_tempo(now);if(r!=XTC_OK && r!=XTC_ERR_NOT_FOUND && r!=XTC_ERR_INVALID_ARGUMENT)w->report_error("Tap tempo unchanged",r);},this);
|
||||
loop_button_ = new Fl_Check_Button(1000, 82, 100, 28, "Loop");
|
||||
loop_button_->callback([](Fl_Widget *widget,void *c){auto *w=static_cast<MainWindow *>(c);
|
||||
xtc_result r=w->set_loop(static_cast<uint64_t>(w->settings_.loop_start),static_cast<uint64_t>(w->settings_.loop_end),static_cast<Fl_Check_Button *>(widget)->value()!=0);
|
||||
|
|
@ -1409,6 +1413,7 @@ xtc_result MainWindow::install_project(xtc_project *candidate, bool reset_histor
|
|||
history_.swap(new_history);
|
||||
for(auto &snapshot:mix_snapshots_)snapshot.reset();
|
||||
if(track_inserts_)track_inserts_->clear_session();
|
||||
xtc_tap_tempo_reset(&tap_state_);
|
||||
}
|
||||
graph_widget_->reset_layout();
|
||||
graph_widget_->set_graph(xtc_project_graph(candidate));
|
||||
|
|
@ -2750,3 +2755,12 @@ xtc_result MainWindow::open_track_inserts(uint64_t track)
|
|||
}
|
||||
return XTC_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
xtc_result MainWindow::tap_tempo(double seconds)
|
||||
{
|
||||
if(recorder_)return XTC_ERR_INVALID_STATE;
|
||||
auto state=tap_state_;unsigned int tempo=0;auto r=xtc_tap_tempo_push(&state,seconds,&tempo);
|
||||
if(r==XTC_ERR_NOT_FOUND){tap_state_=state;return r;}
|
||||
if(r!=XTC_OK)return r;
|
||||
r=set_tempo(tempo);if(r==XTC_OK)tap_state_=state;return r;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include "xtc/xtc_backend.h"
|
||||
#include "xtc/xtc_mixer.h"
|
||||
#include "xtc/xtc_plugin.h"
|
||||
#include "xtc/xtc_tempo.h"
|
||||
#include "xtc/xtc_control.h"
|
||||
#include "xtc/xtc_settings.h"
|
||||
|
||||
|
|
@ -42,6 +43,7 @@ class MainWindow : public Fl_Double_Window {
|
|||
* the current document. No engine/model changes happen in audio callbacks. */
|
||||
xtc_result create_track(bool midi);
|
||||
xtc_result set_tempo(unsigned int milli_bpm);
|
||||
xtc_result tap_tempo(double seconds);
|
||||
xtc_result collect_project(const char *directory);
|
||||
xtc_result navigate_marker(int direction);
|
||||
xtc_result loop_section();
|
||||
|
|
@ -224,6 +226,7 @@ class MainWindow : public Fl_Double_Window {
|
|||
|
||||
private:
|
||||
friend class TrackInsertsPanel;
|
||||
xtc_tap_tempo tap_state_{};
|
||||
TrackInsertsPanel *track_inserts_=nullptr;
|
||||
using MixSnapshot=std::unique_ptr<xtc_mix_snapshot,decltype(&xtc_mix_snapshot_destroy)>;
|
||||
MixSnapshot mix_snapshots_[2]{{nullptr,xtc_mix_snapshot_destroy},{nullptr,xtc_mix_snapshot_destroy}};
|
||||
|
|
|
|||
|
|
@ -263,6 +263,9 @@ int creative_workflow_tests()
|
|||
panel->meter()->do_callback();CHECK(!panel->meter()->clipped());
|
||||
panel->select_track(first_track);CHECK(panel->meter()->left_db()==-90 && !panel->meter()->clipped());
|
||||
CHECK(window.new_project()==XTC_OK && !panel->clipboard_ready() && panel->paste_chain()==XTC_ERR_NOT_FOUND);
|
||||
CHECK(find(&window,"Tap"));CHECK(window.tap_tempo(0)==XTC_ERR_NOT_FOUND && window.tap_tempo(.4)==XTC_OK);
|
||||
CHECK(xtc_project_tempo(s->project())==150000 && window.undo()==XTC_OK && xtc_project_tempo(s->project())==120000);
|
||||
CHECK(window.new_project()==XTC_OK && window.tap_tempo(.9)==XTC_ERR_NOT_FOUND);
|
||||
CHECK(!panel->comparison_ready(0) && !panel->comparison_ready(1));
|
||||
window.do_callback();puts("Creative workflow GUI checks passed.");return failures ? 1 : 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,26 @@
|
|||
#include "xtc/xtc_plugin.h"
|
||||
#include "xtc/xtc_tempo.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
static void tap_tests(void)
|
||||
{
|
||||
xtc_tap_tempo s={0},before;unsigned int tempo=999;
|
||||
assert(xtc_tap_tempo_push(&s,0,&tempo)==XTC_ERR_NOT_FOUND && tempo==999);
|
||||
assert(xtc_tap_tempo_push(&s,.5,&tempo)==XTC_OK && tempo==120000);
|
||||
before=s;assert(xtc_tap_tempo_push(&s,.55,&tempo)==XTC_ERR_INVALID_ARGUMENT && !memcmp(&s,&before,sizeof(s)) && tempo==120000);
|
||||
assert(xtc_tap_tempo_push(&s,NAN,&tempo)==XTC_ERR_INVALID_ARGUMENT && !memcmp(&s,&before,sizeof(s)));
|
||||
assert(xtc_tap_tempo_push(&s,.4,&tempo)==XTC_ERR_INVALID_ARGUMENT && !memcmp(&s,&before,sizeof(s)));
|
||||
assert(xtc_tap_tempo_push(&s,1,&tempo)==XTC_OK && xtc_tap_tempo_push(&s,1.5,&tempo)==XTC_OK && xtc_tap_tempo_push(&s,2,&tempo)==XTC_OK);
|
||||
assert(xtc_tap_tempo_push(&s,2.25,&tempo)==XTC_OK && tempo==137143);
|
||||
assert(xtc_tap_tempo_push(&s,10,&tempo)==XTC_ERR_NOT_FOUND && tempo==137143 && !s.count);
|
||||
assert(xtc_tap_tempo_push(&s,10.25,&tempo)==XTC_OK && tempo==240000);
|
||||
xtc_tap_tempo_reset(&s);assert(!s.started && !s.count);
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
tap_tests();
|
||||
xtc_track_chain c={0},out={0};
|
||||
c.output_gain=.8f;c.count=1;c.slots[0].enabled=1;c.slots[0].wet=.4f;
|
||||
assert(xtc_plugin_probe_format(XTC_PLUGIN_BUILTIN,XTC_BUILTIN_PATH,2,48000,&c.slots[0].plugin)==XTC_OK);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue