diff --git a/config.json b/config.json index 81a601c..9401d65 100644 --- a/config.json +++ b/config.json @@ -171,6 +171,14 @@ "prerequisites": [], "difficulty": 3 }, + { + "slug": "scale-generator", + "name": "Scale Generator", + "uuid": "c9d72016-4314-4086-8642-1e9e006e19ab", + "practices": [], + "prerequisites": [], + "difficulty": 3 + }, { "slug": "spiral-matrix", "name": "Spiral Matrix", diff --git a/exercises/practice/scale-generator/.docs/instructions.md b/exercises/practice/scale-generator/.docs/instructions.md new file mode 100644 index 0000000..ebb7deb --- /dev/null +++ b/exercises/practice/scale-generator/.docs/instructions.md @@ -0,0 +1,68 @@ +# Instructions + +## Chromatic Scales + +Scales in Western music are based on the chromatic (12-note) scale. +This scale can be expressed as the following group of pitches: + +> A, A♯, B, C, C♯, D, D♯, E, F, F♯, G, G♯ + +A given sharp note (indicated by a ♯) can also be expressed as the flat of the note above it (indicated by a ♭) so the chromatic scale can also be written like this: + +> A, B♭, B, C, D♭, D, E♭, E, F, G♭, G, A♭ + +The major and minor scale and modes are subsets of this twelve-pitch collection. +They have seven pitches, and are called diatonic scales. +The collection of notes in these scales is written with either sharps or flats, depending on the tonic (starting note). +Here is a table indicating whether the flat expression or sharp expression of the scale would be used for a given tonic: + +| Key Signature | Major | Minor | +| ------------- | --------------------- | -------------------- | +| Natural | C | a | +| Sharp | G, D, A, E, B, F♯ | e, b, f♯, c♯, g♯, d♯ | +| Flat | F, B♭, E♭, A♭, D♭, G♭ | d, g, c, f, b♭, e♭ | + +Note that by common music theory convention the natural notes "C" and "a" follow the sharps scale when ascending and the flats scale when descending. +For the scope of this exercise the scale is only ascending. + +### Task + +Given a tonic, generate the 12 note chromatic scale starting with the tonic. + +- Shift the base scale appropriately so that all 12 notes are returned starting with the given tonic. +- For the given tonic, determine if the scale is to be returned with flats or sharps. +- Return all notes in uppercase letters (except for the `b` for flats) irrespective of the casing of the given tonic. + +## Diatonic Scales + +The diatonic scales, and all other scales that derive from the chromatic scale, are built upon intervals. +An interval is the space between two pitches. + +The simplest interval is between two adjacent notes, and is called a "half step", or "minor second" (sometimes written as a lower-case "m"). +The interval between two notes that have an interceding note is called a "whole step" or "major second" (written as an upper-case "M"). +The diatonic scales are built using only these two intervals between adjacent notes. + +Non-diatonic scales can contain other intervals. +An "augmented second" interval, written "A", has two interceding notes (e.g., from A to C or D♭ to E) or a "whole step" plus a "half step". +There are also smaller and larger intervals, but they will not figure into this exercise. + +### Task + +Given a tonic and a set of intervals, generate the musical scale starting with the tonic and following the specified interval pattern. + +This is similar to generating chromatic scales except that instead of returning 12 notes, you will return N+1 notes for N intervals. +The first note is always the given tonic. +Then, for each interval in the pattern, the next note is determined by starting from the previous note and skipping the number of notes indicated by the interval. + +For example, starting with G and using the seven intervals MMmMMMm, there would be the following eight notes: + +| Note | Reason | +| ---- | ------------------------------------------------- | +| G | Tonic | +| A | M indicates a whole step from G, skipping G♯ | +| B | M indicates a whole step from A, skipping A♯ | +| C | m indicates a half step from B, skipping nothing | +| D | M indicates a whole step from C, skipping C♯ | +| E | M indicates a whole step from D, skipping D♯ | +| F♯ | M indicates a whole step from E, skipping F | +| G | m indicates a half step from F♯, skipping nothing | diff --git a/exercises/practice/scale-generator/.meta/config.json b/exercises/practice/scale-generator/.meta/config.json new file mode 100644 index 0000000..40b5ceb --- /dev/null +++ b/exercises/practice/scale-generator/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "pfertyk" + ], + "files": { + "solution": [ + "scale_generator.gd" + ], + "test": [ + "scale_generator_test.gd" + ], + "example": [ + ".meta/example.gd" + ] + }, + "blurb": "Generate musical scales, given a starting note and a set of intervals." +} diff --git a/exercises/practice/scale-generator/.meta/example.gd b/exercises/practice/scale-generator/.meta/example.gd new file mode 100644 index 0000000..d3e2c97 --- /dev/null +++ b/exercises/practice/scale-generator/.meta/example.gd @@ -0,0 +1,30 @@ +const ASCENDING_INTERVALS = ['m', 'M', 'A'] +const CHROMATIC_SCALE = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'] +const FLAT_CHROMATIC_SCALE = ['A', 'Bb', 'B', 'C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab'] +const FLAT_KEYS = ['F', 'Bb', 'Eb', 'Ab', 'Db', 'Gb', 'd', 'g', 'c', 'f', 'bb', 'eb'] + +@export var tonic : String + + +func chromatic(): + return _reorder_chromatic_scale() + + +func interval(intervals): + var last_index = 0 + var pitches = [] + var scale = _reorder_chromatic_scale() + + for interval in intervals: + pitches.append(scale[last_index]) + last_index += ASCENDING_INTERVALS.find(interval) + 1 + + pitches.append(tonic.capitalize()) + + return pitches + + +func _reorder_chromatic_scale(): + var chromatic_scale = (FLAT_CHROMATIC_SCALE if tonic in FLAT_KEYS else CHROMATIC_SCALE) + var index = chromatic_scale.find(tonic.capitalize()) + return chromatic_scale.slice(index) + chromatic_scale.slice(0, index) diff --git a/exercises/practice/scale-generator/.meta/tests.toml b/exercises/practice/scale-generator/.meta/tests.toml new file mode 100644 index 0000000..1cab430 --- /dev/null +++ b/exercises/practice/scale-generator/.meta/tests.toml @@ -0,0 +1,136 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[10ea7b14-8a49-40be-ac55-7c62b55f9b47] +description = "Chromatic scales -> Chromatic scale with sharps" + +[af8381de-9a72-4efd-823a-48374dbfe76f] +description = "Chromatic scales -> Chromatic scale with flats" + +[7195998a-7be7-40c9-8877-a1d7949e061b] +description = "Scales with specified intervals -> Simple major scale" +reimplements = "6f5b1410-1dd7-4c6c-b410-6b7e986f6f1e" + +[fe853b97-1878-4090-b218-4029246abb91] +description = "Scales with specified intervals -> Major scale with sharps" +reimplements = "13a92f89-a83e-40b5-b9d4-01136931ba02" + +[d60cb414-cc02-4fcb-ad7a-fc7ef0a9eead] +description = "Scales with specified intervals -> Major scale with flats" +reimplements = "aa3320f6-a761-49a1-bcf6-978e0c81080a" + +[77dab9b3-1bbc-4f9a-afd8-06da693bcc67] +description = "Scales with specified intervals -> Minor scale with sharps" +reimplements = "63daeb2f-c3f9-4c45-92be-5bf97f61ff94" + +[5fa1728f-5b66-4b43-9b7c-84359b7069d4] +description = "Scales with specified intervals -> Minor scale with flats" +reimplements = "616594d0-9c48-4301-949e-af1d4fad16fd" + +[f3f1c353-8f7b-4a85-a5b5-ae06c2645823] +description = "Scales with specified intervals -> Dorian mode" +reimplements = "390bd12c-5ac7-4ec7-bdde-4e58d5c78b0a" + +[5fe14e5a-3ddc-4202-a158-2c1158beb5d0] +description = "Scales with specified intervals -> Mixolydian mode" +reimplements = "846d0862-0f3e-4f3b-8a2d-9cc74f017848" + +[e6307799-b7f6-43fc-a6d8-a4834d6e2bdb] +description = "Scales with specified intervals -> Lydian mode" +reimplements = "7d49a8bb-b5f7-46ad-a207-83bd5032291a" + +[7c4a95cd-ecf4-448d-99bc-dbbca51856e0] +description = "Scales with specified intervals -> Phrygian mode" +reimplements = "a4e4dac5-1891-4160-a19f-bb06d653d4d0" + +[f476f9c9-5a13-473d-bb6c-f884cf8fd9f2] +description = "Scales with specified intervals -> Locrian mode" +reimplements = "ef3650af-90f8-4ad9-9ef6-fdbeae07dcaa" + +[87fdbcca-d3dd-46d5-9c56-ec79e25b19f4] +description = "Scales with specified intervals -> Harmonic minor" +reimplements = "70517400-12b7-4530-b861-fa940ae69ee8" + +[b28ecc18-88db-4fd5-a973-cfe6361e2b24] +description = "Scales with specified intervals -> Octatonic" +reimplements = "37114c0b-c54d-45da-9f4b-3848201470b0" + +[a1c7d333-6fb3-4f3b-9178-8a0cbe043134] +description = "Scales with specified intervals -> Hexatonic" +reimplements = "496466e7-aa45-4bbd-a64d-f41030feed9c" + +[9acfd139-0781-4926-8273-66a478c3b287] +description = "Scales with specified intervals -> Pentatonic" +reimplements = "bee5d9ec-e226-47b6-b62b-847a9241f3cc" + +[31c933ca-2251-4a5b-92dd-9d5831bc84ad] +description = "Scales with specified intervals -> Enigmatic" +reimplements = "dbee06a6-7535-4ab7-98e8-d8a36c8402d1" + +[6f5b1410-1dd7-4c6c-b410-6b7e986f6f1e] +description = "Scales with specified intervals -> Simple major scale" +include = false + +[13a92f89-a83e-40b5-b9d4-01136931ba02] +description = "Scales with specified intervals -> Major scale with sharps" +include = false + +[aa3320f6-a761-49a1-bcf6-978e0c81080a] +description = "Scales with specified intervals -> Major scale with flats" +include = false + +[63daeb2f-c3f9-4c45-92be-5bf97f61ff94] +description = "Scales with specified intervals -> Minor scale with sharps" +include = false + +[616594d0-9c48-4301-949e-af1d4fad16fd] +description = "Scales with specified intervals -> Minor scale with flats" +include = false + +[390bd12c-5ac7-4ec7-bdde-4e58d5c78b0a] +description = "Scales with specified intervals -> Dorian mode" +include = false + +[846d0862-0f3e-4f3b-8a2d-9cc74f017848] +description = "Scales with specified intervals -> Mixolydian mode" +include = false + +[7d49a8bb-b5f7-46ad-a207-83bd5032291a] +description = "Scales with specified intervals -> Lydian mode" +include = false + +[a4e4dac5-1891-4160-a19f-bb06d653d4d0] +description = "Scales with specified intervals -> Phrygian mode" +include = false + +[ef3650af-90f8-4ad9-9ef6-fdbeae07dcaa] +description = "Scales with specified intervals -> Locrian mode" +include = false + +[70517400-12b7-4530-b861-fa940ae69ee8] +description = "Scales with specified intervals -> Harmonic minor" +include = false + +[37114c0b-c54d-45da-9f4b-3848201470b0] +description = "Scales with specified intervals -> Octatonic" +include = false + +[496466e7-aa45-4bbd-a64d-f41030feed9c] +description = "Scales with specified intervals -> Hexatonic" +include = false + +[bee5d9ec-e226-47b6-b62b-847a9241f3cc] +description = "Scales with specified intervals -> Pentatonic" +include = false + +[dbee06a6-7535-4ab7-98e8-d8a36c8402d1] +description = "Scales with specified intervals -> Enigmatic" +include = false diff --git a/exercises/practice/scale-generator/scale_generator.gd b/exercises/practice/scale-generator/scale_generator.gd new file mode 100644 index 0000000..d3456b1 --- /dev/null +++ b/exercises/practice/scale-generator/scale_generator.gd @@ -0,0 +1,9 @@ +@export var tonic : String + + +func chromatic(self): + pass + + +func interval(self, intervals): + pass diff --git a/exercises/practice/scale-generator/scale_generator_test.gd b/exercises/practice/scale-generator/scale_generator_test.gd new file mode 100644 index 0000000..130be79 --- /dev/null +++ b/exercises/practice/scale-generator/scale_generator_test.gd @@ -0,0 +1,104 @@ +# Test chromatic scales +func test_chromatic_scale_with_sharps(scale): + scale.tonic = "C" + var expected = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] + return [scale.chromatic(), expected] + + +func test_chromatic_scale_with_flats(scale): + scale.tonic = "F" + var expected = ["F", "Gb", "G", "Ab", "A", "Bb", "B", "C", "Db", "D", "Eb", "E"] + return [scale.chromatic(), expected] + + +# Test scales with specified intervals +func test_simple_major_scale(scale): + scale.tonic = "C" + var expected = ["C", "D", "E", "F", "G", "A", "B", "C"] + return [scale.interval("MMmMMMm"), expected] + + +func test_major_scale_with_sharps(scale): + scale.tonic = "G" + var expected = ["G", "A", "B", "C", "D", "E", "F#", "G"] + return [scale.interval("MMmMMMm"), expected] + + +func test_major_scale_with_flats(scale): + scale.tonic = "F" + var expected = ["F", "G", "A", "Bb", "C", "D", "E", "F"] + return [scale.interval("MMmMMMm"), expected] + + +func test_minor_scale_with_sharps(scale): + scale.tonic = "f#" + var expected = ["F#", "G#", "A", "B", "C#", "D", "E", "F#"] + return [scale.interval("MmMMmMM"), expected] + + +func test_minor_scale_with_flats(scale): + scale.tonic = "bb" + var expected = ["Bb", "C", "Db", "Eb", "F", "Gb", "Ab", "Bb"] + return [scale.interval("MmMMmMM"), expected] + + +func test_dorian_mode(scale): + scale.tonic = "d" + var expected = ["D", "E", "F", "G", "A", "B", "C", "D"] + return [scale.interval("MmMMMmM"), expected] + + +func test_mixolydian_mode(scale): + scale.tonic = "Eb" + var expected = ["Eb", "F", "G", "Ab", "Bb", "C", "Db", "Eb"] + return [scale.interval("MMmMMmM"), expected] + + +func test_lydian_mode(scale): + scale.tonic = "a" + var expected = ["A", "B", "C#", "D#", "E", "F#", "G#", "A"] + return [scale.interval("MMMmMMm"), expected] + + +func test_phrygian_mode(scale): + scale.tonic = "e" + var expected = ["E", "F", "G", "A", "B", "C", "D", "E"] + return [scale.interval("mMMMmMM"), expected] + + +func test_locrian_mode(scale): + scale.tonic = "g" + var expected = ["G", "Ab", "Bb", "C", "Db", "Eb", "F", "G"] + return [scale.interval("mMMmMMM"), expected] + + +func test_harmonic_minor(scale): + scale.tonic = "d" + var expected = ["D", "E", "F", "G", "A", "Bb", "Db", "D"] + return [scale.interval("MmMMmAm"), expected] + + +func test_octatonic(scale): + scale.tonic = "C" + var expected = ["C", "D", "D#", "F", "F#", "G#", "A", "B", "C"] + return [scale.interval("MmMmMmMm"), expected] + + +func test_hexatonic(scale): + scale.tonic = "Db" + var expected = ["Db", "Eb", "F", "G", "A", "B", "Db"] + return [scale.interval("MMMMMM"), expected] + + +func test_pentatonic(scale): + scale.tonic = "A" + var expected = ["A", "B", "C#", "E", "F#", "A"] + return [scale.interval("MMAMA"), expected] + + +func test_enigmatic(scale): + scale.tonic = "G" + var expected = ["G", "G#", "B", "C#", "D#", "F", "F#", "G"] + return [scale.interval("mAMMMmm"), expected] + +