# By www.jk-quantized.com
#
# Based on code by Soledad Penades
#   https://soledadpenades.com/2009/10/29/fastest-way-to-generate-wav-files-in-python-using-the-wave-module/
#
# Modified to generate custom audio given an array of frequencies and respective durations
#
# Todo - floating has native support by wav format, implement for precision

import wave
import struct

from math import sin, pi, asin
# import random
# import perlin


output_filename = 'wavtest.wav'

# Signal data ---
durations = [
	406.250, 203.125, 203.125, 406.250, 203.125, 203.125, 406.250, 203.125, 203.125, 406.250, 
	203.125, 203.125, 609.375, 203.125, 406.250, 406.250, 406.250, 406.250, 203.125, 203.125, 
	203.125, 203.125, 609.375, 203.125, 406.250, 203.125, 203.125, 609.375, 203.125, 406.250, 
	203.125, 203.125, 406.250, 203.125, 203.125, 406.250, 406.250, 406.250, 406.250, 406.250
]
frequencies = [
	659.25511, 493.8833, 523.25113, 587.32954, 523.25113, 493.8833, 440.0, 440.0, 523.25113, 
	659.25511, 587.32954, 523.25113, 493.8833, 523.25113, 587.32954, 659.25511, 523.25113, 
	440.0, 440.0, 440.0, 493.8833, 523.25113, 587.32954, 698.45646, 880.0, 783.99087, 
	698.45646, 659.25511, 523.25113, 659.25511, 587.32954, 523.25113, 493.8833, 493.8833, 
	523.25113, 587.32954, 659.25511, 523.25113, 440.0, 440.0
]

# Config settings ---
nchannels = 2
sample_width = 2
sample_rate = 44100  # 44100
nframes = 0
compression_type = 'NONE'
compression_name = 'not compressed'

# Setup ---
scale_factor = 32767  # 2**16 / 2
values = []

# Main ---
def createWav():

	print( 'Creating', output_filename, '...' )

	wav_output = wave.open( output_filename, 'w')
	wav_output.setparams( ( nchannels, sample_width, sample_rate, nframes, compression_type, compression_name ) )

	# generate values --
	# sineWave()
	# sawtoothWave()
	# triangleWave()
	# perlinNoise()
	# randomNoise()
	sineWaveFromArray()
	# squareWaveFromArray()
	# triangleWaveFromArray()
	# sawtoothWaveFromArray()

	value_str = b''.join( values )
	wav_output.writeframes( value_str )

	wav_output.close()

	print( 'Done!' )


# Gen values ---
def fract(x):
	return x % 1


def sineWaveFromArray():

	# -- Sine waves from arrays	

	for i in range( len(frequencies) ):

		frequency = frequencies[i]
		duration = durations[i]

		period = 1 / frequency
		samplesPerPeriod = int( sample_rate * period )

		nPeriods = duration / 1000 * frequency
		nPeriods = int( nPeriods )

		nSamples = samplesPerPeriod * nPeriods

		for sample in range(nSamples):
			pct = sample / samplesPerPeriod
			value = sin( 2*pi * pct ) * scale_factor
			value = int( value )  # don't round until very end of calcs
			packed_value = struct.pack('h', value)
			values.append(packed_value) # channel 1
			values.append(packed_value) # channel 2


def squareWaveFromArray():

	# -- Square waves from arrays	

	for i in range( len(frequencies) ):

		frequency = frequencies[i]
		duration = durations[i]

		period = 1 / frequency
		samplesPerPeriod = int( sample_rate * period )

		nPeriods = duration / 1000 * frequency
		nPeriods = int( nPeriods )

		nSamples = samplesPerPeriod * nPeriods

		for sample in range(nSamples):
			pct = sample / samplesPerPeriod
			pct = fract( pct )
			value = scale_factor if pct < 0.5 else -scale_factor
			packed_value = struct.pack('h', value)
			values.append(packed_value) # channel 1
			values.append(packed_value) # channel 2


def triangleWaveFromArray():

	# -- Triangle waves from arrays	

	for i in range( len(frequencies) ):

		frequency = frequencies[i]
		duration = durations[i]

		period = 1 / frequency
		samplesPerPeriod = int( sample_rate * period )

		nPeriods = duration / 1000 * frequency
		nPeriods = int( nPeriods )

		nSamples = samplesPerPeriod * nPeriods

		for sample in range(nSamples):
			pct = sample / samplesPerPeriod
			value = 2 / pi * asin( sin( 2*pi * pct ) ) * scale_factor  # https://en.wikipedia.org/wiki/Triangle_wave
			value = int( value )  # don't round until very end of calcs
			packed_value = struct.pack('h', value)
			values.append(packed_value) # channel 1
			values.append(packed_value) # channel 2


