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
1 change: 1 addition & 0 deletions changelog/2026-11-02-octatrack-sample-editor-snap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Octatrack beta base adds optional zero-crossing SNAP alignment to confirmed slice-grid creation and replacement. FUNC+AED cycles from stock SLICE GRID through registered sample-editor actions and back; existing ABI-1 clients keep working without the optional API tail.
Binary file not shown.
67 changes: 67 additions & 0 deletions sdk/machines/octatrack/SAMPLE_EDITOR.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,70 @@ flashed base and saved sixteen-module set after this new run needs a user
power cycle; no post-test power-cycle result is claimed here. The earlier
power-off/interrupted-run outcomes and later event-36 build qualification
remain separate.

## Confirmed grid actions and SNAP alignment

The beta base exposes an optional, size-checked ABI-1 API tail for SNAP
alignment. Existing ABI-1 clients retain the original prefix and `replace`
behavior. SNAP copies a normal grid proposal and uses the native
CREATE/REPLACE confirmation; PCM analysis starts only after YES. The base reads
bounded native sample-frame pairs on engine work, using the opening's
LEFT/RIGHT/BOTH selection, and publishes the complete table only after the
source and owner still match. Cancelled, stale, failed or over-limit work leaves
the old marker table intact. No module PCM pointer or callback crosses the API.

From stock SLICE GRID, FUNC+AED selects the first registered action. Further
FUNC+AED presses advance through actions in registration order; from the last
action the chord returns to stock SLICE GRID. With no registered action, the
chord remains stock. The base handles the chord only for that grid/action cycle
and leaves unrelated dialogs alone. An action joining the cycle must retire its
sample-editor token on FUNC/AED before passing the key through to stock, so the
base can safely advance or return to the grid. The base does not guess an
action's private modal state or dismiss it on the action's behalf.

Hardware evidence must name the exact complete base build. The 2 November MKII
SNAP result in the Beat Slicer draft covers one mono16 Static fixture on a
separately instrumented measurement base; it does not qualify this beta base.
See the draft's dated hardware record for exact boundaries, timing scope and
remaining cases.

## Exact beta-base whole-set trial — 3 November 2026

On the owner's Octatrack running OS 1.40C, the exact beta base RAM-booted from
this PR had identity `efab14e51c4c4a3816cc81f76a15b538940066ae1c4a35e763273abd8fe066d5`.
The frontend reviewed and trialled the observed 17-module set plus published
Beat Slicer 0.1.66 (`6650ef77a837a61a570a300c3bd0b2d4dfd4c9bed6e52bb355034d39bf5ff90d`,
5,780 bytes). The base self-test passed: 18/18 intact, seven DSP modules and
7/7 exercised, zero failures; peak CPU was 53.4%, track groups peaked at 67.0%
and 71.1%, and the longest frame gap was 417 microseconds.

The first whole-set rollback returned `needs-recovery`. Bounded retries kept the
same transaction in recovery; retry 17 returned `ok`. The base then confirmed
normal phase and the original `efab14e5…` active digest. A fresh frontend status
read independently confirmed the exact base active, an empty inventory and the
passing 18-module self-test record. The reason for the initial recovery refusal
is unknown.

No Keep, flash, project/sample save or power cycle occurred. The hardware panel
was not operated during this trial. 0.1.66 has no SNAP control, so this run
supplies no SNAP OFF/ON result. The separate panel trials below cover both
review questions.

## Exact beta-base panel trials — 4 November 2026

The same beta base `efab14e51c4c4a3816cc81f76a15b538940066ae1c4a35e763273abd8fe066d5`
was RAM-booted on the owner's OS 1.40C Octatrack. Before the boot, the flashed
base reported 17 live and saved modules. The owner confirmed stopped transport
and recording and a disposable project. Each trial contained Beat Slicer only.

- Published 0.1.66, package SHA-256 `6650ef77a837a61a570a300c3bd0b2d4dfd4c9bed6e52bb355034d39bf5ff90d`:
the owner reported that FUNC+AED while CREATE BEAT GRID was open brought up
stock CREATE SLICE GRID. Undo returned the beta base to its empty baseline.
- Private 0.1.68 candidate, package SHA-256 `195d2b46e2620bd15118478aa02d626d6d803692e02c40db4f45a67f6927b643`:
the owner reported creating a grid with SNAP OFF and another with SNAP ON,
both completing normally. Undo and a fresh status read returned the beta base
to its empty baseline.

