From 8e0ce829f0a3af243fe05f291f5326b2efdc976a Mon Sep 17 00:00:00 2001 From: Jeff Kaufman Date: Sun, 7 Feb 2021 16:14:51 +0000 Subject: [PATCH] Allow multiple people to use the mixing console, and monitor multiple people. Fixes #211. --- html/demo.js | 65 ++++++++++++++++++++++++++++---------------- html/index.html | 9 +++++++ html/net.js | 7 ++--- server.py | 72 ++++++++++++++++++++++++++++++++++++------------- 4 files changed, 109 insertions(+), 44 deletions(-) diff --git a/html/demo.js b/html/demo.js index ed0abfc..5d56675 100644 --- a/html/demo.js +++ b/html/demo.js @@ -998,27 +998,22 @@ window.backingTrack.addEventListener("change", (e) => { const consoleChannels = new Map(); window.consoleChannels = consoleChannels; -let monitoredUserId = null; - +let nMonitoredUsers = 0; +let iterationsWithNoMonitoredUsers = 0; function mixerMonitorButtonClick(userid) { if (singer_client) { - if (monitoredUserId) { - consoleChannels.get(monitoredUserId).children[5].classList.remove('activeButton'); - } - if (monitoredUserId === userid) { - singer_client.x_send_metadata("monitoredUserId", "end"); - monitoredUserId = null; - if (micPaused) { - toggle_mic(); - } - } - else { - singer_client.x_send_metadata("monitoredUserId", userid); - monitoredUserId = userid; - consoleChannels.get(userid).children[5].classList.add('activeButton'); - if (!micPaused) { - toggle_mic(); - } + const monitorButton = consoleChannels.get(userid).children[5]; + monitorButton.classList.add('edited'); + + if (monitorButton.classList.contains('activeButton')) { + singer_client.x_send_metadata("unmonitor", userid); + nMonitoredUsers--; + } else { + window.hearMonitor.checked = true; + window.hearMonitorDiv.style.display = "block"; + singer_client.x_send_metadata("monitor", userid); + nMonitoredUsers++; + iterationsWithNoMonitoredUsers = 0; } } } @@ -1109,6 +1104,7 @@ function update_active_users( const userid = user_summary[i][3]; const rms_volume = user_summary[i][4]; const muted = user_summary[i][5]; + const is_monitored = user_summary[i][6]; let est_bucket = estimateBucket(offset_s); if (!showBuckets) { @@ -1122,7 +1118,7 @@ function update_active_users( } } - mic_volume_inputs.push([name, userid, mic_volume, rms_volume, offset_s]); + mic_volume_inputs.push([name, userid, mic_volume, rms_volume, offset_s, is_monitored]); userids.add(userid); // Don't update user buckets when we are not looking at that screen. @@ -1233,6 +1229,7 @@ function update_active_users( } } + nMonitoredUsers = 0; for (var i = 0; i < mic_volume_inputs.length; i++) { const name = mic_volume_inputs[i][0]; @@ -1240,6 +1237,7 @@ function update_active_users( const vol = mic_volume_inputs[i][2]; const rms_volume = mic_volume_inputs[i][3]; const offset_s = mic_volume_inputs[i][4]; + const is_monitored = mic_volume_inputs[i][5]; const channel = consoleChannels.get(userid); const post_volume = vol < 0.0000001? @@ -1267,12 +1265,30 @@ function update_active_users( else { channelVolumeInput.value = vol; } - - + const monitorButton = channel.children[5]; + monitorButton.classList.remove("edited"); + monitorButton.classList.toggle("activeButton", is_monitored); + if (is_monitored) { + nMonitoredUsers++; + } + } + if (nMonitoredUsers > 0) { + iterationsWithNoMonitoredUsers = 0; + } else { + iterationsWithNoMonitoredUsers++; + } + const definitelyNotMonitoring = iterationsWithNoMonitoredUsers > 3; + window.hearMonitorDiv.style.display = definitelyNotMonitoring ? "none" : "block"; + if (definitelyNotMonitoring) { + window.hearMonitor.checked = false; } } - +window.hearMonitor.addEventListener("change", () => { + if (window.hearMonitor.checked) { + singer_client.x_send_metadata("begin_monitor", 1); + } +}); async function stop() { if (app_state != APP_RUNNING && @@ -1878,6 +1894,9 @@ async function start_singing() { if (in_spectator_mode) { singer_client.x_send_metadata("spectator", 1); } + if (window.hearMonitor.checked) { + singer_client.x_send_metadata("hear_monitor", 1); + } if (delay_seconds) { if (delay_seconds > 0) { diff --git a/html/index.html b/html/index.html index f16c6ee..1b1c15f 100644 --- a/html/index.html +++ b/html/index.html @@ -177,6 +177,9 @@ .activeButton { background-color: red; } +button.edited { + background-color: yellow; +} input.editing { background-color: wheat; } @@ -531,6 +534,9 @@ .healthy { background-color: #0a0; } +#hearMonitorDiv { + display: none; +} @@ -602,6 +608,9 @@

Best to leave these alone, since they do affect everyone

Mixing Console

