Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions deploy.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# Build and deploy mod-host to the device.
# Syncs source, compiles on-device (aarch64), installs over the
# mod-host-pistomp deb's files, then restarts mod-host.
#
# This is a *development* shortcut: it overwrites files owned by the
# mod-host-pistomp package, so apt will consider them modified and the next
# `apt install --reinstall mod-host-pistomp` (or a version bump) reverts them.
# For a real release, land on master and bump the version in pi-gen-pistomp.
#
# Usage:
# ./deploy.sh
set -euo pipefail

HOST="${PISTOMP_HOST:-pistomp.local}"
USER="${PISTOMP_USER:-pistomp}"
TARGET="${USER}@${HOST}"
PREFIX="/usr"
REMOTE_SRC="/tmp/mod-host"

echo "==> Syncing source to ${TARGET}:${REMOTE_SRC}"
ssh "${TARGET}" "mkdir -p ${REMOTE_SRC}"
rsync -az --delete \
--exclude='.git' --exclude='*.o' --exclude='*.so' --exclude='*.dylib' \
--exclude='*.dSYM' --exclude='mod-host' --exclude='src/info.h' \
./ "${TARGET}:${REMOTE_SRC}/"

# Build deps. The device is a runtime image, so the -dev packages the deb's
# chroot has are not necessarily here. Missing libreadline-dev is a hard
# compile error; missing libfftw3-dev silently drops -DHAVE_FFTW335 and gives
# you a binary that differs from the shipped deb, which is worse.
echo "==> Checking build deps"
ssh "${TARGET}" "set -e; missing=; \
[ -e /usr/include/readline/readline.h ] || missing=\"\$missing libreadline-dev\"; \
pkg-config --atleast-version=3.3.5 fftw3 fftw3f 2>/dev/null || missing=\"\$missing libfftw3-dev\"; \
pkg-config --exists jack || missing=\"\$missing libjack-jackd2-dev\"; \
if [ -n \"\$missing\" ]; then \
echo \" installing:\$missing\"; \
sudo apt-get update -qq && sudo apt-get install -y -qq \$missing; \
else echo ' ok'; fi"

echo "==> Building on device"
ssh "${TARGET}" "set -e; \
pkg-config --atleast-version=1.9.0 jack || { \
echo 'ERROR: pkg-config cannot resolve jack >= 1.9.0 on device' >&2; \
echo \" Found: \$(pkg-config --modversion jack 2>&1)\" >&2; \
exit 1; }; \
make -C ${REMOTE_SRC} -j\$(nproc)"

ssh "${TARGET}" "nm -D --undefined-only ${REMOTE_SRC}/mod-host | grep -q jack_internal_client_load" \
|| { echo "ERROR: built binary lacks jack_internal_client_load (HAVE_JACK2 off)" >&2; exit 1; }

# Stop first: overwriting the running executable in place gives ETXTBSY.
# mod-ui has Requires=mod-host, so stopping mod-host stops mod-ui too (and
# mod-ala-pi-stomp, which mod-ui Wants) — and systemd does NOT bring dependents
# back when mod-host starts again. Starting mod-ui is what restores the whole
# stack: it pulls mod-host via Requires and pi-stomp via Wants.
echo "==> Stopping mod-host"
ssh "${TARGET}" "sudo systemctl stop mod-host"

echo "==> Installing to ${PREFIX}"
ssh "${TARGET}" "sudo make -C ${REMOTE_SRC} install PREFIX=${PREFIX}"

echo "==> Starting mod-host + mod-ui"
ssh "${TARGET}" "sudo systemctl start mod-ui"
ssh "${TARGET}" "systemctl is-active mod-host mod-ui mod-ala-pi-stomp || true"

echo "==> Done"
137 changes: 123 additions & 14 deletions src/effects.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,7 @@
#include <math.h>
#include <pthread.h>
#include <sys/stat.h>
#include <time.h>