def sawtoothWaveFromArray():

	# -- Sawtooth waves from arrays	

	for i in range( len(frequencies) ):

		frequency = frequencies[i]
		duration = durations[i]

		period = 1 / frequency
		samplesPerPeriod = int( sample_rate * period )

		nPeriods = duration / 1000 * frequency
		nPeriods = int( nPeriods )

		nSamples = samplesPerPeriod * nPeriods

		for sample in range(nSamples):
			pct = sample / samplesPerPeriod
			pct = fract( pct ) #  0..1
			pct = 2 * pct - 1  # -1..1
			value = pct * scale_factor
			value = int( value )  # don't round until very end of calcs
			packed_value = struct.pack('h', value)
			values.append(packed_value) # channel 1
			values.append(packed_value) # channel 2


def perlinNoise():

	sample_length = sample_rate * 10 # 30 seconds of noise

	nOctaves = 4
	falloff  = 0.5
	perlin.noiseDetail( nOctaves, falloff )

	for i in range(0, sample_length):
		value = perlin.noise(i)                 #   0..1
		value = 2 * value - 1                   #  -1..1
		value = int( value * scale_factor )     # -sf..sf
		packed_value = struct.pack('h', value)
		values.append(packed_value) # channel 1
		values.append(packed_value) # channel 2


def randomNoise():

	# -- Random noise
	# --   by Sole

	sample_length = sample_rate * 30 # 30 seconds of noise

	for i in range(0, sample_length):
		value = random.randint(-32767, 32767)
		packed_value = struct.pack('h', value)
		values.append(packed_value) # channel 1
		values.append(packed_value) # channel 2


def sineWave():

	# -- Sine wave
	# --   based on https://www.youtube.com/watch?v=_VtCoeMZ5nc
	# --   Where x-coord is sample, y-coord is value

	frequency = 220
	nPeriods = 5000

	period = 1 / frequency
	samplesPerPeriod = int( sample_rate * period )
	nSamples = samplesPerPeriod * nPeriods

	for sample in range(nSamples):
		pct = sample / samplesPerPeriod
		value = sin( 2*pi * pct ) * scale_factor
		value = int( value )  # don't round until very end of calcs
		packed_value = struct.pack('h', value)
		values.append(packed_value) # channel 1
		values.append(packed_value) # channel 2


def squareWave():

	# -- Square wave
	# --   based on https://www.youtube.com/watch?v=_VtCoeMZ5nc
	# --   Where x-coord is sample, y-coord is value

	frequency = 220
	nPeriods = 5000

	period = 1 / frequency
	samplesPerPeriod = int( sample_rate * period )
	nSamples = samplesPerPeriod * nPeriods

	for sample in range(nSamples):
		pct = sample / samplesPerPeriod
		pct = fract( pct )
		value = scale_factor if pct < 0.5 else -scale_factor
		packed_value = struct.pack('h', value)
		values.append(packed_value) # channel 1
		values.append(packed_value) # channel 2


def triangleWave():

	frequency = 220
	nPeriods = 5000

	period = 1 / frequency
	samplesPerPeriod = int( sample_rate * period )
	nSamples = samplesPerPeriod * nPeriods

	for sample in range(nSamples):
		pct = sample / samplesPerPeriod
		value = 2 / pi * asin( sin( 2*pi * pct ) ) * scale_factor  # https://en.wikipedia.org/wiki/Triangle_wave
		value = int( value )  # don't round until very end of calcs
		packed_value = struct.pack('h', value)
		values.append(packed_value) # channel 1
		values.append(packed_value) # channel 2


def sawtoothWave():

	frequency = 220
	nPeriods = 5000

	period = 1 / frequency
	samplesPerPeriod = int( sample_rate * period )
	nSamples = samplesPerPeriod * nPeriods

	for sample in range(nSamples):
		pct = sample / samplesPerPeriod
		pct = fract( pct ) #  0..1
		pct = 2 * pct - 1  # -1..1
		value = pct * scale_factor
		value = int( value )  # don't round until very end of calcs
		packed_value = struct.pack('h', value)
		values.append(packed_value) # channel 1
		values.append(packed_value) # channel 2


# Run ---
createWav()