Remove legacy piano fallback and add state-indexed song events
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// Maintainer-controlled web client version.
|
||||
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
||||
window.CHGRID_WEB_VERSION = "2026.02.23 R217";
|
||||
window.CHGRID_WEB_VERSION = "2026.02.23 R218";
|
||||
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
||||
window.CHGRID_TIME_ZONE = "America/Detroit";
|
||||
|
||||
@@ -478,67 +478,67 @@ async function loadPianoDemo(): Promise<void> {
|
||||
const data = (await response.json()) as {
|
||||
defaultSongId?: unknown;
|
||||
songs?: unknown;
|
||||
recording?: unknown;
|
||||
};
|
||||
pianoDemoSongs.clear();
|
||||
pianoDemoDefaultSongId = '';
|
||||
|
||||
const parseLegacyEvents = (rawEvents: unknown): PianoDemoEvent[] => {
|
||||
const parsed: PianoDemoEvent[] = [];
|
||||
if (!Array.isArray(rawEvents)) return parsed;
|
||||
for (const entry of rawEvents) {
|
||||
if (!entry || typeof entry !== 'object') continue;
|
||||
const record = entry as Record<string, unknown>;
|
||||
const t = Number(record.t);
|
||||
const midi = Number(record.midi);
|
||||
const keyId = String(record.keyId ?? '').trim();
|
||||
const on = record.on === true;
|
||||
if (!Number.isFinite(t) || !Number.isFinite(midi) || !keyId) continue;
|
||||
parsed.push({
|
||||
t: Math.max(0, Math.round(t)),
|
||||
keyId: keyId.slice(0, 32),
|
||||
midi: Math.max(0, Math.min(127, Math.round(midi))),
|
||||
on,
|
||||
instrument: typeof record.instrument === 'string' ? record.instrument : undefined,
|
||||
voiceMode: record.voiceMode === 'mono' ? 'mono' : record.voiceMode === 'poly' ? 'poly' : undefined,
|
||||
attack: Number.isFinite(Number(record.attack)) ? Math.max(0, Math.min(100, Math.round(Number(record.attack)))) : undefined,
|
||||
decay: Number.isFinite(Number(record.decay)) ? Math.max(0, Math.min(100, Math.round(Number(record.decay)))) : undefined,
|
||||
release: Number.isFinite(Number(record.release)) ? Math.max(0, Math.min(100, Math.round(Number(record.release)))) : undefined,
|
||||
brightness: Number.isFinite(Number(record.brightness)) ? Math.max(0, Math.min(100, Math.round(Number(record.brightness)))) : undefined,
|
||||
emitRange: Number.isFinite(Number(record.emitRange)) ? Math.max(5, Math.min(20, Math.round(Number(record.emitRange)))) : undefined,
|
||||
});
|
||||
}
|
||||
parsed.sort((a, b) => a.t - b.t);
|
||||
return parsed;
|
||||
};
|
||||
|
||||
if (data.songs && typeof data.songs === 'object') {
|
||||
const songs = data.songs as Record<string, unknown>;
|
||||
for (const [songId, rawSong] of Object.entries(songs)) {
|
||||
if (!rawSong || typeof rawSong !== 'object') continue;
|
||||
const song = rawSong as Record<string, unknown>;
|
||||
const meta = song.meta as Record<string, unknown> | undefined;
|
||||
const states = Array.isArray(song.states) ? song.states : [];
|
||||
const keys = Array.isArray(song.keys) ? song.keys.filter((value): value is string => typeof value === 'string') : [];
|
||||
const compactEvents = Array.isArray(song.events) ? song.events : [];
|
||||
const events: PianoDemoEvent[] = [];
|
||||
const resolveState = (stateIndex: number): Partial<PianoDemoEvent> => {
|
||||
if (stateIndex < 0 || stateIndex >= states.length) {
|
||||
return {};
|
||||
}
|
||||
const row = states[stateIndex];
|
||||
if (!Array.isArray(row) || row.length < 7) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
instrument: typeof row[0] === 'string' ? row[0] : undefined,
|
||||
voiceMode: row[1] === 'mono' ? 'mono' : row[1] === 'poly' ? 'poly' : undefined,
|
||||
attack: typeof row[2] === 'number' ? Math.max(0, Math.min(100, Math.round(row[2]))) : undefined,
|
||||
decay: typeof row[3] === 'number' ? Math.max(0, Math.min(100, Math.round(row[3]))) : undefined,
|
||||
release: typeof row[4] === 'number' ? Math.max(0, Math.min(100, Math.round(row[4]))) : undefined,
|
||||
brightness: typeof row[5] === 'number' ? Math.max(0, Math.min(100, Math.round(row[5]))) : undefined,
|
||||
emitRange: typeof row[6] === 'number' ? Math.max(5, Math.min(20, Math.round(row[6]))) : undefined,
|
||||
};
|
||||
};
|
||||
for (const compact of compactEvents) {
|
||||
if (!Array.isArray(compact) || compact.length < 4) continue;
|
||||
const [rawT, rawKeyIdx, rawMidi, rawOn] = compact;
|
||||
const [rawT, rawKeyIdx, rawMidi, rawOn, rawStateIdx] = compact;
|
||||
if (typeof rawT !== 'number' || typeof rawKeyIdx !== 'number' || typeof rawMidi !== 'number') continue;
|
||||
const keyId = keys[Math.max(0, Math.round(rawKeyIdx))];
|
||||
if (!keyId) continue;
|
||||
const eventState = typeof rawStateIdx === 'number' ? resolveState(Math.round(rawStateIdx)) : {};
|
||||
events.push({
|
||||
t: Math.max(0, Math.round(rawT)),
|
||||
keyId: keyId.slice(0, 32),
|
||||
midi: Math.max(0, Math.min(127, Math.round(rawMidi))),
|
||||
on: Boolean(rawOn),
|
||||
instrument: typeof meta?.instrument === 'string' ? meta.instrument : undefined,
|
||||
voiceMode: meta?.voiceMode === 'mono' ? 'mono' : meta?.voiceMode === 'poly' ? 'poly' : undefined,
|
||||
attack: Number.isFinite(Number(meta?.attack)) ? Math.max(0, Math.min(100, Math.round(Number(meta?.attack)))) : undefined,
|
||||
decay: Number.isFinite(Number(meta?.decay)) ? Math.max(0, Math.min(100, Math.round(Number(meta?.decay)))) : undefined,
|
||||
release: Number.isFinite(Number(meta?.release)) ? Math.max(0, Math.min(100, Math.round(Number(meta?.release)))) : undefined,
|
||||
brightness: Number.isFinite(Number(meta?.brightness)) ? Math.max(0, Math.min(100, Math.round(Number(meta?.brightness)))) : undefined,
|
||||
emitRange: Number.isFinite(Number(meta?.emitRange)) ? Math.max(5, Math.min(20, Math.round(Number(meta?.emitRange)))) : undefined,
|
||||
instrument: eventState.instrument ?? (typeof meta?.instrument === 'string' ? meta.instrument : undefined),
|
||||
voiceMode: eventState.voiceMode ?? (meta?.voiceMode === 'mono' ? 'mono' : meta?.voiceMode === 'poly' ? 'poly' : undefined),
|
||||
attack:
|
||||
eventState.attack ??
|
||||
(Number.isFinite(Number(meta?.attack)) ? Math.max(0, Math.min(100, Math.round(Number(meta?.attack)))) : undefined),
|
||||
decay:
|
||||
eventState.decay ??
|
||||
(Number.isFinite(Number(meta?.decay)) ? Math.max(0, Math.min(100, Math.round(Number(meta?.decay)))) : undefined),
|
||||
release:
|
||||
eventState.release ??
|
||||
(Number.isFinite(Number(meta?.release)) ? Math.max(0, Math.min(100, Math.round(Number(meta?.release)))) : undefined),
|
||||
brightness:
|
||||
eventState.brightness ??
|
||||
(Number.isFinite(Number(meta?.brightness)) ? Math.max(0, Math.min(100, Math.round(Number(meta?.brightness)))) : undefined),
|
||||
emitRange:
|
||||
eventState.emitRange ??
|
||||
(Number.isFinite(Number(meta?.emitRange)) ? Math.max(5, Math.min(20, Math.round(Number(meta?.emitRange)))) : undefined),
|
||||
});
|
||||
}
|
||||
events.sort((a, b) => a.t - b.t);
|
||||
@@ -554,14 +554,6 @@ async function loadPianoDemo(): Promise<void> {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Backward fallback for legacy flat recording JSON format.
|
||||
const legacyEvents = parseLegacyEvents(data.recording);
|
||||
if (legacyEvents.length > 0) {
|
||||
const legacyId = 'unterlandersheimweh';
|
||||
pianoDemoSongs.set(legacyId, { id: legacyId, events: legacyEvents });
|
||||
pianoDemoDefaultSongId = legacyId;
|
||||
}
|
||||
} catch {
|
||||
// Demo remains unavailable if loading/parsing fails.
|
||||
}
|
||||
|
||||
@@ -184,9 +184,11 @@
|
||||
- `brightness`: integer, range `0-100`, default `55`.
|
||||
- `emitRange`: integer, range `5-20`, default `15`.
|
||||
- `songId`: server-managed song reference used for piano demo/playback content.
|
||||
- `recording`: server-managed array of note events (`t`, `keyId`, `midi`, `on`) captured from piano mode recording.
|
||||
- `recordingLengthMs`: server-managed recording duration in milliseconds (`0..30000`).
|
||||
- Legacy fallback only during migration; new recordings are stored in server song registry by `songId`.
|
||||
- Recorded/demo song payload is stored in server song registry (`runtime/piano_songs.json`) using compact format:
|
||||
- `meta`: shared synth parameters
|
||||
- `keys`: keyId dictionary
|
||||
- `states`: parameter-state dictionary (for mid-song instrument/param changes)
|
||||
- `events`: `[t, keyIndex, midi, on, stateIndex]`
|
||||
|
||||
## Packet Shapes
|
||||
|
||||
|
||||
@@ -106,12 +106,8 @@ PROPERTY_METADATA: dict[str, dict[str, object]] = {
|
||||
def validate_update(_item: WorldItem, next_params: dict) -> dict:
|
||||
"""Validate and normalize piano params."""
|
||||
|
||||
# Recording data is server-managed and not directly editable from client updates.
|
||||
preserved_recording = _item.params.get("recording")
|
||||
preserved_recording_length = _item.params.get("recordingLengthMs")
|
||||
# Song references are server-managed and not directly editable from client updates.
|
||||
preserved_song_id = _item.params.get("songId")
|
||||
next_params.pop("recording", None)
|
||||
next_params.pop("recordingLengthMs", None)
|
||||
next_params.pop("songId", None)
|
||||
|
||||
instrument = str(next_params.get("instrument", "piano")).strip().lower()
|
||||
@@ -180,10 +176,6 @@ def validate_update(_item: WorldItem, next_params: dict) -> dict:
|
||||
raise ValueError("emitRange must be between 5 and 20.")
|
||||
next_params["emitRange"] = emit_range
|
||||
|
||||
if isinstance(preserved_recording, list):
|
||||
next_params["recording"] = preserved_recording
|
||||
if isinstance(preserved_recording_length, (int, float)):
|
||||
next_params["recordingLengthMs"] = max(0, min(30_000, int(preserved_recording_length)))
|
||||
if isinstance(preserved_song_id, str) and preserved_song_id.strip():
|
||||
next_params["songId"] = preserved_song_id.strip()
|
||||
|
||||
|
||||
@@ -297,6 +297,8 @@ class SignalingServer:
|
||||
song_id = f"item:{item.id}:recording"
|
||||
keys: list[str] = []
|
||||
key_to_index: dict[str, int] = {}
|
||||
states: list[list[object]] = []
|
||||
state_to_index: dict[tuple[object, ...], int] = {}
|
||||
compact_events: list[list[int]] = []
|
||||
for event in events:
|
||||
if not isinstance(event, dict):
|
||||
@@ -305,6 +307,24 @@ class SignalingServer:
|
||||
key_id = str(event.get("keyId", "")).strip()
|
||||
midi = int(event.get("midi", 0)) if isinstance(event.get("midi"), (int, float)) else 0
|
||||
on = 1 if event.get("on") is True else 0
|
||||
instrument = str(event.get("instrument", "piano")).strip().lower() or "piano"
|
||||
voice_mode = str(event.get("voiceMode", "poly")).strip().lower()
|
||||
if voice_mode not in {"mono", "poly"}:
|
||||
voice_mode = "poly"
|
||||
attack = int(event.get("attack", 15)) if isinstance(event.get("attack"), (int, float)) else 15
|
||||
decay = int(event.get("decay", 45)) if isinstance(event.get("decay"), (int, float)) else 45
|
||||
release = int(event.get("release", 35)) if isinstance(event.get("release"), (int, float)) else 35
|
||||
brightness = int(event.get("brightness", 55)) if isinstance(event.get("brightness"), (int, float)) else 55
|
||||
emit_range = int(event.get("emitRange", 15)) if isinstance(event.get("emitRange"), (int, float)) else 15
|
||||
state_key = (
|
||||
instrument,
|
||||
voice_mode,
|
||||
max(0, min(100, attack)),
|
||||
max(0, min(100, decay)),
|
||||
max(0, min(100, release)),
|
||||
max(0, min(100, brightness)),
|
||||
max(5, min(20, emit_range)),
|
||||
)
|
||||
if not key_id:
|
||||
continue
|
||||
index = key_to_index.get(key_id)
|
||||
@@ -312,20 +332,27 @@ class SignalingServer:
|
||||
index = len(keys)
|
||||
keys.append(key_id)
|
||||
key_to_index[key_id] = index
|
||||
compact_events.append([max(0, min(PIANO_RECORDING_MAX_MS, t)), index, max(0, min(127, midi)), on])
|
||||
state_index = state_to_index.get(state_key)
|
||||
if state_index is None:
|
||||
state_index = len(states)
|
||||
states.append(list(state_key))
|
||||
state_to_index[state_key] = state_index
|
||||
compact_events.append([max(0, min(PIANO_RECORDING_MAX_MS, t)), index, max(0, min(127, midi)), on, state_index])
|
||||
compact_events.sort(key=lambda row: row[0])
|
||||
first_state = states[0] if states else ["piano", "poly", 15, 45, 35, 55, 15]
|
||||
self.item_service.piano_songs[song_id] = {
|
||||
"meta": {
|
||||
"instrument": str(item.params.get("instrument", "piano")).strip().lower(),
|
||||
"voiceMode": str(item.params.get("voiceMode", "poly")).strip().lower(),
|
||||
"attack": int(item.params.get("attack", 15)) if isinstance(item.params.get("attack"), (int, float)) else 15,
|
||||
"decay": int(item.params.get("decay", 45)) if isinstance(item.params.get("decay"), (int, float)) else 45,
|
||||
"release": int(item.params.get("release", 35)) if isinstance(item.params.get("release"), (int, float)) else 35,
|
||||
"brightness": int(item.params.get("brightness", 55)) if isinstance(item.params.get("brightness"), (int, float)) else 55,
|
||||
"emitRange": int(item.params.get("emitRange", 15)) if isinstance(item.params.get("emitRange"), (int, float)) else 15,
|
||||
"instrument": first_state[0],
|
||||
"voiceMode": first_state[1],
|
||||
"attack": first_state[2],
|
||||
"decay": first_state[3],
|
||||
"release": first_state[4],
|
||||
"brightness": first_state[5],
|
||||
"emitRange": first_state[6],
|
||||
"recordingLengthMs": elapsed_ms,
|
||||
},
|
||||
"keys": keys,
|
||||
"states": states,
|
||||
"events": compact_events,
|
||||
}
|
||||
self.item_service.save_piano_songs()
|
||||
@@ -359,25 +386,29 @@ class SignalingServer:
|
||||
song_payload = self.item_service.piano_songs.get(song_id) if song_id else None
|
||||
if isinstance(song_payload, dict):
|
||||
keys = song_payload.get("keys")
|
||||
states = song_payload.get("states")
|
||||
compact_events = song_payload.get("events")
|
||||
meta = song_payload.get("meta")
|
||||
if isinstance(keys, list) and isinstance(compact_events, list):
|
||||
instrument = None
|
||||
voice_mode = None
|
||||
attack = None
|
||||
decay = None
|
||||
release = None
|
||||
brightness = None
|
||||
emit_range = None
|
||||
base_state = None
|
||||
if isinstance(meta, dict):
|
||||
instrument = str(meta.get("instrument", "")).strip().lower() or None
|
||||
instrument = str(meta.get("instrument", "")).strip().lower() or "piano"
|
||||
raw_voice_mode = str(meta.get("voiceMode", "")).strip().lower()
|
||||
voice_mode = raw_voice_mode if raw_voice_mode in {"mono", "poly"} else None
|
||||
attack = int(meta.get("attack", 15)) if isinstance(meta.get("attack"), (int, float)) else None
|
||||
decay = int(meta.get("decay", 45)) if isinstance(meta.get("decay"), (int, float)) else None
|
||||
release = int(meta.get("release", 35)) if isinstance(meta.get("release"), (int, float)) else None
|
||||
brightness = int(meta.get("brightness", 55)) if isinstance(meta.get("brightness"), (int, float)) else None
|
||||
emit_range = int(meta.get("emitRange", 15)) if isinstance(meta.get("emitRange"), (int, float)) else None
|
||||
voice_mode = raw_voice_mode if raw_voice_mode in {"mono", "poly"} else "poly"
|
||||
attack = int(meta.get("attack", 15)) if isinstance(meta.get("attack"), (int, float)) else 15
|
||||
decay = int(meta.get("decay", 45)) if isinstance(meta.get("decay"), (int, float)) else 45
|
||||
release = int(meta.get("release", 35)) if isinstance(meta.get("release"), (int, float)) else 35
|
||||
brightness = int(meta.get("brightness", 55)) if isinstance(meta.get("brightness"), (int, float)) else 55
|
||||
emit_range = int(meta.get("emitRange", 15)) if isinstance(meta.get("emitRange"), (int, float)) else 15
|
||||
base_state = (
|
||||
instrument,
|
||||
voice_mode,
|
||||
max(0, min(100, attack)),
|
||||
max(0, min(100, decay)),
|
||||
max(0, min(100, release)),
|
||||
max(0, min(100, brightness)),
|
||||
max(5, min(20, emit_range)),
|
||||
)
|
||||
for row in compact_events:
|
||||
if not isinstance(row, list) or len(row) < 4:
|
||||
continue
|
||||
@@ -390,54 +421,38 @@ class SignalingServer:
|
||||
raw_key = keys[key_idx]
|
||||
if not isinstance(raw_key, str) or not raw_key.strip():
|
||||
continue
|
||||
state = base_state
|
||||
if len(row) >= 5 and isinstance(states, list) and isinstance(row[4], (int, float)):
|
||||
state_idx = int(row[4])
|
||||
if 0 <= state_idx < len(states):
|
||||
state_row = states[state_idx]
|
||||
if isinstance(state_row, list) and len(state_row) >= 7:
|
||||
candidate_instrument = str(state_row[0]).strip().lower() or "piano"
|
||||
candidate_voice_mode = str(state_row[1]).strip().lower()
|
||||
state = (
|
||||
candidate_instrument,
|
||||
candidate_voice_mode if candidate_voice_mode in {"mono", "poly"} else "poly",
|
||||
max(0, min(100, int(state_row[2]) if isinstance(state_row[2], (int, float)) else 15)),
|
||||
max(0, min(100, int(state_row[3]) if isinstance(state_row[3], (int, float)) else 45)),
|
||||
max(0, min(100, int(state_row[4]) if isinstance(state_row[4], (int, float)) else 35)),
|
||||
max(0, min(100, int(state_row[5]) if isinstance(state_row[5], (int, float)) else 55)),
|
||||
max(5, min(20, int(state_row[6]) if isinstance(state_row[6], (int, float)) else 15)),
|
||||
)
|
||||
if state is None:
|
||||
continue
|
||||
events.append(
|
||||
{
|
||||
"t": max(0, min(PIANO_RECORDING_MAX_MS, int(raw_time))),
|
||||
"keyId": raw_key[:32],
|
||||
"midi": max(0, min(127, int(raw_midi))),
|
||||
"on": bool(raw_on),
|
||||
"instrument": instrument,
|
||||
"voiceMode": voice_mode,
|
||||
"attack": max(0, min(100, attack)) if isinstance(attack, int) else None,
|
||||
"decay": max(0, min(100, decay)) if isinstance(decay, int) else None,
|
||||
"release": max(0, min(100, release)) if isinstance(release, int) else None,
|
||||
"brightness": max(0, min(100, brightness)) if isinstance(brightness, int) else None,
|
||||
"emitRange": max(5, min(20, emit_range)) if isinstance(emit_range, int) else None,
|
||||
}
|
||||
)
|
||||
# Backward fallback for older items that still carry recording payload in params.
|
||||
if not events:
|
||||
raw_events = item.params.get("recording")
|
||||
if isinstance(raw_events, list):
|
||||
for event in raw_events:
|
||||
if not isinstance(event, dict):
|
||||
continue
|
||||
raw_time = event.get("t")
|
||||
raw_key = event.get("keyId")
|
||||
raw_midi = event.get("midi")
|
||||
raw_on = event.get("on")
|
||||
if not isinstance(raw_time, (int, float)) or not isinstance(raw_key, str) or not isinstance(raw_midi, (int, float)) or not isinstance(raw_on, bool):
|
||||
continue
|
||||
raw_instrument = event.get("instrument")
|
||||
raw_voice_mode = event.get("voiceMode")
|
||||
raw_attack = event.get("attack")
|
||||
raw_decay = event.get("decay")
|
||||
raw_release = event.get("release")
|
||||
raw_brightness = event.get("brightness")
|
||||
raw_emit_range = event.get("emitRange")
|
||||
events.append(
|
||||
{
|
||||
"t": max(0, min(PIANO_RECORDING_MAX_MS, int(raw_time))),
|
||||
"keyId": raw_key[:32] or "KeyA",
|
||||
"midi": max(0, min(127, int(raw_midi))),
|
||||
"on": raw_on,
|
||||
"instrument": str(raw_instrument).strip().lower() if isinstance(raw_instrument, str) else None,
|
||||
"voiceMode": str(raw_voice_mode).strip().lower() if isinstance(raw_voice_mode, str) else None,
|
||||
"attack": max(0, min(100, int(raw_attack))) if isinstance(raw_attack, (int, float)) else None,
|
||||
"decay": max(0, min(100, int(raw_decay))) if isinstance(raw_decay, (int, float)) else None,
|
||||
"release": max(0, min(100, int(raw_release))) if isinstance(raw_release, (int, float)) else None,
|
||||
"brightness": max(0, min(100, int(raw_brightness))) if isinstance(raw_brightness, (int, float)) else None,
|
||||
"emitRange": max(5, min(20, int(raw_emit_range))) if isinstance(raw_emit_range, (int, float)) else None,
|
||||
"instrument": state[0],
|
||||
"voiceMode": state[1],
|
||||
"attack": state[2],
|
||||
"decay": state[3],
|
||||
"release": state[4],
|
||||
"brightness": state[5],
|
||||
"emitRange": state[6],
|
||||
}
|
||||
)
|
||||
events.sort(key=lambda entry: int(entry["t"]))
|
||||
@@ -1100,9 +1115,7 @@ class SignalingServer:
|
||||
return
|
||||
song_id = str(item.params.get("songId", "")).strip()
|
||||
has_song = isinstance(self.item_service.piano_songs.get(song_id), dict) if song_id else False
|
||||
recording = item.params.get("recording")
|
||||
has_legacy_recording = isinstance(recording, list) and len(recording) > 0
|
||||
if not has_song and not has_legacy_recording:
|
||||
if not has_song:
|
||||
await self._send_item_result(client, False, "use", "No recording saved on this piano.", item.id)
|
||||
return
|
||||
self._cancel_piano_playback(item.id)
|
||||
|
||||
@@ -555,8 +555,10 @@ async def test_piano_recording_toggle_and_save(monkeypatch: pytest.MonkeyPatch)
|
||||
payload = server.item_service.piano_songs.get(song_id)
|
||||
assert isinstance(payload, dict)
|
||||
keys = payload.get("keys")
|
||||
states = payload.get("states")
|
||||
events = payload.get("events")
|
||||
assert isinstance(keys, list) and "KeyA" in keys
|
||||
assert isinstance(states, list) and len(states) >= 1
|
||||
assert isinstance(events, list) and len(events) >= 2
|
||||
|
||||
|
||||
@@ -567,7 +569,13 @@ async def test_piano_playback_starts_task(monkeypatch: pytest.MonkeyPatch) -> No
|
||||
client = ClientConnection(websocket=ws, id="u1", nickname="tester", x=5, y=6)
|
||||
server.clients[ws] = client
|
||||
item = server.item_service.default_item(client, "piano")
|
||||
item.params["recording"] = [{"t": 0, "keyId": "KeyA", "midi": 60, "on": True}]
|
||||
item.params["songId"] = "item:test-song"
|
||||
server.item_service.piano_songs["item:test-song"] = {
|
||||
"meta": {"instrument": "piano", "voiceMode": "poly", "attack": 15, "decay": 45, "release": 35, "brightness": 55, "emitRange": 15},
|
||||
"keys": ["KeyA"],
|
||||
"states": [["piano", "poly", 15, 45, 35, 55, 15]],
|
||||
"events": [[0, 0, 60, 1, 0]],
|
||||
}
|
||||
server.item_service.add_item(item)
|
||||
|
||||
send_payloads: list[object] = []
|
||||
|
||||
Reference in New Issue
Block a user