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
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.