Skip to content

Commit

Permalink
Merge pull request #13 from PetrIvan/feature/filter-upgrade
Browse files Browse the repository at this point in the history
Improved filtering by notes
  • Loading branch information
PetrIvan authored Jun 19, 2024
2 parents 7bb9adf + 8632fce commit 4f9f94d
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 6 deletions.
73 changes: 72 additions & 1 deletion src/components/suggestions/search_bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export default function SearchBar() {
setIncludeVariants,
searchNotes,
setSearchNotes,
matchType,
setMatchType,
matchAnyVariant,
setMatchAnyVariant,
] = useStore(
(state) => [
state.setEnabledShortcuts,
Expand All @@ -23,6 +27,10 @@ export default function SearchBar() {
state.setIncludeVariants,
state.searchNotes,
state.setSearchNotes,
state.matchType,
state.setMatchType,
state.matchAnyVariant,
state.setMatchAnyVariant,
],
shallow
);
Expand Down Expand Up @@ -57,6 +65,30 @@ export default function SearchBar() {
};
}, []);

// Match dropdown
const matchTypes = ["At least", "At most", "Exact"];
const [isDropdownOpen, setIsDropdownOpen] = useState(false);

let matchDropdown = (
<div className="absolute z-[15] top-full mt-[0.5dvw] bg-zinc-800 rounded-[0.5dvw] w-full text-[2.5dvh]">
<ul>
{matchTypes.map((match) => (
<li key={match}>
<button
className="w-full bg-zinc-800 rounded-[0.5dvw] p-[0.5dvw] hover:bg-zinc-900"
onClick={() => {
setMatchType(matchTypes.indexOf(match) as 0 | 1 | 2);
setIsDropdownOpen(false);
}}
>
{match}
</button>
</li>
))}
</ul>
</div>
);

