Consonance or dissonance?
Jelly, 21 bytes

Takes input as a list of two strings. Returns 0 for dissonant or 1 for consonant.

OḢ6×%21_Lµ€IA“¬ɠṘ’æ»Ḃ


Try it online!

OḢ6×%21_Lµ€IA“¬ɠṘ’æ»Ḃ   - main link
         µ€             - for each note             e.g. ["A#", "C"]
O                       -   convert to ASCII codes  --&gt;  [[65, 35], 67]
 Ḣ                      -   keep the first element  --&gt;  [65, 67]
  6×                    -   multiply by 6           --&gt;  [390, 402]
    %21                 -   modulo 21               --&gt;  [12, 3]
       _L               -   subtract the length     --&gt;  [12, 3] - [2, 1] = [10, 2]
           IA           - absolute difference       --&gt;  8
             “¬ɠṘ’      - the integer 540205
                  æ»    - right-shift               --&gt;  540205 &gt;&gt; 8 = 2110
                    Ḃ   - isolate the LSB           --&gt;  2110 &amp; 1 = 0


Making-of

We first should note that the function F that we're looking for is commutative: for any pair of notes (A, B), we have F(A, B) = F(B, A).

Since there are not too many possible inputs and only 2 possible outputs to deal with, it must be possible to find a rather simple hash function H, such that |H(A) - H(B)| produces a limited range of values and is collision-free for all possible pairs of notes (A, B) with respect to the expected output.

We're going to test the set of functions H(mul, mod), which are defined as:

H(mul, mod)(s) = ((ORD(s[0]) * mul) MOD mod) - LEN(s)


Where ORD(s[0]) is the ASCII code of the first character of the note and LEN(s) is the length of the note (2 if there's a '#' and 1 if not).

Below is a commented version of the JS code that was used to find a couple of valid pairs (mul, mod) and the resulting bitmasks. There are many possible solutions, but * 6 % 21 is the shortest one with this method.



// all 12 notes
note = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];

// reference implementation that takes 2 notes
// and returns 0 for dissonant or 1 for consonant
F = (a, b) =&gt;
  +!~[1, 2, 6, 10, 11].indexOf((note.indexOf(a) - note.indexOf(b) + 12) % 12)

for(mul = 1; mul &lt; 100; mul++) {
  for(mod = 1; mod &lt; 31; mod++) {
    // hash function
    H = s =&gt; s.charCodeAt() * mul % mod - s.length;

    table = {};

    // for all possible pairs of notes (a, b)
    if(note.every(a =&gt; note.every(b =&gt; {
      // compute the table index
      idx = Math.abs(H(a) - H(b));

      // and the expected result
      res = F(a, b);

      // if the slot is empty, store the correct value
      if(table[idx] === undefined) {
        table[idx] = res;
        return 1;
      }
      // else, make sure that the correct value is already there
      return table[idx] == res;
    }))) {
      // if all tests pass: translate the table into a binary mask
      for(n = 0, msk = 0; n &lt; 31; n++) {
        msk |= (table[n] || 0) &lt;&lt; n;
      }
      // and display the result
      console.log('mul ' + mul + ', mod ' + mod + ' --&gt; ' + msk);
    }
  }
}