The following is a .wav file generator that I wrote briefly while (re)learning about audio sampling:
import math
# Generates the .wav file header for a given set of samples and specs
def genHeader(sampleRate, bitsPerSample, channels, samples):
datasize = len(samples) * channels * bitsPerSample // 8
o = bytes("RIFF",'ascii') # (4byte) Marks file as RIFF
o += (datasize + 36).to_bytes(4,'little') # (4byte) File size in bytes excluding this and RIFF marker
o += bytes("WAVE",'ascii') # (4byte) File type
o += bytes("fmt ",'ascii') # (4byte) Format Chunk Marker
o += (16).to_bytes(4,'little') # (4byte) Length of above format data
o += (1).to_bytes(2,'little') # (2byte) Format type (1 - PCM)
o += (channels).to_bytes(2,'little') # (2byte)
o += (sampleRate).to_bytes(4,'little') # (4byte)
o += (sampleRate * channels * bitsPerSample // 8).to_bytes(4,'little') # (4byte)
o += (channels * bitsPerSample // 8).to_bytes(2,'little') # (2byte)
o += (bitsPerSample).to_bytes(2,'little') # (2byte)
o += bytes("data",'ascii') # (4byte) Data Chunk Marker
o += (datasize).to_bytes(4,'little') # (4byte) Data size in bytes
return o
# Expects samples in the form [[channel1sample0,channel2sample0,...],[channel1sample1,channel2sample1,...]]
# Samples are values between -1 and 1 then upscaled to the correct amplitude (-2**15 -> 2**15 roughly) for 16 bit
def genFile(sampleRate, bitsPerSample, channels, samples):
with open("test.wav","wb") as f:
f.write(genHeader(sampleRate, bitsPerSample, channels, samples))
m = 2**(bitsPerSample-1)-1
for i in range(len(samples)):
for j in range(channels):
f.write(int(samples[i][j] * m).to_bytes(bitsPerSample//8,'little',signed=True))
# Generates samples for a given frequency, sample rate and duration
# c is the current amplitude of the wave between -1 and 1
def genSamples(freq, sampleRate, duration, wave, c):
if wave == 'square':
c, samples = genSamples(freq,sampleRate,duration,'sin')
for i in range(len(samples)):
if(samples[i] > 0):
samples[i] = 1
elif(samples[i] < 0):
samples[i] = -1
else:
samples[i] = 0
elif wave == 'sin':
sampleCount = int(sampleRate * duration)
waves = duration * freq
inc = waves/sampleCount * 360
samples = []
for i in range(sampleCount):
samples += [math.sin(math.radians(c))]
c += inc
else:
print("NOT IMPLEMENTED")
exit()
return c, samples
# This is just a temporary method until multiple channels is implemented
def prepSamples(samples):
samplesOut = []
for i in range(len(samples)):
samplesOut += [[samples[i]]]
return samplesOut
# Generate samples from 20Hz to 20kHz
# Use c to have waves of 1 frequency continue from where previous
# frequency left of
samples = []
c = 0
for i in range(20,20001):
c, r = genSamples(i, 44100, 0.003003003003003, "sin", c)
samples += r
samples = prepSamples(samples)
genFile(44100, 16, 1, samples)
From this code review I'm most interested in comments w.r.t the genSamples function, the program as a whole seems to work well for frequencies between 20Hz and 18000Hz although there are some strange distortions after that as my sin wave becomes distorted (normally I wouldn't be able to hear 18kHz+ but as the the distortions are clearly audible). Any suggestions on how to improve the wave generation functionality would be greatly appreciated.
Note
I am aware of PEP8 and I am less interested in the styling of the code as this was just a quick write up to test what I'd been reading and to try out a couple of things, the code will be tidied when I more fully understand what I'm doing and how I want to continue. Until then I'd appreciate it if just the functionality of the code were to be critiqued.
Also note that the current setup of the code takes some time to run and any performance gains that are possible would also be appreciated.