This plays Ode to Joy by Beethoven in sine waves. It was the first 64 bit Linux program I wrote. I think it's quite well commented. Tested on Fedora and Ubuntu. You'll need the program SDL and its development package (which provides sdl-config).
format ELF64
public main
extrn 'SDL_Init' as SDL_Init:qword
extrn 'SDL_Quit' as SDL_Quit:qword
extrn 'SDL_OpenAudio' as SDL_OpenAudio:qword
extrn 'SDL_CloseAudio' as SDL_CloseAudio:qword
extrn 'SDL_PauseAudio' as SDL_PauseAudio:qword
extrn 'SDL_Delay' as SDL_Delay:qword
extrn 'exit' as exit:qword
AUDIOS16LSB equ 0x8010
SDL_INIT_AUDIO equ 0x00000010
struc SDL_AudioSpec
{
.freq dd ?
.format dw ?
.channels db ?
.silence db ?
.samples dw ?
dw ?
.size dd ?
.callback dq ?
.userdata dq ?
}
SAMPLE_RATE equ 44100 ; samples per second
TEMPO equ 160 ; beats per minute
CROTCHETS_PER_SECOND equ (4 * (TEMPO / 60)) ; quarter beats per second
CROTCHET_DURATION equ (1000 / CROTCHETS_PER_SECOND) ; length of a quarter beat in ms
SAMPLES_PER_CROTCHET equ (SAMPLE_RATE / CROTCHETS_PER_SECOND) ; samples per quarter beat
; macro for declaration of note data
macro NOTE f,d
{
local .frequency, .duration
.frequency dq f ; floating point frequency in Hz
.duration dq d ; duration in quarter notes
}
section '.data' writable
audio_desired SDL_AudioSpec
NEGATIVE_ONE dq -1.0
MAX_SIGNED_SHORT dq 32767.0 ; max signed value that can be stored in a 2 byte integer
TWO_TIMES_PI dq 6.2831
ANGLE dq 0.0 ; current input to sine function
STEP_SIZE dq 0.0 ; rate at which to step through sine function
AMPLITUDE dq 1.0 ; current volume
SAMPLES_THIS_NOTE dq 0 ; samples REMAINING for the current note
CROTCHETS_THIS_NOTE dq 0 ; current note length in quarter beats
TEMP dq 0
SAMPLE dw 0 ; storage for integer audio sample
align 8
MUSIC_PLAYING dq 1 ; controls when the program ends
MUSIC_REST dq 0 ; is the current note a rest?
NOTE_POINTER dq MUSIC ; pointer to current note
; frequencies for the notes
G2 equ 195.998
C3 equ 261.626
D3 equ 293.664
E3 equ 329.628
F3 equ 349.228
G3 equ 391.996
; music melody
MUSIC:
NOTE E3,2
NOTE 0,2
NOTE E3,2
NOTE 0,2
NOTE F3,2
NOTE 0,2
NOTE G3, 2
NOTE 0,2
NOTE G3, 2
NOTE 0,2
NOTE F3, 2
NOTE 0,2
NOTE E3, 2
NOTE 0,2
NOTE D3, 2
NOTE 0,2
NOTE C3, 2
NOTE 0,2
NOTE C3, 2
NOTE 0,2
NOTE D3, 2
NOTE 0,2
NOTE E3, 2
NOTE 0,2
NOTE E3, 4
NOTE 0,2
NOTE D3, 2
NOTE D3, 6
NOTE 0, 2
NOTE E3,2
NOTE 0,2
NOTE E3,2
NOTE 0,2
NOTE F3,2
NOTE 0,2
NOTE G3, 2
NOTE 0,2
NOTE G3, 2
NOTE 0,2
NOTE F3, 2
NOTE 0,2
NOTE E3, 2
NOTE 0,2
NOTE D3, 2
NOTE 0,2
NOTE C3, 2
NOTE 0,2
NOTE C3, 2
NOTE 0,2
NOTE D3, 2
NOTE 0,2
NOTE E3, 2
NOTE 0,2
NOTE D3, 4
NOTE 0,2
NOTE C3, 2
NOTE C3, 6
NOTE 0, 2
NOTE D3, 6
NOTE 0, 2
NOTE E3, 2
NOTE 0, 2
NOTE C3, 2
NOTE 0, 2
NOTE D3, 4
NOTE E3, 2
NOTE F3, 2
NOTE E3, 2
NOTE 0, 2
NOTE C3, 2
NOTE 0, 2
NOTE D3, 4
NOTE E3, 2
NOTE F3, 2
NOTE E3, 2
NOTE 0, 2
NOTE D3, 2
NOTE 0, 2
NOTE C3, 2
NOTE 0, 2
NOTE D3, 2
NOTE 0, 2
NOTE G2, 6
NOTE 0, 2
NOTE E3,2
NOTE 0,2
NOTE E3,2
NOTE 0,2
NOTE F3,2
NOTE 0,2
NOTE G3, 2
NOTE 0,2
NOTE G3, 2
NOTE 0,2
NOTE F3, 2
NOTE 0,2
NOTE E3, 2
NOTE 0,2
NOTE D3, 2
NOTE 0,2
NOTE C3, 2
NOTE 0,2
NOTE C3, 2
NOTE 0,2
NOTE D3, 2
NOTE 0,2
NOTE E3, 2
NOTE 0,2
NOTE D3, 4
NOTE 0,2
NOTE C3, 2
NOTE C3, 6
NOTE 0, 2
dq -1.0 ; note data ends
section '.text' executable
main:
push rbp
mov rbp, rsp
mov rax, 0
mov rdi, SDL_INIT_AUDIO
call SDL_Init
mov [audio_desired.freq], SAMPLE_RATE
mov [audio_desired.format], AUDIOS16LSB
mov [audio_desired.channels], 1
mov [audio_desired.silence], 0
mov [audio_desired.samples], 4096
mov [audio_desired.size], 8192
mov [audio_desired.callback], audio_callback
mov [audio_desired.userdata], 0
mov rax, 0
mov rdi, audio_desired
mov rsi, 0
call SDL_OpenAudio
; unpause audio
mov rax, 0
mov rdi, 0
call SDL_PauseAudio
call next_note
; loop till we hit the last note
.main_loop:
; small delay to avoid maxing out CPU
mov rax, 0
mov rdi, CROTCHET_DURATION
call SDL_Delay
mov rax, [MUSIC_PLAYING]
cmp rax, 0
jz .exit_main_loop
jmp .main_loop
.exit_main_loop:
mov rax, 0
call SDL_CloseAudio
mov rax, 0
call SDL_Quit
mov rax, 0
mov rdi, 0
call exit
; SDL calls this function when it requires audio data
audio_callback:
push rbx rbp r12 r13 r14 r15
sub rsp, 8
; get the number of bytes of audio samples
; divide by two because each sample comprises 2 bytes
; store in rbx
mov rax, rdx
mov rbx, 2
mov rdx, 0
idiv rbx
mov rbx, rax
mov rdi, rsi ; put audio buffer pointer in rdi
; loop to write the audio samples into the buffer
mov rcx, 0 ; initialise counter
.loop:
; if there are no samples remaining to be written for the current note, progress to the next note
cmp [SAMPLES_THIS_NOTE], 0
jnz .skip_next_note
push rdi rcx
call next_note
pop rcx rdi
.skip_next_note:
cmp rcx, rbx ; exit the loop if we've reached the end of the audio buffer
jge .exit_loop
cmp [MUSIC_REST], 1 ; if the current note is a rest, skip writing the samples
je .rest
; calculate amplitude (volume)
; amplitude = samples_still_unwritten / (crotchets_this_note * samples_per_crotchet)
mov rax, [CROTCHETS_THIS_NOTE]
imul rax, SAMPLES_PER_CROTCHET
mov [TEMP], rax
fild [TEMP]
mov rsi, [SAMPLES_THIS_NOTE]
mov [TEMP], rsi
fild [TEMP]
fdivrp st1, st
fstp qword [AMPLITUDE]
fwait
; obtain our sine value and store it in memory as a 2 byte word
; sample = sin(angle) * amplitude * max_signed_short
fld qword [AMPLITUDE]
fld qword [MAX_SIGNED_SHORT]
fld qword [ANGLE]
fsin
fmulp st1, st
fmulp st1, st
fistp word [SAMPLE]
; transfer the sample into the audio buffer
mov si, [SAMPLE]
mov word [rdi + rcx * 2], si
; increase angle by step size
fld qword [STEP_SIZE]
fld qword [ANGLE]
faddp st1, st
; compare the angle to (2.0 * Pi)
fcom qword [TWO_TIMES_PI]
fstsw ax
fwait
sahf
jb .skip ; if the angle is less than (2.0 * Pi), skip the next instruction
fsub qword [TWO_TIMES_PI] ; reduce the angle by (2.0 * Pi) to bring it back within the range 0.0 to 2.0 * Pi
.skip:
fstp qword [ANGLE] ; write the new angle
.rest:
dec [SAMPLES_THIS_NOTE] ; decrease sample counter
inc rcx ; increase offset into audio buffer
jmp .loop
.exit_loop:
add rsp, 8
pop r15 r14 r13 r12 rbp rbx
ret
next_note:
push rbx
; restore amplitude (volume) to 1.0
fld1
fstp qword [AMPLITUDE]
; load the new note's frequency (measured in Hz) and duration (measured in quarter-notes)
; also store the length of the note in quarter-notes, and the length of the note in samples
mov rsi, [NOTE_POINTER]
mov rsi, [rsi + 8]
mov [CROTCHETS_THIS_NOTE], rsi
imul rsi, SAMPLES_PER_CROTCHET
mov [SAMPLES_THIS_NOTE], rsi
mov rbx, [NOTE_POINTER]
mov rbx, [rbx]
cmp rbx, [NEGATIVE_ONE]; negative one marks the end of the music
jne .skip
mov [MUSIC_PLAYING], 0 ; tell the main thread the program has finished
pop rbx
ret
.skip:
cmp rbx, 0.0 ; if frequency is 0.0, the note is a rest so next few calculations are not needed
jz .rest
mov [MUSIC_REST], 0 ; not a rest
; calculate the rate at which we need to step through the sine function's domain
; step_size = (2.0 * Pi) / (sample_rate / frequency)
mov [TEMP], rbx
fld [TEMP]
mov [TEMP], SAMPLE_RATE
fild [TEMP]
fdivrp st1, st
fld [TWO_TIMES_PI]
fdivrp st1, st
fstp [STEP_SIZE]
fwait
jmp .skip_rest
.rest:
mov [MUSIC_REST], 1 ; note is a rest
.skip_rest:
; progress to the next note
mov rbx, [NOTE_POINTER]
add rbx, 16
mov [NOTE_POINTER], rbx
mov [ANGLE], 0.0 ; reset angle for new frequency
pop rbx
ret