24
\$\begingroup\$

Play the initial section of the Final Fantasy Prelude. This is a 4-octave up-down arpeggio of the following chords:

Cadd2, Amadd2, Cadd2, Amadd2, Fadd2, Gadd2, G♯maj7, A♯maj7

Rules:

  • Actual sound must play when running your code. What environment is used is up to you, for example it could be a fantasy console, or a computer with an IBM PC compatible internal speaker.
  • Any instrument is fine, including simple beeps, as long as the melody is clearly recognizable.
  • The tempo is 80 bpm, and the individual notes are sixteenth notes, so playing the arpeggio of the 8 chords should take 48 seconds.
  • Looping the song is allowed but not mandatory.
  • The first note is a C3, which has a frequency of \$440\cdot2^{-21/12}\approx130.8\$ Hz. After 4 octaves of going up, the arpeggio goes back down starting at C7, using the notes of the same chord. Then the next chord starts. The other chords start at: A2, C3, A2, F2, G2, G♯2 and A♯2.
  • Slight inaccuracies in frequencies and tempo are allowed.
  • If it makes things easier, you may substitute A♭maj7 and B♭maj7 for the last two chords.

Scoring:

  • This is code golf, so shortest code wins.
  • The score is multiplied by the number of languages and external tools you are using. So calling system("sox …") from C has a multiplier of 3, as it uses C, a shell and sox.
  • The interpreter (if any) of your program does not count for the multiplier. So if your program is just an MP3 file, its interpreter can be a music player, so it has a multiplier of 1.

Music theory quickstart:

A note, like C, can appear in many octaves. If you go one octave up, its frequency doubles, if you go one down, the frequency halves. Most "western" music uses the 12 tone equal temperament, which means there are twelve notes in one octave, such that the ratio of frequencies between any two consecutive notes is the same, so \$\sqrt[12]{2}\$, or equivalently \$2^{1/12}\$.

Chords are combination of notes that are normally played simultaneously, but in this case they are meant to be played individually. To avoid having to learn chord notation, here are the note numbers for the first octave of each chord:

  • 0, 2, 4, 7
  • -3, -1, 0, 4
  • 0, 2, 4, 7
  • -3, -1, 0, 4
  • -7, -5, -3, 0
  • -5, -3, -1, 2
  • -4, 0, 3, 7
  • -2, 2, 5, 9

Where number 0 has been chosen for C3. So to convert a given note number \$n\$ to a frequency, use the formula \$440\cdot2^{(n-21)/12}\$. To go one octave up, just add 12 to \$n\$.

\$\endgroup\$
13
  • 1
    \$\begingroup\$ @l4m2 It's four octaves up starting from C₃: [0,2,4,7,12,14,…, 43], then four octaves down starting from C₇ : [48,43,40,…2], then the next chords are arpeggiated in the same way. \$\endgroup\$ Commented Sep 17, 2024 at 17:42
  • 1
    \$\begingroup\$ @G.Sliepen I'm not a music person. What do the numbers "[0,2,4,7,12,14,…, 43]" correspond to? Those numbers appear in your comment but not in the question itself. \$\endgroup\$ Commented Sep 17, 2024 at 18:59
  • 5
    \$\begingroup\$ @G.Sliepen Can you list all of the semitones—or all of the frequencies—for the piece in order? Maybe I'm alone in this but it seems like otherwise the bulk of this task will be translating chord names to semitones or frequencies, rather than actually golfing. \$\endgroup\$ Commented Sep 17, 2024 at 19:19
  • 8
    \$\begingroup\$ That is unhelpful. It's forcing people to learn music theory to obtain a sequence of numbers that you could easily have listed in the question. I will ask CoPilot \$\endgroup\$ Commented Sep 18, 2024 at 8:58
  • 3
    \$\begingroup\$ Jordan, @roblogic, I've added a section about converting the chords to frequencies. Is this enough information? \$\endgroup\$ Commented Sep 18, 2024 at 17:39

10 Answers 10

12
\$\begingroup\$

ZX Spectrum 48K Basic, 246 203 bytes

  10 LET t=VAL ".14": LET a$="<0
247<?0237<<0247<?0237<C0247<A024
7<@047;<>047;<": FOR i=SGN PI TO
 LEN a$ STEP VAL "6": FOR j=NOT
PI TO INT PI: FOR k=SGN PI TO VA
L "4": BEEP t,VAL "12"*j+CODE a$
(i+k)-CODE a$(i): NEXT k: NEXT j
: FOR j=INT PI TO NOT PI STEP -S
GN PI: FOR k=VAL "5" TO VAL "2"
STEP -SGN PI: BEEP t,VAL "12"*j+
CODE a$(i+k)-CODE a$(i): NEXT k:
 NEXT j: NEXT i

The above is how the program lists on screen as the ZX Spectrum's screen is 32 characters wide. The byte count was calculated by subtracting the system variables for the end and start of the program; this will be one less than the save file length as an empty program has a save file length of 1. Unfortunately I was using an online emulator which only has a save snapshot option, so you'll have to retype this yourself to test it. Note that if you try to enter this program literally in 128K mode it will try to tokenise the <> inside the string, breaking the program, so either replace it with <"+">, do some magic with POKE, or use 48K mode and type <> as two characters rather than the token. In both modes you may also need to use the keyboard layout to enter some special characters such as - or *. Explanation:

  10

