diff --git a/app/app/lib/mock.ts b/app/app/lib/mock.ts new file mode 100644 index 0000000..544bb04 --- /dev/null +++ b/app/app/lib/mock.ts @@ -0,0 +1,144 @@ +import type { Song, SongSummary } from "./types"; + +const OCEAN: Song = { + meta: { + title: "A Drop In The Ocean", + artist: "Ron Pope", + capo: null, + original_key: "Em", + tuning: null, + tempo: null, + }, + sections: [ + { + kind: "chorus", + label: "Chorus", + lines: [ + { + text: "A drop in the ocean,", + chords: [{ offset: 0, chord: "Em" }, { offset: 12, chord: "C" }], + }, + { + text: "A change in the weather,", + chords: [{ offset: 2, chord: "G" }, { offset: 16, chord: "D" }], + }, + { + text: "I was praying that you and me might end up together.", + chords: [ + { offset: 6, chord: "Em" }, + { offset: 17, chord: "C" }, + { offset: 33, chord: "G" }, + { offset: 44, chord: "D" }, + ], + }, + ], + }, + { + kind: "verse", + label: "Verse", + lines: [ + { + text: "I don't wanna waste the weekend,", + chords: [{ offset: 0, chord: "C" }, { offset: 15, chord: "G" }], + }, + { + text: "If you don't love me, pretend", + chords: [{ offset: 3, chord: "D" }, { offset: 21, chord: "Em" }], + }, + { + text: "A few more hours, then it's time to go.", + chords: [{ offset: 2, chord: "C" }, { offset: 18, chord: "G" }, { offset: 30, chord: "D" }], + }, + ], + }, + { + kind: "bridge", + label: "Bridge", + lines: [ + { + text: "Still I can't let you be,", + chords: [{ offset: 0, chord: "Am" }, { offset: 11, chord: "G" }, { offset: 13, chord: "D" }], + }, + ], + }, + ], +}; + +const NAKED: Song = { + meta: { + title: "Naked", + artist: "James Arthur", + capo: null, + original_key: "G", + tuning: null, + tempo: null, + }, + sections: [ + { + kind: "chorus", + label: "Chorus", + lines: [ + { + text: "I'm not going to wait until you're done", + chords: [{ offset: 0, chord: "G" }, { offset: 18, chord: "Bm" }], + }, + { + text: "Pretending you don't need anyone", + chords: [{ offset: 16, chord: "Em" }, { offset: 27, chord: "C" }], + }, + { + text: "I'm standing here naked", + chords: [{ offset: 19, chord: "G" }], + }, + ], + }, + { + kind: "verse", + label: "Verse", + lines: [ + { + text: "I lay awake thinking of all I wasted", + chords: [{ offset: 0, chord: "G" }, { offset: 22, chord: "Bm" }], + }, + { + text: "All of the time we had I took for granted", + chords: [{ offset: 11, chord: "Em" }, { offset: 32, chord: "C" }], + }, + ], + }, + ], +}; + +const SONGS_MAP: Record = { + "song-ocean": OCEAN, + "song-naked": NAKED, +}; + +function previewChords(song: Song): string[] { + const seen = new Set(); + const result: string[] = []; + for (const section of song.sections) { + for (const line of section.lines) { + for (const cp of line.chords) { + if (!seen.has(cp.chord)) { + seen.add(cp.chord); + result.push(cp.chord); + } + } + } + if (result.length >= 5) break; + } + return result.slice(0, 5); +} + +export const MOCK_SONGS: SongSummary[] = Object.entries(SONGS_MAP).map( + ([id, song]) => ({ + id, + meta: song.meta, + preview_chords: previewChords(song), + }) +); + +export function getMockSong(id: string): Song | null { + return SONGS_MAP[id] ?? null; +} diff --git a/app/app/lib/transpose.ts b/app/app/lib/transpose.ts new file mode 100644 index 0000000..da96576 --- /dev/null +++ b/app/app/lib/transpose.ts @@ -0,0 +1,34 @@ +import type { Song } from "./types"; + +const NOTES_SHARP = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; +const NOTES_FLAT = ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"]; + +function transposeChord(chord: string, semitones: number): string { + const match = chord.match(/^([A-G][#b]?)(.*)/); + if (!match) return chord; + const [, root, descriptor] = match; + const idx = NOTES_SHARP.indexOf(root) !== -1 + ? NOTES_SHARP.indexOf(root) + : NOTES_FLAT.indexOf(root); + if (idx === -1) return chord; + const newIdx = ((idx + semitones) % 12 + 12) % 12; + const notes = semitones >= 0 ? NOTES_SHARP : NOTES_FLAT; + return notes[newIdx] + descriptor; +} + +export function transposeSong(song: Song, semitones: number): Song { + if (semitones === 0) return song; + return { + ...song, + sections: song.sections.map((section) => ({ + ...section, + lines: section.lines.map((line) => ({ + ...line, + chords: line.chords.map((cp) => ({ + ...cp, + chord: transposeChord(cp.chord, semitones), + })), + })), + })), + }; +} diff --git a/app/app/lib/types.ts b/app/app/lib/types.ts new file mode 100644 index 0000000..eb79291 --- /dev/null +++ b/app/app/lib/types.ts @@ -0,0 +1,37 @@ +export interface ChordPosition { + offset: number; + chord: string; +} + +export interface LyricLine { + text: string; + chords: ChordPosition[]; +} + +export interface Section { + kind: string; + label: string | null; + lines: LyricLine[]; +} + +export interface SongMeta { + title: string; + artist: string; + capo: number | null; + original_key: string | null; + tuning: string | null; + tempo: number | null; +} + +export interface Song { + meta: SongMeta; + sections: Section[]; +} + +// Trimmed version used in the library grid +export interface SongSummary { + id: string; + meta: SongMeta; + // First 5 unique chord names from the song, in order of appearance + preview_chords: string[]; +}