Skip to content

Commit 7369a2a

Browse files
Tim020claude
andauthored
feat: Allow multiple microphones per character (#797)
Implements issue #796 to support assigning multiple microphones to a single character in a scene. This enables primary/backup microphone configurations for lead characters who aren't offstage long enough to swap mic packs. Changes: - Remove validation preventing multiple mics per character in MicAllocations.vue - Update view mode to display comma-separated mic names - Enhance conflict tooltips to show per-mic conflict details - Update Timeline 'By Character' and 'By Cast' modes to display stacked bars - Add documentation for multi-mic feature and use cases Maintains constraint that one mic can only be assigned to one character per scene. Conflict detection tracks conflicts individually per microphone. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 9893169 commit 7369a2a

File tree

3 files changed

+168
-71
lines changed

3 files changed

+168
-71
lines changed

client/src/vue_components/show/config/mics/MicAllocations.vue

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -117,19 +117,18 @@
117117
:id="`cell-${data.item.Character}-${scene.id}`"
118118
:key="scene.id"
119119
class="allocation-cell"
120-
:class="getConflictClass(getConflictForCell(data.item.Character, scene.id))"
120+
:class="getConflictClassForCell(data.item.Character, scene.id)"
121121
>
122122
{{ allocationByCharacter[data.item.Character][scene.id] }}
123123
<b-icon-exclamation-triangle
124-
v-if="getConflictForCell(data.item.Character, scene.id)"
124+
v-if="getConflictsForCell(data.item.Character, scene.id).length > 0"
125125
class="conflict-icon"
126126
/>
127127
<b-tooltip
128-
v-if="getConflictForCell(data.item.Character, scene.id)"
129128
:target="`cell-${data.item.Character}-${scene.id}`"
130129
triggers="hover"
131130
>
132-
{{ getConflictForCell(data.item.Character, scene.id).message }}
131+
{{ getTooltipText(data.item.Character, scene.id) }}
133132
</b-tooltip>
134133
</div>
135134
</template>
@@ -236,21 +235,30 @@ export default {
236235
},
237236
allocationByCharacter() {
238237
const charData = {};
238+
// Initialize with empty arrays for each character/scene combination
239239
this.CHARACTER_LIST.map((character) => (character.id)).forEach((characterId) => {
240240
const sceneData = {};
241241
this.sortedScenes.map((scene) => (scene.id)).forEach((sceneId) => {
242-
sceneData[sceneId] = null;
242+
sceneData[sceneId] = [];
243243
});
244244
charData[characterId] = sceneData;
245245
}, this);
246+
// Collect all mics assigned to each character in each scene
246247
Object.keys(this.MIC_ALLOCATIONS).forEach((micId) => {
247248
this.sortedScenes.map((scene) => (scene.id)).forEach((sceneId) => {
248249
if (this.allAllocations[micId][sceneId] != null) {
249-
charData[this.allAllocations[micId][sceneId]][
250-
sceneId] = this.MICROPHONE_BY_ID(micId).name;
250+
const characterId = this.allAllocations[micId][sceneId];
251+
charData[characterId][sceneId].push(this.MICROPHONE_BY_ID(micId).name);
251252
}
252253
}, this);
253254
}, this);
255+
// Convert arrays to comma-separated strings (or null if empty)
256+
Object.keys(charData).forEach((characterId) => {
257+
Object.keys(charData[characterId]).forEach((sceneId) => {
258+
const mics = charData[characterId][sceneId];
259+
charData[characterId][sceneId] = mics.length > 0 ? mics.join(', ') : null;
260+
});
261+
});
254262
return charData;
255263
},
256264
...mapGetters(['MICROPHONES', 'CURRENT_SHOW', 'ACT_BY_ID', 'SCENE_BY_ID', 'CHARACTER_LIST',
@@ -291,21 +299,13 @@ export default {
291299
return true;
292300
}
293301
294-
let disabled = false;
295302
// Check this mic isn't allocated to anyone else for this scene
296303
if (this.internalState[micId][sceneId] != null
297304
&& this.internalState[micId][sceneId] !== characterId) {
298305
return true;
299306
}
300-
// Check another mic isn't already assigned for this scene
301-
this.MICROPHONES.map((mic) => (mic.id)).forEach((innerMicId) => {
302-
if (this.internalState[innerMicId][sceneId] != null
303-
&& this.internalState[innerMicId][sceneId] === characterId
304-
&& innerMicId !== micId) {
305-
disabled = true;
306-
}
307-
}, this);
308-
return disabled;
307+
308+
return false;
309309
},
310310
toggleAllocation(micId, sceneId, characterId) {
311311
if (this.internalState[micId][sceneId] === characterId) {
@@ -337,6 +337,56 @@ export default {
337337
if (!conflict) return '';
338338
return conflict.severity === 'WARNING' ? 'conflict-warning' : 'conflict-info';
339339
},
340+
341+
getConflictsForCell(characterId, sceneId) {
342+
// Find all conflicts for this character in this scene (across all their mics)
343+
const allConflicts = Object.values(this.CONFLICTS_BY_SCENE).flat();
344+
if (!allConflicts || allConflicts.length === 0) {
345+
return [];
346+
}
347+
348+
// Find all conflicts where this scene is the "change INTO" scene for this character
349+
return allConflicts.filter((c) => c.adjacentSceneId === sceneId
350+
&& c.adjacentCharacterId === characterId);
351+
},
352+
353+
getConflictClassForCell(characterId, sceneId) {
354+
const conflicts = this.getConflictsForCell(characterId, sceneId);
355+
if (conflicts.length === 0) return '';
356+
357+
// Prioritize WARNING over INFO
358+
const hasWarning = conflicts.some((c) => c.severity === 'WARNING');
359+
return hasWarning ? 'conflict-warning' : 'conflict-info';
360+
},
361+
362+
getTooltipText(characterId, sceneId) {
363+
// Get all mics assigned to this character in this scene
364+
const mics = [];
365+
Object.keys(this.MIC_ALLOCATIONS).forEach((micId) => {
366+
if (this.allAllocations[micId][sceneId] === characterId) {
367+
mics.push({
368+
id: parseInt(micId, 10),
369+
name: this.MICROPHONE_BY_ID(micId).name,
370+
});
371+
}
372+
});
373+
374+
// Get conflicts for this character/scene
375+
const conflicts = this.getConflictsForCell(characterId, sceneId);
376+
377+
// Build tooltip text
378+
let tooltipText = `Assigned mics: ${mics.map((m) => m.name).join(', ')}`;
379+
380+
if (conflicts.length > 0) {
381+
tooltipText += '\n\nConflicts:';
382+
conflicts.forEach((conflict) => {
383+
const micName = this.MICROPHONE_BY_ID(conflict.micId).name;
384+
tooltipText += `\n${micName}: ${conflict.message}`;
385+
});
386+
}
387+
388+
return tooltipText;
389+
},
340390
...mapActions(['UPDATE_MIC_ALLOCATIONS', 'GET_MIC_ALLOCATIONS']),
341391
},
342392
};
@@ -356,6 +406,10 @@ export default {
356406
padding: 0.25rem 0.5rem;
357407
border-radius: 0.25rem;
358408
min-width: 3rem;
409+
max-width: 15rem;
410+
overflow: hidden;
411+
text-overflow: ellipsis;
412+
white-space: nowrap;
359413
}
360414
361415
.conflict-warning {

client/src/vue_components/show/config/mics/MicTimeline.vue

Lines changed: 74 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -438,12 +438,13 @@ export default {
438438
},
439439
generateBarsForCharacter(characterId, rowIndex, bars) {
440440
// Find all mics allocated to this character
441-
const segments = [];
441+
const segmentsByMic = new Map();
442442
443443
Object.keys(this.allocations).forEach((micId) => {
444444
const micAllocs = this.allocations[micId];
445445
if (!Array.isArray(micAllocs)) return;
446446
447+
const segments = [];
447448
let currentSegment = null;
448449
449450
this.scenes.forEach((scene, sceneIndex) => {
@@ -467,30 +468,41 @@ export default {
467468
});
468469
469470
if (currentSegment) segments.push(currentSegment);
470-
});
471471
472-
// Create bars
473-
segments.forEach((segment, segmentIndex) => {
474-
const mic = this.MICROPHONE_BY_ID(segment.micId);
475-
const startX = this.getSceneX(segment.startIndex);
476-
const width = this.sceneWidth * (segment.endIndex - segment.startIndex + 1);
472+
if (segments.length > 0) {
473+
segmentsByMic.set(parseInt(micId, 10), segments);
474+
}
475+
});
477476
478-
bars.push({
479-
id: `char-${characterId}-seg-${segmentIndex}`,
480-
x: startX,
481-
y: this.getRowY(rowIndex) + this.barPadding,
482-
width,
483-
height: this.rowHeight - 2 * this.barPadding,
484-
color: this.getColorForEntity(segment.micId, 'mic'),
485-
cssClass: '',
486-
label: mic?.name || `Mic ${segment.micId}`,
487-
tooltip: `${mic?.name || `Mic ${segment.micId}`} (Scenes ${segment.startIndex + 1}-${segment.endIndex + 1})`,
488-
data: {
489-
characterId,
490-
micId: segment.micId,
491-
startScene: segment.startIndex,
492-
endScene: segment.endIndex,
493-
},
477+
// Create bars with vertical stacking for multiple mics
478+
const micIds = Array.from(segmentsByMic.keys());
479+
const barHeightPerMic = (this.rowHeight - 2 * this.barPadding) / Math.max(micIds.length, 1);
480+
481+
micIds.forEach((micId, micIndex) => {
482+
const segments = segmentsByMic.get(micId);
483+
segments.forEach((segment, segmentIndex) => {
484+
const mic = this.MICROPHONE_BY_ID(segment.micId);
485+
const startX = this.getSceneX(segment.startIndex);
486+
const width = this.sceneWidth * (segment.endIndex - segment.startIndex + 1);
487+
const yOffset = micIndex * barHeightPerMic;
488+
489+
bars.push({
490+
id: `char-${characterId}-mic-${micId}-seg-${segmentIndex}`,
491+
x: startX,
492+
y: this.getRowY(rowIndex) + this.barPadding + yOffset,
493+
width,
494+
height: barHeightPerMic,
495+
color: this.getColorForEntity(characterId, 'character'),
496+
cssClass: '',
497+
label: mic?.name || `Mic ${segment.micId}`,
498+
tooltip: `${mic?.name || `Mic ${segment.micId}`} (Scenes ${segment.startIndex + 1}-${segment.endIndex + 1})`,
499+
data: {
500+
characterId,
501+
micId: segment.micId,
502+
startScene: segment.startIndex,
503+
endScene: segment.endIndex,
504+
},
505+
});
494506
});
495507
});
496508
},
@@ -500,13 +512,14 @@ export default {
500512
(char) => char.cast_member?.id === castId,
501513
);
502514
503-
const segments = [];
515+
const segmentsByMic = new Map();
504516
505517
// For each mic, find allocations for any character played by this cast member
506518
Object.keys(this.allocations).forEach((micId) => {
507519
const micAllocs = this.allocations[micId];
508520
if (!Array.isArray(micAllocs)) return;
509521
522+
const segments = [];
510523
let currentSegment = null;
511524
512525
this.scenes.forEach((scene, sceneIndex) => {
@@ -537,34 +550,45 @@ export default {
537550
});
538551
539552
if (currentSegment) segments.push(currentSegment);
540-
});
541553
542-
// Create bars
543-
segments.forEach((segment, segmentIndex) => {
544-
const mic = this.MICROPHONE_BY_ID(segment.micId);
545-
const characterNames = Array.from(segment.characterIds)
546-
.map((charId) => this.CHARACTER_BY_ID(charId)?.name || 'Unknown')
547-
.join(', ');
548-
const startX = this.getSceneX(segment.startIndex);
549-
const width = this.sceneWidth * (segment.endIndex - segment.startIndex + 1);
554+
if (segments.length > 0) {
555+
segmentsByMic.set(parseInt(micId, 10), segments);
556+
}
557+
});
550558
551-
bars.push({
552-
id: `cast-${castId}-seg-${segmentIndex}`,
553-
x: startX,
554-
y: this.getRowY(rowIndex) + this.barPadding,
555-
width,
556-
height: this.rowHeight - 2 * this.barPadding,
557-
color: this.getColorForEntity(segment.micId, 'mic'),
558-
cssClass: '',
559-
label: mic?.name || `Mic ${segment.micId}`,
560-
tooltip: `${mic?.name || `Mic ${segment.micId}`} - ${characterNames} (Scenes ${segment.startIndex + 1}-${segment.endIndex + 1})`,
561-
data: {
562-
castId,
563-
micId: segment.micId,
564-
characterIds: Array.from(segment.characterIds),
565-
startScene: segment.startIndex,
566-
endScene: segment.endIndex,
567-
},
559+
// Create bars with vertical stacking for multiple mics
560+
const micIds = Array.from(segmentsByMic.keys());
561+
const barHeightPerMic = (this.rowHeight - 2 * this.barPadding) / Math.max(micIds.length, 1);
562+
563+
micIds.forEach((micId, micIndex) => {
564+
const segments = segmentsByMic.get(micId);
565+
segments.forEach((segment, segmentIndex) => {
566+
const mic = this.MICROPHONE_BY_ID(segment.micId);
567+
const characterNames = Array.from(segment.characterIds)
568+
.map((charId) => this.CHARACTER_BY_ID(charId)?.name || 'Unknown')
569+
.join(', ');
570+
const startX = this.getSceneX(segment.startIndex);
571+
const width = this.sceneWidth * (segment.endIndex - segment.startIndex + 1);
572+
const yOffset = micIndex * barHeightPerMic;
573+
574+
bars.push({
575+
id: `cast-${castId}-mic-${micId}-seg-${segmentIndex}`,
576+
x: startX,
577+
y: this.getRowY(rowIndex) + this.barPadding + yOffset,
578+
width,
579+
height: barHeightPerMic,
580+
color: this.getColorForEntity(castId, 'cast'),
581+
cssClass: '',
582+
label: mic?.name || `Mic ${segment.micId}`,
583+
tooltip: `${mic?.name || `Mic ${segment.micId}`} - ${characterNames} (Scenes ${segment.startIndex + 1}-${segment.endIndex + 1})`,
584+
data: {
585+
castId,
586+
micId: segment.micId,
587+
characterIds: Array.from(segment.characterIds),
588+
startScene: segment.startIndex,
589+
endScene: segment.endIndex,
590+
},
591+
});
568592
});
569593
});
570594
},

docs/pages/show_config/microphones.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,22 @@ To assign a microphone to a character, follow these steps:
3737
#### Allocation Constraints
3838

3939
- You **cannot** allocate the same microphone to multiple characters in the same scene
40-
- You **cannot** allocate multiple microphones to a single character for a given scene
41-
- These constraints ensure practical microphone management during live shows
40+
- This reflects the physical constraint that one microphone can only be worn by one person at a time
41+
42+
#### Multiple Microphones Per Character
43+
44+
DigiScript supports assigning multiple microphones to a single character in a scene. This is useful for:
45+
46+
- **Primary + Backup Configuration**: Lead characters who aren't offstage long enough to swap out microphone packs can have both a primary and backup microphone assigned
47+
- **Redundancy**: Ensuring continuity in case of technical failures during performance
48+
- **Technical Flexibility**: Accommodating different microphone types or configurations for the same character
49+
50+
To assign multiple microphones to a character:
51+
1. Select the first microphone from the dropdown and allocate it to the character
52+
2. Select the second microphone from the dropdown and allocate it to the same character
53+
3. The character will now show both microphones in the view mode (e.g., "Mic 1, Mic 2")
54+
55+
In the Timeline view, characters with multiple microphones will display stacked bars showing all assigned microphones with consistent color-coding.
4256

4357
#### Saving Allocations
4458

@@ -50,14 +64,19 @@ The saved view provides a clear overview of microphone usage throughout the enti
5064

5165
#### Conflict Detection
5266

53-
DigiScript automatically detects potential microphone conflicts when the same microphone is allocated to different characters in adjacent scenes. Conflicts are highlighted in the allocations matrix to alert you to quick-changes that may require attention:
67+
DigiScript automatically detects potential microphone conflicts when the same microphone is allocated to different characters in adjacent scenes. Conflicts are tracked individually per microphone - a character with multiple microphones may have conflicts on some mics but not others. Conflicts are highlighted in the allocations matrix to alert you to quick-changes that may require attention:
5468

5569
![](../../images/config_show/mics_conflict_highlighting.png)
5670

5771
- **Orange highlights** indicate conflicts within the same act (tight changeover required)
5872
- **Blue highlights** indicate conflicts across act boundaries (interval provides changeover time)
5973

60-
Hovering over a highlighted allocation shows details about the conflict, including which scenes and characters are involved.
74+
Hovering over a highlighted allocation shows:
75+
- The full list of microphones assigned to that character in that scene
76+
- Details about which specific microphones have conflicts
77+
- Information about which scenes and characters are involved in each conflict
78+
79+
When a character has multiple microphones, the tooltip will clearly indicate which microphone(s) have conflicts, allowing you to plan changeovers for specific microphones while others remain assigned.
6180

6281
### Microphone Timeline View
6382

0 commit comments

Comments
 (0)