Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions apps/desktop/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ function succeededResult() {
rehearsalPriority: "high",
simplification: "Stay on roots if the chorus entrance gets muddy.",
setupNote: "Keep the attack short so the verse breathes.",
manualOverrides: []
manualOverrides: [],
overlapWarnings: [
"Density warning: competing with Keyboard Left Hand in low register."
]
},
{
id: "lead-vocal",
Expand All @@ -97,8 +100,13 @@ function succeededResult() {
},
source: "user"
}
]
],
overlapWarnings: []
}
],
partGraph: [
{ role_id: "bass-guitar", is_active: true, handoff_to: ["lead-vocal"], handoff_from: [] },
{ role_id: "lead-vocal", is_active: true, handoff_to: [], handoff_from: ["bass-guitar"] }
]
}
],
Expand Down
79 changes: 77 additions & 2 deletions apps/desktop/src/features/chords/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,79 @@
import type { RehearsalSong } from "@bandscope/shared-types";

/** Documented. */
export function ChordsFeature(props: { title: string }) {
return <section><h2>{props.title}</h2></section>;
export function ChordsFeature(props: { title: string; song?: RehearsalSong | null }) {
const { title, song } = props;

if (!song) {
return (
<section style={{ padding: "24px" }}>
<h2>{title}</h2>
<p style={{ color: "#999" }}>No song loaded. Start an analysis to see chord data.</p>
</section>
);
}

// Collect unique chords across all sections and roles
const chordsBySectionLabel = new Map<string, { chord: string; functionLabel: string; source: string; roleName: string }[]>();
for (const section of song.sections) {
const entries: { chord: string; functionLabel: string; source: string; roleName: string }[] = [];
for (const role of section.roles) {
entries.push({
chord: role.harmony.chord,
functionLabel: role.harmony.functionLabel,
source: role.harmony.source,
roleName: role.name,
});
}
chordsBySectionLabel.set(section.label, entries);
}

return (
<section style={{ padding: "24px" }}>
<h2>{title}</h2>
<div style={{ display: "flex", gap: "16px", flexWrap: "wrap" }}>
{song.sections.map((section) => (
<div
key={section.id}
style={{
flex: "0 0 auto",
minWidth: "200px",
border: "1px solid #e8e8e8",
borderRadius: "8px",
padding: "16px",
backgroundColor: "#fff",
}}
>
<h3 style={{ margin: "0 0 8px 0", textTransform: "capitalize" }}>
{section.label}
</h3>
{section.roles.map((role) => (
<div
key={role.id}
style={{
marginTop: "8px",
padding: "8px",
backgroundColor: role.harmony.source === "user" ? "#e6f7ff" : "#f9f9f9",
borderRadius: "4px",
}}
>
<div style={{ fontWeight: "bold", fontSize: "1.1em" }}>
{role.harmony.chord}
{role.harmony.source === "user" && (
<span style={{ fontSize: "0.7em", color: "#1890ff", marginLeft: "4px" }}>(User)</span>
)}
</div>
<div style={{ fontSize: "0.85em", color: "#666" }}>
{role.harmony.functionLabel}
</div>
<div style={{ fontSize: "0.8em", color: "#999" }}>
{role.name}
</div>
</div>
))}
</div>
))}
</div>
</section>
);
}
45 changes: 43 additions & 2 deletions apps/desktop/src/features/home/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,45 @@
import type { RehearsalSong } from "@bandscope/shared-types";

/** Documented. */
export function HomeFeature(props: { title: string }) {
return <section><h2>{props.title}</h2></section>;
export function HomeFeature(props: { title: string; song?: RehearsalSong | null }) {
const { title, song } = props;

return (
<section style={{ padding: "24px" }}>
<h2>{title}</h2>
{song ? (
<div>
<p style={{ fontSize: "1.1em", color: "#333", marginBottom: "16px" }}>
🎵 <strong>{song.title}</strong>
</p>
<div style={{ display: "flex", gap: "16px", flexWrap: "wrap" }}>
<div style={{ padding: "12px 16px", backgroundColor: "#f0f5ff", borderRadius: "8px", minWidth: "120px" }}>
<div style={{ fontSize: "0.85em", color: "#666" }}>Sections</div>
<div style={{ fontSize: "1.5em", fontWeight: "bold" }}>{song.sections.length}</div>
</div>
<div style={{ padding: "12px 16px", backgroundColor: "#f6ffed", borderRadius: "8px", minWidth: "120px" }}>
<div style={{ fontSize: "0.85em", color: "#666" }}>Roles</div>
<div style={{ fontSize: "1.5em", fontWeight: "bold" }}>
{new Set(song.sections.flatMap(s => s.roles.map(r => r.id))).size}
</div>
</div>
<div style={{ padding: "12px 16px", backgroundColor: "#fff7e6", borderRadius: "8px", minWidth: "120px" }}>
<div style={{ fontSize: "0.85em", color: "#666" }}>Export</div>
<div style={{ fontSize: "1.5em", fontWeight: "bold" }}>{song.exportSummary.format}</div>
</div>
</div>
{song.exportSummary.headline && (
<p style={{ marginTop: "16px", color: "#595959", fontStyle: "italic" }}>
{song.exportSummary.headline}
</p>
)}
</div>
) : (
<div style={{ textAlign: "center", padding: "32px", color: "#999", backgroundColor: "#fafafa", borderRadius: "8px" }}>
<p>🎵 Choose a local audio file or import from YouTube to get started.</p>
<p style={{ fontSize: "0.9em" }}>BandScope will analyze harmony, form, groove, and player cues for your rehearsal.</p>
</div>
)}
</section>
);
}
56 changes: 54 additions & 2 deletions apps/desktop/src/features/player/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,56 @@
import type { RehearsalSong } from "@bandscope/shared-types";

/** Documented. */
export function PlayerFeature(props: { title: string }) {
return <section><h2>{props.title}</h2></section>;
export function PlayerFeature(props: { title: string; song?: RehearsalSong | null }) {
const { title, song } = props;

if (!song) {
return (
<section style={{ padding: "24px" }}>
<h2>{title}</h2>
<p style={{ color: "#999" }}>No song loaded. Start an analysis to use the player.</p>
</section>
);
}

return (
<section style={{ padding: "24px" }}>
<h2>{title}</h2>
<div
style={{
padding: "16px",
backgroundColor: "#fafafa",
borderRadius: "8px",
border: "1px solid #e8e8e8",
}}
>
<div style={{ marginBottom: "12px" }}>
<strong>{song.title}</strong>
<span style={{ color: "#666", marginLeft: "8px" }}>
{song.sections.length} {song.sections.length === 1 ? "section" : "sections"}
</span>
</div>
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
{song.sections.map((section) => (
<span
key={section.id}
style={{
padding: "4px 12px",
borderRadius: "16px",
backgroundColor: "#fff",
border: "1px solid #d9d9d9",
fontSize: "0.85em",
textTransform: "capitalize",
}}
>
{section.label}
</span>
))}
</div>
<div style={{ marginTop: "16px", color: "#999", fontSize: "0.85em" }}>
Audio playback requires the desktop app with a local audio source.
</div>
</div>
</section>
);
}
66 changes: 64 additions & 2 deletions apps/desktop/src/features/ranges/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,66 @@
import type { RehearsalSong } from "@bandscope/shared-types";

/** Documented. */
export function RangesFeature(props: { title: string }) {
return <section><h2>{props.title}</h2></section>;
export function RangesFeature(props: { title: string; song?: RehearsalSong | null }) {
const { title, song } = props;

if (!song) {
return (
<section style={{ padding: "24px" }}>
<h2>{title}</h2>
<p style={{ color: "#999" }}>No song loaded. Start an analysis to see range data.</p>
</section>
);
}

return (
<section style={{ padding: "24px" }}>
<h2>{title}</h2>
{song.sections.map((section) => (
<div key={section.id} style={{ marginBottom: "24px" }}>
<h3 style={{ textTransform: "capitalize", marginBottom: "8px" }}>{section.label}</h3>
<div style={{ display: "flex", gap: "12px", flexWrap: "wrap" }}>
{section.roles.map((role) => (
<div
key={role.id}
style={{
padding: "12px",
border: "1px solid #e8e8e8",
borderRadius: "8px",
minWidth: "160px",
backgroundColor: "#fff",
}}
>
<div style={{ fontWeight: "bold", fontSize: "0.9em", marginBottom: "4px" }}>
{role.name}
</div>
<div style={{ fontSize: "0.85em", color: "#333" }}>
🎵 {role.range.lowestNote} — {role.range.highestNote}
</div>
{role.overlapWarnings.length > 0 && (
<div style={{ marginTop: "8px" }}>
{role.overlapWarnings.map((warning, wIndex) => (
<div
key={wIndex}
style={{
fontSize: "0.8em",
color: "#fa8c16",
marginTop: "4px",
padding: "4px 6px",
backgroundColor: "#fff7e6",
borderRadius: "4px",
}}
>
⚠️ {warning}
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
))}
</section>
);
}
48 changes: 47 additions & 1 deletion apps/desktop/src/features/settings/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,50 @@
import { SUPPORTED_AUDIO_FORMATS } from "@bandscope/shared-types";

/** Documented. */
export function SettingsFeature(props: { title: string }) {
return <section><h2>{props.title}</h2></section>;
const { title } = props;

return (
<section style={{ padding: "24px" }}>
<h2>{title}</h2>
<div style={{ maxWidth: "480px" }}>
<div style={{ marginBottom: "16px" }}>
<h3 style={{ fontSize: "1em", margin: "0 0 8px 0" }}>Supported Audio Formats</h3>
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
{SUPPORTED_AUDIO_FORMATS.map((format) => (
<span
key={format}
style={{
padding: "4px 12px",
borderRadius: "4px",
backgroundColor: "#f0f5ff",
border: "1px solid #adc6ff",
fontSize: "0.85em",
}}
>
.{format}
</span>
))}
</div>
</div>

<div style={{ marginBottom: "16px" }}>
<h3 style={{ fontSize: "1em", margin: "0 0 8px 0" }}>Analysis Pipeline</h3>
<ul style={{ margin: 0, paddingLeft: "20px", color: "#595959" }}>
<li>Decode audio source</li>
<li>Draft section and role extraction</li>
<li>Separate stems by category</li>
<li>Persist analysis results</li>
</ul>
</div>

<div>
<h3 style={{ fontSize: "1em", margin: "0 0 8px 0" }}>About</h3>
<p style={{ color: "#666", fontSize: "0.9em", margin: 0 }}>
BandScope is a local-first rehearsal prep tool. All analysis runs on your device.
</p>
</div>
</div>
</section>
);
}
9 changes: 9 additions & 0 deletions apps/desktop/src/features/workspace/SectionRoadmap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ export function SectionRoadmap({ song, activeRole, onSongUpdate }: SectionRoadma
✨ {role.simplification}
</div>
)}
{role.overlapWarnings.length > 0 && (
<div style={{ marginTop: "4px" }}>
{role.overlapWarnings.map((warning, wIdx) => (
<div key={wIdx} style={{ fontSize: "0.8em", color: "#fa541c", marginTop: "2px" }}>
⚠️ {warning}
</div>
))}
</div>
)}
</div>
))}
</div>
Expand Down
6 changes: 5 additions & 1 deletion apps/desktop/src/lib/export.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,12 @@ describe("export generation", () => {
rehearsalPriority: "high",
simplification: "simple",
setupNote: "setup",
manualOverrides: []
manualOverrides: [],
overlapWarnings: []
}
],
partGraph: [
{ role_id: "r1", is_active: true, handoff_to: [], handoff_from: [] }
]
}
]
Expand Down
Loading
Loading