These are owner-reported panel outcomes, not measured boundary or audio results.
No Keep, flash, project/sample save or power cycle occurred in these trials;
the temporary beta base was still running after Undo.
24 changes: 12 additions & 12 deletions sdk/machines/octatrack/base-binding.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
{
"schema": 1,
"identity": "72defb09b9d463fe62ab876f350f442d10e32ec23e55918a22b9b756c5cb3da6",
"arena": 1089508832,
"pool": 1087548496,
"identity": "8ff4b08a8244f7fb733ac10a0fa381a75cafccb64dca8bdeb2ada674c595307d",
"arena": 1089521120,
"pool": 1087554640,
"symbols": {
"modwerk_controller_write_begin": 1084872232,
"modwerk_controller_write_end": 1084872260,
"modwerk_cue_cfg_clamp": 1085077656,
"modwerk_raise_ev_mute_keep": 1085080010
"modwerk_cue_cfg_clamp": 1085083076,
"modwerk_raise_ev_mute_keep": 1085085430
},
"controllers": {
"sidechain-compressor": "178d200073896dd2416f07bb952f835087f9250778e773fc9426a5165db259ce",
"output-matrix": "17e9afe714d2707147804360bbf4e614240d9d5afc037df5fec0d2a1fc56c3e1",
"analog-bassdrum": "579b9b5e76b000a8b9c95f6131a547fbb5117f497d4bc3ee0f98385edc96e5a3"
},
"variants": {
"tracks-main-cue": "72defb09b9d463fe62ab876f350f442d10e32ec23e55918a22b9b756c5cb3da6",
"tracks": "6487294575677340a2d89d252a2f3359efa2a366a5b31f19ad7a6986fd816a71",
"tracks-post": "b52defaedf224357fc01aa2d4a77cb46d95bea6f932d5cbb09ea0a21506992b2",
"main-cue": "3c2a80b4b6d3deaa897e0400fb0f77e2018096d89816f89c6ff7ddbe3905cb46",
"main": "13885dd5b0f80b00596052706ff82308f705ab4f7997569103b2609be1381fa6",
"master": "a6ad81fbd2b41e5e7cbc77de55c5f4b86852689107f231e9bc9f1bcafacc1710",
"off": "8d297b113bd56ed30ba7438838ee8a9bab314232b0d1d4c4fbb3561ebeb2fca2"
"tracks-main-cue": "8ff4b08a8244f7fb733ac10a0fa381a75cafccb64dca8bdeb2ada674c595307d",
"tracks": "835f13224a3278299c78a9c4a80afff9ac61d3b1ec9f5e5465132ca74ebd8a71",
"tracks-post": "3e9d6aa5865f0c6fd760c7988f41af4b86bd6efe80646c2fdd2daeebb679673a",
"main-cue": "2aadebf506fb8006397d452b285785d2301b00119c4ddbd47b4b6507c3ae383e",
"main": "60ae4c6b02f30d9961e08acac887f90abb75403d470e93267bb4c3e85480dd6c",
"master": "6d28b15637edc3e5c0f3d1a24e5d5ab7fc91bc9e4d0ab3aac9356e902cf3b744",
"off": "48401d2c97109b73eab11ce38eedff761f7c1090bbe750b3cfb4b1732f3ab368"
}
}
6 changes: 4 additions & 2 deletions sdk/machines/octatrack/elekloader/build_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
'table_admission.c', 'table_admission.h', 'table_declarations.c', 'table_declarations.h',
'machine_contributors.c', 'machine_contributors.h',
'sample_editor_actions.c', 'sample_editor_actions.h')
SAMPLE_EDITOR_SOURCES = ('sample_editor_service.c', 'sample_editor_service.h',
SAMPLE_EDITOR_SOURCES = ('sample_editor_service.c', 'sample_editor_service.h', 'sample_editor_snap.h',
'sample_editor_board.c', 'sample_editor_board.h', 'sample_editor_menu.c', 'sample_editor_hooks.s')
RETIREMENT_SOURCES = tuple('analog-bd-parts/' + n for n in ('analog_bd_part_restore.c', 'analog_bd_part_restore.h',
'analog_bd_part_board.c', 'analog_bd_part_board.h'))
Expand Down Expand Up @@ -2151,7 +2151,9 @@ def resident_factory(tag, hi):
recipe['cflags'].append('-DMODWERK_SAMPLE_EDITOR')
recipe['sources'].append('sample_editor_hooks.s')
recipe['subscribe'] += [dict(event='ev_tick', fn='mw_sample_ui_tick', order=89),
dict(event='ev_key', fn='mw_sample_ui_key', order=87)]
dict(event='ev_key', fn='mw_sample_ui_key', order=87),
dict(event='ev_enc', fn='mw_sample_ui_enc', order=87),
dict(event='ev_key', fn='mw_sample_ui_shortcut_key', order=91)]
recipe['subscribe'].append(dict(event='ev_draw', fn='modwerk_boot_draw', order=10)) # boot_anim.c, while it runs
# The host time limit counts on the same tick (ep0.c).
recipe['subscribe'].append(dict(event='ev_tick', fn='modwerk_ep0_tick', order=91))
Expand Down
19 changes: 18 additions & 1 deletion sdk/machines/octatrack/include/modwerk_sample_editor.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#ifndef MODWERK_SAMPLE_EDITOR_H
#define MODWERK_SAMPLE_EDITOR_H
#include <stdint.h>
#include <stddef.h>
#define MODWERK_SAMPLE_EDITOR_ABI 1u
#define MODWERK_SAMPLE_EDITOR_SLICES 64u
#define MODWERK_SAMPLE_ACTION_BYTES 64u
Expand All @@ -15,7 +16,8 @@ enum modwerk_sample_reply {
MODWERK_SAMPLE_OK, MODWERK_SAMPLE_BUSY, MODWERK_SAMPLE_STALE,
MODWERK_SAMPLE_UNSUPPORTED, MODWERK_SAMPLE_INVALID,
MODWERK_SAMPLE_PRESERVATION, MODWERK_SAMPLE_CONFIRMING,
MODWERK_SAMPLE_APPLIED, MODWERK_SAMPLE_NO_CHANGE, MODWERK_SAMPLE_FAULT
MODWERK_SAMPLE_APPLIED, MODWERK_SAMPLE_NO_CHANGE, MODWERK_SAMPLE_FAULT,
MODWERK_SAMPLE_ANALYZING, MODWERK_SAMPLE_ANALYSIS_FAILED, MODWERK_SAMPLE_ANALYSIS_LIMIT
};
struct modwerk_slice_record { uint32_t start, end, loop; };
struct modwerk_sample_view {
Expand Down Expand Up @@ -49,6 +51,21 @@ struct modwerk_sample_editor_api {
* confirm() above offers preservation-only edits. */
unsigned (*replace)(uint32_t token, const struct modwerk_sample_grid *);
};
/* Keep the ABI-1 prefix type and sizeof unchanged for existing source builds. */
struct modwerk_sample_editor_options_api {
struct modwerk_sample_editor_api base;
/* Optional ABI-1 tail, available only when bytes covers this member.
* SNAP uses the opening's native waveform channel selection. This copies
* a replacement proposal and shows the same CREATE/REPLACE question.
* Only YES starts base-owned bounded analysis; no PCM/pointer/callback
* crosses the interface. A stale/cancelled/failed job publishes nothing.
* The final slice end may align outside trim, within actual sample bounds.
* No additional alignment question, implicit save or user Undo is added. */
unsigned (*replace_options)(uint32_t token,const struct modwerk_sample_grid *,uint32_t options);
} __attribute__((aligned(4)));
#define MODWERK_SAMPLE_EDITOR_V1_BYTES sizeof(struct modwerk_sample_editor_api)
#define MODWERK_SAMPLE_SNAP_ZERO 1u
#define MODWERK_SAMPLE_EDITOR_HAS_OPTIONS 1
/* One typed contribution to the proposed base-owned action table. The record
* is 64 bytes and four-byte aligned on ColdFire; label is inline, printable,
* NUL + zero padding. ColdFire C otherwise permits two-byte struct alignment.
Expand Down
69 changes: 65 additions & 4 deletions sdk/machines/octatrack/module-adapters/sample_editor_board.c
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* SPDX-License-Identifier: MIT
* Native 1.40C base adapter. The source build checks every owned seam against
* the private stock image. Enabled only by the private sample-editor option.
* Flex/Static and inactive recorder metadata only; no PCM access.
* Flex/Static and inactive recorder metadata; bounded native PCM pair reads
* run only on the engine for a confirmed SNAP request.
*/
#include "sample_editor_board.h"
#ifdef MW_SAMPLE_BOARD_HOST
Expand Down Expand Up @@ -32,6 +33,10 @@ static uint32_t idle_seen,job_active,refresh_pending,position_hint;
static uint32_t waveform_candidate,waveform_seen;
static const void *job_message;
static unsigned owner_hint;
/* Stock's unknown-command path drains this resident no-op record. The engine
* observer owns the work; there is no stock completion or package callback. */
static const unsigned char scan_message[40] __attribute__((aligned(4)))={255};
static uint32_t scan_queued,scan_running,scan_more;
struct mw_sample_editor mw_sample_editor;
/* File-backed settings are written by UI/engine/storage tasks, excluded by
* the level-4 lock. Recording into the edited buffer and audition are excluded.
Expand Down Expand Up @@ -116,7 +121,7 @@ static int source_stable(const struct mw_sample_capture *before,uint32_t setting
static int ready(void *unused)
{
(void)unused;
return audio_preemptible() && idle_seen && !job_active && !READ_PACKED(0x460d17d2u) && !READ(0x460e6bccu) &&
return audio_preemptible() && idle_seen && (!job_active || scan_running) && !READ_PACKED(0x460d17d2u) && !READ(0x460e6bccu) &&
READ(0x460d1cb8u)==1u && !READ(0x460e76a0u) &&
mw_sample_transport_ready() && mw_sample_storage_ready();
}
Expand All @@ -133,6 +138,8 @@ static unsigned capture(void *unused,struct mw_sample_capture *out)
uint32_t controls=(kind?0x46c922c4u:0x46c90a78u)+44u*slot;
uint32_t settings=(kind?0x100b14f0u:0x100d5b30u)+1096u*slot;
for(uint32_t i=0;i<11u;++i)out->controls[i]=READ(controls+4u*i);
uint32_t selected=READ(0x400ceb38u);
out->channels=selected<=2u?selected+1u:0u;
if(out->controls[0]>3u || out->controls[2] || (!recorder && !out->controls[4]) ||
out->controls[4]>0x7fffffffu || (!kind && out->controls[9]==UINT32_MAX))return MODWERK_SAMPLE_UNSUPPORTED;
struct modwerk_sample_view *v=&out->view;
Expand Down Expand Up @@ -220,7 +227,22 @@ static unsigned publish(void *unused,const struct mw_sample_capture *before,cons
return matches(settings,v->count,v->records) && READ(0x400ceb3cu)==selected?
(stale?MODWERK_SAMPLE_STALE:MODWERK_SAMPLE_NO_CHANGE):MODWERK_SAMPLE_FAULT;
}
static const struct mw_sample_backend backend={0,lock,unlock,ready,owner,capture,publish};
static int read_pair(void *unused,const struct mw_sample_capture *before,uint32_t position,
int32_t *left,int32_t *right)
{
(void)unused;
if(!scan_running || !audio_preemptible() || position>=before->view.frames ||
before->view.frames-position<2u)return 0;
#ifdef MW_SAMPLE_BOARD_HOST
int mw_sample_host_read_pair(const struct mw_sample_capture *,uint32_t,int32_t *,int32_t *);
return mw_sample_host_read_pair(before,position,left,right);
#else
typedef int (*reader)(uint32_t,uint32_t,uint32_t,int32_t *,int32_t *);
reader native=(reader)(before->view.kind?0x40095984u:0x40093064u);
return native(before->view.slot,position,2u,left,right)>=0;
#endif
}
static const struct mw_sample_backend backend={0,lock,unlock,ready,owner,capture,publish,read_pair};
void mw_sample_editor_bootstrap(void)
{
if(!mw_sample_editor.initialized)(void)mw_sample_editor_init(&mw_sample_editor,&backend);
Expand Down Expand Up @@ -262,9 +284,38 @@ static unsigned api_replace(uint32_t token,const struct modwerk_sample_grid *gri
unsigned why=buffer(grid,sizeof *grid,0)?mw_sample_editor_replace(&mw_sample_editor,token,grid):MODWERK_SAMPLE_INVALID;
modwerk_machine_unlock(saved);return why;
}
static unsigned api_replace_options(uint32_t token,const struct modwerk_sample_grid *grid,uint32_t options)
{
uint32_t saved=modwerk_machine_lock();
unsigned why=buffer(grid,sizeof *grid,0)?
mw_sample_editor_replace_options(&mw_sample_editor,token,grid,options):MODWERK_SAMPLE_INVALID;
modwerk_machine_unlock(saved);return why;
}
const struct modwerk_sample_editor_api mw_sample_editor_api={
MODWERK_SAMPLE_EDITOR_ABI,sizeof(struct modwerk_sample_editor_api),api_copy,api_confirm,api_cancel,api_replace
};
static const struct modwerk_sample_editor_options_api options_api={
{MODWERK_SAMPLE_EDITOR_ABI,sizeof(struct modwerk_sample_editor_options_api),
api_copy,api_confirm,api_cancel,api_replace},api_replace_options
};
const struct modwerk_sample_editor_api *mw_sample_editor_public_api(void)
{return &options_api.base;}
void mw_sample_analysis_queue(void)
{
/* Queue insertion is a short atomic decision, never PCM or table work.
* Do not wrap stock's unchecked ring on a full/unknown queue. */
uint32_t saved=modwerk_machine_mask();
if(mw_sample_editor.state==MW_SAMPLE_ANALYZE && !scan_queued && !job_active &&
!READ_PACKED(0x460d17d2u) && READ_PACKED(0x460d17deu)>=1u) {
scan_queued=1;
#ifdef MW_SAMPLE_BOARD_HOST
void mw_sample_host_post(const void *);mw_sample_host_post(scan_message);
#else
((void (*)(void *,const void *))0x40000c3cu)((void *)0x460d17ceu,scan_message);
#endif
}
modwerk_machine_unmask(saved);
}
void mw_sample_owner_changing(const struct runtime_dsp *from)
{
uint32_t saved=modwerk_machine_mask();
Expand All @@ -287,12 +338,19 @@ void mw_sample_engine_job(const void *message)
* Its byte alone never makes it read-only: the separately guarded native
* dispatcher branch must confirm what actually ran. A mutated/unknown
* dispatch retires the token at idle before access can reopen. */
if(!mw_sample_ep0_wake(message)) {
unsigned ours=message==scan_message && scan_queued;
if(ours){scan_queued=0;scan_running=1;}
else if(!mw_sample_ep0_wake(message)) {
if(message && *(const volatile uint8_t *)message==16u) {
waveform_candidate=1;job_message=message;
} else mw_sample_editor_changed_locked(&mw_sample_editor);
}
modwerk_machine_unmask(saved);
if(ours) {
unsigned why=mw_sample_editor_analyze_step(&mw_sample_editor);
scan_more=why==MODWERK_SAMPLE_ANALYZING;
scan_running=0;
}
}
void mw_sample_waveform_job(const void *message)
{
Expand All @@ -316,6 +374,9 @@ void mw_sample_engine_idle(void)
if(waveform_candidate && !waveform_seen)mw_sample_editor_changed_locked(&mw_sample_editor);
waveform_candidate=waveform_seen=0;job_message=0;job_active=0;idle_seen=1;
modwerk_machine_unmask(saved);
/* A transient BUSY pauses until the next UI tick. Never keep the engine
* spinning on no-op records while USB/storage owns the source boundary. */
if(scan_more){scan_more=0;mw_sample_analysis_queue();}
}
void mw_sample_refresh_ui(void)
{
Expand Down
4 changes: 4 additions & 0 deletions sdk/machines/octatrack/module-adapters/sample_editor_board.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "sample_editor_actions.h"
extern struct mw_sample_editor mw_sample_editor;
extern const struct modwerk_sample_editor_api mw_sample_editor_api;
const struct modwerk_sample_editor_api *mw_sample_editor_public_api(void);
void mw_sample_editor_bootstrap(void);
int mw_sample_owner_current_locked(const struct runtime_owner *);
void mw_sample_owner_changing(const struct runtime_dsp *);
Expand All @@ -14,8 +15,11 @@ void mw_sample_engine_job(const void *);
* from a reusable queue record's opcode. Access remains closed until idle. */
void mw_sample_waveform_job(const void *);
void mw_sample_refresh_ui(void);
void mw_sample_analysis_queue(void);
/* The UI task's base-owned menu/confirmation adapter. */
void mw_sample_ui_tick(void);
int mw_sample_ui_key(int,int);
int mw_sample_ui_enc(int,int);
int mw_sample_ui_shortcut_key(int,int);
void mw_sample_menu_retire_locked(const struct runtime_dsp *);
#endif
Loading
Loading