#ifdef _WIN32
#include <direct.h>
Expand DownExpand Up@@ -197,6 +198,13 @@ typedef struct {
// transport defaults
#define TRANSPORT_TICKS_PER_BEAT 1920.0

// beat_sync flags. A bar heartbeat gives a beat_in_bar that a client can use
// for phase. A tempo change or a meter change does not. g_transport_reset
// makes the next JackTimebase calculate the timeline again from the absolute
// frame at the new tempo. The bar and the beat then change to new values.
#define BEAT_SYNC_FLAG_NEW_BAR 0x1
#define BEAT_SYNC_FLAG_TEMPO_CHANGED 0x2


/*
************************************************************************************************************************
Expand DownExpand Up@@ -281,6 +289,7 @@ enum PostPonedEventType {
POSTPONED_MIDI_PROGRAM_CHANGE,
POSTPONED_MIDI_MAP,
POSTPONED_TRANSPORT,
POSTPONED_BEAT_SYNC,
POSTPONED_JACK_MIDI_CONNECT,
POSTPONED_LOG_TRACE, // stack allocated, rt-safe
POSTPONED_LOG_MESSAGE, // heap allocated
Expand DownExpand Up@@ -604,6 +613,14 @@ typedef struct POSTPONED_TRANSPORT_EVENT_T {
float bpm;
} postponed_transport_event_t;

typedef struct POSTPONED_BEAT_SYNC_EVENT_T {
uint64_t t_us; // CLOCK_MONOTONIC microseconds at which this sample was taken ("now", not back-dated)
float bpm;
float bpb;
double beat_in_bar; // fractional beat position within the current bar at `t_us`, from JACK BBT
int32_t flags; // BEAT_SYNC_FLAG_*: the cause of this sample. A client does not calculate it.
} postponed_beat_sync_event_t;

typedef struct POSTPONED_JACK_MIDI_CONNECT_EVENT_T {
jack_port_id_t port;
} postponed_jack_midi_connect_event_t;
Expand All@@ -628,6 +645,7 @@ typedef struct POSTPONED_EVENT_T {
postponed_midi_program_change_event_t program_change;
postponed_midi_map_event_t midi_map;
postponed_transport_event_t transport;
postponed_beat_sync_event_t beat_sync;
postponed_jack_midi_connect_event_t jack_midi_connect;
postponed_log_trace_event_t log_trace;
postponed_log_message_event_t log_message;
Expand DownExpand Up@@ -725,6 +743,7 @@ static jack_port_t *g_audio_out2_port;
static jack_port_t *g_midi_in_port;
static jack_position_t g_jack_pos;
static bool g_jack_rolling;
static int32_t g_last_beat_sync_bar = -1;
static volatile double g_transport_bpb;
static volatile double g_transport_bpm;
static volatile bool g_transport_reset;
Expand DownExpand Up@@ -1336,6 +1355,24 @@ static void RunPostPonedEvents(int ignored_effect_id)
got_transport = true;
break;

case POSTPONED_BEAT_SYNC:
// A clock-sample: (t_us=now, bpm, bpb, beat_in_bar, flags).
// Consumers forward-extrapolate
// pos(t) = beat_in_bar + (t - t_us) * bpm / 60 from this; every
// sample fully replaces prior anchor state (not a delta), so a
// dropped/late one just means more extrapolation, never a wrong
// lock. All delivered (not a state to dedup). The phase is
// correct only if NEW_BAR is set. Refer to the BEAT_SYNC_FLAG_*
// notes.
snprintf(buf, FEEDBACK_BUF_SIZE, "beat_sync %llu %f %f %f %d",
(unsigned long long)eventptr->event.beat_sync.t_us,
eventptr->event.beat_sync.bpm,
eventptr->event.beat_sync.bpb,
eventptr->event.beat_sync.beat_in_bar,
eventptr->event.beat_sync.flags);
socket_send_feedback_debug(buf);
break;

case POSTPONED_JACK_MIDI_CONNECT:
if (g_jack_global_client != NULL) {
const jack_port_id_t port_id = eventptr->event.jack_midi_connect.port;
Expand DownExpand Up@@ -2472,32 +2509,104 @@ static bool UpdateGlobalJackPosition(enum UpdatePositionFlag flag, bool do_post)
g_jack_pos.beats_per_minute = g_transport_bpm;
}

// Post a beat_sync clock-sample so external clients (pi-Stomp) can drive a
// metronome/count-in LED without running their own JACK client. This is a
// *sample of the transport clock* (t_us=now, bpm, bpb, beat_in_bar), not a
// back-dated "downbeat event" — the consumer forward-extrapolates
// pos(t) = beat_in_bar + (t - t_us) * bpm/60, so correctness never depends
// on cadence (the absolute bar count is DAW-context, not needed here).
// Emitted on two triggers:
// - a new bar (heartbeat; also what re-anchors after a stop/start)
// - a discrete bpm/bpb change while rolling (so a tap-tempo/CC-driven
// change re-anchors the pi the same process cycle, not ~1 bar later)
// A dropped/late sample just means more extrapolation, never a wrong lock.
bool posted = false;

if (g_jack_rolling && (g_jack_pos.valid & JackPositionBBT) != 0x0 && g_jack_pos.beats_per_minute > 0.0)
{
const int32_t bar = (int32_t)g_jack_pos.bar - 1;
const bool new_bar = (bar != g_last_beat_sync_bar);
// UPDATE_POSITION_FORCED means that a caller has just set bpm or bpb.
// Those callers write the global before they call, thus old_bpm and
// old_bpb already hold the new value and can not show the change.
const bool bpm_or_bpb_changed = (flag == UPDATE_POSITION_FORCED) ||
((flag == UPDATE_POSITION_IF_CHANGED) &&
(doubles_differ_enough(old_bpb, g_transport_bpb) ||
doubles_differ_enough(old_bpm, g_transport_bpm)));

if (new_bar || bpm_or_bpb_changed)
{
g_last_beat_sync_bar = bar;

const double tick = (g_jack_pos.valid & JackTickDouble) ? g_jack_pos.tick_double : g_jack_pos.tick;
const double beat_in_bar = (g_jack_pos.beat - 1) + (tick / g_jack_pos.ticks_per_beat);

struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
const uint64_t now_us = (uint64_t)now.tv_sec * 1000000ULL + (uint64_t)now.tv_nsec / 1000ULL;

postponed_event_list_data* const posteventptr = rtsafe_memory_pool_allocate_atomic(g_rtsafe_mem_pool);

if (posteventptr)
{
posteventptr->event.type = POSTPONED_BEAT_SYNC;
posteventptr->event.beat_sync.t_us = now_us;
posteventptr->event.beat_sync.bpm = g_transport_bpm;
posteventptr->event.beat_sync.bpb = g_transport_bpb;
posteventptr->event.beat_sync.beat_in_bar = beat_in_bar;
posteventptr->event.beat_sync.flags = (new_bar ? BEAT_SYNC_FLAG_NEW_BAR : 0) |
(bpm_or_bpb_changed ? BEAT_SYNC_FLAG_TEMPO_CHANGED : 0);

pthread_mutex_lock(&g_rtsafe_mutex);
list_add_tail(&posteventptr->siblings, &g_rtsafe_list);
pthread_mutex_unlock(&g_rtsafe_mutex);

posted = true;
}
}
}
else
{
// stopped or no valid BBT: forget the last bar so a fresh roll re-anchors immediately
g_last_beat_sync_bar = -1;
}

if (flag == UPDATE_POSITION_SKIP)
return false;
{
if (posted && do_post)
sem_post(&g_postevents_semaphore);
return posted;
}
if (flag == UPDATE_POSITION_IF_CHANGED &&
old_rolling == g_jack_rolling &&
!doubles_differ_enough(old_bpb, g_transport_bpb) &&
!doubles_differ_enough(old_bpm, g_transport_bpm))
return false;
{
if (posted && do_post)
sem_post(&g_postevents_semaphore);
return posted;
}

postponed_event_list_data* const posteventptr = rtsafe_memory_pool_allocate_atomic(g_rtsafe_mem_pool);

if (!posteventptr)
return false;
if (posteventptr)
{
posteventptr->event.type = POSTPONED_TRANSPORT;
posteventptr->event.transport.rolling = g_jack_rolling;
posteventptr->event.transport.bpb = g_transport_bpb;
posteventptr->event.transport.bpm = g_transport_bpm;

posteventptr->event.type = POSTPONED_TRANSPORT;
posteventptr->event.transport.rolling = g_jack_rolling;
posteventptr->event.transport.bpb = g_transport_bpb;
posteventptr->event.transport.bpm = g_transport_bpm;
pthread_mutex_lock(&g_rtsafe_mutex);
list_add_tail(&posteventptr->siblings, &g_rtsafe_list);
pthread_mutex_unlock(&g_rtsafe_mutex);

pthread_mutex_lock(&g_rtsafe_mutex);
list_add_tail(&posteventptr->siblings, &g_rtsafe_list);
pthread_mutex_unlock(&g_rtsafe_mutex);
posted = true;
}

if (do_post)
if (posted && do_post)
sem_post(&g_postevents_semaphore);

return true;
return posted;
}

static int ProcessGlobalClient(jack_nframes_t nframes, void *arg)
Expand DownExpand Up@@ -6888,7 +6997,7 @@ int effects_set_beats_per_minute(double bpm)
g_transport_bpm = bpm;
g_transport_reset = true;
TriggerJackTimebase(false);
UpdateGlobalJackPosition(UPDATE_POSITION_FORCED, false);
UpdateGlobalJackPosition(UPDATE_POSITION_FORCED, true);
} else {
result = ERR_JACK_VALUE_OUT_OF_RANGE;
}
Expand Down