Skip to content
Merged
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
4 changes: 3 additions & 1 deletion docs/JAM.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,9 @@ M4 is done. Where it stands:
applies inside the step (`TW_NOTE`) -- so a key into a piece's
`input midi` composes the same thing everywhere, and is logged for a
late joiner. A key a bar ahead onto a plain channel is heard by its
player at once and by everyone else a bar later.
player at once and by everyone else a bar later. A Stop, or a Play over
the run, lets go of every stamped key still down, since a release
stamped past it is never reached.
- Keys stamped for one time are applied in an order made from the
sender's id and counter, not in the order they arrived: two quantised
seats meet on grid lines all the time, and a quantizer passes on what
Expand Down
85 changes: 85 additions & 0 deletions wasm/web/protocoltest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,70 @@ async function staleIndex (createThinkWeb, piece, dsps)
`${muted.join(' and ')}, wanted 0 and 1`;
}

/* A stamped key pressed before the run ends and released after it, into
the piece on `seat' and onto `plain', which the piece does not take: the
way the run ends, a Stop or a Play over it, has to let go of both. The
keys left sounding, as `channel:note', from the record the page lights
its keyboard from. */
async function keysAtTheEnd (createThinkWeb, piece, dsps, seat, plain, play)
{
const { M, ok } = await loadPiece(createThinkWeb, {
gen: piece.text, instruments: dsps,
});

if (!ok)
return ['did not load'];

M.ccall('tw_load', 'number', ['number', 'string'],
[plain, dsps['rhodes.dsp']]);

const held = new Map();
let frames = 0;
const render = () =>
{
M._tw_render(128);
frames += 128;

const base = M._tw_keys() >>> 0;

for (let k = 0; k < M._tw_key_count(); k++)
{
const [channel, note, , on] =
M.HEAP32.subarray((base + k * 24 + 8) >> 2);
const key = `${channel}:${note}`;

held.set(key, (held.get(key) ?? 0) + (on ? 1 : -1));
}

M._tw_keys_clear();
};

engineApply(M, { type: 'begin', frame: 0 });

for (const channel of [seat, plain])
for (const [at, on] of [[0.5, true], [1.5, false]])
engineApply(M, { type: 'noteat', at, channel, note: 60,
velocity: 90, on, tie: 0 });

if (!play)
engineApply(M, { type: 'at', op: 'stop', at: 1 });

while (M._tw_now() < 1)
render();

if (play)
{
M.ccall('tw_piece_load', 'number', ['string', 'number'],
[piece.text, -1]);
engineApply(M, { type: 'begin', frame: frames + 1024 });
}

while (frames < 3 * 48000)
render();

return [...held].filter(([, n]) => n > 0).map(([key]) => key);
}

/* The first shown knob, as the page reads it (see session). */
function firstKnob (M)
{
Expand Down Expand Up @@ -1679,6 +1743,27 @@ if (import.meta.url === pathToFileURL(process.argv[1]).href)
'is dropped\n');
}

{
const piece = pieces(build).find((p) => p.name === 'free.gen');

for (const [play, how] of [[false, 'a Stop'], [true, 'a Play']])
{
const lit = await keysAtTheEnd(createThinkWeb, piece, dsps, 0, 10,
play);

if (lit.length > 0)
{
failures++;
process.stdout.write(`FAIL ${piece.name.padEnd(14)} keys ` +
`released past ${how}: ` +
`${lit.join(', ')} left sounding\n`);
}
else
process.stdout.write(`ok ${piece.name.padEnd(14)} ${how} ` +
'lets go of keys released past it\n');
}
}

