Prompt-Driven Development Journal 0001: Musical data modeling

Last updated: March 18th 2023

Table of Contents #

  1. Introduction.
  2. An array of musical frequencies.
    1. Aside: A note is not a frequency.
  3. Pick random data from array.
  4. Function to play basic sound .
    1. The failed attempt.
    2. The successful attempt.
  5. Conclusion.
    1. What we got so far.
    2. When more context doesn't help.

1. Introduction #

Are the buzzword morons of the software world already making secular religions, methodologies, "Best Practices" and so on around this stuff? Are they already trying to sell you Packt Publishing "Modern Prompt-Driven Development with ChatGPT" books that become obsolete before they hit the shelves?

It's only a matter of time.

Anyway, today the idea was to have ChatGPT generate separate parts of code, instead of everything at once.

2. An array of musical frequencies #

Prompt:

In javascript for the web, make an array of sound frequencies (to be used by an oscillator, for example). The array should have two octaves of frequencies, in the 12-tone temperament system, starting at the frequency for A1 (55). Put the logic used to build the array in a function. Make sure the logic adds both the sharps and their enharmonic flats. Eg. In the array, there should be an item for C#1 and another for Db1, and both should have the same frequency.

ChatGPT (Mar 14 Version):

function buildFrequencyArray() {
  const A1 = 55; // frequency of A1
  const notesPerOctave = 12; // number of notes in an octave
  const octaves = 2; // number of octaves to include

  const frequencyArray = [];

  for (let octave = 0; octave < octaves; octave++) {
    for (let note = 0; note < notesPerOctave; note++) {
      const frequency = A1 * Math.pow(2, octave) * Math.pow(2, note / notesPerOctave);
      const noteName = getNoteName(note);
      frequencyArray.push({ frequency, noteName });
    }
  }

  return frequencyArray;
}

function getNoteName(note) {
  const noteNames = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'];
  return noteNames[note % 12] + Math.floor(note / 12 + 1);
}

const frequencyArray = buildFrequencyArray();
console.log(frequencyArray);

The output on node:

> console.log(frequencyArray);
[
  { frequency: 55, noteName: 'A1' },
  { frequency: 58.27047018976124, noteName: 'A#1' },
  { frequency: 61.735412657015516, noteName: 'B1' },
  { frequency: 65.40639132514966, noteName: 'C1' },
  { frequency: 69.29565774421802, noteName: 'C#1' },
  { frequency: 73.4161919793519, noteName: 'D1' },
  { frequency: 77.78174593052023, noteName: 'D#1' },
  { frequency: 82.40688922821748, noteName: 'E1' },
  { frequency: 87.30705785825097, noteName: 'F1' },
  { frequency: 92.49860567790861, noteName: 'F#1' },
  { frequency: 97.99885899543733, noteName: 'G1' },
  { frequency: 103.82617439498628, noteName: 'G#1' },
  { frequency: 110, noteName: 'A1' },
  { frequency: 116.54094037952248, noteName: 'A#1' },
  { frequency: 123.47082531403103, noteName: 'B1' },
  { frequency: 130.8127826502993, noteName: 'C1' },
  { frequency: 138.59131548843604, noteName: 'C#1' },
  { frequency: 146.8323839587038, noteName: 'D1' },
  { frequency: 155.56349186104046, noteName: 'D#1' },
  { frequency: 164.81377845643496, noteName: 'E1' },
  { frequency: 174.61411571650194, noteName: 'F1' },
  { frequency: 184.99721135581723, noteName: 'F#1' },
  { frequency: 195.99771799087466, noteName: 'G1' },
  { frequency: 207.65234878997256, noteName: 'G#1' }
]

Neat.

For the things I have in mind, I would have used two separate properties for the pitch class and the octave. Ie. Instead of { frequency: 110, noteName: 'A1' }, I would have { frequency: 110, pitchClass: 'A', octave: 1}.

Of course, ChatGPT can't know what I have in mind (it's not that powerful yet!).

I know: It could probably have guess what I had in mind, if I had given it more context. But funnily enough, I've found that "filling it up" with lots of context and conversation in preparation for the code generation prompt made ChatGPT too "helpful," and out-of-scope-y. Maybe it's been learning bad programmer behavior too!

2.1. Aside: A note is not a frequency #

Here's a bit of music theory (or music-theoretical data modeling... theory?): A "note" is not a frequency. There's a lot more to a note: The pitch, the pitch class, the duration, the articulation, etc. So, just so you know: Referring to a pair of values like [ A1, 110 ] as a "note" is a bit of a stretch of the term.

