Aggravating. CC65 has similar forced diversions into 6502 ASM, but CC65 is a glorified assembler macro. (Which does make it lightning-fast.) Watcom is old enough that this was relevant and Open Watcom is active enough that bugs still get fixed, and here it is tripping over itself.
... nevermind, I solved it.
INT 21h will accept a normal function name in DX, for AX=0x251C (set interrupt vector 0x1C). The function will crash. But you can set it, and it will run. Once. Even _asm{ iret } won't return properly. The problem is, interrupts only push / pop the bare minimum of state. A function takes more setup and leaves a different stack.
But you can pad the function with _asm{ nop nop nop ... } and jump somewhere in the middle of those. Eight seems to work fine and I have no reason to push for fewer. The bare minimum presumably differs with the complexity of the function itself. So in the interest of doing things as sensibly as this jank allows - I wrote a wrapper function that only calls play_music normally and then does _asm{ iret }.
TLDR:
AX = 0x251C, DX = your_function_wrapper, DX += 8, INT 21h / int86 0x21. DS probably has to be zero as well.
void your_function_wrapper() {
_asm{ nop (8x, each one goes on a new line) }
your_function();
_asm{ iret }
}