diff --git a/src/why_does_that_sound_good/algo/chord.cljc b/src/why_does_that_sound_good/algo/chord.cljc index fe443ea..b2df5e9 100644 --- a/src/why_does_that_sound_good/algo/chord.cljc +++ b/src/why_does_that_sound_good/algo/chord.cljc @@ -3,30 +3,9 @@ [why-does-that-sound-good.pitch :as pitch] [why-does-that-sound-good.utils :as utils])) -(def CHORD-INTERVAL-NAMES - {0 :1 - 1 :m2 - 2 :M2 - 3 :m3 - 4 :M3 - 5 :4 - 6 :-5 - 7 :5 - 8 :+5 - 9 :6 - 10 :m7 - 11 :M7 - 12 :o - 13 :-9 - 14 :9 - 15 :-10 - 17 :11 - 18 :+11 - 21 :13}) - (def ALL-CHORDS (into {} (for [root-pitch (vals pitch/REVERSE-NOTES) - [chord-type intervals] pitch/CHORD + [chord-type {:keys [intervals]}] pitch/CHORD :let [chord-desc {:root root-pitch :chord-type chord-type} pitches (set (map #(pitch/interval->pitch root-pitch %) intervals))]] [chord-desc pitches]))) @@ -76,7 +55,7 @@ {matched-chord-pitches true unmatched-chord-pitches false} (group-by #(some? (val %)) chord-pitch->note-names)] {:matched-note-names (flatten (vals matched-chord-pitches)) - :unmatched-pitches (or (keys unmatched-chord-pitches) (list)) + :unmatched-chord-pitches (or (keys unmatched-chord-pitches) (list)) :unmatched-note-names (flatten (filter (fn [note-names] (not (some #(= % note-names) (vals chord-pitch->note-names)))) (vals note-pitch->note-names)))})) @@ -88,7 +67,7 @@ [original-notes chord-pitches] (let [original-octave-center (utils/median (map pitch/note->octave original-notes)) matches (match-pitches-to-note-names chord-pitches original-notes)] - (loop [chord-pitches-remaining (:unmatched-pitches matches) + (loop [chord-pitches-remaining (:unmatched-chord-pitches matches) original-note-names-remaining (:unmatched-note-names matches) chord-notes []] (if (empty? chord-pitches-remaining) @@ -105,11 +84,14 @@ (conj chord-notes chord-note))))))) (defn chord->readable-intervals [{:keys [root chord-type]}] - (let [intervals (pitch/CHORD chord-type)] - (reduce (fn [m interval] + (let [chord-details (get pitch/CHORD chord-type) + intervals (:intervals chord-details) + interval-names (:interval-names chord-details)] + (reduce (fn [m [i interval]] (let [pitch (pitch/interval->pitch root interval) - readable-interval (CHORD-INTERVAL-NAMES interval)] - (assoc m pitch readable-interval))) {} intervals))) + readable-interval (nth interval-names i)] + (assoc m pitch readable-interval))) {} + (map-indexed vector intervals)))) (defn block->chords [block & {:keys [min-chord-similarity find-closest?] :or {min-chord-similarity 0.90 find-closest? false}}] diff --git a/src/why_does_that_sound_good/algo/scale.cljc b/src/why_does_that_sound_good/algo/scale.cljc index cfa6ceb..7eb4eea 100644 --- a/src/why_does_that_sound_good/algo/scale.cljc +++ b/src/why_does_that_sound_good/algo/scale.cljc @@ -70,16 +70,17 @@ :or {octave 4}}] (let [root-note (pitch/note (str (name root-pitch) octave)) all-scale-intervals (scale-pitches->intervals scale-pitches root-pitch) - diatonic-chord-types (filter (fn [[_ chord-intervals]] - (set/subset? chord-intervals all-scale-intervals)) - pitch/CHORD)] - (map (fn [[chord-type chord-intervals]] - {:root root-pitch - :chord-type chord-type - :chord-intervals chord-intervals - :chord-pitches->readable-intervals (chord/chord->readable-intervals {:root root-pitch :chord-type chord-type}) - :chord-notes (map #(+ root-note %) (sort chord-intervals))}) - diatonic-chord-types))) + diatonic-chords (filter (fn [[_ {:keys [intervals]}]] + (set/subset? intervals all-scale-intervals)) + pitch/CHORD)] + (->> diatonic-chords + (map (fn [[chord-type {:keys [intervals]}]] + {:root root-pitch + :chord-type chord-type + :chord-pitches->readable-intervals (chord/chord->readable-intervals {:root root-pitch :chord-type chord-type}) + :chord-notes (map #(+ root-note %) (sort intervals))})) + ;; Sort by increasing complexity + (sort-by #(get pitch/CHORD-ORDER (:chord-type %)))))) (defn scale->diatonic-chords "For each pitch in scale, construct their diatonic chords" @@ -105,7 +106,6 @@ (let [root-pitch (:root chord) all-chords-for-root (get all-chords root-pitch)] (assoc all-chords root-pitch - ;; TODO: after similarity, sort by 'popularity'/complexity (sort-by (comp - #(or (:similarity %) 0)) (map (fn [all-chord] (if (= (:chord-type chord) (:chord-type all-chord)) diff --git a/src/why_does_that_sound_good/components/live_suggestion_panel.cljs b/src/why_does_that_sound_good/components/live_suggestion_panel.cljs index 5f8b330..d09c27f 100644 --- a/src/why_does_that_sound_good/components/live_suggestion_panel.cljs +++ b/src/why_does_that_sound_good/components/live_suggestion_panel.cljs @@ -31,7 +31,8 @@ [:div {:class "flex flex-col gap-y items-start"} [:span - {:class "font-semibold text-xl"} + {:class "font-semibold text-xl" + :title (utils/chord-tooltip s)} (utils/music-structure->str s)] [similarity-badge (:similarity s)]] [:div diff --git a/src/why_does_that_sound_good/components/scales_pane.cljs b/src/why_does_that_sound_good/components/scales_pane.cljs index 13ec7ec..d2276f4 100644 --- a/src/why_does_that_sound_good/components/scales_pane.cljs +++ b/src/why_does_that_sound_good/components/scales_pane.cljs @@ -46,7 +46,7 @@ [chevron-up-icon]] [:div.flex.flex-col.items-center.cursor-pointer {:on-click #(re-frame/dispatch [::events/on-notes-play (:chord-notes chord)]) - :title "Click to play"} + :title (str/join "\n\n" ["Click to play" (utils/chord-tooltip chord)])} [:span.font-semibold chord-str] (when-let [s (:similarity chord)] [similarity-badge s])] diff --git a/src/why_does_that_sound_good/events.cljs b/src/why_does_that_sound_good/events.cljs index 05938bb..190959c 100644 --- a/src/why_does_that_sound_good/events.cljs +++ b/src/why_does_that_sound_good/events.cljs @@ -427,8 +427,10 @@ (let [section-block-ids (:block-ids section) blocks (vals (select-keys (get-in db [:data :blocks]) section-block-ids)) pregenerated-block-chord-suggestions (select-keys (:chord-suggestions db) section-block-ids)] - (when (<= 2 (count blocks)) - (assoc m section-id (scale/mem-blocks->scales blocks :pregenerated-block-chord-suggestions pregenerated-block-chord-suggestions :find-closest? true))))) + (if (<= 2 (count blocks)) + (let [scales (scale/mem-blocks->scales blocks :pregenerated-block-chord-suggestions pregenerated-block-chord-suggestions :find-closest? true)] + (assoc m section-id scales)) + (assoc m section-id {})))) {} (get-in db [:data :sections]))))) diff --git a/src/why_does_that_sound_good/pitch.cljc b/src/why_does_that_sound_good/pitch.cljc index 226ee71..d5a1638 100644 --- a/src/why_does_that_sound_good/pitch.cljc +++ b/src/why_does_that_sound_good/pitch.cljc @@ -1,50 +1,160 @@ (ns why-does-that-sound-good.pitch) -;; From overtone/music/pitch +(def interval-name->semitone + {:1 0 + :M2 2 + :m3 3 + :M3 4 + :4 5 + :d5 6 + :5 7 + :A5 8 + :m6 8 + :M6 9 + :d7 9 + :m7 10 + :M7 11 + :m9 13 + :M9 14 + :A9 15 + :m10 15 + :11 17 + :A11 18 + :m13 20 + :M13 21}) + +;; From tonal.js +;; https://github.com/tonaljs/tonal/blob/febd2404924bd8bfe9712aa407ee88943125b9d5/packages/chord-type/data.ts + +(def BASE-CHORD + (array-map + ;; Major + "M" {:interval-names [:1 :M3 :5] :aliases ["^","","maj"] :name "major"} + "maj7" {:interval-names [:1 :M3 :5 :M7] :aliases ["Δ","ma7","M7","Maj7","^7"] :name "major seventh"} + "maj9" {:interval-names [:1 :M3 :5 :M7 :M9] :aliases ["Δ9","^9"] :name "major ninth"} + "maj13" {:interval-names [:1 :M3 :5 :M7 :M9 :M13] :aliases ["Maj13","^13"] :name "major thirteenth"} + "6" {:interval-names [:1 :M3 :5 :M6] :aliases ["add6","add13","M6"] :name "sixth"} + "6add9" {:interval-names [:1 :M3 :5 :M6 :M9] :aliases ["6/9","69","M69"] :name "sixth added ninth"} + "M7b6" {:interval-names [:1 :M3 :m6 :M7] :aliases ["^7b6"] :name "major seventh flat sixth"} + "maj#4" {:interval-names [:1 :M3 :5 :M7 :A11] :aliases ["Δ#4","Δ#11","M7#11","^7#11","maj7#11"] :name "major seventh sharp eleventh"} + ;; Minor + ;;; Normal + "m" {:interval-names [:1 :m3 :5] :aliases ["min","-"] :name "minor"} + "m7" {:interval-names [:1 :m3 :5 :m7] :aliases ["min7","mi7","-7"] :name "minor seventh"} + "m/ma7" {:interval-names [:1 :m3 :5 :M7] :aliases ["m/maj7","mM7","mMaj7","m/M7","-Δ7","mΔ","-^7"] :name "minor/major seventh"} + "m6" {:interval-names [:1 :m3 :5 :M6] :aliases ["-6"] :name "minor sixth"} + "m9" {:interval-names [:1 :m3 :5 :m7 :M9] :aliases ["-9"] :name "minor ninth"} + "mM9" {:interval-names [:1 :m3 :5 :M7 :M9] :aliases ["mMaj9","-^9"] :name "minor/major ninth"} + "m11" {:interval-names [:1 :m3 :5 :m7 :M9 :11] :aliases ["-11"] :name "minor eleventh"} + "m13" {:interval-names [:1 :m3 :5 :m7 :M9 :M13] :aliases ["-13"] :name "minor thirteenth"} + ;;; Diminished + "dim" {:interval-names [:1 :m3 :d5] :aliases ["°","o"] :name "diminished"} + "dim7" {:interval-names [:1 :m3 :d5 :d7] :aliases ["°7","o7"] :name "diminished seventh"} + "m7b5" {:interval-names [:1 :m3 :d5 :m7] :aliases ["ø","-7b5","h7","h"] :name "half-diminished"} + ;; Dominant/Seventh + ;;; Normal + "7" {:interval-names [:1 :M3 :5 :m7] :aliases ["dom"] :name "dominant seventh"} + "9" {:interval-names [:1 :M3 :5 :m7 :M9] :aliases [] :name "dominant ninth"} + "13" {:interval-names [:1 :M3 :5 :m7 :M9 :M13] :aliases [] :name "dominant thirteenth"} + "7#11" {:interval-names [:1 :M3 :5 :m7 :A11] :aliases ["7#4"] :name "lydian dominant seventh"} + ;;; Altered + "7b9" {:interval-names [:1 :M3 :5 :m7 :m9] :aliases [] :name "dominant flat ninth"} + "7#9" {:interval-names [:1 :M3 :5 :m7 :A9] :aliases [] :name "dominant sharp ninth"} + "alt7" {:interval-names [:1 :M3 :m7 :m9] :aliases [] :name "altered"} + ;;; Suspended + "sus4" {:interval-names [:1 :4 :5] :aliases ["sus"] :name "suspended fourth"} + "sus2" {:interval-names [:1 :M2 :5] :aliases [] :name "suspended second"} + "7sus4" {:interval-names [:1 :4 :5 :m7] :aliases ["7sus"] :name "suspended fourth seventh"} + "11" {:interval-names [:1 :5 :m7 :M9 :11] :aliases [] :name "eleventh"} + "b9sus" {:interval-names [:1 :4 :5 :m7 :m9] :aliases ["phryg","7b9sus","7b9sus4"] :name "suspended fourth flat ninth"} + ;; Other + "5" {:interval-names [:1 :5] :aliases [] :name "fifth"} + "aug" {:interval-names [:1 :M3 :A5] :aliases ["+","+5","^#5"] :name "augmented"} + "m#5" {:interval-names [:1 :m3 :A5] :aliases ["-#5","m+"] :name "minor augmented"} + "maj7#5" {:interval-names [:1 :M3 :A5 :M7] :aliases ["maj7+5","+maj7","^7#5"] :name "augmented seventh"} + "maj9#11" {:interval-names [:1 :M3 :5 :M7 :M9 :A11] :aliases ["Δ9#11","^9#11"] :name "major sharp eleventh (lydian)"} + "sus24" {:interval-names [:1 :M2 :4 :5] :aliases ["sus4add9"] :name ""} + "maj9#5" {:interval-names [:1 :M3 :A5 :M7 :M9] :aliases ["Maj9#5"] :name ""} + "7#5" {:interval-names [:1 :M3 :A5 :m7] :aliases ["+7","7+","7aug","aug7"] :name ""} + "7#5#9" {:interval-names [:1 :M3 :A5 :m7 :A9] :aliases ["7#9#5","7alt"] :name ""} + "9#5" {:interval-names [:1 :M3 :A5 :m7 :M9] :aliases ["9+"] :name ""} + "9#5#11" {:interval-names [:1 :M3 :A5 :m7 :M9 :A11] :aliases [] :name ""} + "7#5b9" {:interval-names [:1 :M3 :A5 :m7 :m9] :aliases ["7b9#5"] :name ""} + "7#5b9#11" {:interval-names [:1 :M3 :A5 :m7 :m9 :A11] :aliases [] :name ""} + "+add#9" {:interval-names [:1 :M3 :A5 :A9] :aliases [] :name ""} + "M#5add9" {:interval-names [:1 :M3 :A5 :M9] :aliases ["+add9"] :name ""} + "M6#11" {:interval-names [:1 :M3 :5 :M6 :A11] :aliases ["M6b5","6#11","6b5"] :name ""} + "M7add13" {:interval-names [:1 :M3 :5 :M6 :M7 :M9] :aliases [] :name ""} + "69#11" {:interval-names [:1 :M3 :5 :M6 :M9 :A11] :aliases [] :name ""} + "m69" {:interval-names [:1 :m3 :5 :M6 :M9] :aliases ["-69"] :name ""} + "7b6" {:interval-names [:1 :M3 :5 :m6 :m7] :aliases [] :name ""} + "maj7#9#11" {:interval-names [:1 :M3 :5 :M7 :A9 :A11] :aliases [] :name ""} + "M13#11" {:interval-names [:1 :M3 :5 :M7 :M9 :A11 :M13] :aliases ["maj13#11","M13+4","M13#4"] :name ""} + "M7b9" {:interval-names [:1 :M3 :5 :M7 :m9] :aliases [] :name ""} + "7#11b13" {:interval-names [:1 :M3 :5 :m7 :A11 :m13] :aliases ["7b5b13"] :name ""} + "7add6" {:interval-names [:1 :M3 :5 :m7 :M13] :aliases ["67","7add13"] :name ""} + "7#9#11" {:interval-names [:1 :M3 :5 :m7 :A9 :A11] :aliases ["7b5#9","7#9b5"] :name ""} + "13#9#11" {:interval-names [:1 :M3 :5 :m7 :A9 :A11 :M13] :aliases [] :name ""} + "7#9#11b13" {:interval-names [:1 :M3 :5 :m7 :A9 :A11 :m13] :aliases [] :name ""} + "13#9" {:interval-names [:1 :M3 :5 :m7 :A9 :M13] :aliases [] :name ""} + "7#9b13" {:interval-names [:1 :M3 :5 :m7 :A9 :m13] :aliases [] :name ""} + "9#11" {:interval-names [:1 :M3 :5 :m7 :M9 :A11] :aliases ["9+4","9#4"] :name ""} + "13#11" {:interval-names [:1 :M3 :5 :m7 :M9 :A11 :M13] :aliases ["13+4","13#4"] :name ""} + "9#11b13" {:interval-names [:1 :M3 :5 :m7 :M9 :A11 :m13] :aliases ["9b5b13"] :name ""} + "7b9#11" {:interval-names [:1 :M3 :5 :m7 :m9 :A11] :aliases ["7b5b9","7b9b5"] :name ""} + "13b9#11" {:interval-names [:1 :M3 :5 :m7 :m9 :A11 :M13] :aliases [] :name ""} + "7b9b13#11" {:interval-names [:1 :M3 :5 :m7 :m9 :A11 :m13] :aliases ["7b9#11b13","7b5b9b13"] :name ""} + "13b9" {:interval-names [:1 :M3 :5 :m7 :m9 :M13] :aliases [] :name ""} + "7b9b13" {:interval-names [:1 :M3 :5 :m7 :m9 :m13] :aliases [] :name ""} + "7b9#9" {:interval-names [:1 :M3 :5 :m7 :m9 :A9] :aliases [] :name ""} + "Madd9" {:interval-names [:1 :M3 :5 :M9] :aliases ["2","add9","add2"] :name ""} + "Maddb9" {:interval-names [:1 :M3 :5 :m9] :aliases [] :name ""} + "Mb5" {:interval-names [:1 :M3 :d5] :aliases [] :name ""} + "13b5" {:interval-names [:1 :M3 :d5 :M6 :m7 :M9] :aliases [] :name ""} + "M7b5" {:interval-names [:1 :M3 :d5 :M7] :aliases [] :name ""} + "M9b5" {:interval-names [:1 :M3 :d5 :M7 :M9] :aliases [] :name ""} + "7b5" {:interval-names [:1 :M3 :d5 :m7] :aliases [] :name ""} + "9b5" {:interval-names [:1 :M3 :d5 :m7 :M9] :aliases [] :name ""} + "7no5" {:interval-names [:1 :M3 :m7] :aliases [] :name ""} + "7b13" {:interval-names [:1 :M3 :m7 :m13] :aliases [] :name ""} + "9no5" {:interval-names [:1 :M3 :m7 :M9] :aliases [] :name ""} + "13no5" {:interval-names [:1 :M3 :m7 :M9 :M13] :aliases [] :name ""} + "9b13" {:interval-names [:1 :M3 :m7 :M9 :m13] :aliases [] :name ""} + "madd4" {:interval-names [:1 :m3 :4 :5] :aliases [] :name ""} + "mMaj7b6" {:interval-names [:1 :m3 :5 :m6 :M7] :aliases [] :name ""} + "mMaj9b6" {:interval-names [:1 :m3 :5 :m6 :M7 :M9] :aliases [] :name ""} + "m7add11" {:interval-names [:1 :m3 :5 :m7 :11] :aliases ["m7add4"] :name ""} + "madd9" {:interval-names [:1 :m3 :5 :M9] :aliases [] :name ""} + "dim7M7" {:interval-names [:1 :m3 :d5 :M6 :M7] :aliases ["o7M7"] :name ""} + "dimM7" {:interval-names [:1 :m3 :d5 :M7] :aliases ["oM7"] :name ""} + "mb6M7" {:interval-names [:1 :m3 :m6 :M7] :aliases [] :name ""} + "m7#5" {:interval-names [:1 :m3 :m6 :m7] :aliases [] :name ""} + "m9#5" {:interval-names [:1 :m3 :m6 :m7 :M9] :aliases [] :name ""} + "m11A" {:interval-names [:1 :m3 :A5 :m7 :M9 :11] :aliases [] :name ""} + "mb6b9" {:interval-names [:1 :m3 :m6 :m9] :aliases [] :name ""} + "m9b5" {:interval-names [:1 :M2 :m3 :d5 :m7] :aliases [] :name ""} + "M7#5sus4" {:interval-names [:1 :4 :A5 :M7] :aliases [] :name ""} + "M9#5sus4" {:interval-names [:1 :4 :A5 :M7 :M9] :aliases [] :name ""} + "7#5sus4" {:interval-names [:1 :4 :A5 :m7] :aliases [] :name ""} + "M7sus4" {:interval-names [:1 :4 :5 :M7] :aliases [] :name ""} + "M9sus4" {:interval-names [:1 :4 :5 :M7 :M9] :aliases [] :name ""} + "9sus4" {:interval-names [:1 :4 :5 :m7 :M9] :aliases ["9sus"] :name ""} + "13sus4" {:interval-names [:1 :4 :5 :m7 :M9 :M13] :aliases ["13sus"] :name ""} + "7sus4b9b13" {:interval-names [:1 :4 :5 :m7 :m9 :m13] :aliases ["7b9b13sus4"] :name ""} + "4" {:interval-names [:1 :4 :m7 :m10] :aliases ["quartal"] :name ""} + "11b9" {:interval-names [:1 :5 :m7 :m9 :11] :aliases [] :name ""})) -(def CHORD - {:maj #{0 4 7} - :min #{0 3 7} - :maj7 #{0 4 7 11} - :dom7 #{0 4 7 10} - :min7 #{0 3 7 10} - :aug #{0 4 8} - :dim #{0 3 6} - :dim7 #{0 3 6 9} - :+5 #{0 4 8} - :m+5 #{0 3 8} - :sus2 #{0 2 7} - :sus4 #{0 5 7} - :6 #{0 4 7 9} - :m6 #{0 3 7 9} - :7sus2 #{0 2 7 10} - :7sus4 #{0 5 7 10} - :7-5 #{0 4 6 10} - :m7-5 #{0 3 6 10} - :7+5 #{0 4 8 10} - :m7+5 #{0 3 8 10} - :9 #{0 4 7 10 14} - :m9 #{0 3 7 10 14} - :m7+9 #{0 3 7 10 14} - :maj9 #{0 4 7 11 14} - :9sus4 #{0 5 7 10 14} - :6*9 #{0 4 7 9 14} - :m6*9 #{0 3 9 7 14} - :7-9 #{0 4 7 10 13} - :m7-9 #{0 3 7 10 13} - :7-10 #{0 4 7 10 15} - :9+5 #{0 10 13} - :m9+5 #{0 10 14} - :7+5-9 #{0 4 8 10 13} - :m7+5-9 #{0 3 8 10 13} - :11 #{0 4 7 10 14 17} - :m11 #{0 3 7 10 14 17} - :maj11 #{0 4 7 11 14 17} - :11+ #{0 4 7 10 14 18} - :m11+ #{0 3 7 10 14 18} - :13 #{0 4 7 10 14 17 21} - :m13 #{0 3 7 10 14 17 21}}) +;; Add semitone intervals for fuzzy search/distance +(def CHORD (reduce (fn [m [chord-type chord-details]] + (assoc-in m [chord-type :intervals] (mapv #(interval-name->semitone %) (:interval-names chord-details)))) + BASE-CHORD + BASE-CHORD)) +;; Keep chord order above for scale diatonic chords to go from less to more complex +(def CHORD-ORDER (into {} (map (fn [[i k]] [k i]) (map-indexed vector (keys BASE-CHORD))))) + + +;; From overtone/music/pitch (def SCALE (let [ionian-sequence [2 2 1 2 2 2 1] hex-sequence [2 2 1 2 2 3] diff --git a/src/why_does_that_sound_good/utils.cljc b/src/why_does_that_sound_good/utils.cljc index 89077de..f1de253 100644 --- a/src/why_does_that_sound_good/utils.cljc +++ b/src/why_does_that_sound_good/utils.cljc @@ -75,3 +75,15 @@ (if (get-in m ks) (update-in m ks conj v) (assoc-in m ks [v]))) + +(defn chord-tooltip + "More chord info on hover (long name, aliases)" + [chord-desc] + (let [chord-details (get pitch/CHORD (:chord-type chord-desc))] + (str/join "\n" [(let [chord-name (:name chord-details)] + (str " Name: " (if (= "" chord-name) "N/A" (str (name (:root chord-desc)) " " chord-name)))) + (let [aliases (:aliases chord-details)] + (str " Aliases:" + (if (seq aliases) + (str "\n - " (str/join "\n - " (map #(str (name (:root chord-desc)) %) aliases))) + " N/A")))]))) diff --git a/test/why_does_that_sound_good/algo/chord_test.cljc b/test/why_does_that_sound_good/algo/chord_test.cljc index c575613..86400ea 100644 --- a/test/why_does_that_sound_good/algo/chord_test.cljc +++ b/test/why_does_that_sound_good/algo/chord_test.cljc @@ -19,20 +19,20 @@ (deftest block->chords-test (are [block expected-chords] (= expected-chords (chord/block->chords block :find-closest? true)) {:id 1 :notes #{60 64 67}} '({:root :C - :chord-type :maj + :chord-type "M" :chord-pitches #{:C :G :E} - :similarity 1 + :similarity 1.0 :original-block-id 1 :lowest-note-root? 1 :chord-pitches->readable-intervals {:C :1 :G :5 :E :M3} :chord-notes (60 64 67)} {:root :E - :chord-type :m+5 + :chord-type "m#5" :chord-pitches #{:E :G :C} - :similarity 1 + :similarity 1.0 :original-block-id 1 :lowest-note-root? 0 - :chord-pitches->readable-intervals {:E :1 :G :m3 :C :+5} + :chord-pitches->readable-intervals {:E :1 :G :m3 :C :A5} :chord-notes (60 64 67)}) {:id 1 :notes #{}} nil)) diff --git a/test/why_does_that_sound_good/algo/scale_test.cljc b/test/why_does_that_sound_good/algo/scale_test.cljc index dc12561..0d90acf 100644 --- a/test/why_does_that_sound_good/algo/scale_test.cljc +++ b/test/why_does_that_sound_good/algo/scale_test.cljc @@ -23,75 +23,94 @@ (deftest pitches->scales-test (testing "find-closest? true" - (is (= '({:root :C, - :scale-type :major, - :scale-pitches (:C :D :E :F :G :A :B), + (is (= '({:root :C + :scale-type :major + :scale-pitches (:C :D :E :F :G :A :B) :similarity 1.0} - {:root :A, - :scale-type :minor, - :scale-pitches (:A :B :C :D :E :F :G), + {:root :A + :scale-type :minor + :scale-pitches (:A :B :C :D :E :F :G) :similarity 1.0}) (scale/pitches->scales #{:C :D :E :F :G :A :B} :find-closest? true)))) (testing "min-similarity" - (is (= '({:root :C, - :scale-type :major, - :scale-pitches (:C :D :E :F :G :A :B), + (is (= '({:root :C + :scale-type :major + :scale-pitches (:C :D :E :F :G :A :B) :similarity 0.9375} - {:root :A, - :scale-type :melodic-major, - :scale-pitches (:A :B :C# :D :E :F :G), + {:root :A + :scale-type :melodic-major + :scale-pitches (:A :B :C# :D :E :F :G) :similarity 0.9375} - {:root :D, - :scale-type :melodic-minor, - :scale-pitches (:D :E :F :G :A :B :C#), + {:root :D + :scale-type :melodic-minor + :scale-pitches (:D :E :F :G :A :B :C#) :similarity 0.9375} - {:root :A, - :scale-type :minor, - :scale-pitches (:A :B :C :D :E :F :G), + {:root :A + :scale-type :minor + :scale-pitches (:A :B :C :D :E :F :G) :similarity 0.9375}) (scale/pitches->scales #{:C :C# :D :E :F :G :A :B} :min-scale-similarity 0.90))))) (deftest scale-pitch->diatonic-chords-test - (is (= '({:root :C, - :chord-type :maj, - :chord-intervals #{0 7 4}, - :chord-pitches->readable-intervals {:C :1, :G :5, :E :M3}, + (is (= '({:root :C + :chord-type "M" + :chord-pitches->readable-intervals {:C :1 :E :M3 :G :5} :chord-notes (60 64 67)} - {:root :C, - :chord-type :6*9, - :chord-intervals #{0 7 4 9 14}, - :chord-pitches->readable-intervals {:C :1, :G :5, :E :M3, :A :6, :D :9}, - :chord-notes (60 64 67 69 74)} - {:root :C, - :chord-type :maj7, - :chord-intervals #{0 7 4 11}, - :chord-pitches->readable-intervals {:C :1, :G :5, :E :M3, :B :M7}, + {:root :C + :chord-type "maj7" + :chord-pitches->readable-intervals {:C :1 :E :M3 :G :5 :B :M7} :chord-notes (60 64 67 71)} - {:root :C, - :chord-type :maj9, - :chord-intervals #{0 7 4 11 14}, - :chord-pitches->readable-intervals {:C :1, :G :5, :E :M3, :B :M7, :D :9}, + {:root :C + :chord-type "maj9" + :chord-pitches->readable-intervals + {:C :1 :E :M3 :G :5 :B :M7 :D :M9} :chord-notes (60 64 67 71 74)} - {:root :C, - :chord-type :maj11, - :chord-intervals #{0 7 4 17 11 14}, + {:root :C + :chord-type "maj13" :chord-pitches->readable-intervals - {:C :1, :G :5, :E :M3, :F :11, :B :M7, :D :9}, - :chord-notes (60 64 67 71 74 77)} - {:root :C, - :chord-type :sus2, - :chord-intervals #{0 7 2}, - :chord-pitches->readable-intervals {:C :1, :G :5, :D :M2}, - :chord-notes (60 62 67)} - {:root :C, - :chord-type :6, - :chord-intervals #{0 7 4 9}, - :chord-pitches->readable-intervals {:C :1, :G :5, :E :M3, :A :6}, + {:C :1 :E :M3 :G :5 :B :M7 :D :M9 :A :M13} + :chord-notes (60 64 67 71 74 81)} + {:root :C + :chord-type "6" + :chord-pitches->readable-intervals {:C :1 :E :M3 :G :5 :A :M6} :chord-notes (60 64 67 69)} - {:root :C, - :chord-type :sus4, - :chord-intervals #{0 7 5}, - :chord-pitches->readable-intervals {:C :1, :G :5, :F :4}, - :chord-notes (60 65 67)}) - (scale/scale-pitch->diatonic-chords (get scale/ALL-SCALES {:root :C :scale-type :major}) - :C)))) + {:root :C + :chord-type "6add9" + :chord-pitches->readable-intervals + {:C :1 :E :M3 :G :5 :A :M6 :D :M9} + :chord-notes (60 64 67 69 74)} + {:root :C + :chord-type "sus4" + :chord-pitches->readable-intervals {:C :1 :F :4 :G :5} + :chord-notes (60 65 67)} + {:root :C + :chord-type "sus2" + :chord-pitches->readable-intervals {:C :1 :D :M2 :G :5} + :chord-notes (60 62 67)} + {:root :C + :chord-type "5" + :chord-pitches->readable-intervals {:C :1 :G :5} + :chord-notes (60 67)} + {:root :C + :chord-type "sus24" + :chord-pitches->readable-intervals {:C :1 :D :M2 :F :4 :G :5} + :chord-notes (60 62 65 67)} + {:root :C + :chord-type "M7add13" + :chord-pitches->readable-intervals + {:C :1 :E :M3 :G :5 :A :M6 :B :M7 :D :M9} + :chord-notes (60 64 67 69 71 74)} + {:root :C + :chord-type "Madd9" + :chord-pitches->readable-intervals {:C :1 :E :M3 :G :5 :D :M9} + :chord-notes (60 64 67 74)} + {:root :C + :chord-type "M7sus4" + :chord-pitches->readable-intervals {:C :1 :F :4 :G :5 :B :M7} + :chord-notes (60 65 67 71)} + {:root :C + :chord-type "M9sus4" + :chord-pitches->readable-intervals + {:C :1 :F :4 :G :5 :B :M7 :D :M9} + :chord-notes (60 65 67 71 74)}) + (scale/scale-pitch->diatonic-chords (get scale/ALL-SCALES {:root :C :scale-type :major}) :C))))