Skip to content

(Preliminary) sample cursor (nt)#205

Draft
prayerie wants to merge 7 commits intoNitrousTracker:newfrom
prayerie:samplecursor-nt
Draft

(Preliminary) sample cursor (nt)#205
prayerie wants to merge 7 commits intoNitrousTracker:newfrom
prayerie:samplecursor-nt

Conversation

@prayerie
Copy link

@prayerie prayerie commented Feb 17, 2026

see: NitrousTracker/libntxm#34

Draws a sample cursor over the sample display whenever a note should be playing.

I haven't tried drawing with a sprite instead yet so I'm just leaving what I have so far and I'll update this pr

some notes:

i added an overlay widget show/hide callback to GUI, so we can avoid drawing the cursor when a messagebox appears over it (otherwise the sample display overlaps the messagebox). specifically this also allows us to erase cursors just before the messagebox draws (so we don't just see cursors frozen in time). This could also be used to solve #150 perhaps. Drawing with sprites instead would probably make this easier.

the sample display also has been changed to use a drawing request system, instead of direct calls to draw, as otherwise conflicting draws may happen during the same frame (e.g. making a selection whilst a cursor is moving). The sample display isn't continuously drawn if the song is stopped, only if it's showing cursors. Again using sprites would probably solve this issue (i did it in the way i did so the cursor could draw under the sampledisplay zoom buttons/loop handles but perhaps it isn't that big of a deal if it overlaps them)

the function calcCursor is quite verbose looking but covers various edge cases: the cursor reversing from a ping pong loop where the step size would put it past the end of the sample, a cursor within a loop region smaller than the step size itself, etc. The current implementation here does not have accuracy issues from my testing

The timer handler calls calcCursor directly instead of using ticks (i tried the latter but couldn't get it to be nearly as accurate lol), i suppose it is then plausible that under extremely high load the cursor will lag behind. I have tested this with an extremely performance intensive xm i made (16 channels, 255 bpm, 0 tmp, playing notes and calculating effects in every cell) and it doesn't seem to cause cursor drift issues, but worth noting

More notes are in review comments

Additionally if you like here are two xm files I made for testing cursor drift: the first one is a single sine wave sample of around 5s with two visible and audible blips, and the sample pingpong loops basically indefinitely (i.e. load the xm, press play, open the sample display, and see if the audible blips occur at the same time as the cursor passing over them). For me testing this drift didn't become noticeable until around 5 minutes.

https://github.com/prayerie/nitrictracker/raw/refs/heads/main/cursor_test_1.xm

The second test file is similar but it plays multiple notes simultaneously at different notes using forward instead of pingpong loop:

https://github.com/prayerie/nitrictracker/raw/refs/heads/main/cursor_test_2.xm

#define SCROLLPIXELS 25 // Scroll that many pixels when a scroll button is pressed
#define MIN_SCROLLTHINGY_WIDTH 15
#define DRAW_HEIGHT (height-SCROLLBUTTON_HEIGHT-2) // height of the visible window
#define DRAW_HEIGHT (height-SCROLLBUTTON_HEIGHT-3) // height of the visible window
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes an issue where the sample display draws over 1px of the horizontal scrollbar


void cursorTimerHandler(void)
{
sampledisplay->calcCursor();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

admittedly not much better than just doing this in vblank, although this results in less drift anyway. i tried calculating using ticks but couldn't get it as accurate


void startCursorTimer(void)
{
TIMER1_DATA = TIMER_FREQ_64(60); // Call handler every frame
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

per libnds timers.h timer1 doesn't seem to be used for anything else so i presume it's ok

overlayShortcuts = 0;
}

bool GUI::hasOverlayWidget(u8 screen)
Copy link
Author

@prayerie prayerie Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we use a sprite for the cursor we could do away with setOnOverlayChanged below as we wouldn't need to do a final draw to hide the cursor, but hasOverlayWidget would still be useful regardless (so we know when to hide it), in addition to potentially being useful for the lbpot being occluded by the typewriter thing


Sample *smp;

u8 smpidx, instidx;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needed so we only draw the relevant cursor. i considered doing this as a single u16 instsmp_idx ((instidx << 8) | smpidx)) but it seemed unnecessarily elaborate

continue;
}

if (looptype != NO_LOOP && looplen <= step && playpos > loopstart)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this section isn't technically necessary but is for the case where the length of the loop is less than the step size of the cursor (ie the cursor is 'trapped' in a really small loop), so it draws the cursor visibly between the two loop points, rather than being hidden by one of the loop handles

Comment on lines 311 to 314
u16 draw_cursor_at = sampleToPixel(playpos >> 32);

if (draw_cursor_at < width)
cursors_xpos[chn] = draw_cursor_at;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively this could have been done in draw, calculating the x position directly, admittedly cursors_xpos is a little redundant

@prayerie prayerie force-pushed the samplecursor-nt branch 2 times, most recently from 7d5c745 to b3a9271 Compare February 18, 2026 08:55
Draw the loop handles as sprites. This is still buggy (they stay there when you zoom in lol), plus need to look at the touch targets. Moved oamUpdate a bit further down to reduce input latency, etc. I'm not sure if the loop handle graphic should be an 8x8 png processed through grit (seems a bit excessive?) or just hardcoded
for(s32 i=1; i<s32(width-1); ++i)
{
bool draw_selection_here = (draw_selection && i >= selleft && i < selright);
bool draw_selection_here = (draw_selection && i >= selleft && i <= selright);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes selection not drawing on the final column of the sample display (i wonder if this was intentional? there was always 1px unselected even when you select all)

}
if( (loop_start_pos >= 0) && (loop_start_pos <= width-2) )
{
oamSetXY(&oamSub, 16, loop_start_pos-4, DRAW_HEIGHT+18);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo just directly update oamMemory since we are only changing the x pos

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant