7
\$\begingroup\$

Preface

The available keys are the keys of the A through G and A minor through G minor.

The Challenge

Create a program that creates and saves to a playable audio file a "song" of using (uniformly distributed) random chords. The song must be of length n and in key k. The chords in each "song" must conform to the key. If you don't know what belongs in a particular key, the image below may help.

Keys and Chords

Rules and Guidelines

  • Must conform to key

  • Chords must be chosen randomly with even distribution

  • Assume each chord is held for 1 beat in 4:4 time at 120 bpm, therefore each chord lasts .5 seconds

  • The same chord is allowed to be selected multiple times in a row, though it must be randomly

  • The program should theoretically be able to generate a "song" of any positive time value

  • Assume n > 1 in all cases

  • Input will be a length n (in seconds) and a key k as a major or minor key A, Amin, B, Bmin, etc

  • Output must be in the form of a playable audio file

Examples (as strings)

[key], [length] -> [output]

C, 1 -> Dmin, F

B, 6 -> D# min, C# min, F#, G# min, E, B, F#, C# min, C# min, D# min, E, G# min
\$\endgroup\$
  • 2
    \$\begingroup\$ Alright, what's the audio output library with the shortest function names... starts googling \$\endgroup\$ – SIGSTACKFAULT Jan 4 '17 at 15:26
  • \$\begingroup\$ @Blacksilver I live to torture golfers (; \$\endgroup\$ – GracefulLemming Jan 4 '17 at 18:03
  • \$\begingroup\$ If I chose to implement the correct chords instead of defaulting to no accidentals, would that still be a valid submission? \$\endgroup\$ – Patrick Roberts Jan 4 '17 at 19:54
  • 2
    \$\begingroup\$ Using no accidentals for the root note of the chords does NOT allow you to ignore accidentals. Taking your second example, we have Dmin D F A, Cmin C Eb G, F F A C, Gmin G Bb D E E G# B, B B D# F#, F F A C etc, ie alternating between the keys of Bmajor/G#minor (5 sharps) and Bb major/Gminor (2 flats.) If you allow ALL notes to be non-accidentals, you effectively restrict the problem to the key of Cmaj. I think you need to think about that rule 2 again. It's totally at odds with rule 1 which says must conform with the key as much as possible (which by the way is too vague.) \$\endgroup\$ – Level River St Jan 4 '17 at 20:16
  • 1
    \$\begingroup\$ @LevelRiverSt fixed. The limitation made more sense in my mind \$\endgroup\$ – GracefulLemming Jan 4 '17 at 20:18
1
\$\begingroup\$

JavaScript (ES6) + synth-js, 303 bytes

k=>l=>eval("for(m=/(.)(#?)(m?)/.exec(k),W=synth.WAV,w=new W,g=m[3]?[2,1,2,2,1,3,1]:[2,2,1,2,2,2,1],s=W.semitone(m[1]+4+m[2]),n=0;n<l*2;n++)for(r=Math.random()*7|0,c=i=g.reduce((u,h,j)=>u+(j<r)*h,0);c-i<12;c+=g[r++%7]+g[r++%7])w.writeNote({note:W.note(s+c),time:.5,amplitude:.25},[],1,c-i<9);w.toBlob()")

De-obfuscated:

(key) => (length) => {
  let match = /(.)(#?)(m?)/.exec(key),
      W = synth.WAV,
      wav = new W,
      // if minor, use minor gaps else major gaps
      gaps = match[3] ? [2, 1, 2, 2, 1, 3, 1] : [2, 2, 1, 2, 2, 2, 1],
      // get semitone index of scale root
      semitone = W.semitone(match[1] + 4 + match[2]);

  // 2 notes per second
  for (let note = 0; note < length * 2; note++) {

        // choose 1 of 7 random chords
    let random = Math.random() * 7 | 0,
        // get number of half steps from scale root to chord root
        initial = gaps.reduce(
          (sum, half, index) => sum + (index < random) * half,
        0);

    // of every tetrad, maximum of 11 half steps from root to 4th is possible
    // tetrads composed of thirds, so add two gaps for every increment
    for (let current = initial; current - initial < 12; current += gaps[random++ % 7] + gaps[random++ % 7]) {

      // write note data as binary
      wav.writeNote({note: W.note(semitone + current), time: .5, amplitude: .25}, [], 1, current - initial < 9);
    }
  }

  // return blob of wav binary data
  return wav.toBlob();
}

For those of you who might ask "Why isn't new Audio(URL.createObjectURL(...)) part of your byte-count?", according to MDN:

A Blob object represents a file-like object of immutable, raw data. Blobs represent data that isn't necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user's system.

Since this function returns a Blob with MIME-type audio/wav, it qualifies as a "playable audio file" as requested in the specification for the challenge.

Demo:

f=
k=>l=>eval("for(m=/(.)(#?)(m?)/.exec(k),W=synth.WAV,w=new W,g=m[3]?[2,1,2,2,1,3,1]:[2,2,1,2,2,2,1],s=W.semitone(m[1]+4+m[2]),n=0;n<l*2;n++)for(r=Math.random()*7|0,c=i=g.reduce((u,h,j)=>u+(j<r)*h,0);c-i<12;c+=g[r++%7]+g[r++%7])w.writeNote({note:W.note(s+c),time:.5,amplitude:.25},[],1,c-i<9);w.toBlob()")

h=
()=>new Audio(URL.createObjectURL(f(k.value)(l.value))).play()
<script src=//unpkg.com/synth-js/dst/synth.min.js></script>
<input id=k value=B>
<input id=l value=1>
<button type=button onclick=h()>Play</button>

If you want to verify that all chords which can be randomly chosen are correct, here is a table of all possible chords below, given valid input:

function markup(key) {
  let match = /(.)(#?)(m?)/.exec(key),
      numerals = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII'],
      gaps = match[3] ? [2,1,2,2,1,3,1] : [2,2,1,2,2,2,1],
      semitone = synth.WAV.semitone(match[1] + 4 + match[2]),
      string = '  ' + key + ':\n';

  for (let root = 0; root < 7; root++) {

    let initial = gaps.reduce(
          (sum, half, index) => sum + (index < root) * half,
        0),
        index = root;

    string += '    ' + numerals[root] + ': ';

    for (let current = initial; current - initial < 12; current += gaps[index++ % 7] + gaps[index++ % 7]) {
      
      string += synth.WAV.note(semitone + current) + ' ';
    }

    string += '\n';
  }

  string += '\n';

  return string;
}

let keys = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
let html = '<pre>';

html += 'Major Scales:\n';

keys.forEach(key => {
  html += markup(key);
});

html += 'Minor Scales:\n';

keys.forEach(key => {
  html += markup(key + 'm');
});

html += '</pre>';

document.write(html);
<script src=//unpkg.com/synth-js/dst/synth.min.js></script>

| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ Why is this noncompeting? \$\endgroup\$ – Conor O'Brien Jan 5 '17 at 13:15
  • \$\begingroup\$ @ConorO'Brien "...but is unfortunately non-competing because it relies on a utility which I have yet to officially publish." The dependency for it was technically written after the challenge was posted, which I was under the impression deemed the answer as unable to compete. \$\endgroup\$ – Patrick Roberts Jan 5 '17 at 18:03
  • \$\begingroup\$ Just to clarify, the updated library has been on my computer for a couple months, I just haven't uploaded the class until I wrote my answer because I've been working on cleaning the code and writing up documentation. \$\endgroup\$ – Patrick Roberts Jan 5 '17 at 21:55
  • \$\begingroup\$ @PatrickRoberts Would it be ok if i added this jsfiddle: jsfiddle.net/v098s9u6 \$\endgroup\$ – GracefulLemming Jan 5 '17 at 23:33
  • \$\begingroup\$ @Caleb that's kind of the point of the stack snippet I already have for the demo.. why do you want to add the jsfiddle? \$\endgroup\$ – Patrick Roberts Jan 6 '17 at 0:33

Not the answer you're looking for? Browse other questions tagged or ask your own question.