return (
<div className="h-[4dvw] flex flex-row items-center justify-start space-x-[1dvw]">
<div className="h-[4dvw] bg-zinc-800 flex flex-row items-center justify-start rounded-[1dvw] p-[0.5dvw]">
Expand Down Expand Up @@ -141,9 +173,46 @@ export default function SearchBar() {
</button>
{isPianoOpen && (
<div
className="absolute top-[100%] bg-zinc-950 p-[1dvw] pb-[2dvw] rounded-[0.5dvw] flex flex-row items-center justify-center"
className="absolute top-[100%] bg-zinc-950 p-[1dvw] pb-[2dvw] rounded-[0.5dvw] flex flex-col items-center justify-center z-10"
ref={pianoRef}
>
<div className="flex flex-row items-center justify-center pb-[1dvw] text-[2.5dvh]">
<span className="select-none mr-[1dvw]">Match:</span>
<div
className="bg-zinc-800 rounded-[0.5dvw] p-[0.5dvw] mr-[1dvw] hover:bg-zinc-900 w-[7dvw] cursor-pointer whitespace-nowrap"
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
>
<div className="flex flex-row items-center justify-between">
{matchTypes[matchType]}
<svg
className="w-[0.8dvw] h-[0.8dvw] mr-[0.5dvw] inline-block"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 10 6"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="m1 1 4 4 4-4"
/>
</svg>
</div>

<div className="relative w-full h-full">
{isDropdownOpen && matchDropdown}
</div>
</div>
<span className="select-none mr-[1dvw]">Any variant:</span>
<input
type="checkbox"
className="h-[1.2dvw] w-[1.2dvw] bg-zinc-800 rounded-[0.25dvw] focus:outline-none"
checked={matchAnyVariant}
onChange={() => setMatchAnyVariant(!matchAnyVariant)}
/>
</div>
<Piano
notes={searchNotes}
octaveOffset={3}
Expand All @@ -161,6 +230,8 @@ export default function SearchBar() {
onClick={() => {
setSearchQuery("");
setSearchNotes([]);
setMatchType(0);
setMatchAnyVariant(true);
}}
>
<img
Expand Down
85 changes: 80 additions & 5 deletions src/components/suggestions/suggestions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { tokenToChord } from "@/data/token_to_chord";
import { chordToNotes } from "@/data/chord_to_notes";

import { shallow } from "zustand/shallow";
import { isEqual } from "lodash";
import { clone, isEqual } from "lodash";
import { playChord } from "@/playback/player";

import SearchBar from "./search_bar";
Expand All @@ -29,6 +29,8 @@ interface Props {
decayFactor: number;
searchQuery: string;
searchNotes: number[];
matchType: number;
matchAnyVariant: boolean;
enabledShortcuts: boolean;
suggestionsIncludeVariants: boolean;
setVariantsOpen: (open: boolean) => void;
Expand Down Expand Up @@ -63,6 +65,8 @@ function arePropsEqual(prevProps: Props, newProps: Props) {
prevProps.modelPath !== newProps.modelPath ||
prevProps.decayFactor !== newProps.decayFactor ||
prevProps.searchQuery !== newProps.searchQuery ||
prevProps.matchType !== newProps.matchType ||
prevProps.matchAnyVariant !== newProps.matchAnyVariant ||
prevProps.enabledShortcuts !== newProps.enabledShortcuts ||
prevProps.suggestionsIncludeVariants !==
newProps.suggestionsIncludeVariants ||
Expand Down Expand Up @@ -126,6 +130,8 @@ export default function Suggestions() {
decayFactor,
searchQuery,
searchNotes,
matchType,
matchAnyVariant,
enabledShortcuts,
suggestionsIncludeVariants,
setVariantsOpen,
Expand All @@ -147,6 +153,8 @@ export default function Suggestions() {
state.decayFactor,
state.searchQuery,
state.searchNotes,
state.matchType,
state.matchAnyVariant,
state.enabledShortcuts,
state.includeVariants,
state.setVariantsOpen,
Expand All @@ -172,6 +180,8 @@ export default function Suggestions() {
decayFactor={decayFactor}
searchQuery={searchQuery}
searchNotes={searchNotes}
matchType={matchType}
matchAnyVariant={matchAnyVariant}
enabledShortcuts={enabledShortcuts}
suggestionsIncludeVariants={suggestionsIncludeVariants}
setVariantsOpen={setVariantsOpen}
Expand All @@ -196,6 +206,8 @@ const MemoizedSuggestions = React.memo(function MemoizedSuggestions({
decayFactor,
searchQuery,
searchNotes,
matchType,
matchAnyVariant,
enabledShortcuts,
suggestionsIncludeVariants,
setVariantsOpen,
Expand Down Expand Up @@ -333,6 +345,30 @@ const MemoizedSuggestions = React.memo(function MemoizedSuggestions({
return keywords.every((keyword) => name.includes(keyword));
}

function notesMatch(
notes: number[],
searchNotes: number[],
matchType: number
) {
// Remove duplicate notes
notes = notes.filter((note, index) => notes.indexOf(note) === index);
searchNotes = searchNotes.filter(
(note, index) => searchNotes.indexOf(note) === index
);

if (matchType === 0) {
// At least match (i.e. searchNotes is a subset of notes)
return searchNotes.every((note) => notes.includes(note));
} else if (matchType === 1) {
// At most match (i.e. notes is a subset of searchNotes)
return notes.every((note) => searchNotes.includes(note));
} else {
// Exact match
if (searchNotes.length !== notes.length) return false;
return searchNotes.every((note) => notes.includes(note));
}
}

function getChordsList() {
let chordsList = [];
for (let i = 0; i < chordProbs.length; i++) {
Expand Down Expand Up @@ -361,10 +397,49 @@ const MemoizedSuggestions = React.memo(function MemoizedSuggestions({

// Filter out chords that don't contain the search notes
if (searchNotes.length > 0) {
const notes = chordToNotes[tokenToChord[token][variant]].map(
(note) => note % 12
);
if (!searchNotes.every((note) => notes.includes(note % 12))) continue;
let normSearchNotes = clone(searchNotes);
if (matchAnyVariant) {
let notes = chordToNotes[tokenToChord[token][variant]];

// Normalize both notes and searchNotes to be in the same octave
notes = notes.map((note) => note % 12);
normSearchNotes = normSearchNotes.map((note) => note % 12);

// Check if the notes match (variants are irrelevant due to the normalization)
if (!notesMatch(notes, normSearchNotes, matchType)) continue;
} else {
// Check all variants
let allVariantNotes: number[][] = [];
for (let j = 0; j < tokenToChord[token].length; j++) {
let variantNotes = chordToNotes[tokenToChord[token][j]];

// Normalize notes to start from the lowest octave
const lowestOctave = Math.round(Math.min(...variantNotes) / 12);
variantNotes = variantNotes.map((note) => note - lowestOctave * 12);

allVariantNotes.push(variantNotes);
}

// Normalize searchNotes to start from the lowest octave
const lowestOctave = Math.round(Math.min(...normSearchNotes) / 12);
normSearchNotes = normSearchNotes.map(
(note) => note - lowestOctave * 12
);

// Check if any variant matches
let anyMatch = false;
for (let j = 0; j < allVariantNotes.length; j++) {
let notes = allVariantNotes[j];

if (notesMatch(notes, normSearchNotes, matchType)) {
variant = j;
anyMatch = true;
break;
}
}

if (!anyMatch) continue;
}
}

chordsList.push(
Expand Down
9 changes: 9 additions & 0 deletions src/state/use_store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ interface StoreState {
setSearchQuery: (searchQuery: string) => void;
searchNotes: number[];
setSearchNotes: (searchNotes: number[]) => void;
matchType: number;
setMatchType: (matchType: number) => void;
matchAnyVariant: boolean;
setMatchAnyVariant: (matchAnyOctave: boolean) => void;
includeVariants: boolean;
setIncludeVariants: (includeVariants: boolean) => void;

Expand Down Expand Up @@ -333,6 +337,11 @@ export const useStore = createWithEqualityFn<StoreState>()(
setSearchQuery: (searchQuery: string) => set({ searchQuery }),
searchNotes: [],
setSearchNotes: (searchNotes: number[]) => set({ searchNotes }),
matchType: 0,
setMatchType: (matchType: number) => set({ matchType }),
matchAnyVariant: true,
setMatchAnyVariant: (matchAnyOctave: boolean) =>
set({ matchAnyVariant: matchAnyOctave }),
includeVariants: false,
setIncludeVariants: (includeVariants: boolean) =>
set({ includeVariants }),
Expand Down

0 comments on commit 4f9f94d

Please sign in to comment.