# Consonance or dissonance?

Given two note names, you are to write a program that determines if the interval formed by these two notes is consonant or dissonant.

# Introduction

In Western music, there are only 12 "different" tones. Their names, sorted from lowest to highest, are these: C, C#, D, D#, E, F, F#, G, G#, A, A#, B. The sequence is cyclical, i. e. it continues with another C after the B, infinitely.

The distance between two tones is called an interval. The interval between any two notes that are adjacent in the series above (e. g. C — C# or E — F) is called a semitone. The interval between more distant notes is defined as the number of semitone steps needed to get from the first to the second (while possibly wrapping around the sequence). Some examples: D to E = 2 semitones, C to G = 7 semitones, B to D# = 4 semitones (this wraps around the sequence).1

Now, these intervals are divided into two categories: consonant (pleasantly sounding if you play the two notes at once) and dissonant (not so much).

Let's define the consonant intervals to be: 0, 3, 4, 5, 7, 8 and 9 semitones.

The rest of them is dissonant, namely: 1, 2, 6, 10 and 11 semitones.

# The Challenge

Write a "program" (in the usual broad sense of the word: a function is perfectly OK) to do the following:

• Take two note names (strings from the sequence above) as an input. You may take them however you like (from stdin, as arguments, separated by whatever you want, even feel free to take them as a list of characters (e. g. ["C","#"]). However, you may not assign any other names to the notes (especially you may not number them from 0 to 11 and use the numbers).

• For you music geeks out there, the notes will be specified without the octave. In this case, it also does not matter in which order the notes come and which is lower and which is higher. Finally, you don't need to handle any names not in the list above. No other enharmonics like E#, no flats, double-alterations and so on.

• Pick any two different values. Your program must output one of them whenever the interval formed by the two notes in the input is consonant, and the other if they are not. (Could be True and False, but even π and e if you want :))

• This is a code-golf. The shortest program in bytes in each language wins. Have fun!

# Examples and Test Cases

Note 1    Note 2    Output    Interval [semitones]
C          D     Dissonant   2
A#         A#    Consonant   0
G          D     Consonant   7 (wraparound)
D#         A     Dissonant   6
F          E     Dissonant   11
A          C     Consonant   3


I don't add more of them since there aren't any particularly treacherous cases in this.

This is a first challenge of mine, so any constructive criticism is warmly welcome :—). If you find the theory explanation sloppy, feel free to ask questions. Finally, please do not tell me that this is a dupe of this or this. I made sure it's not. (The latter is quite similar but more complex. I thought that putting up a little simpler challenge will make it easier for people to join.)

1: I tried to simplify this explanation as far as I could. There's a lot more theory around intervals. Please don't bash me for leaving it out.

# 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  -->  [[65, 35], 67]
Ḣ                      -   keep the first element  -->  [65, 67]
6×                    -   multiply by 6           -->  [390, 402]
%21                 -   modulo 21               -->  [12, 3]
_L               -   subtract the length     -->  [12, 3] - [2, 1] = [10, 2]
IA           - absolute difference       -->  8
“¬ɠṘ’      - the integer 540205
æ»    - right-shift               -->  540205 >> 8 = 2110
Ḃ   - isolate the LSB           -->  2110 & 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) =>
+!~[1, 2, 6, 10, 11].indexOf((note.indexOf(a) - note.indexOf(b) + 12) % 12)

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

table = {};

// for all possible pairs of notes (a, b)
if(note.every(a => note.every(b => {
// 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 < 31; n++) {
msk |= (table[n] || 0) << n;
}
// and display the result
console.log('mul ' + mul + ', mod ' + mod + ' --> ' + msk);
}
}
}

• How do you even come up with these things?.. Are you getting these kind of 'algorithms' by hand or by brute-force? And regardless of the answer of the second question: how?!.. :S "literal integer 540205; right shifted with (ASCII code; multiply by 6; modulo 21; keep first; subtract length...); bitwise-AND 1". Your answers keep impressing me every time.. Sep 19, 2017 at 14:15
• @KevinCruijssen I've added the original JS code that was used to find these values. Sep 19, 2017 at 15:37
• Thanks for the added explanation. I'm still as impressed as first, but you gave a clear explanation of how you came up with it. Too bad I can only upvote once. Sep 19, 2017 at 20:53

