Compose a Song!

7

1

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

GracefulLemming

Posted 2017-01-04T06:45:11.060

Reputation: 863

Question was closed 2017-01-06T17:47:04.827

2Alright, what's the audio output library with the shortest function names... starts googling – SIGSTACKFAULT – 2017-01-04T15:26:37.990

@Blacksilver I live to torture golfers (; – GracefulLemming – 2017-01-04T18:03:43.693

If I chose to implement the correct chords instead of defaulting to no accidentals, would that still be a valid submission? – Patrick Roberts – 2017-01-04T19:54:03.123

@PatrickRoberts go for it! – GracefulLemming – 2017-01-04T19:54:36.860

2Using 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.) – Level River St – 2017-01-04T20:16:05.740

1@LevelRiverSt fixed. The limitation made more sense in my mind – GracefulLemming – 2017-01-04T20:18:59.103

OK, looking much better. One more thing: It's not clear to me why you've included both major and minor scales. You haven´t specified a way to differentiate between them in the input, so I would think it is best to stick to major. Cmaj and Amin (for example) form a pair with exactly the same notes. Otherwise you need to specify how to differentiate in the input. – Level River St – 2017-01-04T20:21:32.813

Finally, the audio file rule is OK, but I would like it if the program could be allowed to play the music directly :-) – Level River St – 2017-01-04T20:23:54.927

@LevelRiverSt I considered that, however I would like to be able to save samples in case something interesting occurs. With being played directly if some curious sequence happens there is no way I can replicate it. Also. Working on the input – GracefulLemming – 2017-01-04T20:32:51.260

Answers

1

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>

Patrick Roberts

Posted 2017-01-04T06:45:11.060

Reputation: 2 475

1Why is this noncompeting? – Conor O'Brien – 2017-01-05T13:15:25.950

@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. – Patrick Roberts – 2017-01-05T18:03:02.647

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. – Patrick Roberts – 2017-01-05T21:55:31.630

@PatrickRoberts Would it be ok if i added this jsfiddle: https://jsfiddle.net/v098s9u6/

– GracefulLemming – 2017-01-05T23:33:26.650

@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? – Patrick Roberts – 2017-01-06T00:33:45.393

@PatrickRoberts just to have an easy-run copy, either way its your preference i was just suggesting – GracefulLemming – 2017-01-06T01:19:15.697