Render radio mono/left/right as centered mono output
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
// Maintainer-controlled web client version.
|
// Maintainer-controlled web client version.
|
||||||
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
||||||
window.CHGRID_WEB_VERSION = "2026.02.21 R78";
|
window.CHGRID_WEB_VERSION = "2026.02.21 R79";
|
||||||
|
|||||||
@@ -147,6 +147,13 @@ type SharedRadioSource = {
|
|||||||
};
|
};
|
||||||
type ItemRadioOutput = {
|
type ItemRadioOutput = {
|
||||||
streamUrl: string;
|
streamUrl: string;
|
||||||
|
channel: RadioChannelMode;
|
||||||
|
sharedSource: MediaElementAudioSourceNode;
|
||||||
|
sourceInput: GainNode;
|
||||||
|
channelSplitter: ChannelSplitterNode | null;
|
||||||
|
channelMerger: ChannelMergerNode | null;
|
||||||
|
channelLeftGain: GainNode | null;
|
||||||
|
channelRightGain: GainNode | null;
|
||||||
effectInput: GainNode;
|
effectInput: GainNode;
|
||||||
effectRuntime: EffectRuntime | null;
|
effectRuntime: EffectRuntime | null;
|
||||||
effect: EffectId;
|
effect: EffectId;
|
||||||
@@ -469,6 +476,24 @@ function getOrCreateSharedRadioSource(streamUrl: string): SharedRadioSource | nu
|
|||||||
function cleanupRadioRuntime(itemId: string): void {
|
function cleanupRadioRuntime(itemId: string): void {
|
||||||
const output = itemRadioOutputs.get(itemId);
|
const output = itemRadioOutputs.get(itemId);
|
||||||
if (!output) return;
|
if (!output) return;
|
||||||
|
if (output.channelSplitter) {
|
||||||
|
try {
|
||||||
|
output.sharedSource.disconnect(output.channelSplitter);
|
||||||
|
} catch {
|
||||||
|
// Ignore stale graph disconnects.
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
output.sharedSource.disconnect(output.sourceInput);
|
||||||
|
} catch {
|
||||||
|
// Ignore stale graph disconnects.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.channelLeftGain?.disconnect();
|
||||||
|
output.channelRightGain?.disconnect();
|
||||||
|
output.channelSplitter?.disconnect();
|
||||||
|
output.channelMerger?.disconnect();
|
||||||
|
output.sourceInput.disconnect();
|
||||||
output.effectInput.disconnect();
|
output.effectInput.disconnect();
|
||||||
disconnectEffectRuntime(output.effectRuntime);
|
disconnectEffectRuntime(output.effectRuntime);
|
||||||
output.gain.disconnect();
|
output.gain.disconnect();
|
||||||
@@ -496,6 +521,65 @@ function normalizeRadioChannel(channel: unknown): RadioChannelMode {
|
|||||||
return (RADIO_CHANNEL_OPTIONS as readonly string[]).includes(normalized) ? normalized : 'stereo';
|
return (RADIO_CHANNEL_OPTIONS as readonly string[]).includes(normalized) ? normalized : 'stereo';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function connectRadioChannelSource(
|
||||||
|
audioCtx: AudioContext,
|
||||||
|
sharedSource: MediaElementAudioSourceNode,
|
||||||
|
channel: RadioChannelMode,
|
||||||
|
destination: GainNode,
|
||||||
|
): {
|
||||||
|
sourceInput: GainNode;
|
||||||
|
channelSplitter: ChannelSplitterNode | null;
|
||||||
|
channelMerger: ChannelMergerNode | null;
|
||||||
|
channelLeftGain: GainNode | null;
|
||||||
|
channelRightGain: GainNode | null;
|
||||||
|
} {
|
||||||
|
const sourceInput = audioCtx.createGain();
|
||||||
|
sourceInput.gain.value = 1;
|
||||||
|
|
||||||
|
if (channel === 'stereo') {
|
||||||
|
sharedSource.connect(sourceInput);
|
||||||
|
sourceInput.connect(destination);
|
||||||
|
return {
|
||||||
|
sourceInput,
|
||||||
|
channelSplitter: null,
|
||||||
|
channelMerger: null,
|
||||||
|
channelLeftGain: null,
|
||||||
|
channelRightGain: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitter = audioCtx.createChannelSplitter(2);
|
||||||
|
const merger = audioCtx.createChannelMerger(1);
|
||||||
|
sharedSource.connect(splitter);
|
||||||
|
|
||||||
|
let leftGain: GainNode | null = null;
|
||||||
|
let rightGain: GainNode | null = null;
|
||||||
|
if (channel === 'mono') {
|
||||||
|
leftGain = audioCtx.createGain();
|
||||||
|
rightGain = audioCtx.createGain();
|
||||||
|
leftGain.gain.value = 0.5;
|
||||||
|
rightGain.gain.value = 0.5;
|
||||||
|
splitter.connect(leftGain, 0);
|
||||||
|
splitter.connect(rightGain, 1);
|
||||||
|
leftGain.connect(merger, 0, 0);
|
||||||
|
rightGain.connect(merger, 0, 0);
|
||||||
|
} else if (channel === 'left') {
|
||||||
|
splitter.connect(merger, 0, 0);
|
||||||
|
} else {
|
||||||
|
splitter.connect(merger, 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
merger.connect(sourceInput);
|
||||||
|
sourceInput.connect(destination);
|
||||||
|
return {
|
||||||
|
sourceInput,
|
||||||
|
channelSplitter: splitter,
|
||||||
|
channelMerger: merger,
|
||||||
|
channelLeftGain: leftGain,
|
||||||
|
channelRightGain: rightGain,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function applyRadioEffect(
|
function applyRadioEffect(
|
||||||
output: ItemRadioOutput,
|
output: ItemRadioOutput,
|
||||||
audioCtx: AudioContext,
|
audioCtx: AudioContext,
|
||||||
@@ -529,7 +613,8 @@ async function ensureRadioRuntime(item: WorldItem): Promise<void> {
|
|||||||
if (!audioCtx) return;
|
if (!audioCtx) return;
|
||||||
|
|
||||||
const existing = itemRadioOutputs.get(item.id);
|
const existing = itemRadioOutputs.get(item.id);
|
||||||
if (existing && existing.streamUrl === streamUrl) {
|
const channel = normalizeRadioChannel(item.params.channel);
|
||||||
|
if (existing && existing.streamUrl === streamUrl && existing.channel === channel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -542,7 +627,7 @@ async function ensureRadioRuntime(item: WorldItem): Promise<void> {
|
|||||||
const gain = audioCtx.createGain();
|
const gain = audioCtx.createGain();
|
||||||
gain.gain.value = 0;
|
gain.gain.value = 0;
|
||||||
const effectInput = audioCtx.createGain();
|
const effectInput = audioCtx.createGain();
|
||||||
shared.source.connect(effectInput);
|
const channelSource = connectRadioChannelSource(audioCtx, shared.source, channel, effectInput);
|
||||||
const effect = normalizeRadioEffect(item.params.effect);
|
const effect = normalizeRadioEffect(item.params.effect);
|
||||||
const effectValue = normalizeRadioEffectValue(item.params.effectValue);
|
const effectValue = normalizeRadioEffectValue(item.params.effectValue);
|
||||||
const effectRuntime = connectEffectChain(audioCtx, effectInput, gain, effect, effectValue);
|
const effectRuntime = connectEffectChain(audioCtx, effectInput, gain, effect, effectValue);
|
||||||
@@ -553,7 +638,22 @@ async function ensureRadioRuntime(item: WorldItem): Promise<void> {
|
|||||||
} else {
|
} else {
|
||||||
gain.connect(audioCtx.destination);
|
gain.connect(audioCtx.destination);
|
||||||
}
|
}
|
||||||
itemRadioOutputs.set(item.id, { streamUrl, effectInput, effectRuntime, effect, effectValue, gain, panner });
|
itemRadioOutputs.set(item.id, {
|
||||||
|
streamUrl,
|
||||||
|
channel,
|
||||||
|
sharedSource: shared.source,
|
||||||
|
sourceInput: channelSource.sourceInput,
|
||||||
|
channelSplitter: channelSource.channelSplitter,
|
||||||
|
channelMerger: channelSource.channelMerger,
|
||||||
|
channelLeftGain: channelSource.channelLeftGain,
|
||||||
|
channelRightGain: channelSource.channelRightGain,
|
||||||
|
effectInput,
|
||||||
|
effectRuntime,
|
||||||
|
effect,
|
||||||
|
effectValue,
|
||||||
|
gain,
|
||||||
|
panner,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncRadioStationPlayback(): Promise<void> {
|
async function syncRadioStationPlayback(): Promise<void> {
|
||||||
@@ -605,12 +705,8 @@ function updateRadioStationSpatialAudio(): void {
|
|||||||
output.gain.gain.linearRampToValueAtTime(gainValue * normalizedVolume, audioCtx.currentTime + 0.1);
|
output.gain.gain.linearRampToValueAtTime(gainValue * normalizedVolume, audioCtx.currentTime + 0.1);
|
||||||
if (output.panner) {
|
if (output.panner) {
|
||||||
let resolvedPan = Math.max(-1, Math.min(1, panValue));
|
let resolvedPan = Math.max(-1, Math.min(1, panValue));
|
||||||
if (channel === 'mono') {
|
if (channel !== 'stereo') {
|
||||||
resolvedPan = 0;
|
resolvedPan = 0;
|
||||||
} else if (channel === 'left') {
|
|
||||||
resolvedPan = -1;
|
|
||||||
} else if (channel === 'right') {
|
|
||||||
resolvedPan = 1;
|
|
||||||
}
|
}
|
||||||
output.panner.pan.linearRampToValueAtTime(resolvedPan, audioCtx.currentTime + 0.1);
|
output.panner.pan.linearRampToValueAtTime(resolvedPan, audioCtx.currentTime + 0.1);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user