Arbitrary line number (always costs 2 bytes).

LET t=VAL ".14"

Timing variable, adjusted to make the overall run time about 48s of real time. (Processing overhead makes the program take about 50% longer than the actual notes).

LET a$="<0247<?0237<<0247<?0237<C0247<A0247<@047;<>047;<"

String of tonic and arpeggio data.

FOR i=SGN PI TO LEN a$ STEP VAL "6"

This steps through each tonic in the data string, thus repeating 8 times. SGN PI is used as it's even shorter than VAL "1", which saves a few bytes because numeric literals take up six bytes for their internal representation.

FOR j=NOT PI TO INT PI

Repeat for four octaves; NOT PI is 0 and INT PI is of course 3.

FOR k=SGN PI TO VAL "4"

Repeat for four notes.

BEEP t,VAL "12"*j+CODE a$(i+k)-CODE a$(i)

Compute the note index (C₄=0) and play it.

NEXT k: NEXT j

Repeat for the rest of the ascending arpeggio.

FOR j=INT PI TO NOT PI STEP -SGN PI

Loop back down through the octaves.

FOR k=VAL "5" TO VAL "2" STEP -SGN PI

Loop down through the notes.

BEEP t,VAL "12"*j+CODE a$(i+k)-CODE a$(i)

Compute and play the note.

NEXT k: NEXT j: NEXT i

Repeat for the remainder of the prelude.

Edit: Saved 43 bytes thanks to @Arnauld's suggesting of indexing into a single data string.

ZX Spectrum 128K Basic, 195 bytes

  10 LET a$="CDEG  #BGED abCE  A
     ECb  fgaC  FCag  gabD  GDBa
       $aC$EG$AG$EC$bDFA $BAFd "
     : FOR i= NOT PI TO VAL "84"
      STEP VAL "12": FOR j= INT
     PI TO VAL "6" STEP INT PI :
      FOR k=j TO VAL "9"-j STEP
     SGN ( VAL "9"-j-j): PLAY "O
     "+ STR$ k+" 2"+(a$( TO VAL
     "24")+a$)(i+j+j- VAL "5" TO
      i+j+j): NEXT k: NEXT j: NE
     XT i

The above is how the program displays during edit mode as the ZX Spectrum's screen is 32 characters wide and it wraps lines to column 5. The byte count was calculated by subtracting the system variables for the end and start of the program as the code is automatically tokenised (in edit mode tokens are always preceded with spaces but if you list the program then some of those will disappear) although when typing in the program you won't need to enter spaces except inside the string literal or between consecutive tokens. Explanation:

  10

Arbitrary line number (always costs 2 bytes).

LET a$="CDEG  #BGED abCE  AECb  fgaC  FCag  gabD  GDBa  $aC$E$G$AG$EC$bDFA $BAFd "

Arpeggio data. Lower case letters play an octave below, so each octave command actually selects two octaves from which notes can be played, but to be able to play all of the arpeggios using the given four octaves I have had to cheat and play the C₇ as a B𝄰₆. Note that the first two arpeggios are only shown once here and doubled up below.

FOR i= NOT PI TO VAL "84" STEP VAL "12"

Loop eight times, stepping though the arpeggio data string.

FOR j = INT PI TO VAL "6" STEP INT PI

Switch between starting at octave 3 and octave 6.

FOR k=j TO VAL "9"-j STEP SGN ( VAL "9"-j-j)

Loop through the four octaves.

PLAY "O"+ STR$ k+" 2"+(a$( TO VAL "24")+a$)(i+j+j- VAL "5" TO i+j+j)

Select the octave and the note duration (the default tempo is 120 bpm but a note length code of 2 gives us the desired duration at this tempo) and play the current arpeggio at this octave.

NEXT k: NEXT j: NEXT i

Repeat for the remainder of the prelude.

The figure of 195 bytes compares well to the 272 byte string needed to play the prelude in a single PLAY command: (line wrapping not part of the string)