/* Keys into a piece. */
{
const piece = pieces(build).find((p) => p.name === 'hands.gen');
Expand Down
72 changes: 46 additions & 26 deletions wasm/web/thinkweb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -504,8 +504,8 @@ int epoch_;
/* Where each stamped key's on went, by channel and note, so that its off
goes the same way (TW_NOTE): an edit between the two can change whether
the piece takes input on the channel, and an off sent the other way
leaves the note held. Emptied at each begin, where a late joiner's
catching up starts from too. */
leaves the note held. Let go of at each stop and begin (releaseKeys),
where a late joiner's catching up starts from too. */
enum KeyRoute { KEY_PIECE, KEY_CHANNEL, KEY_HEARD };
std::map<std::pair<int, int>, KeyRoute> keyRoutes_;

Expand Down Expand Up @@ -539,6 +539,46 @@ void playedKey (int channel, int note, int velocity, bool on)
on ? 1 : 0 };
}

/* A stamped key, the way its route goes: nowhere for KEY_HEARD, whose
player sounded it live and lets go of it live. */
void keyByRoute (KeyRoute route, int channel, int note, double velocity,
bool on)
{
if (route == KEY_PIECE)
{
/* CMD_MIDI_ON's event, at the time it was stamped for rather than
the top of whichever window it arrived in -- which is what makes
a key into the piece compose the same thing on every peer. */
thcEvent ev = {};

ev.type = on ? THC_EV_NOTE : THC_EV_NOTEOFF;
ev.at = sched_->now();
ev.channel = channel;
ev.u.note.note = note;
ev.u.note.velocity = (int)velocity;
ev.u.note.duration = 0;
ev.u.note.level = 1;

if (sched_->injectMidiEvent(ev) || !on)
playedKey(channel, note, (int)velocity, on);
}
else if (route == KEY_CHANNEL)
keyOnChannel(channel, (float)note, (float)velocity, on,
sched_->now());
}

/* Every stamped key still down, let go: at a stop or a begin the run its
keys were pressed in is over, and a release stamped later than that is
never reached -- dropped with the run, or waiting for a time a stopped
transport does not get to -- if it was sent at all. */
void releaseKeys (void)
{
for (const auto &k : keyRoutes_)
keyByRoute(k.second, k.first.first, k.first.second, 0, false);

keyRoutes_.clear();
}

void newEpoch (void)
{
epoch_++;
Expand Down Expand Up @@ -637,6 +677,7 @@ void applyDue (double start, int len)
ring out -- see thcScheduler::halt. */
case TW_STOP:
sched_->halt();
releaseKeys();
break;

case TW_REWIND:
Expand Down Expand Up @@ -690,7 +731,7 @@ void beginDue (double start, int len)
that at the load (tw_piece_load). */
sched_->reset();
newEpoch();
keyRoutes_.clear();
releaseKeys();

/* From the top, or from where a seek said: played up to there without
a sound, and the frame below pinned to wherever that is. */
Expand Down Expand Up @@ -1005,6 +1046,7 @@ void applyScheduled (const Scheduled &given)
{
case TW_STOP:
sched_->halt();
releaseKeys();
break;


Expand Down Expand Up @@ -1171,29 +1213,7 @@ void applyScheduled (const Scheduled &given)
keyRoutes_.erase(was);
}

if (route == KEY_PIECE)
{
/* CMD_MIDI_ON's event, at the time it was stamped for
rather than the top of whichever window it arrived
in -- which is what makes a key into the piece compose
the same thing on every peer. */
thcEvent ev = {};

ev.type = c.on ? THC_EV_NOTE : THC_EV_NOTEOFF;
ev.at = sched_->now();
ev.channel = c.channel;
ev.u.note.note = c.note;
ev.u.note.velocity = (int)c.value;
ev.u.note.duration = 0;
ev.u.note.level = 1;

if (sched_->injectMidiEvent(ev) || !c.on)
playedKey(c.channel, c.note, (int)c.value, c.on);
}
else if (route == KEY_CHANNEL)
keyOnChannel(c.channel, (float)c.note, (float)c.value, c.on,
sched_->now());

keyByRoute(route, c.channel, c.note, c.value, c.on);
break;
}

Expand Down
Loading