Add generated UI notification sounds
This commit is contained in:
@@ -7,3 +7,7 @@
|
|||||||
/halls/**/*.json
|
/halls/**/*.json
|
||||||
/static/**/*.d.ts
|
/static/**/*.d.ts
|
||||||
/static/third-party/tasks-vision
|
/static/third-party/tasks-vision
|
||||||
|
/distro-packages/Arch-Linux/skald-git/pkg/
|
||||||
|
/distro-packages/Arch-Linux/skald-git/skald/
|
||||||
|
/distro-packages/Arch-Linux/skald-git/src/
|
||||||
|
/distro-packages/Arch-Linux/skald-git/*.pkg.tar.*
|
||||||
|
|||||||
+7
-2
@@ -164,12 +164,12 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
<input id="preprocessingbox" type="checkbox" checked/>
|
<input id="preprocessingbox" type="checkbox"/>
|
||||||
<label for="preprocessingbox">Noise suppression</label>
|
<label for="preprocessingbox">Noise suppression</label>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
<input id="hqaudiobox" type="checkbox"/>
|
<input id="hqaudiobox" type="checkbox" checked/>
|
||||||
<label for="hqaudiobox">High-quality audio</label>
|
<label for="hqaudiobox">High-quality audio</label>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -179,6 +179,11 @@
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Other Settings</legend>
|
<legend>Other Settings</legend>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<input id="notificationsoundsbox" type="checkbox" checked/>
|
||||||
|
<label for="notificationsoundsbox">Notification sounds</label>
|
||||||
|
</form>
|
||||||
|
|
||||||
<form id="sendform">
|
<form id="sendform">
|
||||||
<label for="sendselect" class="sidenav-label-first">Send:</label>
|
<label for="sendselect" class="sidenav-label-first">Send:</label>
|
||||||
<select id="sendselect" class="select select-inline">
|
<select id="sendselect" class="select select-inline">
|
||||||
|
|||||||
+176
-9
@@ -76,6 +76,7 @@ let probingState = null;
|
|||||||
|
|
||||||
* @property {boolean} [preprocessing]
|
* @property {boolean} [preprocessing]
|
||||||
* @property {boolean} [hqaudio]
|
* @property {boolean} [hqaudio]
|
||||||
|
* @property {boolean} [notificationSounds]
|
||||||
* @property {boolean} [forceRelay]
|
* @property {boolean} [forceRelay]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -248,10 +249,150 @@ function reflectSettings() {
|
|||||||
store = true;
|
store = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(settings.hasOwnProperty('notificationSounds')) {
|
||||||
|
getInputElement('notificationsoundsbox').checked =
|
||||||
|
settings.notificationSounds;
|
||||||
|
} else {
|
||||||
|
settings.notificationSounds =
|
||||||
|
getInputElement('notificationsoundsbox').checked;
|
||||||
|
store = true;
|
||||||
|
}
|
||||||
|
|
||||||
if(store)
|
if(store)
|
||||||
storeSettings(settings);
|
storeSettings(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {AudioContext} */
|
||||||
|
let notificationAudioContext = null;
|
||||||
|
let userNotificationSoundsReady = false;
|
||||||
|
|
||||||
|
function notificationSoundsEnabled() {
|
||||||
|
return !!getSettings().notificationSounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNotificationAudioContext() {
|
||||||
|
if(!notificationSoundsEnabled())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
let AudioContextConstructor =
|
||||||
|
window.AudioContext || window.webkitAudioContext;
|
||||||
|
if(!AudioContextConstructor)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if(!notificationAudioContext)
|
||||||
|
notificationAudioContext = new AudioContextConstructor();
|
||||||
|
|
||||||
|
if(notificationAudioContext.state === 'suspended')
|
||||||
|
notificationAudioContext.resume().catch(e => {
|
||||||
|
console.warn('Could not resume notification audio:', e);
|
||||||
|
});
|
||||||
|
|
||||||
|
return notificationAudioContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} start
|
||||||
|
* @param {number} duration
|
||||||
|
* @param {number} fromFrequency
|
||||||
|
* @param {number} toFrequency
|
||||||
|
* @param {number} gainValue
|
||||||
|
* @param {OscillatorType} [type]
|
||||||
|
*/
|
||||||
|
function scheduleGlide(start, duration, fromFrequency, toFrequency, gainValue, type) {
|
||||||
|
let context = getNotificationAudioContext();
|
||||||
|
if(!context)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let oscillator = context.createOscillator();
|
||||||
|
let gain = context.createGain();
|
||||||
|
oscillator.type = type || 'sine';
|
||||||
|
oscillator.frequency.setValueAtTime(fromFrequency, start);
|
||||||
|
oscillator.frequency.exponentialRampToValueAtTime(toFrequency, start + duration);
|
||||||
|
gain.gain.setValueAtTime(0, start);
|
||||||
|
gain.gain.linearRampToValueAtTime(gainValue, start + 0.02);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.0001, start + duration);
|
||||||
|
oscillator.connect(gain);
|
||||||
|
gain.connect(context.destination);
|
||||||
|
oscillator.start(start);
|
||||||
|
oscillator.stop(start + duration + 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} offset
|
||||||
|
* @param {number} duration
|
||||||
|
* @param {number} frequency
|
||||||
|
* @param {number} gainValue
|
||||||
|
* @param {OscillatorType} [type]
|
||||||
|
*/
|
||||||
|
function scheduleTone(offset, duration, frequency, gainValue, type) {
|
||||||
|
let context = getNotificationAudioContext();
|
||||||
|
if(!context)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let start = context.currentTime + offset;
|
||||||
|
let oscillator = context.createOscillator();
|
||||||
|
let gain = context.createGain();
|
||||||
|
oscillator.type = type || 'sine';
|
||||||
|
oscillator.frequency.setValueAtTime(frequency, start);
|
||||||
|
gain.gain.setValueAtTime(0, start);
|
||||||
|
gain.gain.linearRampToValueAtTime(gainValue, start + 0.015);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.0001, start + duration);
|
||||||
|
oscillator.connect(gain);
|
||||||
|
gain.connect(context.destination);
|
||||||
|
oscillator.start(start);
|
||||||
|
oscillator.stop(start + duration + 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
function playNotificationSound(name) {
|
||||||
|
let context = getNotificationAudioContext();
|
||||||
|
if(!context)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let now = context.currentTime;
|
||||||
|
switch(name) {
|
||||||
|
case 'hand-raised':
|
||||||
|
scheduleGlide(now, 0.36, 420, 1320, 0.035, 'sine');
|
||||||
|
break;
|
||||||
|
case 'hand-lowered':
|
||||||
|
scheduleGlide(now, 0.34, 1180, 360, 0.03, 'sine');
|
||||||
|
break;
|
||||||
|
case 'user-joined':
|
||||||
|
scheduleTone(0, 0.18, 660, 0.035, 'sine');
|
||||||
|
scheduleTone(0.18, 0.26, 880, 0.032, 'sine');
|
||||||
|
break;
|
||||||
|
case 'user-left':
|
||||||
|
scheduleTone(0, 0.16, 660, 0.025, 'triangle');
|
||||||
|
scheduleTone(0.14, 0.22, 440, 0.022, 'triangle');
|
||||||
|
break;
|
||||||
|
case 'recording-started':
|
||||||
|
scheduleTone(0, 0.16, 523.25, 0.03, 'sine');
|
||||||
|
scheduleTone(0.09, 0.18, 659.25, 0.026, 'sine');
|
||||||
|
scheduleTone(0.18, 0.24, 783.99, 0.024, 'sine');
|
||||||
|
break;
|
||||||
|
case 'recording-stopped':
|
||||||
|
scheduleTone(0, 0.16, 783.99, 0.026, 'sine');
|
||||||
|
scheduleTone(0.12, 0.18, 659.25, 0.024, 'sine');
|
||||||
|
scheduleTone(0.24, 0.24, 392, 0.022, 'sine');
|
||||||
|
break;
|
||||||
|
case 'chat-message':
|
||||||
|
scheduleTone(0, 0.09, 880, 0.018, 'triangle');
|
||||||
|
scheduleTone(0.08, 0.11, 1174.66, 0.014, 'triangle');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlockNotificationAudio() {
|
||||||
|
getNotificationAudioContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('pointerdown', unlockNotificationAudio);
|
||||||
|
document.addEventListener('keydown', unlockNotificationAudio);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if we should use the mobile layout. This should be kept
|
* Returns true if we should use the mobile layout. This should be kept
|
||||||
* in sync with the CSS.
|
* in sync with the CSS.
|
||||||
@@ -589,6 +730,15 @@ getInputElement('hqaudiobox').onchange = function(e) {
|
|||||||
updateSettings({hqaudio: this.checked});
|
updateSettings({hqaudio: this.checked});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getInputElement('notificationsoundsbox').onchange = function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if(!(this instanceof HTMLInputElement))
|
||||||
|
throw new Error('Unexpected type for this');
|
||||||
|
updateSettings({notificationSounds: this.checked});
|
||||||
|
if(this.checked)
|
||||||
|
getNotificationAudioContext();
|
||||||
|
};
|
||||||
|
|
||||||
document.getElementById('mutebutton').onclick = function(e) {
|
document.getElementById('mutebutton').onclick = function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let localMute = getSettings().localMute;
|
let localMute = getSettings().localMute;
|
||||||
@@ -1053,7 +1203,7 @@ async function addLocalMedia(localId) {
|
|||||||
/** @type{boolean|MediaTrackConstraints} */
|
/** @type{boolean|MediaTrackConstraints} */
|
||||||
let audio = false;
|
let audio = false;
|
||||||
if(settings.audio === 'default') {
|
if(settings.audio === 'default') {
|
||||||
audio = true;
|
audio = {};
|
||||||
} else if(settings.audio && settings.audio !== 'off') {
|
} else if(settings.audio && settings.audio !== 'off') {
|
||||||
audio = {deviceId: settings.audio};
|
audio = {deviceId: settings.audio};
|
||||||
}
|
}
|
||||||
@@ -1609,7 +1759,7 @@ function addUser(id, userinfo) {
|
|||||||
user.classList.add("user-p");
|
user.classList.add("user-p");
|
||||||
user.setAttribute('role', 'button');
|
user.setAttribute('role', 'button');
|
||||||
user.setAttribute('tabindex', '0');
|
user.setAttribute('tabindex', '0');
|
||||||
setUserStatus(id, user, userinfo);
|
setUserStatus(id, user, userinfo, false);
|
||||||
user.addEventListener('click', function(e) {
|
user.addEventListener('click', function(e) {
|
||||||
let elt = e.currentTarget;
|
let elt = e.currentTarget;
|
||||||
if(!elt || !(elt instanceof HTMLElement))
|
if(!elt || !(elt instanceof HTMLElement))
|
||||||
@@ -1664,32 +1814,36 @@ function changeUser(id, userinfo) {
|
|||||||
console.warn('Unknown user ' + id);
|
console.warn('Unknown user ' + id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setUserStatus(id, elt, userinfo);
|
setUserStatus(id, elt, userinfo, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @param {HTMLElement} elt
|
* @param {HTMLElement} elt
|
||||||
* @param {user} userinfo
|
* @param {user} userinfo
|
||||||
|
* @param {boolean} notify
|
||||||
*/
|
*/
|
||||||
function setUserStatus(id, elt, userinfo) {
|
function setUserStatus(id, elt, userinfo, notify) {
|
||||||
let username = userinfo.username ? userinfo.username : '(anon)';
|
let username = userinfo.username ? userinfo.username : '(anon)';
|
||||||
elt.textContent = username;
|
elt.textContent = username;
|
||||||
|
|
||||||
let wasRaised = elt.classList.contains('user-status-raisehand');
|
let wasRaised = elt.classList.contains('user-status-raisehand');
|
||||||
|
let isRemote = id !== (serverConnection && serverConnection.id);
|
||||||
if(userinfo.data.raisehand) {
|
if(userinfo.data.raisehand) {
|
||||||
elt.classList.add('user-status-raisehand');
|
elt.classList.add('user-status-raisehand');
|
||||||
elt.setAttribute('aria-label', `${username} (hand raised)`);
|
elt.setAttribute('aria-label', `${username} (hand raised)`);
|
||||||
// Announce when hand is newly raised (not on initial load)
|
if(notify && !wasRaised) {
|
||||||
if(!wasRaised && id !== (serverConnection && serverConnection.id)) {
|
|
||||||
let announcement = document.getElementById('chat-announcements');
|
let announcement = document.getElementById('chat-announcements');
|
||||||
if(announcement) {
|
if(announcement && isRemote) {
|
||||||
announcement.textContent = `${username} raised their hand`;
|
announcement.textContent = `${username} raised their hand`;
|
||||||
}
|
}
|
||||||
|
playNotificationSound('hand-raised');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
elt.classList.remove('user-status-raisehand');
|
elt.classList.remove('user-status-raisehand');
|
||||||
elt.setAttribute('aria-label', username);
|
elt.setAttribute('aria-label', username);
|
||||||
|
if(notify && wasRaised)
|
||||||
|
playNotificationSound('hand-lowered');
|
||||||
}
|
}
|
||||||
|
|
||||||
let microphone=false;
|
let microphone=false;
|
||||||
@@ -1727,10 +1881,14 @@ function gotUser(id, kind) {
|
|||||||
switch(kind) {
|
switch(kind) {
|
||||||
case 'add':
|
case 'add':
|
||||||
addUser(id, serverConnection.users[id]);
|
addUser(id, serverConnection.users[id]);
|
||||||
|
if(userNotificationSoundsReady && id !== serverConnection.id)
|
||||||
|
playNotificationSound('user-joined');
|
||||||
if(Object.keys(serverConnection.users).length == 3)
|
if(Object.keys(serverConnection.users).length == 3)
|
||||||
reconsiderSendParameters();
|
reconsiderSendParameters();
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
|
if(userNotificationSoundsReady && id !== serverConnection.id)
|
||||||
|
playNotificationSound('user-left');
|
||||||
delUser(id);
|
delUser(id);
|
||||||
if(Object.keys(serverConnection.users).length < 3)
|
if(Object.keys(serverConnection.users).length < 3)
|
||||||
scheduleReconsiderParameters();
|
scheduleReconsiderParameters();
|
||||||
@@ -1869,6 +2027,9 @@ async function gotJoined(kind, hall, perms, status, data, error, message) {
|
|||||||
serverConnection.username
|
serverConnection.username
|
||||||
);
|
);
|
||||||
openSafariStream();
|
openSafariStream();
|
||||||
|
window.setTimeout(() => {
|
||||||
|
userNotificationSoundsReady = true;
|
||||||
|
}, 1000);
|
||||||
if(kind === 'change')
|
if(kind === 'change')
|
||||||
return;
|
return;
|
||||||
break;
|
break;
|
||||||
@@ -2121,10 +2282,14 @@ function gotUserMessage(id, dest, username, time, privileged, kind, error, messa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let text = '' + message;
|
let text = '' + message;
|
||||||
if(text === 'Recording started')
|
if(text === 'Recording started') {
|
||||||
announceUrgent(text);
|
announceUrgent(text);
|
||||||
else
|
playNotificationSound('recording-started');
|
||||||
|
} else {
|
||||||
announceChat(text);
|
announceChat(text);
|
||||||
|
if(text === 'Recording stopped')
|
||||||
|
playNotificationSound('recording-stopped');
|
||||||
|
}
|
||||||
displayMessage(text);
|
displayMessage(text);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -2462,6 +2627,8 @@ function addToChatbox(id, peerId, dest, nick, time, privileged, history, kind, m
|
|||||||
if(serverConnection && peerId === serverConnection.id)
|
if(serverConnection && peerId === serverConnection.id)
|
||||||
speaker = 'You';
|
speaker = 'You';
|
||||||
announceChat(speaker ? `${speaker}: ${messageText}` : messageText);
|
announceChat(speaker ? `${speaker}: ${messageText}` : messageText);
|
||||||
|
if(serverConnection && peerId && peerId !== serverConnection.id)
|
||||||
|
playNotificationSound('chat-message');
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,238 +1,154 @@
|
|||||||
# Skald Hard Fork - Master TODO
|
# Skald Recording Quality Roadmap
|
||||||
|
#
|
||||||
|
# Current state:
|
||||||
|
# The original hard-fork checklist is retired. Skald is live on the user's
|
||||||
|
# server, Galene rooms have been migrated as Skald halls, and basic recording
|
||||||
|
# has been verified with real users.
|
||||||
#
|
#
|
||||||
# Goal:
|
# Goal:
|
||||||
# Turn this Galene-derived repository into Skald: a new, audio-only,
|
# Make Skald's single mixed Ogg Opus recordings sound good, stay reliable
|
||||||
# hall-based conferencing server. This is a hard fork, so compatibility with
|
# under larger audio-only halls, and avoid avoidable CPU/process overhead.
|
||||||
# Galene names, URLs, video behavior, and recording format is intentionally
|
|
||||||
# not the default.
|
|
||||||
#
|
#
|
||||||
# Legend:
|
# Legend:
|
||||||
# - item = needs doing
|
# - item = needs doing.
|
||||||
# + item = implemented, needs testing
|
# + item = implemented, needs testing.
|
||||||
# X item = tested and done
|
# X item = tested and done.
|
||||||
#
|
#
|
||||||
# Non-negotiable target state:
|
# Constraints:
|
||||||
# - Project name is Skald everywhere.
|
# - Keep recordings as one mixed Ogg Opus file per hall recording session.
|
||||||
# - Halls replace groups/rooms everywhere user-facing and in internal code
|
# - Preserve clear recording start/stop announcements and operator feedback.
|
||||||
# where practical.
|
# - Do not block WebRTC packet handling on recording work.
|
||||||
# - Video is removed, not hidden.
|
# - Favor small, testable stages over one large rewrite.
|
||||||
# - Incoming video is rejected.
|
# - Keep live-server safety in mind; staged improvements should be deployable
|
||||||
# - The browser UI is audio-only and accessible.
|
# independently.
|
||||||
# - Recordings are single mixed Ogg Opus audio files.
|
|
||||||
# - No legacy Galene compatibility paths or fallback shims unless explicitly
|
|
||||||
# added later by choice.
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# PHASE 0: BASELINE INVENTORY AND GUARDRAILS
|
# PHASE 0: BASELINE AND MEASUREMENT
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
X Record the current upstream/fork point in CHANGES or a new fork note
|
X Confirm the current live deployment can start and stop a Skald recording.
|
||||||
X Capture current baseline: `go test ./...` result before behavior changes
|
X Confirm migrated Galene rooms work as Skald halls on meet.stormux.org.
|
||||||
X Capture current baseline: `go build ./...` result before behavior changes
|
- Document the current recording pipeline in skald.md for maintainers:
|
||||||
X Inventory generated/static assets, including static/example and third-party assets
|
incoming Opus RTP -> per-source ffmpeg decoder -> PCM mixer -> ffmpeg Opus
|
||||||
X Inventory all public URLs: /group/, /public-groups.json, /recordings/, /galene-api/
|
encoder -> Ogg Opus file.
|
||||||
X Inventory all config/data paths: groups/, data/, recordings/, skaldctl.json
|
- Add lightweight recording metrics in logs or debug output:
|
||||||
X Inventory all Galene binary/service names in docs, scripts, examples, and tests
|
active recorded sources, dropped source frames, mixer write errors, ffmpeg
|
||||||
X Decide whether existing stored data will be migrated manually or treated as incompatible
|
decoder starts/stops, and encoder failures.
|
||||||
X Keep the old LICENSE intact and preserve required attribution
|
- Add a synthetic recording benchmark or smoke tool that can simulate multiple
|
||||||
|
active Opus sources without needing browser clients.
|
||||||
|
- Record baseline CPU, process count, and output quality for 1, 5, 10, and 25
|
||||||
|
active sources on a representative server.
|
||||||
|
- Define practical target limits for "normal hall", "large moderated hall",
|
||||||
|
and "stress test" recording scenarios.
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# PHASE 1: FOUNDATION AND TOTAL REBRANDING
|
# PHASE 1: MIX QUALITY BEFORE PIPELINE REWRITE
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
X Rename Go module: github.com/jech/galene -> git.stormux.org/storm/skald
|
- Add mixer tests for multiple simultaneous PCM sources, including clipping
|
||||||
X Update all internal Go import paths across source and tests
|
cases and quiet/loud source combinations.
|
||||||
X Rename galene.go -> skald.go and keep the main package entrypoint working
|
- Replace raw summing with a safer mix strategy:
|
||||||
X Rename galenectl/ directory and binary to skaldctl/
|
per-source attenuation, headroom, or another deterministic approach that
|
||||||
X Rename static/galene.css -> static/skald.css
|
reduces clipping when multiple users speak.
|
||||||
X Rename static/galene.js -> static/skald.js
|
- Add a simple limiter to prevent harsh clipping in the final mixed PCM.
|
||||||
X Rename static/galene.html -> static/skald.html
|
- Add tests proving the limiter preserves normal speech-level samples and
|
||||||
X Update webserver static serving to load skald.html and renamed assets
|
bounds overloaded samples.
|
||||||
X Update all HTML, JS, CSS, docs, tests, and examples that reference renamed assets
|
- Add optional per-source level tracking so logs or future UI can identify
|
||||||
X Rename galene-*.md docs to skald-*.md
|
sources that dominate the recording.
|
||||||
X Replace "Galene", "Galène", and "galene" with "Skald" and "skald" where they refer to this project
|
- Verify recordings with 2-5 simultaneous speakers sound cleaner than the
|
||||||
X Keep historical upstream references only where they are attribution, changelog history, or license context
|
current clamped sum.
|
||||||
X Rename default binary, service, and install examples from galene to skald
|
- Verify the limiter does not create pumping, distortion, or excessive volume
|
||||||
X Rename galenectl config file to skaldctl.json
|
loss for a single speaker.
|
||||||
X Update CHANGES with Skald fork point and first Skald development section
|
|
||||||
X Verify `go test ./...` compiles after the pure project rename pass
|
|
||||||
X Search for remaining Galene names and classify each survivor as attribution/history or a bug
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# PHASE 2: HALL TERMINOLOGY AND DATA MODEL
|
# PHASE 2: SOURCE LIFECYCLE AND SILENCE HANDLING
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
X Rename Go package group/ to hall/ if the package-wide rename stays tractable
|
- Track whether each recording source is active, silent, stalled, or closed.
|
||||||
X Rename package declarations, types, variables, funcs, tests, and comments from group to hall
|
- Treat missing frames as silence without blocking the mixer.
|
||||||
X Rename Group type to Hall and update all compile errors intentionally
|
- Drop or de-prioritize stale silent sources after a short timeout.
|
||||||
X Rename group description files/concepts to hall description files/concepts
|
- Ensure mute/unmute transitions do not leave stale audio in the mixed output.
|
||||||
X Rename default directory flag: -groups -> -halls
|
- Ensure participant join/leave during recording never stops the recording file.
|
||||||
X Rename default data directory: groups/ -> halls/
|
- Add tests for source join, leave, mute, unmute, stall, and recovery during an
|
||||||
X Rename public listing concept from public groups to public halls
|
active recording.
|
||||||
X Rename subgroups to subhalls everywhere, including JSON fields and commands
|
- Verify a long recording remains valid when sources repeatedly join and leave.
|
||||||
X Rename auto-subgroups -> auto-subhalls
|
|
||||||
X Rename allow-subgroups -> allow-subhalls
|
|
||||||
X Rename include-subgroups -> include-subhalls
|
|
||||||
X Rename group action names to hall action names where exposed to clients
|
|
||||||
X Rename chat/help commands such as /subgroups to /subhalls
|
|
||||||
X Update tests to use generic placeholder names, not real usernames
|
|
||||||
X Verify no runtime user-facing "group", "groups", "room", or "rooms" remain except attribution/history
|
|
||||||
X Verify old group JSON/config paths are not silently accepted unless explicitly reintroduced later
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# PHASE 3: URLS, API, PROTOCOL, AND CONTROL TOOL
|
# PHASE 3: REDUCE PROCESS OVERHEAD
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
X Rename browser hall URLs from /group/name/ to /hall/name/
|
- Evaluate Go-native Opus decode options against the current ffmpeg decoder
|
||||||
X Rename /public-groups.json to /public-halls.json
|
subprocesses:
|
||||||
X Rename /galene-api/ to /skald-api/
|
sound quality, maintenance risk, packaging impact, CGO requirements, and CPU.
|
||||||
X Rename API collection paths from .groups to .halls
|
- Decide whether in-process Opus decoding is worth the dependency and packaging
|
||||||
X Rename protocol join fields from group to hall
|
tradeoff.
|
||||||
X Rename protocol callbacks/events from joined group semantics to joined hall semantics
|
- If keeping ffmpeg decoders, pool or supervise decoder processes more
|
||||||
X Update static/protocol.js public API: ServerConnection.join and related callbacks
|
deliberately and improve failure recovery.
|
||||||
X Update static/management.js to use /skald-api/v0/.halls/
|
- If replacing ffmpeg decoders, implement one in-process decoder path behind
|
||||||
X Update static/mainpage.js to open /hall/ URLs and fetch /public-halls.json
|
tests before removing the subprocess path.
|
||||||
X Update stats endpoint naming only if it exposes Skald/group terminology
|
- Verify recording still handles dynamic payload type, RTP sequence behavior,
|
||||||
X Update skaldctl command names: create-hall, update-hall, delete-hall, list-halls
|
packet loss, and source restarts.
|
||||||
X Update skaldctl flags: -hall, -include-subhalls, -auto-subhalls, etc.
|
- Measure CPU and process-count improvement with 10+ active sources.
|
||||||
X Update skaldctl help text and error messages
|
- Keep one final encoder path to Ogg Opus unless there is a proven reason to
|
||||||
X Update API and webserver tests for /hall/ and /skald-api/
|
replace it too.
|
||||||
X Verify old /group/, /public-groups.json, /galene-api/, and .groups paths fail clearly
|
|
||||||
X Verify runtime protocol backward compatibility with Skald is intentionally broken
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# PHASE 4: BACKEND - STRIP VIDEO COMPLETELY
|
# PHASE 4: ENCODER AND CONTAINER HARDENING
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
+ Remove video codec constants and helpers from codecs/ package
|
- Decide whether the final Ogg Opus encoder should remain ffmpeg or move
|
||||||
+ Remove or rewrite video-only codec tests
|
in-process later.
|
||||||
+ Strip video/vp8, video/vp9, video/av1, and video/h264 from SDP negotiation
|
- Make encoder startup errors operator-visible and log the exact ffmpeg error.
|
||||||
+ Ensure audio/opus remains negotiated correctly
|
- Ensure encoder stdin writes cannot block the mixer indefinitely.
|
||||||
+ Reject incoming video m-lines/transceivers at WebRTC peer connection setup
|
- Ensure recorder close handles encoder failure, stdin close failure, and wait
|
||||||
+ Reject or close incoming video tracks if a browser still sends them
|
failure without leaving misleading successful output.
|
||||||
+ Ensure rejection is explicit in logs without crashing the hall
|
- Verify interrupted or failed recordings are either clearly marked as failed or
|
||||||
+ Remove outgoing video track routing from rtpconn/ and hall/
|
produce a playable partial file.
|
||||||
+ Remove simulcast and SVC handling
|
- Verify completed files with ffprobe or equivalent in tests where available.
|
||||||
+ Remove video bandwidth estimation and video throughput selection
|
- Verify completed files play in common players after long recordings.
|
||||||
+ Remove video-specific RTCP behavior such as PLI/FIR/keyframe requests
|
|
||||||
+ Remove screenshare backend support
|
|
||||||
+ Remove presentation/file-as-video support if it depends on video tracks
|
|
||||||
+ Remove background blur support and background-blur-worker.js from the served app
|
|
||||||
X Remove video fields from hall descriptions, stats, API responses, and protocol docs
|
|
||||||
+ Check WHIP/WHEP-style endpoints for video assumptions and either make audio-only or remove
|
|
||||||
+ Ensure browsers that request audio+video get a working audio-only session or a clear video rejection
|
|
||||||
X Verify two browser clients can join one hall and exchange audio only
|
|
||||||
X Verify a client attempting camera or screenshare cannot publish video
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# PHASE 5: FRONTEND - AUDIO-ONLY USER EXPERIENCE
|
# PHASE 5: LARGE-HALL OPERATIONS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
+ Remove all <video> elements and video containers from skald.html
|
- Add documentation for recommended large-hall permissions:
|
||||||
+ Remove camera buttons and presentation buttons tied to camera/video behavior
|
most users observe/message, only presenters publish audio, operators control
|
||||||
+ Remove screenshare button and related menu commands
|
recording.
|
||||||
+ Remove video quality, resolution, throughput, simulcast, and collapse controls
|
- Document expected scaling limits in terms of active speakers, not just total
|
||||||
+ Remove video device selector and camera permission flow
|
connected users.
|
||||||
+ Remove hideVideo(), showVideo(), getMaxVideoThroughput(), and related JS paths
|
- Add guidance for max-clients, TURN bandwidth, CPU monitoring, and recording
|
||||||
+ Remove background blur UI, worker references, and CSS
|
expectations for 50, 100, and 250 user events.
|
||||||
+ Keep microphone mute/unmute behavior intact
|
- Add an operator-facing warning or log message when active recorded sources
|
||||||
+ Keep audio input and output device selection intact
|
exceed a conservative threshold.
|
||||||
+ Keep chat, user list, moderation, locking, recording controls, and status messages
|
- Test a large moderated hall with many listeners and a few active speakers.
|
||||||
+ Rename visible text in skald.html, index.html, stats.html, mainpage.js, management.js, and protocol errors
|
- Test a stress hall with many active speakers and document the observed limit.
|
||||||
+ Replace public groups UI with public halls UI
|
|
||||||
+ Remove video CSS from skald.css and common.css
|
|
||||||
- Check layout after removing the video pane; no dead space or hidden required controls
|
|
||||||
- Preserve keyboard access with Tab and Shift+Tab through all controls
|
|
||||||
- Ensure controls have labels or accessible names after icon/button cleanup
|
|
||||||
X Verify audio-only UI loads without 404s for renamed assets
|
|
||||||
X Verify joining, muting, chat, recording controls, and leaving work from keyboard
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# PHASE 6: RECORDING REWRITE - SINGLE MIXED OGG OPUS
|
# PHASE 6: FINAL CLEANUP AND RELEASE READINESS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
+ Remove diskwriter dependency on github.com/at-wat/ebml-go and WebM/Matroska writing
|
- Update skald.md and skald-install.md with the final recording architecture.
|
||||||
+ Remove per-track/per-participant WebM recording logic
|
- Update package dependencies if recording dependency choices change.
|
||||||
+ Remove video recording fields such as hasVideo, width, height, keyframes, and video MIME handling
|
- Run focused recording tests and webserver recording-control tests.
|
||||||
X Decide final mixer implementation before coding:
|
- Run `go test ./...` in a clean tree without distro package build artifacts.
|
||||||
* Decode all active Opus RTP audio to 48 kHz PCM
|
- Run live browser testing for recording start, stop, join, leave, mute, and
|
||||||
* Maintain a per-source jitter/timing buffer so join/leave does not corrupt the mix
|
playback.
|
||||||
* Mix to stereo with clipping prevention or limiting
|
- Do a final review for accidental regressions in recording announcements,
|
||||||
* Insert silence for missing frames rather than blocking the mixer
|
hall permissions, and migrated-hall compatibility.
|
||||||
* Encode mixed PCM to Ogg Opus
|
|
||||||
+ Evaluate Go-native decode/encode/container libraries versus ffmpeg subprocess
|
|
||||||
+ If using ffmpeg, add a startup check and clear admin/operator error when ffmpeg is missing
|
|
||||||
+ If using ffmpeg, use nonblocking stdin writes and drain stderr safely
|
|
||||||
X If using ffmpeg, command shape starts from:
|
|
||||||
ffmpeg -f s16le -ar 48000 -ac 2 -i pipe:0 -c:a libopus -b:a 128k output.ogg
|
|
||||||
+ Ensure recording work cannot block WebRTC packet handling
|
|
||||||
+ Name recording files skald-recording-YYYYMMDD-HHMMSS.ogg
|
|
||||||
+ Avoid usernames or live personal identifiers in recording filenames
|
|
||||||
+ Store one recording file per hall recording session
|
|
||||||
+ Close recordings cleanly on explicit stop
|
|
||||||
+ Close recordings cleanly when the hall empties
|
|
||||||
+ Handle participant join/leave during recording without stopping the file
|
|
||||||
+ Handle mute/unmute during recording without stale audio
|
|
||||||
X Handle recording start when no one is speaking
|
|
||||||
X Add focused mixer/recording tests with synthetic audio where possible
|
|
||||||
X Verify produced Ogg Opus file with `ffprobe` or equivalent
|
|
||||||
- Verify produced file plays in a standard audio player
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# PHASE 7: CONFIGURATION, INSTALLATION, AND DEPLOYMENT SURFACES
|
# PHASE 7: WEB UI AUDIO DEFAULTS AND EVENT SOUNDS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
X Update command-line flags and help output for Skald and halls
|
+ Default new sessions to high-quality audio.
|
||||||
X Update default examples for halls/, data/, recordings/, and skaldctl.json
|
+ Default new sessions to no browser noise suppression, echo cancellation, or
|
||||||
X Update systemd service examples to skald user, binary, and paths
|
automatic gain control.
|
||||||
X Update reverse proxy examples to /hall/ and /skald-api/
|
+ Ensure the default microphone path also receives explicit audio constraints.
|
||||||
X Update TURN/STUN docs only where project name or URLs changed
|
+ Add generated browser notification sounds without binary sound assets.
|
||||||
X Update Docker/container or packaging files if present later
|
+ Add a user-facing Notification sounds setting.
|
||||||
X Update .gitignore for renamed binaries, config files, and generated artifacts
|
+ Play generated sounds for recording start, recording stop, raised hand,
|
||||||
X Verify there are no stale install commands that build or run skald/skaldctl
|
lowered hand, user join, user leave, and incoming chat message.
|
||||||
|
- Verify sounds play after login in Chromium.
|
||||||
# ============================================================================
|
- Verify sounds play after login in Firefox.
|
||||||
# PHASE 8: DOCUMENTATION
|
- Verify sounds never replace screen-reader live-region announcements.
|
||||||
# ============================================================================
|
- Tune sound levels and shapes with live users.
|
||||||
|
|
||||||
X Rewrite README.md to describe Skald as audio-only hall conferencing
|
|
||||||
X Rewrite skald-install.md for new project name, binary names, and hall paths
|
|
||||||
X Rewrite skald.md admin/usage docs for audio-only behavior
|
|
||||||
X Rewrite skald-client.md for audio-only client authors
|
|
||||||
X Rewrite skald-protocol.md for hall terminology and audio-only tracks
|
|
||||||
X Rewrite skald-api.md for /skald-api/ and .halls paths
|
|
||||||
X Remove video, camera, screenshare, simulcast, SVC, and WebM recording instructions
|
|
||||||
X Add Ogg Opus recording documentation
|
|
||||||
X Document that incoming video is rejected by design
|
|
||||||
X Document that Skald protocol/API compatibility is intentionally broken
|
|
||||||
X Document any required external ffmpeg dependency if the recorder uses ffmpeg
|
|
||||||
X Check docs for old real-person sample usernames and replace with placeholders
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# PHASE 9: VERIFICATION AND CLEANUP
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
X Run `go test ./...` and fix failures caused by the fork work
|
|
||||||
X Run `go vet ./...` and resolve real issues
|
|
||||||
X Run focused webserver/API tests for /hall/ and /skald-api/
|
|
||||||
X Run focused protocol/frontend checks if the project has a JS typecheck/test path
|
|
||||||
X Search source/docs for remaining Galene names
|
|
||||||
X Search source/docs for remaining /group/, .groups, public-groups, galenectl, and galene-api
|
|
||||||
X Search source/docs for remaining video, camera, screenshare, simulcast, WebM, and Matroska references
|
|
||||||
X Search source/docs for remaining group/room terminology in user-facing surfaces
|
|
||||||
X Classify allowed survivors as attribution/history/third-party icon names only
|
|
||||||
X Build skald binary
|
|
||||||
X Build skaldctl binary
|
|
||||||
X Run a local server and verify static pages load without 404s
|
|
||||||
X Test: create a hall with skaldctl
|
|
||||||
X Test: list halls with skaldctl
|
|
||||||
X Test: join one hall from two browsers and confirm two-way audio
|
|
||||||
X Test: attempt to publish video and confirm rejection
|
|
||||||
X Test: start recording and confirm one Ogg Opus file appears
|
|
||||||
- Test: join a third participant during recording and confirm the mix continues
|
|
||||||
- Test: mute/unmute during recording and confirm the output reflects it
|
|
||||||
X Test: leave all participants and confirm recording stops cleanly
|
|
||||||
- Test: play the recording in a standard audio player
|
|
||||||
X Test: restart server and verify halls/config load from the new paths
|
|
||||||
X Remove obsolete files that are no longer served or built
|
|
||||||
- Do a final diff review for accidental compatibility shims or stale Skald naming
|
|
||||||
|
|||||||
Reference in New Issue
Block a user