T80O3(1CDEGO5cdegCDEGO7cdegCgedcO5GEDCgedcO3GED
abCEABO5ceabCEABO7ceaeaO5BAECbaecO3BAECb)
fgaCFGAO5cfgaCFGAO7cfcO5AGFCagfcO3AGFCag
gabDGABO5dgabDGABO7dgdO5BAGDbagdO3BAGDba
$aC$EG$AO5c$eg$aC$EG$AO7c$eg$ag$ecO5$AG$EC$ag$ecO3$AG$EC
$bDFA$BO5dfa$bDFA$BO7dfa$bafdO5$BAFD$bafdO3$BAFD
\$\endgroup\$
6
  • \$\begingroup\$ I don't know the subtleties of the ZX Spectrum Basic tokenizer, but would it save bytes to do VAL "12"*j+CODE a$(k)-CODE a$(0) with k incremented and $a and $b merged into the same string? Or could you even discard DATA entirely and use a single string instead with some additional indexing math? \$\endgroup\$ Commented Sep 18, 2024 at 15:51
  • \$\begingroup\$ @Arnauld I'm not sure about the indexing mathematics but certainly your first idea looks good. (It would be CODE a$(NOT PI) of course... \$\endgroup\$ Commented Sep 18, 2024 at 17:37
  • 1
    \$\begingroup\$ Actually, CODE defaults to the first character, so it's just CODE a$. \$\endgroup\$ Commented Sep 18, 2024 at 17:41
  • 1
    \$\begingroup\$ About the single string: I mean something like VAL "12"*j+CODE a$(VAL "6"*i+k)-CODE a$(VAL "6"*i) with a$="<0247<?0237<<0247<?0237<C0247<A0247<@047;<>047;<". \$\endgroup\$ Commented Sep 18, 2024 at 17:46
  • 1
    \$\begingroup\$ @Arnauld Merging pairs of strings got it down to 211 bytes but a single string gets it down to 203 bytes, but unfortunately only in 48K mode, because the 128K tokeniser tries to tokenise the <> in the string... \$\endgroup\$ Commented Sep 18, 2024 at 18:06
11
\$\begingroup\$

MATL (1 language), 78 77 bytes

0'!4XC@nRAxt~&'F5:ZaKeK1X"tP[AaABOlHN]0Y(_&vhYs"1875:YPE*76/@12/W*Y,E]&h1e4Y#

This uses equal temperament, with a C3 frequency of 131.6 Hz (more accurate values could be achieved at the cost of 2 or 3 bytes, namely replacing 76 in the code by 76.4 or 76.45).

The timbre corresponds to a sine wave hard-clipped at half its amplitude (this clipping costs 1 byte but produces nicer sound than a pure sine wave; the latter can be obtained by removing the E near the end of the code).

Try it at MATL online!

How it works

Number 0 is first pushed to the stack. This will serve as the initial note. The rest of the notes will be defined by consecutive differences, measured in semitones.

The basic structure of the song is described by the following 4×8 matrix:

2 2 2 2 2 2 4 4
2 1 2 1 2 2 3 3
3 4 3 4 3 3 4 4
5 5 5 5 5 5 1 1

which contains the increments (in semitones) of the ascending arpeggios, each column corresponding to one of the 8 chords in order.