And, if you know your Fourier, or your Acoustics, you know that no note on Earth is just one frequency ever. So 110 is technically a fundamental frequency conceptually.

3. Pick random data from array #

I followed up the first prompt with a more generic one about getting items from array.

Prompt:

Javascript function that receives an array xs, a number n, and a predicate function f. It should return an array of n different items from xs, selected at random, as long as f returns true for each item selected. If f returns false, try again for another item, until you f(item) returns true.

ChatGPT (Mar 14 Version):

function getRandomItemsWithPredicate(xs, n, f) {
  const result = [];

  while (result.length < n) {
    const randomItem = xs[Math.floor(Math.random() * xs.length)];
    if (f(randomItem) && !result.includes(randomItem)) {
      result.push(randomItem);
    }
  }

  return result;
}

Quick test on the console:

> const xs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
undefined
> const isEven = x => x % 2 == 0
undefined
> getRandomItemsWithPredicate(xs, 3, isEven)
[ 10, 2, 8 ]
> getRandomItemsWithPredicate(xs, 3, isEven)
[ 4, 6, 8 ]
> getRandomItemsWithPredicate(xs, 3, isEven)
[ 10, 8, 4 ]
> getRandomItemsWithPredicate(xs, 3, isEven)
[ 2, 8, 10 ]
> getRandomItemsWithPredicate(xs, 3, isEven)
[ 2, 8, 10 ]
> getRandomItemsWithPredicate(xs, 3, isEven)
[ 10, 2, 8 ]
> 

4. Function to play basic sound #

Now I wanted a simple function to play multiple sounds by array of frequencies, but it took a couple of attempts.

4.1. The failed attempt #

As an example of it doing silly things when it has too much context, I tried this prompt as a follow-up to the previous one:

Prompt (that failed):

Write a function in javascript that takes an array of frequencies (meant to be fundamentals of musical notes), and uses simple web audio oscillators to play them simultaneously (like a chord.) It should also take a second argument specifying the duration of the tones.

And it gave me some broken code which I'll skip here cos it's too many lines and you don't wanna read that shit. Basically it tried to not just play a chord, but have it "fade out."

In ChatGPT's own words (it always adds a little explanation after it gives you code. Like a programmer explaining his answer at a job interview):

This function creates an AudioContext object, which is the main entry point to the Web Audio API. It then creates an oscillator node for each frequency in the input array, sets its frequency and waveform, and starts it playing. It also creates a gain node, which controls the volume of the output, and connects each oscillator to it. The function sets the gain to fade out over the specified duration, using a linear ramp. Finally, it stops the oscillators and closes the context after the duration has elapsed.

As you can see, it added gain nodes to try to achieve a fade out sound. But the code was broken.

I think what made it go out of scope here was the word "chord." It must have decided that most code out there that has the notion of "chord" adds things like fades, etc. to make it musical.

4.2. The successful attempt #

This time I told it to keep it simple, and explicitly told it not to do any fancy shit. And it worked.

Prompt:

Write a function in javascript that takes an array of frequencies (meant to be fundamentals of musical notes), and uses simple web audio oscillators to play them simultaneously (like a chord.) It should also take a second argument specifying the duration of the tones. As simple as possible. No fading out or anything beyond start and stop.

ChatGPT (Mar 14 Version):

Sure, here's a simpler implementation that just creates and starts the oscillators, and stops them after the specified duration:

function playChord(frequencies, duration) {
  // create an AudioContext
  const AudioContext = window.AudioContext || window.webkitAudioContext;
  const context = new AudioContext();

  // create an oscillator node for each frequency and start it
  frequencies.forEach((frequency) => {
    const oscillator = context.createOscillator();
    oscillator.type = 'sine'; // use a sine wave for simplicity
    oscillator.frequency.value = frequency;
    oscillator.connect(context.destination);
    oscillator.start(0);
    oscillator.stop(context.currentTime + duration);
  });

  // stop the context after the duration
  setTimeout(() => {
    context.close();
  }, duration * 1000);
}

5. Conclusion #

5.1. What we got so far #

We got:

  1. A buildFrequencyArray function.
  2. A getRandomItemsWithPredicate function.
  3. And a playChord function.

Surely you can see how these functions (or variations of them) could be used together.

But I'll leave that for a future entry.

5.2. When more context doesn't help #

It is clear to me that the "art" of the thing is in

  1. Knowing how high or low in scope you wanna prompt it.
  2. Dropping just enough context during the "conversation," so that it does precisely what's requested, and nothing beyond.

In the meantime, have a...

(COMMERCIAL INTERRUPTION!: Have you listened to my latest releases? Death metal and grindcore, Dark ambient, Strange EP / History lesson.)

...nice day.

Related