# APL (Dyalog), 62 39 bytes

Uses ⎕IO←0; 0 is consonant, 1 is dissonant. Takes list of base note chars as left argument and list of sharps as right argument.

{⎕A[|-/('C D EF G A '⍳⍺)+⍵=⍕#]∊'BCGKL'}


Try it online!

{} anonymous function where ⍺ is the left argument and ⍵ is the right argument

⎕A[]∊'BCGKL' is the Alphabet, indexed by the following, a member of the string?

⍕# format the root namespace (yields the sharp character)

⍵= are the right argument chars (the sharps) equal to that?

()+ add the following:

'C D EF G A '⍳⍺ indices of the left argument chars in the string

-/ difference between those

| absolute value

• Would you mind adding an explanation for those of us who are unfamiliar with APL? Sep 18, 2017 at 23:17
Sep 18, 2017 at 23:47

# MATL, 3027 26 bytes

,j'DJEFPGIALBC'&mQs]ZP7Mdm


Inputs the two notes in different lines. Outputs 0 for consonant, 1 for dissonant.

### Explanation

The 11-character string

DJEFPGIALBC


encodes both the notes and the dissonant intervals, as follows.

The program first finds the 1-based indices of the input characters in the above string. A non-sharp input like D will give 1, E will give 3, ..., C will give 11. These numbers can also be considered 1×1 numeric arrays. A sharp input like C# will give the 1×2 array [11 0], meaning that C was found at position 11 and # was not found.

Observe that letters JPIL will never be present in the input. For now they are only used as placeholders, so that for example note E is two semitones above D. But they will also be useful to define dissonant intervals.

The numbers in the first entry of the 1×1 or 1×2 array correspond to note pitch in semitones, not counting sharp symbols (yet). Observe that the scale defined by these numbers doesn't start at C; but that doesn't matter because we only want intervals, that is, differences between notes. Subtracting the obtained numbers would give either the interval or 12 minus the interval. But first we need to consider the sharp symbol.

To consider sharp notes, a golfy way (in MATL) is to add 1 to each entry of the 1×1 or 1×2 array obtained previously and then sum the array (2 bytes). Thus non-sharp notes are increased by 1 and sharp notes by 2. This makes sharp notes 1 semitone higher than non-sharp notes, as required. We are also adding an extra semitone to all notes, but that doesn't change the intervals between them. So now note D will give pitch number 2, D# will give 3, ..., C will give 12, C# will give 13.

Dissonant intervals are 1, 2, 6, 10, or 11. These have a modulo-12 symmetry: an interval between two notes is dissonant if and only if the interval with the notes in reverse order, modulo 12, is dissonant.

If we compute the consecutive differences of the string 'DJEFPGIALBC' we get the numeric vector

6 -5 1 10 -9 2 -8 11 -10 1


which contains precisely the dissonant intervals, in addition to some negative values, which will be neither useful nor harmful. Observe that it is the choice of additional letters JPIL in the string 'DJEFPGIALBC' that defines (via consecutive differences) the dissonant intervals.

To see if the two input notes are dissonant we take the absolute difference of their pitch numbers. For example, C and D# will give numbers 12 and 3 respectively, and the absolute difference is 9. The actual difference would be -9, and the actual interval would be 3 (obtained as -9 modulo 12). But thanks to the symmetry referred to above, we can consider 9 instead of 3. Since 9 is not present in the vector of consecutive differences, the notes are consonant.

• I like the way how you encoded both the notes and the dissonant intervals in the same string. Sep 19, 2017 at 9:32

# JavaScript (ES6), 68 64 bytes

Takes the notes as two strings in currying syntax (a)(b). Returns 0 for dissonant or 1 for consonant.

a=>b=>488055>>(g=s=>'C D EF G A'.search(s[0])-!s[1])(a)-g(b)+9&1


### Test cases

let f =

a=>b=>488055>>(g=s=>'C D EF G A'.search(s[0])-!s[1])(a)-g(b)+9&1

console.log(f('C' )('D' )) // 0 (Dissonant)
console.log(f('A#')('A#')) // 1 (Consonant)
console.log(f('G' )('D' )) // 1 (Consonant)
console.log(f('D#')('A' )) // 0 (Dissonant)
console.log(f('F' )('E' )) // 0 (Dissonant)
console.log(f('A' )('C' )) // 1 (Consonant)

### Formatted and commented

a => b =>                       // given the two notes 'a' and 'b'
488055 >>                     // 19-bit lookup bitmask: 1110111001001110111
(g = s =>                   // we use g() to convert a note 's' into a semitone index
'C D EF G A'.search(s[0]) // position of the note: -1 for 'B' (not found) to 9 for 'A'
- !s[1]                   // subtract 1 semitone if the '#' is not there
)(a)                        // compute the result for 'a'  --> [ -2 ...  9]
- g(b)                      // subtract the result for 'b' --> [-11 ... 11]
+ 9                         // add 9                       --> [ -2 ... 20]
& 1                           // test the bitmask at this position (0 if negative or > 18)


# Jelly, 26 bytes

i@€ØAo.SḤ’d5ḅ4µ€ạ/“¢£©½¿‘ċ


A monadic link taking a list of the two notes (as lists of characters) and returning 0 for consonant and 1 for dissonant.

Try it online! or see all inputs in the test-suite.

### How?

i@€ØAo.SḤ’d5ḅ4µ€ạ/“¢£©½¿‘ċ - Link: list of lists of characters, notes
µ€           - for €ach note in notes: (call the resulting list x)
ØA                      -   yield the uppercase alphabet
i@€                        -   first index of c in ^ for €ach character, c
-     ...note '#' is not there so yields 0 (A->1, B->2,...)
.                    -   literal one half
o                     -   or (vectorised)  - e.g. "C#" -> [3, 0] -> [3, 0.5]
S                   -   sum
Ḥ                  -   double - that is ...  C C#  D D#  E  F F#  G G#  A A#  B
->  6  7  8  9 10 12 13 14 15  2  3  4
’                 -   decrement         ->  5  6  7  8  9 11 12 13 14  1  2  3
5               -   literal five
d                -   divmod                (e.g. 9 -> [1,4] or 11 -> [2,1])
4             -   literal four
ḅ              -   convert from base     (e.g. [1,4] -> 8 or [2,1] -> 9)
->  4  5  6  7  8  9 10 11 12  1  2  3
/         - reduce x with:
ạ          -   absolute difference   (e.g. ["G#", "A"] -> [12, 1] -> 11)
“¢£©½¿‘  - code-page indices = [1, 2, 6, 10, 11]
ċ - count occurrences (1 if in the list, 0 if not)


O_65ị“¢[ḋṃ’b⁴¤+L$€Ḣ€ạ/e“cṾ’b12¤  Try it online! wheeeeee 32 bytes too long # Explanation O_65ị“¢[ḋṃ’b⁴¤+L$€Ḣ€ạ/e“cṾ’b12¤  Main link
O                                Cast each character to an int using Python ord
_65                             Subtract 65 (A is 0, G is 7)
“¢[ḋṃ’b⁴¤                   [2, 3, 5, 7, 9, 10, 0]
“¢[ḋṃ’                      37058720
b                     Digits in base
⁴                    16
ị                            Index into this list; this creates the gaps for sharps
€               For each sublist
+L$Add the length to each element (Sharpens sharp notes) + Add L Length € For each sublist Ḣ Take the first element ạ/ Absolute difference between the two (unoctaved) pitches # It's convenient that every interval's inverse (?) has the same consonance/dissonance e Is the semitone difference in “cṾ’b12¤ [1, 2, 6, 10, 11]? “cṾ’ 25178 b base 12 12  • Hey, that's a great answer! I was wondering if someone makes use of the symmetry, and you did. And I like your method for matching the note names with numbers as well! +1. Sep 18, 2017 at 21:53 • The semitone difference may be symmetrical but you still get duff results - for example "G#", "A" (dissonant) yields a difference of 11 which is not in [1,2,6]. Sep 18, 2017 at 22:26 • @JonathanAllan oh uh well that's embarrassing; I thought that absolute difference fixed that... ._. will fix lol Sep 18, 2017 at 23:43 • @JonathanAllan fixed for a couple extra bytes (3 IIRC) Sep 18, 2017 at 23:52 # Mathematica, 55 bytes function arguments bytes FreeQ[1|2|6|10|11]@Abs[#-#2&@@SoundPitchToNumber/@#]& [{"C","F#"}] 55  Map the internal built-in SoundPitchToNumber on the input (list of two strings), take the absolute difference, then pattern match for dissonant interval numbers. ## Just for fun (non-competing) Here are some shorter functions that violate the restriction “you may not assign any other names to the notes.” The rudimentary Music package has predefined note constants (like A4 = 440.) and the function HertzToCents (which can be golfed). Instead of strings, we will use the note constants as arguments, but given in a different format for each function. FreeQ[1|2|6|10|11]@Abs@@Round[.01HertzToCents@#]& [{C3,Fsharp3}] 50+9=59 FreeQ[1|2|6|10|11]@Abs@Round[17Log[#2/#]]& [C3,Fsharp3] 43+9=52 FreeQ[1|2|6|10|11]@Abs@Round[17Log@#]& [C3/Fsharp3] 39+9=48  The package import <<Music; takes 9 bytes. This function converts a string (like "F#") into a note constant (like Fsharp3): Symbol[StringReplace[#,"#"->"sharp"]<>"3"]& 44  To accept intervals larger than an octave, replace Abs[…] with Mod[…,12]. Why are some intervals considered dissonant? An interval is a ratio of two frequencies. If the ratio has a “simple” numerator and denominator, it tends to be more consonant. In 5-limit tuning, ratios can be factored into integer powers of only prime numbers less than or equal to 5. No interval in equal temperament, besides the octave, is a just interval; they are merely close approximations using powers of the 12th root of 2. Instead of hard-coding which interval numbers are dissonant, we can find a rational approximation of the interval and then determine if its numerator and denominator are “simple” (meaning that the denominator is less than 5 and the ratio does not divide 7). This table shows each of the steps in that process. Table[ Module[{compoundInterval,simpleInterval,rationalApprox,denomLeq5,div7,consonant}, compoundInterval = Power[2, i/12]; simpleInterval = 2^Mod[Log2[compoundInterval], 1]; rationalApprox = Rationalize[N@simpleInterval, 1/17]; denomLeq5 = Denominator[rationalApprox]<=5; div7 = Denominator[rationalApprox]>1 && rationalApprox\[Divides]7; consonant = FreeQ[1|2|6|10|11][Mod[i,12]]; InputForm/@{ i, simpleInterval, rationalApprox, denomLeq5, div7, denomLeq5 && !div7, consonant } ], {i, 0, 12} ] i sInterval ratio denomLeq5 div7 den&&!div | consonant? 0 1 1 True False True | True 1 2^(1/12) 17/16 False False False | False 2 2^(1/6) 9/8 False False False | False 3 2^(1/4) 6/5 True False True | True 4 2^(1/3) 5/4 True False True | True 5 2^(5/12) 4/3 True False True | True 6 Sqrt[2] 7/5 True True False | False 7 2^(7/12) 3/2 True False True | True 8 2^(2/3) 8/5 True False True | True 9 2^(3/4) 5/3 True False True | True 10 2^(5/6) 7/4 True True False | False 11 2^(11/12) 11/6 False False False | False 12 1 1 True False True | True  The rational approximation lies within 1/17 of the interval because that is the largest threshold that distinguishes between all 12 equal tempered intervals. We match rational numbers with the pattern Rational[a_,b_] (or just a_~_~b_) first, and then match integers with only _. This culminates in the following rather short function that determines if an arbitrary frequency ratio (greater than 1) is consonant or dissonant. Rationalize[#,1/17]/.{a_~_~b_:>b<=5&&!a∣7,_->True}& [Fsharp3/C3] 51+9=60  • Gosh, don't tell me Mathematica has a builtin even for this... :D Sep 19, 2017 at 18:01 # Mathematica, 118 bytes FreeQ[{1,2,6,10,11},Min@Mod[Differences[Min@Position["C|C#|D|D#|E|F|F#|G|G#|A|A#|B"~StringSplit~"|",#]&/@{#,#2}],12]]&  Input form ["A#","D"] Outputs True->Consonant False->Dissonant  thanks @JonathanFrech -16 bytes • Just a remark: you don't need to output strings Consonant and Dissonant. You may output any two values instead of them (0/1,... whatever). That could save some bytes. Sep 18, 2017 at 21:42 • Can you not omit the If[...,0,1] and define True->Consonant; False->Dissonant? Sep 18, 2017 at 22:00 • StringCases["CC#DD#EFF#GG#AA#B",_~~"#"...] – 42 Bytes Sep 19, 2017 at 9:12 • Also, 2 bytes can be saved by replacing {1,2,6,10,11} with 1|2|6|10|11 Sep 19, 2017 at 9:22 • @Skyler See the answer below. – hftf Oct 10, 2017 at 22:52 # Charcoal, 30 bytes ≔B#A#G#FE#D#Cζ§”o∧⌈ς”⁻⌕ζ⮌θ⌕ζ⮌η  Try it online! Link is to verbose version of code. Outputs 1 for consonant, 0 for dissonant. Explanation: ≔B#A#G#FE#D#Cζ Store reversed note names in z θ First input ⮌ Reversed ⌕ζ Find index in z η Second input ⮌ Reversed ⌕ζ Find index in z ⁻ Subtract ”o∧⌈ς” Compressed string 100111011100 § Circularly index Implicitly print  • out of curiosity, is there a mnemonic reason the glyph ⌕ζ is used for "find index"? Sep 18, 2017 at 23:39 • @Jonah ζ is the variable assigned to earlier. – Neil Sep 18, 2017 at 23:46 # J, 68 bytes [:e.&1 2 6 10 11[:(12| -~/)(<;._1',C,C#,D,D#,E,F,F#,G,G#,A,A#,B')i.]  ## explanation A straightforward, not super golfed implementation in J: • Input is given as boxed itemized notes (produced using cut), in order. • Find their indexes in the range of notes: (<;._1',C,C#,D,D#,E,F,F#,G,G#,A,A#,B')i.] • Subtract the first from the second: -~/ • Take the remainder when divided by 12: 12| • Check if it's one of the dissonant notes: e.&1 2 6 10 11 Try it online! # ///, 90 88 bytes /^/"\///^\/\///C^D/##"E/DD"F/E#"G/FD"A/GD"B/AD"#,#/,"B#^B/#"A#/#"A^G#^G^F#/#"F^E^D#^D/#/  Try it online! (all test cases at once) • Put the input after the code. • Separate the note names with ,B# in each test case. • The output is , for consonant, ,# for dissonant. • Support for double-alterations (##) or E# in some particular cases. Otherwise the output is , for consonant, #, for dissonant (thanks to modulo 12 symmetry) • Can handle multiple test cases at once (if separated reasonably) • Lowercase characters are printed exactly. # C (gcc), 91 bytes g(char*s){return (s[1]&1|2**s&15)*4/5;}f(char*x,char*y){return (1952220<<g(x)>>g(y))&2048;}  call: f("A#", "D") Return value: • Consonant: 2048 • Dissonant: 0 Bonus: The function is case insensitive. Try it online! • Are there not two unnecessary spaces in both return (s? Sep 20, 2017 at 7:43 • You may try g(char*s){s=(s[1]&1|2**s&15)*4/5;}f(char*x,char*y){x=1952220<<g(x)>>g(y)&2048;} and nice solution! Sep 20, 2017 at 8:08 # Python 2, 125 117 83 78 77 bytes a,b=map("C C#D D#E F F#G G#A A#B".index,input()) print chr(abs(a-b))in""  Where the "" at the end actually contains the characters "\x02\x04\x0c\x14\x16" Try it Online! (+3 because I forgot 11 or 22 in the list to begin with) -8 bytes from Jonathan Frech and switching to Python 2. -34 bytes with suggestions from Jonathan Frech and using str's index instead of list's. -4 bytes from inlining i and Neil's reversing the string suggestion (only -2 really, as i forgot () around a generator) -5 bytes from un-inlining i & changing the input format -1 bytes from Jonathan Frech with map() and unprintables. Takes input in one line of stdin in the format: 'C','C#'  True is dissonant, False is consonant. Old Explanation: i='C C#D D#E F F#G G#A A#B'.index a,b=input() print abs(i(a)-i(b))in[2,4,12,20]  Python str.index returns the lowest (positive) starting index of a matching substring, so "ABACABA".index("A") == 0 and "ABACABA".index("BA") == 1. Because of this, we can put the note names evenly spaced in a string, and as long as (for example) A comes before A#, the shared A won't be a problem. i='C C#D D#E F F#G G#A A#B'.index  i is now a function that returns the index in 'C C#D D#E F F#G G#A A#B' its argument (a note name) is, which is 2 * (the number of semitones that note is up from C) a,b=input()  Python 2's input() is (mostly) equivalent to eval(input()) in Python3, so with a valid input of the format 'C#','F' (for example), a='C#' and b='F' print abs(i(a)-i(b))in[2,4,12,20]  If the disttance between the first note and the second note in the string is not 2, 4, 12, or 20 (since the note names are represented in 2 characters), then the interval is dissonant, print True, else it is consonant, print False. • Since the input format is not strict, you can use eval(input()) (13 bytes) instead of input().split() (15 bytes). Sep 18, 2017 at 23:22 • 101 bytes Sep 18, 2017 at 23:28 • 98 bytes Sep 18, 2017 at 23:30 • 79 bytes Sep 18, 2017 at 23:47 • You could use Unicode characters () instead of an emtpy string. Sep 19, 2017 at 1:35 # C (gcc), 115 117120 bytes g(char*a){a=*a-65+!!a[1]*(7-*a/70-*a/67);}f(x,y)char*x,*y;{x="(pP$HL<lt<X"[g(x)]*32+"=ZukW-^h1F6"[g(x)]>>g(y)&1;}


Try it online!

Return 1/0 for consonat and dissonat. It is always interesting to do string manipulation with pure C. Take input as f("A#", "C")

# PowerShell, 107 bytes

param($a,$b)[math]::abs(($x=-split'C C# D D# E F F# G G# A A# B').indexof($b)-$x.indexof($a))-in1,2,6,10,11


Try it online!

Outputs True for dissonant and False for consonant.

Takes input $a and $b, the two notes, as strings. Performs a -split operation on the scale, which splits on whitespace, to create an array of the notes, stores that into $x. Finds the .indexof $b in that array, subtracts the index of $a, and then takes the absolute value thereof. Checks whether that number is -in the dissonant ranges. # Python 2, 68 bytes lambda a,b,f='C C#D D#E F F#G G#A A#B'.find:3142>>(f(a)-f(b))/2%12&1  Try it online! Outputs: 1 is dissonant, 0 is consonant. # SQL, 582 bytes SQL Fiddle I still have some golfing to do on it, but I wanted to get it down here before I end up breaking it completely. If the input is in a letter format, then putting those letters in a table with values is okay, right? CREATE TABLE N(N char(2),v int) Insert Into N values('A',1),('A#',2),('B',3),('C',4),('C#',5),('D',6),('D#',7),('E',8),('F',9),('F#',10),('G',11),('G#',12); CREATE TABLE D(D char(9),v int) Insert Into D values('C',0),('D',1),('D',2),('C',3),('C',4),('C',5),('D',6); CREATE FUNCTION I(@A char(2),@B char(2)) RETURNS char(9) as BEGIN DECLARE @E int=(SELECT v from N where n=@A),@F int=(SELECT v from N where n=@B) DECLARE @C char(9) = (SELECT case D when 'D' then 'Dissonant' when 'C' then 'Consonant' END from D where v in(abs(@e-@f),12-abs(@e-@f))) RETURN isnull(@C,'NotANote') END  # Perl 5, 106 bytes ("C,C#,D,D#,E,F,F#,G,G#,A,A#,B,"x2)=~/$F[0],(.*?)$F[1],/;$r=(1+($1=~y/,//))%12;say(grep/$r/,(0,3..5,7..9))


Try it online!

Returns false for dissonant, true for consonant.