To generate this matrix, the string '!4XC@nRAxt~&' is interpreted as a "number" in the base defined by all ASCII characters except single quote, and is decoded into the base formed by the numbers [1 2 3 4 5], producing a row vector (this operation is done by the code F5:Za). Reshaping this with 4 rows (Ke) gives the above matrix. Then, the matrix is extended by replicating it 4 times vertically (K1X"), because each ascending arpeggio is played 4 times. This yields a 16×8 matrix.

A duplicate (t) of this 16×8 matrix is produced, and it is then flipped vertically (P). This represents the descending arpeggios, except that the sign should be negated; and the last row is incorrect, because it does not consider the transition to the next chord. Specifically, the values of the last row should be [5 −1 5 6 0 1 2 NaN] (represented compactly in the code as [AaABOlHN]). The final NaN value indicates that there is no following chord in the last column. Thus, the last row of the second 16×8 matrix is overwritten with that vector (0Y(), and the two matrices are concatenated vertically (&v). The resulting 32×8 matrix describes the repeated up and down arpeggios for each chord, with the correct transition to the next chord. To produce the notes it should be read in column-major order (down, then across).

To transform the increments between notes into the actual notes, the 0 produced at the beginning of the program is concatenated horizontally (h) with the 32×8 matrix. This causes the matrix to be read in column-major order, and gives a 1×257 row vector. Its cumulative sum (Ys) produces the notes, starting at 0, where each note is represented by the number S of semitones up or down from C3. The last value is NaN, which will be interpreted as no note.

A for each loop ("...]) reads each note, represented by the corresponding number S. To generate the waveform for the note, a time vector [1 2 ... 1875] is first created (1875:). The length of this vector, when replayed at 10000 samples per second, determines a tempo of 80 bpm, as required. This vector is element-wise multiplied by 2π (YPE*), divided by 76 (76/), and then multiplied by 2 raised to the number that results from dividing S by 12 (@12/W*). Applying the sine function (Y,) produces a 1×1875 vector with samples of a sinusoid of the frequency correponding to S. The values of this vector are multiplied by 2 (E). The last sinusoid contains NaN values and will produce no sound.

The 257 resulting waveforms, each with 1875 samples, are concatened horizontally (&h) into a single waveform. This waveform is then replayed, without scaling, at the indicated sample rate (1e4Y#). When replaying, sample values are clipped to the interval [1, −1].

\$\endgroup\$
8
\$\begingroup\$

CP-1610 machine code, 125 DECLEs1=156.25 bytes

1. CP-1610 instructions are encoded with 10-bit values (0x000 to 0x3FF), known as DECLEs. Although the Intellivision is also able to work on 16-bit data, programs were really stored in 10-bit ROM back then.

A fantasy console?!? Let's do it on a real one!

This code is meant to be run on a PAL Intellivision (50Hz). It would also work on an NTSC system (60Hz), but at a different pitch and speed.

Demo

Here is what we get on the real hardware (YouTube video), using the LTO Flash!.

Source code

                             ROMW  10          ; use 10-bit ROM width
0x4800                       ORG   $4800       ; map our code at $4800
                   
                     ;; -------------------------------------------------------- ;;
                     ;;  build the note period table in RAM:                     ;;
                     ;;    p(0) = 1432                                           ;;
                     ;;    p(n) = p(n-1) * 483 + 350 >> 9                        ;;
                     ;; -------------------------------------------------------- ;;
4800   001                   SDBD              ; R0 = note period, starting at ...
4801   2B8 098 005           MVII  #1432, R0   ; ... the highest value 1432 for F-2
4804   2BC 2F0               MVII  #$2F0, R4   ; R4 = write pointer in RAM
                   
4806   260           init    MVO@  R0, R4      ; write the period
                   
4807   1D2                   CLRR  R2          ; use R3:R2 as a 32-bit value
4808   1DB                   CLRR  R3          ; (initialized to 0)
                   
4809   2B9 1E3               MVII  #483, R1    ; process multiplication by 483
                   
480B   0C2           mult    ADDR  R0, R2      ; by adding R0 that many times
480C   02B                   ADCR  R3          ; to R3:R2
480D   011                   DECR  R1
480E   22C 004               BNEQ  mult
                   
4810   2FA 15E               ADDI  #350, R2    ; R3:R2 += 350
                                               ; (carry never set -> no ADCR R3)
                   
4812   042                   SWAP  R2          ; R2 = R3:R2 >> 9
4813   043                   SWAP  R3
4814   3BA 0FF               ANDI  #$FF, R2
4816   1DA                   XORR  R3, R2
4817   07A                   SARC  R2
                   
4818   090                   MOVR  R2, R0      ; copy R2 to R0
                   
4819   378 00F               CMPI  #$F, R0     ; loop until R0 = $F
481B   22E 016               BGT   init
                   
                     ;; -------------------------------------------------------- ;;
                     ;;  play the song                                           ;;
                     ;; -------------------------------------------------------- ;;
481D   240 1FB               MVO   R0, $1FB    ; set the volume on channel A to $F
                   
481F   001           loop    SDBD              ; R4 = pointer into chords table
4820   2BC 057 048           MVII  #chords, R4
                   
4823   2A2           chord   MVI@  R4, R2      ; R2 = initial note address
4824   092                   TSTR  R2          ; time to loop?
4825   224 007               BEQ   loop
                   
4827   274                   PSHR  R4          ; save R4 on the stack
4828   2E4                   ADD@  R4, R4      ; R4 = pointer into delta values
4829   2BD 008               MVII  #8, R5      ; R5 = octave counter
                   
482B   093           octave  MOVR  R2, R3      ; copy R2 to R3
482C   2B9 004               MVII  #4, R1      ; R1 = note counter
                   
482E   298           note    MVI@  R3, R0      ; R0 = note period
482F   240 1F0               MVO   R0, $1F0    ; save the low bits
4831   040                   SWAP  R0          ; and the high bits
4832   240 1F4               MVO   R0, $1F4    ; into the PSG registers
                   
4834   001                   SDBD              ; wait for ~184500 cycles
4835   2B8 00C 030           MVII  #12300, R0  ; (approximately 185ms)
                   
4838   010           spin    DECR  R0          ; (6 cycles)
4839   22C 002               BNEQ  spin        ; (9 cycles)
                   
483B   2E3                   ADD@  R4, R3      ; update R3
483C   33B 005               SUBI  #5, R3
483E   011                   DECR  R1          ; decrement the note counter
483F   22C 012               BNEQ  note        ; loop if not zero
                   
4841   37D 005               CMPI  #5, R5      ; compare octave counter with 5
4843   20C 003               BNEQ  phase       ; time to switch the phase? ...
                   
4845   09A                   MOVR  R3, R2      ; ... yes: copy R3 to R2
4846   200 008               B     next
                   
4848   20E 002       phase   BGT   asc         ; ascending phase?
                   
484A   33A 018               SUBI  #24, R2     ; descending phase: -12 semitones
484C   2FA 00C       asc     ADDI  #12, R2     ; ascending phase: +12 semitones
                   
484E   33C 004               SUBI  #4, R4      ; rewind R4
                   
4850   015           next    DECR  R5          ; decrement the octave counter
4851   22C 027               BNEQ  octave      ; loop if not zero
                   
4853   2B4                   PULR  R4          ; advance to the next chord
4854   00C                   INCR  R4
4855   220 033               B     chord
                   
                     ;; -------------------------------------------------------- ;;
                     ;;  chords tables                                           ;;
                     ;; -------------------------------------------------------- ;;
4857   2F7 00F       chords  DECLE $2F7, add2   - $ - 1  ; C/add2
4859   2F4 014               DECLE $2F4, add2_m - $ - 1  ; Am/add2
485B   2F7 00B               DECLE $2F7, add2   - $ - 1  ; C/add2
485D   2F4 010               DECLE $2F4, add2_m - $ - 1  ; Am/add2
485F   2F0 007               DECLE $2F0, add2   - $ - 1  ; F/add2
4861   2F2 005               DECLE $2F2, add2   - $ - 1  ; G/add2
4863   2F3 011               DECLE $2F3, maj7   - $ - 1  ; G#/maj7
4865   2F5 00F               DECLE $2F5, maj7   - $ - 1  ; A#/maj7
4867   000                   DECLE 0
                   
                             ; delta values, with +5 offset
4868   007 007 ...   add2    DECLE 7, 7, 8, 10, 0, 2, 3
486F   007 006 ...   add2_m  DECLE 7, 6, 9, 10, 0, 1, 4
4876   009 008 ...   maj7    DECLE 9, 8, 9,  6, 4, 1, 2
                   
0x487D               end

How it works

About the Programmable Sound Generator (PSG)

The PAL-based Intellivision uses a 4.00MHz clock. The PSG is driven from a clock signal at half this rate. Internally, the PSG divides down its clock by 16 to determine the final square-wave frequency. The frequency of a tone produced by a PAL Intellivision is therefore given by:

$$F\_tone=\frac{4000000}{32\times P\_channel}$$

where \$P\_channel\$ is the period register setting for the given channel.

(adapted from the psg.txt file included in jzIntv)

Building the note period table

The frequency ratio between two consecutive semitones is given by:

$$\sqrt[12]{2}\approx 1,0594631$$

And what we really need is the period ratio between two consecutive semitones:

$$1/\sqrt[12]{2}\approx 0,9438743$$

To apply this ratio to a period \$P_n\$, we use the following approximation:

$$P_{n+1}=\left\lfloor\frac{P_n \times 483 + 350}{512}\right\rfloor$$

We start with \$P_0=1432\$, which is the period for F2 (87.31Hz) on a PAL Intellivision, computed with the formula described in the previous section. We stop when a period of 15 is reached -- an arbitrary choice to have a register ready to initialize the volume on the PSG.

The CP-1610 doesn't have any multiplication instruction, so we have to compute it with either additions and shifts (see the previous revision) or just repeated additions (the current revision). Besides, we need 32-bit values to have enough precision. So we use either one or two pairs of 16-bit registers depending on the method (R0/R1 and R2/R3).

The integer division by 512 is of course much simpler since it boils down to a right-shift by 9.

Chords encoding

Each chord is described by the address of the first note in our period table, followed by a pointer to 7 delta values:

a0, a1, a2, s, d0, d1, d2

where:

  • a0, a1, a2 are the delta values for the ascending phase, repeated 4 times and starting at the next octave each time
  • s is the delta value applied when switching from the ascending to the descending phase
  • d0, d1, d2 are the delta values for the descending phase, repeated 4 times and starting at the previous octave each time

The actual delta values are obtained by subtracting 5 to the stored values.

\$\endgroup\$
3
  • \$\begingroup\$ I found out that I could optimise the 128K Spectrum's PLAY command to get it down to 195 bytes... only to find that you had got your answer back below that... \$\endgroup\$ Commented Sep 19, 2024 at 10:53
  • \$\begingroup\$ @Neil I guess a Z80 assembly answer would be much shorter than mine since it seems to have a built-in note period table somewhere in ROM. \$\endgroup\$ Commented Sep 19, 2024 at 11:03
  • \$\begingroup\$ There is a note frequency table but it's in floating-point and you have to figure out the timing yourself, so just using BEEP is much easier really. \$\endgroup\$ Commented Sep 19, 2024 at 11:27
7
\$\begingroup\$

JavaScript, 320 bytes

I don't golf with JavaScript much so I'm sure there's a lot of opportunities for golfing here. Should work on most browsers.

x=>{a=new AudioContext
with(a.createOscillator()){[y=[0,2,4,7],z=[-3,-1,0,4],y,z,[-7,-5,-3,0],[-5,-3,-1,2],[-4,0,3,7],[-2,2,5,9]].map((c,t)=>{p=s=>frequency.setValueAtTime(440*2**(s-1.75),(t*32+i)*.19)
for(i=0;i<32;i++)p(i<17?c[i%4]/12+(i/4|0):c[3-(i-1)%4]/12+((32-i)/4|0))})
connect(a.destination)
start()
stop(48.64)}}

-3 bytes thanks to Neil

Try it

f =

x=>{a=new AudioContext
with(a.createOscillator()){[y=[0,2,4,7],z=[-3,-1,0,4],y,z,[-7,-5,-3,0],[-5,-3,-1,2],[-4,0,3,7],[-2,2,5,9]].map((c,t)=>{p=s=>frequency.setValueAtTime(440*2**(s-1.75),(t*32+i)*.19)
for(i=0;i<32;i++)p(i<17?c[i%4]/12+(i/4|0):c[3-(i-1)%4]/12+((32-i)/4|0))})
connect(a.destination)
start()
stop(48.64)}}
<button type="button" onclick="f()">Play (Warning: Loud)</button>

\$\endgroup\$
4
  • 1
    \$\begingroup\$ I think you can save a few bytes by making s the number of octaves instead of semitones; where the caller uses +12* this becomes /12+ while 440*2**((s-12)/12) becomes 440*2**(s-1.75) or 2**(s+7.03) if you can tolerate a small pitch adjustment or 2**(s+7) if you can tolerate a slightly larger pitch adjustment. \$\endgroup\$ Commented Sep 20, 2024 at 14:31
  • \$\begingroup\$ Thanks @Neil! .. \$\endgroup\$ Commented Sep 20, 2024 at 17:16
  • \$\begingroup\$ I simplified some of the maths, but it reduced slightly the accuracy of tempo and frequencies. Is it still ok for you? Also the stop function seems useless in my browser, and i replaced the linebreaks with semicolon to help with future copy-pasting: x=>{a=new AudioContext;with(a.createOscillator()){[y=[0,2,4,7],z=[-3,-1,0,4],y,z,[-7,-5,-3,0],[-5,-3,-1,2],[-4,0,3,7],[-2,2,5,9]].map((c,t)=>{p=s=>frequency.setValueAtTime(131*2**s,t*6+i/5);for(i=0;i<32;i++)i<17?p(c[i%4]/12+(i>>2)):p(c[(32-i)%4]/12+(8-i/4|0))});connect(a.destination);start()}} \$\endgroup\$ Commented Sep 20, 2024 at 20:20
  • \$\begingroup\$ @Fhuvi Without the stop the last note is sustained indefinitely, at least on Chrome/macOS and Safari/iOS. \$\endgroup\$ Commented Sep 21, 2024 at 1:12
6
\$\begingroup\$

PowerShell, 401 379 340 304 290 bytes

$C=130,146,164,196,261;$A=110,123,130,174,220;$G=103,130,155,196,207
$q=[console]
function P($c=$C,$h=0){0..7|%{$i=$_;if($i-lt4){0..3|%{$q::Beep(($c[$_])*[math]::Pow(2,$i+$h/12),187)}}else{4..1|%{$q::Beep(($c[$_])*[math]::Pow(2,7-$i+$h/12),187)}}}}
P;P $A;P;P $A;P $C -7;P $C -5;P $G;P $G 2

Attempt This Online!

An extremely brute-force solution, since I'm a novice at PowerShell golf. Defines a function P that plays a chord (array of numbers, truncated to the nearest integer) at a given half-step offset, first up 4 times then down 4 times. Each note is played using the builtin [System.Console]::Beep method.

To run, paste this into a .ps1 file, then run it in PowerShell; or paste it directly into a PowerShell terminal.

  • -22 from general golfs
  • -39 from replacing for-loops with ranges piped into ForEach-Objects.
  • -36 from reusing the back 4 notes of the chords, in reverse, for the descending arpeggios.
  • -14 with default arguments and some simplification

P.S. I keep getting this weird issue where some notes are missed; I modified the code to write out the current note and index that it's on and it is indeed running correctly, but the beeps are being clobbered by something else.

\$\endgroup\$
7
  • 1
    \$\begingroup\$ Looks like you left in a couple spaces in P: $i + $h/12. \$\endgroup\$ Commented Sep 18, 2024 at 15:39
  • 1
    \$\begingroup\$ I don't know PowerShell but in ($c[$n % $c.Length] can $c.Length be replaced with 8, or am I misunderstanding what the value of $c is there? \$\endgroup\$ Commented Sep 18, 2024 at 15:46
  • 1
    \$\begingroup\$ Looking at some PowerShell examples I think you can save some bytes by replacing e.g. for($i=0;$i-lt8;$i++){...$i...} with 0..7|%{...$_...}. 0..7 is a range and % is an alias for ForEach-Object. Unfortunately I don't have a Windows machine handy to test this. \$\endgroup\$ Commented Sep 18, 2024 at 20:59
  • 2
    \$\begingroup\$ Pasting this into Powershell was a surreal experience. \$\endgroup\$ Commented Sep 19, 2024 at 4:07
  • 1
    \$\begingroup\$ My PowerShell is silent. I guess my PC's speaker is only connected to my sound card. Sadfaces... \$\endgroup\$ Commented Sep 19, 2024 at 4:35
5
\$\begingroup\$

Javascript, 270 bytes

x=>{a=new AudioContext
with(a.createOscillator()){
for(i=0;i<256;i++){n=[60,49,60,49,32,40,46,54][i>>5]
q=[1258,810,1804][n%4]
frequency.setValueAtTime(55*2**((b=i&31?b+(i%32>16?-(q>>i%4*3&7):q>>(257-i)%4*3&7):n>>2)/12),i*3/16)}
connect(a.destination)
start()
stop(48)}}

If you have to express the frequency in Herz, the formula given in the question is unneccessary complicated. The note A is defined as 440Hz, each octave below halves that, and the A that is lower than the lowest note played is A₃ at 55Hz.

The start note frequencies of the arpeggios from that become

f = 55 * 2 ** (s / 12)

with the semitones distance form the base note s being

15 12 15 12 8 10 11 13

There are only three different chords being used that can be described by their sequence of semitone differences. The last column is a magic number encoding the differences as octal. For golfing reasons the digits are ordered as 1-2-3-0

0:  major add2  |  2 2 3 5  |  0o2352 = 1258
1:  minor add2  |  2 1 4 5  |  0o1452 =  810
2:  major 7     |  4 3 4 1  |  0o3414 = 1804

The whole sequence can be described as a two-dimensional array

[[15, 0], [12, 1], [15, 0], [12, 1], [8, 0], [10, 0], [11, 2] [13, 2]]

or with the chord index as the lowest two bits in one number

[60, 49, 60, 49, 32, 40, 46, 54]

From there, using the same code as @Jordan for the AudioContext interface (ungolfed):

x => {
  a=new AudioContext
  with (a.createOscillator()) {
    for (i = 0; i < 256; i++) {
      
      n = [60,49,60,49,32,40,46,54][i>>5]  // after each 32 a new arpegio
      q = [1258,810,1804][n%4]             // magic chord number
      b = i&31 ?                           // inside arpeggio sequence,
        b + (                              // add semitone difference
          i%32 > 16 ?
          -(q>>i%4*3&7) :                 // downward
          q>>(257-i)%4*3&7                // upward
        ) :
        n >> 2                            // start new arpeggio at base note
      
      frequency.setValueAtTime(55 * 2 ** (b/12), i * 3 / 16)
    }
    connect(a.destination)
    start()
    stop(48)
  }
}

Try it

f =

x=>{a=new AudioContext
with(a.createOscillator()){
for(i=0;i<256;i++){n=[60,49,60,49,32,40,46,54][i>>5]
q=[1258,810,1804][n%4]
frequency.setValueAtTime(55*2**((b=i&31?b+(i%32>16?-(q>>i%4*3&7):q>>(257-i)%4*3&7):n>>2)/12),i*3/16)}
connect(a.destination)
start()
stop(48)}}
<button type="button" onclick="f()">Play (Warning: Loud)</button>

\$\endgroup\$
2
  • 1
    \$\begingroup\$ The first half seems to be at the right pitch but the second half seems to all be offset by the same amount. \$\endgroup\$ Commented Sep 20, 2024 at 22:01
  • 1
    \$\begingroup\$ @Neil right, I think I fixed it now. Since that needed only to exchange four numbers, the byte count is not affected. \$\endgroup\$ Commented Sep 20, 2024 at 22:59
3
\$\begingroup\$

Chipmunk Basic, 677 303 280 241 228 bytes

dim m(20):d$="GIKNDFGKGIKNDFGK@BDGBDFICGJNEILP":for k=0to 7
for i=0to 4:for o=1to 4:m(o+4*i)=555*2^(asc(mid$(d$,k*4+o))/12+i-8):next:next
for i=1to 17:s(m(i)):next:for i=1to 16:s(m(17-i)):next:next
sub s(x):sound x,.2,50:return

Try the debug version on TutorialsPoint!

The info about chords and octaves in the "music theory" section helped a lot.
Ungolfed:

dim m(20)
d$="GIKNDFGKGIKNDFGK@BDGBDFICGJNEILP"
for k=0to 7                       :' the 8 chords
  for i=0to 4                     :' ascend arpeggio for 5 octaves
    for o=1to 4                   :' broken into 4 notes
      m(o+4*i)=555*2^(asc(mid$(d$,k*4+o))/12+i-8)
      :' ^      ^       ^-- get an ascii value from the middle of d$
      :' |      |           and convert it to a frequency (Hz)
      :' |      +-- base frequency A=440Hz, adjusted slightly
      :' +-- populate m(1) to m(20), but only use 17 notes
    next
  next
  for i=1to 17:s(m(i)):next       :' ascend
  for i=1to 16:s(m(17-i)):next    :' descend
next
sub s(x):sound x,.2,50:return

677 bytes

\$\endgroup\$
0
2
\$\begingroup\$

Zsh +ffplay, 296 241 261 230 bytes x 2

for z (GIKN DFGK GIKN DFGK @BDG BDFI CGJN EILP){W=
for i ({0..4})for o (${(s::)z})W+=($[555*2**(##$o/12.+i-8)])
for q (${W:0:18} ${(Oa)W:0:17})S+=between(t,$[p++]/5,$p/5)*$q+
};ffplay -f lavfi aevalsrc="'0.1*sin(2*PI*t*(${S%+}))'"

Try it Online! Adjusted by ear; frequencies from ffplay seemed a bit off. Uses base of 550 instead of 440.

for z grabs 4 pseudo-hex ASCII characters
for i and for o populate array W with 20 notes based on z; expressed as
  note number: n=$[##$o-71+12*i], combined with
  frequency (Hz): f=$[440*2**((n-21)/12.)]
for q goes up & down 17 notes of W, adding to string S:
  0.2s time increments p++/5,p/5
  frequency q
Finally, we call ffplay using the string S (trailing + truncated).

The linked TIO example doesn't play sound (no ffplay), but you can see the humungous string S.

Debug 296B 241B 261B

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Added the top C₇ as required, at the cost of 22 bytes! :\ \$\endgroup\$ Commented Sep 23, 2024 at 15:49
2
\$\begingroup\$

JavaScript, 324 307 bytes

I used an array containing as many elements as there are notes to play, making sure that each element can be represented by a single hexadecimal character.

This character represents a number of semitones of difference between this note and the previous note (except for the first element which is played as is)

I then realized that some big chunks were repeated (those corresponding to the arpeggios in add2 and Maj7) and that I could factorize these portions of code

EDIT: with the suggestion from @G.Sliepen, @Neil, @Arnauld and @Fhuvi, I got a 307 bytes. Thanks guys!

f=

_=>{a=new AudioContext;with(a.createOscillator(r=(s,n=4)=>(""+s).repeat(n))){[...`6${b=r("889b")+r(1344,c=3)+134}1${d=r("87ab")+r(1254,3)+125}7${b+1+d+0+b+6+b}5${D=r("a9a7")+r(5232,3)+523}4`+D].map((u,i)=>{frequency.setValueAtTime(110*2**(c/12),i/5);c+="0x"+u-6});connect(a.destination);start();stop(51.2)}}
<button type="button" onclick="f()">Play (Warning: Loud)</button>

\$\endgroup\$
5
  • 1
    \$\begingroup\$ For the first half at least, you can start with c=3 and then the formula becomes 110*2**(c/12). \$\endgroup\$ Commented Sep 20, 2024 at 22:06
  • 1
    \$\begingroup\$ Frequencies are correct in the second half now, but the last chord is wrong, you're repeating the second to last chord. \$\endgroup\$ Commented Sep 21, 2024 at 12:09
  • 1
    \$\begingroup\$ You can just do "0x"+x-6. \$\endgroup\$ Commented Sep 21, 2024 at 12:10
  • 1
    \$\begingroup\$ If you want, here's a version with changes in the order of declaration. Golf rules allow to not count f= in the byte count. I didn't correct the bug that G.Sliepen mentioned. (i also replaced the linebreaks with ; to help with some errors when copying from a comment) _=>{a=new AudioContext();with(a.createOscillator()){c=3;[...`6${b="889b889b889b889b134413441344134"}1${d="87ab87ab87ab87ab125412541254125"}7${b}1${d}0${b}6${b}5${D="a9a7a9a7a9a7a9a7523252325232523"}2${D}`].map((u,i)=>{frequency.setValueAtTime(110*2**(c/12),i*.19);c+=("0x"+u-6)});connect(a.destination);start();stop(48.64)}} \$\endgroup\$ Commented Sep 22, 2024 at 0:20
  • \$\begingroup\$ New suggestion with the last bug corrected. It's slightly slower than the expected output but seems within the margin error: _=>{a=new AudioContext;with(a.createOscillator(r=(s,n=4)=>(""+s).repeat(n))){[...`6${b=r("889b")+r(1344,c=3)+134}1${d=r("87ab")+r(1254,3)+125}7${b+1+d+0+b+6+b}5${D=r("a9a7")+r(5232,3)+523}4`+D].map((u,i)=>{frequency.setValueAtTime(110*2**(c/12),i/5);c+="0x"+u-6});connect(a.destination);start();stop(51.2)}} \$\endgroup\$ Commented Oct 1, 2024 at 13:31
1
\$\begingroup\$

C (GCC) using OSS, 201 197 177 174 bytes

i;main(c,n,a){for(open("/dev/dsp",1);write(3,&n,1);n=(a+=901*exp2(n/4+("047;02470237"[n%4+(5732>>c*2&12)]-",/,/310."[c])/12.))>>9)n=i++/1500,c=n/32%8,n%=32,n=n>16?32-n:n;}

The score includes 3 bytes for the -lm compiler flag. It runs on any UNIX system with support for the Open Sound System (OSS), or any other sound system that provides compatibility with OSS. On modern Linux, you might have to sudo modprobe snd_pcm_oss if this is not done automatically already. If you compile with GCC 14 or later, you might need to pass the -std=gnu89 flag, but earlier versions don't need it.

The code opens /dev/dsp, which by default accepts 8-bit, unsigned, 8 kHz mono audio. It has three nested loops:

  • Outer loop for each chord
  • Middle loop for the arpeggio
  • Inner loop to generate samples

However, you'll only see one for-loop; it calculates the chord number and position in the arpeggio from the sample counter i. It also reuses the arpeggio position n for the sample value.

It uses a several lookup tables to find out which note to play:

  • 5732 is 8 2-bit values storing which type of chord to play (major seventh, major and minor)
  • "047;02470237" stores the notes of each of the three chord types
  • ",/,/310." stores the first note of the start of each up-down arpeggio, inverted (higher ASCII code means lower note), as it will be subtracted from the previous table's values

It then calculates the change in phase per sample for the waveform at that frequency in fixed point format, and adds it to the phase accumulator a. This is then converted to a byte to be send straight to the audio device, thus generating a sawtooth wave with the right frequency.

This version loops (for at least six days). To make it write the output to stdout instead of /dev/dsp, change the first 3 in the code to a 1. You can then pipe it through aplay -fU8 -c1 -r8000 or a similar command.

\$\endgroup\$
0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.