+
+ Hear Monitor Mix: +
diff --git a/html/net.js b/html/net.js index efac046..b638c62 100644 --- a/html/net.js +++ b/html/net.js @@ -201,12 +201,14 @@ export class ServerConnection extends ServerConnectionBase { 0, /*littleEndian=*/false); pos += 2; - const muted = + const bits = new DataView(data.slice(pos, pos + 1)).getUint8(0); + const muted = bits & 0b00000001; + const is_monitored = bits & 0b00000010; pos += 1; metadata.user_summary.push([ - delay, name, mic_volume, userid, rms_volume, muted]); + delay, name, mic_volume, userid, rms_volume, muted, is_monitored]); } data = data.slice(pos); } @@ -321,7 +323,6 @@ export async function samples_to_server( backingVolume: 'backing_volume', micVolumes: 'mic_volume', backingTrack: 'track', - monitoredUserId: 'monitor', loopback_mode: 'loopback', } diff --git a/server.py b/server.py index 319b8a6..62d4aec 100755 --- a/server.py +++ b/server.py @@ -33,8 +33,8 @@ # 4 mic_volume: float32, # 4 rms_volume: float32 # 2 delay: uint16 -# 1 muted: boolean -BINARY_USER_CONFIG_FORMAT = struct.Struct(">Q32sffH?") +# 1 muted: uint8 +BINARY_USER_CONFIG_FORMAT = struct.Struct(">Q32sffHB") FRAME_SIZE = 128 @@ -578,20 +578,32 @@ def clean_users(server_clock) -> None: if not active_users() and not state.server_controlled: state.reset() -def setup_monitoring(monitoring_userid, monitored_userid) -> None: - for user in users.values(): - user.is_monitoring = False - user.is_monitored = False +def samples_to_position(samples): + return round(samples / SAMPLE_RATE) - # We turn off monitoring by asking to monitor an invalid user ID. - if monitored_userid not in users: +def jump_user_after(user, position): + target = position + DELAY_INTERVAL + if target == samples_to_position(user.delay_samples): return + user.send("delay_seconds", target) + +def max_monitor_position(): + max_delay_samples = 0 + for user in active_users(): + if user.is_monitored and user.delay_samples > max_delay_samples: + max_delay_samples = user.delay_samples + return samples_to_position(max_delay_samples) - users[monitoring_userid].is_monitoring = True - users[monitored_userid].is_monitored = True +def jump_to_latest_monitored_user(user): + max_pos = max_monitor_position() + current_pos = samples_to_position(user.delay_samples) + if max_pos > 0: + jump_user_after(user, max_pos) - users[monitoring_userid].send("delay_seconds", round( - users[monitored_userid].delay_samples / SAMPLE_RATE) + DELAY_INTERVAL) +def jump_monitors_to_latest_monitored_user(): + for user in active_users(): + if user.is_monitoring: + jump_to_latest_monitored_user(user) def active_users(): server_clock = calculate_server_clock() @@ -611,7 +623,8 @@ def user_summary(requested_user_summary) -> List[Any]: user.mic_volume, user.userid, user.rms_volume, - user.muted)) + user.muted, + user.is_monitored)) summary.sort() return summary @@ -630,12 +643,19 @@ def binary_user_summary(summary): compact by only sending names if they have changed. """ binary_summaries = [struct.pack(">H", len(summary))] - for delay, name, mic_volume, userid, rms_volume, muted in summary: + for delay, name, mic_volume, userid, rms_volume, muted, is_monitored in summary: # delay is encoded as a uint16 if delay < 0: delay = 0 elif delay > 0xffff: delay = 0xffff + + bits = 0 + if muted: + bits += 0b00000001 + if is_monitored: + bits += 0b00000010 + binary_summaries.append( BINARY_USER_CONFIG_FORMAT.pack( int(userid), @@ -643,7 +663,7 @@ def binary_user_summary(summary): mic_volume, rms_volume, delay, - muted)) + bits)) resp = np.frombuffer(b"".join(binary_summaries), dtype=np.uint8) if len(resp) != summary_length(len(summary)): @@ -683,7 +703,9 @@ def update_audio(pos, n_samples, in_data, is_monitored): wrap_assign(audio_queue, pos, new_audio) if is_monitored: - wrap_assign(monitor_queue, pos, in_data) + wrap_assign(monitor_queue, + pos, + wrap_get(monitor_queue, pos, n_samples) + in_data) old_n_people = wrap_get(n_people_queue, pos, n_samples) new_n_people = old_n_people + np.ones(n_samples, np.int16) @@ -1097,9 +1119,23 @@ def handle_post(in_json, in_data) -> Tuple[Any, str]: # Handle all operations that do not require a userid handle_special(query_params, server_clock, user, client_read_clock) + user.is_monitoring = query_params.get("hear_monitor", False) monitor_userid = query_params.get("monitor", None) - if monitor_userid: - setup_monitoring(userid, monitor_userid) + changedMonitoring = False + if monitor_userid and monitor_userid in users: + users[monitor_userid].is_monitored = True + changedMonitoring = True + user.is_monitoring = True + + unmonitor_userid = query_params.get("unmonitor", None) + if unmonitor_userid and unmonitor_userid in users: + users[unmonitor_userid].is_monitored = False + changedMonitoring = True + + if changedMonitoring: + jump_monitors_to_latest_monitored_user() + if query_params.get("begin_monitor", False): + jump_to_latest_monitored_user(user) ### XXX: Debugging note: We used to do clearing of the buffer here, but now ### we do it above, closer to the top of the function.