create wav files from scratch (draw a wav file)

Post your working scripts, libraries and tools for AHK v1.1 and older
User avatar
jeeswg
Posts: 6902
Joined: 19 Dec 2016, 01:58
Location: UK

create wav files from scratch (draw a wav file)

16 Jul 2017, 17:39

If you create sine waves in 16 bit v. 8 bit, you can hear the difference in quality, the 8-bit version sounds machine-like, the 16-bit version sound natural.

Note: I believe that when I created this script, it gave the exact same binary data as Sound Recorder did in Windows XP, so if you created a silent 60-second wave sound via this script and via Sound Recorder, the files would be identical.

The wav files in this example are 58 bytes of header data, and the rest is a block of (1-byte/2-byte) 'samples', a number that specifies the y-coordinate of a wave at a certain point in time. Multiple y-coordinates giving you the shape of a wave.

Code: Select all

;create wav by jeeswg
ListLines, Off

q:: ;triangular waves
w:: ;sine waves
if InStr(A_ThisHotkey, "q")
	vType := "tri"
else if InStr(A_ThisHotkey, "w")
	vType := "sin"

;settings:
vDir := A_Desktop
vPath = %vDir%\z %vType% %A_Now%.wav
vQual := "22050,16,1" ;radio quality: PCM 22.050 kHz, 16 Bit, Mono (Sound Recorder default)
;vQual := "44100,16,2" ;cd quality: PCM 44.100 kHz, 16 Bit, Stereo
;vQual := "22050,8,1" ;radio quality: PCM 22.050 kHz, 8 Bit, Mono (New Wave Sound)
vDuration := 10 ;seconds

;==================================================

oTemp := StrSplit(vQual, ",")
vSamplesPerSec := oTemp.1
vBitsPerSample := oTemp.2
vChannels := oTemp.3 ;number of channels (mono or stereo)
oTemp := ""
vBytesPerSample := vBitsPerSample/8
;if vSamples = 1000, mono file will have 1000 samples, stereo file will have 2000 samples (1000 for left speaker, 1000 for right speaker)
vSamples := vSamplesPerSec*vDuration ;samples per channel
vSizeData := vChannels * vSamples * vBytesPerSample ;bytes in sample area:

if !(vChannels = 1)
{
	MsgBox, % "error: only mono currently supported"
	return
}

;==================================================

;header is 58 bytes
;e.g. FOURCC identifiers: RIFF, 'fmt ', data, fact
VarSetCapacity(vData, 58+vSizeData, 0)

StrPut("RIFF", &vData+0, "CP0") ;chunkID
NumPut(50 + vSizeData, vData, 4) ;chunkSize
StrPut("WAVE", &vData+8, "CP0") ;(format)

;'fmt ' uses a WAVEFORMATEX structure
StrPut("fmt ", &vData+12, "CP0") ;chunkID
NumPut(18, vData, 16) ;chunkSize ;e.g. 18 for PCM
NumPut(1, vData, 20, "UShort") ;wFormatTag ;WAVE_FORMAT_PCM := 0x1
NumPut(vChannels, vData, 22, "UShort") ;nChannels ;1=mono, 2=stereo (number of channels)
NumPut(vSamplesPerSec, vData, 24) ;nSamplesPerSec ;e.g. 22050
NumPut(vChannels*vSamplesPerSec*vBytesPerSample, vData, 28) ;nAvgBytesPerSec ;nChannels * nSamplesPerSec * (wBitsPerSample/8)
NumPut(vChannels*vBytesPerSample, vData, 32, "UShort") ;nBlockAlign ;nChannels * (wBitsPerSample/8)
NumPut(vBitsPerSample, vData, 34, "UShort") ;wBitsPerSample
NumPut(0, vData, 36, "UShort") ;cbSize

StrPut("fact", &vData+38, "CP0") ;chunkID
NumPut(4, vData, 42) ;chunkSize
NumPut(vSamples, vData, 46) ;(number of samples (per channel))

StrPut("data", &vData+50, "CP0") ;chunkID
NumPut(vSamples*vChannels*vBytesPerSample, vData, 54) ;chunkSize ;'NumSamples' * nChannels * (wBitsPerSample/8)

;==================================================

if (vType = "tri")
	Gosub SubTri
else if (vType = "sin")
	Gosub SubSin
return

;==================================================

SubTri:
;diagonal lines:
;e.g. 1: 0, 1, 2, ..., 254, 255, 254, ..., 2, 1
;e.g. 2: 0, 2, 4, ..., 252, 254, 252, ..., 4, 2
;e.g. 4: 0, 4, 8, ..., 248, 252, 248, ..., 8, 4
;e.g. 8: 0, 8, 16, ..., 240, 248, 240, ..., 16, 8

vListNum := "1,2,3,4,5"
if (vBytesPerSample = 1)
	vLimit1 := 0, vLimit2 := 255
else if (vBytesPerSample = 2)
	vLimit1 := -32768, vLimit2 := 32767
Loop, Parse, vListNum, % ","
{
	vIndex2 := A_LoopField
	vList%vIndex2% := vIndex := vLimit1
	vDiff := A_LoopField
	if (vBytesPerSample = 2)
		vDiff *= 256
	Loop
	{
		vIndex += vDiff
		vList%vIndex2% .= "," vIndex
		if (vIndex + vDiff > vLimit2)
			break
	}
	Loop
	{
		vIndex -= vDiff
		vList%vIndex2% .= "," vIndex
		if (vIndex - vDiff <= vLimit1)
			break
	}
}

vAddr := &vData + 58
vEnd := &vData + 58 + vSizeData
vDuration := 2000 ;milliseconds
vSamplesPerDuration := Floor((vSamplesPerSec*vDuration)/1000)
Loop, Parse, vListNum, % ","
{
	vList := vList%A_LoopField%
	StrReplace(vList, ",", "", vSamplesPerWave), vSamplesPerWave += 1
	if (vSamplesPerWave <= 0)
		break
	vEndWave := vAddr + vSamplesPerDuration*vBytesPerSample
	;MsgBox, % vSamplesPerWave
	;MsgBox, % A_Index " " vAddr
	Loop
	{
		if (vAddr + vSamplesPerWave*vBytesPerSample > vEnd)
		|| (vAddr + vSamplesPerWave*vBytesPerSample > vEndWave)
			break
		JEE_WaveWriteList(vAddr, vList, vBytesPerSample)
		vAddr += vSamplesPerWave*vBytesPerSample
	}
}
Gosub SubEnd
return

;==================================================

SubSin:
vAddr := &vData + 58
vEnd := &vData + 58 + vSizeData
vFreq := 261.63 ;middle C
vDuration := 2000 ;milliseconds
vSamplesPerDuration := Floor((vSamplesPerSec*vDuration)/1000)
Loop
{
	vSamplesPerWave := Floor(vSamplesPerSec/vFreq)
	if (vSamplesPerWave <= 0)
		break
	vEndWave := vAddr + vSamplesPerDuration*vBytesPerSample
	;MsgBox, % vSamplesPerWave
	;MsgBox, % A_Index " " vAddr
	Loop
	{
		if (vAddr + vSamplesPerWave*vBytesPerSample > vEnd)
		|| (vAddr + vSamplesPerWave*vBytesPerSample > vEndWave)
			break
		JEE_WaveWriteNote(vAddr, vSamplesPerWave, vBytesPerSample)
		vAddr += vSamplesPerWave*vBytesPerSample
	}
	vFreq += 20
}
Gosub SubEnd
return

;==================================================

SubEnd:
if !FileExist(vPath)
{
	oFile := FileOpen(vPath, "w")
	oFile.RawWrite(vData, 58+vSizeData)
	oFile.Close()
	Run, % vPath
	;MsgBox, % "done"
}
return

;==================================================

;e.g. middle C: 261.63 Hz
;we need 261.63 waves per second
;if there are 22050 samples per second
;we need 22050/261.63 = 84.28 samples per wave
;(write sine wave)
JEE_WaveWriteNote(vAddr, vSamplesPerWave, vBytesPerSample)
{
	if (vBytesPerSample = 1)
		vPtrType := "UChar", vNum1 := 1, vNum2 := 127
	else if (vBytesPerSample = 2)
		vPtrType := "Short", vNum1 := 0, vNum2 := 32767
	else
		return
	vAdjust := 360 / vSamplesPerWave
	vTemp := 0
	vList := ""
	Loop, % Floor(vSamplesPerWave)
	{
		vNum := Round((Sin(vTemp*0.01745329252)+vNum1)*vNum2) ;pi/180 (approximately 0.01745329252)
		vList .= vNum ","
		vTemp += vAdjust
		NumPut(vNum, vAddr+0, (A_Index-1)*vBytesPerSample, vPtrType)
	}
	;MsgBox, % vList
}

;==================================================

JEE_WaveWriteList(vAddr, vList, vBytesPerSample)
{
	if (vBytesPerSample = 1)
		vPtrType := "UChar"
	else if (vBytesPerSample = 2)
		vPtrType := "Short"
	else
		return
	Loop, Parse, vList, % ","
		NumPut(A_LoopField, vAddr+0, (A_Index-1)*vBytesPerSample, vPtrType)
}
Good links:
WAVE File Format
http://web.archive.org/web/200801131952 ... h/wave.htm
Microsoft WAVE soundfile format
http://web.archive.org/web/200812101456 ... aveFormat/
Wave format
https://sharkysoft.com/archive/lava/doc ... ontent.htm
[a 'fmt ' chunk uses a WAVEFORMATEX structure]
WAVEFORMATEX structure (Windows)
https://msdn.microsoft.com/en-us/librar ... s.85).aspx
[use of 'chunkID' and 'chunkSize']
Resource Interchange File Format (RIFF) (Windows)
https://msdn.microsoft.com/en-us/librar ... s.85).aspx

More links:
Wave File Specifications
http://www-mmsp.ece.mcgill.ca/Documents ... /WAVE.html
riff-format.txt
http://www.neurophys.wisc.edu/auditory/riff-format.txt
WAV - Wikipedia
https://en.wikipedia.org/wiki/WAV
Last edited by jeeswg on 24 Jul 2017, 18:23, edited 3 times in total.
homepage | tutorials | wish list | fun threads | donate
WARNING: copy your posts/messages before hitting Submit as you may lose them due to CAPTCHA
User avatar
jeeswg
Posts: 6902
Joined: 19 Dec 2016, 01:58
Location: UK

Re: create wav files from scratch (draw a wav file)

16 Jul 2017, 22:00

Here is a musical SoundBeep example, useful for testing that waves are of the correct pitch:

musical SoundBeeps, MIDI note numbers, scientific pitch notation - AutoHotkey Community
https://autohotkey.com/boards/viewtopic.php?f=6&t=34619
homepage | tutorials | wish list | fun threads | donate
WARNING: copy your posts/messages before hitting Submit as you may lose them due to CAPTCHA
Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: create wav files from scratch (draw a wav file)

18 Jul 2017, 05:17

Really cool jeeswg, well done! :clap:
Thanks for sharing, Cheers. ☕
kilimra
Posts: 7
Joined: 30 Dec 2015, 16:47

Re: create wav files from scratch (draw a wav file)

18 Aug 2017, 22:55

HI, jeeswg, are you interested in writing a script to create GIF? I need it very much.
User avatar
mslonik
Posts: 144
Joined: 21 Feb 2019, 04:38
Location: Poland
Contact:

Re: create wav files from scratch (draw a wav file)

07 Feb 2024, 14:57

Thank you very much @jeeswg, this is exactly what I was looking for.

I reworked your example a bit by adding memory copying to sinus version:

Code: Select all

;	Create .wav files. Based on work of jeeswg: https://www.autohotkey.com/boards/viewtopic.php?f=6&t=34584&hilit=WAV
;	Ammended by mslonik on 2024-02-04.

#Requires AutoHotkey v1.1.34+ 		; Displays an error and quits if a version requirement is not met.    
#SingleInstance, 		force		; Only one instance of this script may run at a time!
#NoEnv  							; Recommended for performance and compatibility with future AutoHotkey releases.
#Warn  							; Enable warnings to assist with detecting common errors.
ListLines, 			Off			; ListLines is disabled to make it harder to determine how script works.
SetWorkingDir, 		% A_ScriptDir	; Ensures a consistent starting directory.

;==================================================

;settings:
vQual 			:= "22050,16,1" 	;radio quality: PCM 22.050 kHz, 16 Bit, Mono (Sound Recorder default)
;vQual 			:= "44100,16,2" 	;cd quality: PCM 44.100 kHz, 16 Bit, Stereo
;vQual 			:= "22050,8,1" 	;radio quality: PCM 22.050 kHz, 8 Bit, Mono (New Wave Sound)
vDuration 		:= 10 ;seconds
oTemp 			:= StrSplit(vQual, ",")
vSamplesPerSec 	:= oTemp.1
vBitsPerSample 	:= oTemp.2
vChannels 		:= oTemp.3 		;number of channels (mono or stereo), only mono = 1 is currently supported
vData			:= 0
vBytesPerSample 	:= vBitsPerSample / 8
;if vSamples = 1000, mono file will have 1000 samples, stereo file will have 2000 samples (1000 for left speaker, 1000 for right speaker)
vSamples 			:= vSamplesPerSec * vDuration 			;samples per channel
vSizeData 		:= vChannels * vSamples * vBytesPerSample 	;bytes in sample area; e.g. 1 * 22 050 [samp/s] * 10 [s] * 2 [bytes] = 220 500 [bytes]

if !(vChannels = 1)
{
	MsgBox, % "error: only mono currently supported"
	return
}


;==================================================

F_WAV_Header()

q:: ;triangular waves
	F_SubTri()
return

w:: ;sine waves
	F_SubSin()
return

e::
	MsgBox, 64, % A_ScriptDir, % A_ScriptName . "`n`n" . "Bye!", 5
	ExitApp, 0
return

;==================================================

F_WAV_Header()
{
;header is 58 bytes long
;e.g. FOURCC identifiers: RIFF, 'fmt ', data, fact

	global	;assume-global mode of operation

	VarSetCapacity(vData, 58 + vSizeData, 0)	;Enlarges a variable's holding capacity; vData is enlarged by 58 bytes (to keep header) and fills it with 0

	StrPut("RIFF", &vData + 0, "CP0") 	;chunkID; copies the string "RIFF" to vData with encoding "CP0" (system default ANSI code page) 
	NumPut(50 + vSizeData, vData, 4) 	;chunkSize; stores a number 50 + vSizeData in vData + offset = 4. 
	StrPut("WAVE", &vData + 8, "CP0") 	;(format)

	;'fmt ' uses a WAVEFORMATEX structure
	StrPut("fmt ", &vData + 12, "CP0") 						;chunkID
	NumPut(18, vData, 16) 									;chunkSize ;e.g. 18 for PCM
	NumPut(1, vData, 20, "UShort") 							;wFormatTag ;WAVE_FORMAT_PCM := 0x1
	NumPut(vChannels, vData, 22, "UShort") 						;nChannels ;1=mono, 2=stereo (number of channels)
	NumPut(vSamplesPerSec, vData, 24) 							;nSamplesPerSec ;e.g. 22050
	NumPut(vChannels * vSamplesPerSec * vBytesPerSample, vData, 28) 	;nAvgBytesPerSec ;nChannels * nSamplesPerSec * (wBitsPerSample/8)
	NumPut(vChannels * vBytesPerSample, vData, 32, "UShort") 		;nBlockAlign ;nChannels * (wBitsPerSample/8)
	NumPut(vBitsPerSample, vData, 34, "UShort") 					;wBitsPerSample
	NumPut(0, vData, 36, "UShort") 							;cbSize
	StrPut("fact", &vData + 38, "CP0") 						;chunkID
	NumPut(4, vData, 42) 									;chunkSize
	NumPut(vSamples, vData, 46) 								;(number of samples (per channel))

	StrPut("data", &vData + 50, "CP0") 						;chunkID
	NumPut(vSamples * vChannels * vBytesPerSample, vData, 54) 		;chunkSize ;'NumSamples' * nChannels * (wBitsPerSample/8)
}

;==================================================

F_SubTri()
{
	;diagonal lines in pseudo-arrays, with different slope ratios:
	;e.g. 1: 0, 1, 2, ..., 254, 255, 254, ..., 2, 1
	;e.g. 2: 0, 2, 4, ..., 252, 254, 252, ..., 4, 2
	;e.g. 4: 0, 4, 8, ..., 248, 252, 248, ..., 8, 4
	;e.g. 8: 0, 8, 16, ..., 240, 248, 240, ..., 16, 8

	global	;assume-global mode of operation
	local	vListNum 			:= "1,2,3,4,5"						;5x pseudo-arrays, 2x seconds eac
		,	vList 			:= ""
		,	vType			:= "tri"
		,	vPath 			:= A_ScriptDir . "\" . A_Year . A_MM . A_DD . "_" . A_Hour . A_Min . "_" .  "WAVgenerator" . "_" .  vType . ".wav"
		,	vLimit1			:= 0
		,	vLimit2			:= 0
		,	vIndex			:= "" 
		,	vIndex2			:= "" 
		,	vDiff			:= "" 
		,	vAddr			:= &vData + 58
		,	vEnd				:= &vData + 58 + vSizeData
		,	vBatchDuration		:= 2
		,	vBatchSamples		:= vSamplesPerSec * vBatchDuration		;e.g. 22 050 [samples/s] * 2 [s] = 44 100 [samples] 
		,	vEndWave			:= 0
	
	if (vBytesPerSample = 1)
		vLimit1 := 0
	, 	vLimit2 := 255
	else if (vBytesPerSample = 2)
		vLimit1 := -32768
	, 	vLimit2 := 32767

	Loop, Parse, vListNum, % ","
	{
		vIndex2 		:= A_LoopField
		vList%vIndex2% := vIndex := vLimit1
		vDiff 		:= A_LoopField
		if (vBytesPerSample = 2)
			vDiff *= 256
		Loop
		{
			vIndex += vDiff
			vList%vIndex2% .= "," vIndex
			if (vIndex + vDiff > vLimit2)
				break
		}
		Loop
		{
			vIndex -= vDiff
			vList%vIndex2% .= "," vIndex
			if (vIndex - vDiff <= vLimit1)
				break
		}
	}

	Loop, Parse, vListNum, % ","
	{
		vList := vList%A_LoopField%
		StrReplace(vList, ",", "", vSamplesPerWave) 
		vSamplesPerWave += 1
		if (vSamplesPerWave <= 0)
			break
		vEndWave := vAddr + vBatchSamples * vBytesPerSample
		;MsgBox, % vSamplesPerWave
		;MsgBox, % A_Index " " vAddr
		Loop
		{
			if (vAddr + vSamplesPerWave * vBytesPerSample > vEnd)
			|| (vAddr + vSamplesPerWave * vBytesPerSample > vEndWave)
				break
			JEE_WaveWriteList(vAddr, vList, vBytesPerSample)
			vAddr += vSamplesPerWave * vBytesPerSample
		}
	}
	F_SavePlay(vPath)
}


;==================================================

F_SubSin()
{
	global	;assume-global mode of operation

	local	vAddr 			:= &vData + 58				;beginning of wave data, initial value
		,	vEnd 			:= &vData + 58 + vSizeData	;vSizeData [b], vEnd [b] 
		,	vFreq 			:= 261.63 				;[Hz]; middle C, see https://en.wikipedia.org/wiki/Piano_key_frequencies
		,	vBatchDuration		:= 2 					;seconds
		,	vBatchSamples		:= vSamplesPerSec * vBatchDuration		;e.g. 22 050 [samples/s] * 2 [s] = 44 100 [samples] 
		,	vSPAddr			:= 0						;Single Period Address
		,	vTemp			:= 0
		,	vPtrSize			:= 0						;in bytes
		,	vType			:= "sin"
		,	vPath 			:= A_ScriptDir . "\" . A_Year . A_MM . A_DD . "_" . A_Hour . A_Min . "_" .  "WAVgenerator" . "_" .  vType . ".wav"

	if (vBytesPerSample = 1)
		vPtrType := "UChar"
	,	vPtrSize := 1
	else if (vBytesPerSample = 2)
		vPtrType := "Short"
	,	vPtrSize := 2
	else
		return

	Loop
	{	
		vSamplesPerWave := Floor(vSamplesPerSec / vFreq)	;or samples per period; e.g. 22 050 [samples/s] / 261.63 [Hz] = 84 [samples]  
		if (vSamplesPerWave <= 0)
			break
		vEndWave 		:= vAddr + vBatchSamples * vBytesPerSample	;[b] 
		vSPAddr	 	:= F_SinglePeriod(vSamplesPerWave, vBytesPerSample)		;Single Period Address

		Loop
		{
			if (vAddr + vSamplesPerWave * vBytesPerSample > vEnd)
			|| (vAddr + vSamplesPerWave * vBytesPerSample > vEndWave)
				break

			F_BinaryCopy(vSPAddr, vAddr, vSamplesPerWave, vPtrType, vPtrSize)
			; JEE_WaveWriteNote(vAddr, vSamplesPerWave, vBytesPerSample)
			vAddr += vSamplesPerWave * vBytesPerSample
		}
		vFreq += 20
	}
	F_SavePlay(vPath)
}

;==================================================

F_BinaryCopy(vWhat, vWhere, vHowMany, vPtrType, vPtrSize)
{
	global	;assume-global mode of operation
	local	vTemp := 0

	Loop, % vHowMany
	{
		vTemp := NumGet(vWhat + 0, (A_Index - 1) * vPtrSize, vPtrType)
		NumPut(vTemp, vWhere + 0, (A_Index - 1) * vPtrSize, vPtrType)
	}	
}

;==================================================

F_SavePlay(vPath)
{
	global	;assume-global mode of operation

	if !FileExist(vPath)
	{
		oFile := FileOpen(vPath, "w")
		oFile.RawWrite(vData, 58 + vSizeData)
		oFile.Close()
	
		SoundPlay, % vPath, wait
	}
}

;==================================================
F_SinglePeriod(vSamplesPerWave, vBytesPerSample)
{
	local	pi 			:= 3.141592653589793
		,	vPtrType		:= "" 
		,	vNum1		:= 0
		,	vNum2		:= 0
		,	vAdjustNew	:= 0
		,	vTempNew		:= 0
		,	vNumNew		:= 0
		,	GrantCap		:= 0
		
		vWholePeriod		:= 0		;global variable to store samples of the whole period
	
	GrantCap := VarSetCapacity(vWholePeriod, vSamplesPerWave  * vBytesPerSample, 0)	;Enlarges a variable's holding capacityand filled with 0

	if (vBytesPerSample = 1)
		vPtrType := "UChar", vNum1 := 1, vNum2 := 127
	else if (vBytesPerSample = 2)
		vPtrType := "Short", vNum1 := 0, vNum2 := 32767
	else
		return

	vAdjustNew 	:= (2 * pi)  / vSamplesPerWave 	;size of single step in [rad]; actually I suppose the formula should be (2 * pi)  / (vSamplesPerWave - 1), but then it doesn't sound anymore like original file, it is more harsh
,	vTempNew 		:= 0
	Loop, % vSamplesPerWave
	{
		vNumNew 	:= Round((Sin(vTempNew) + vNum1) * vNum2) 
		NumPut(vNumNew, vWholePeriod, (A_Index - 1) * vBytesPerSample, vPtrType)	;Do not pass a variable reference if the variable contains the target address; in that case, pass an expression such as MyVar + 0
		vTempNew 	+= vAdjustNew
		GrantCap := NumGet(vWholePeriod, (A_Index - 1) * vBytesPerSample, vPtrType)
	}
	return &vWholePeriod
}

;==================================================

;e.g. middle C: 261.63 Hz
;we need 261.63 waves per second
;if there are 22050 samples per second
;we need 22050/261.63 = 84.28 samples per wave
;(write sine wave)
JEE_WaveWriteNote(vAddr, vSamplesPerWave, vBytesPerSample)
{
	local	pi 			:= 3.141592653589793
		,	OnlySinOld 	:= 0
		,	OnlySinNew 	:= 0
		,	vPtrType		:= "" 
		,	vNum1		:= 0
		,	vNum2		:= 0
		,	vAdjustNew	:= 0
		,	vTempNew		:= 0

	if (vBytesPerSample = 1)
		vPtrType := "UChar", vNum1 := 1, vNum2 := 127
	else if (vBytesPerSample = 2)
		vPtrType := "Short", vNum1 := 0, vNum2 := 32767
	else
		return

	vAdjustNew 	:= (2 * pi)  / vSamplesPerWave 	;size of single step in [rad]; actually I suppose the formula should be (2 * pi)  / (vSamplesPerWave - 1), but then it doesn't sound anymore like original file, it is more harsh
,	vTempNew 		:= 0
	Loop, % vSamplesPerWave
	{
		vNumNew 	:= Round((Sin(vTempNew) + vNum1) * vNum2) 
		NumPut(vNumNew, vAddr + 0, (A_Index - 1) * vBytesPerSample, vPtrType)	;Do not pass a variable reference if the variable contains the target address; in that case, pass an expression such as MyVar + 0
		vTempNew 	+= vAdjustNew
	}
}

;==================================================

JEE_WaveWriteList(vAddr, vList, vBytesPerSample)
{
	global	;assume-global mode of operation
	local	vPtrType := "" 

	if (vBytesPerSample = 1)
		vPtrType := "UChar"
	else if (vBytesPerSample = 2)
		vPtrType := "Short"
	else
		return
	Loop, Parse, vList, % ","
		NumPut(A_LoopField, vAddr + 0, (A_Index - 1) * vBytesPerSample, vPtrType)
}
Next I thought I can prepare F_SoundBeep, written in AutoHotkey analogue of native SoundBeep:

Code: Select all

;	Create .wav file which resembles SoundBeep command. Based on work of jeeswg: https://www.autohotkey.com/boards/viewtopic.php?f=6&t=34584&hilit=WAV
;	Ammended by mslonik on 2024-02-04.

#Requires AutoHotkey v1.1.34+ 		; Displays an error and quits if a version requirement is not met.    
#SingleInstance, 		force		; Only one instance of this script may run at a time!
#NoEnv  							; Recommended for performance and compatibility with future AutoHotkey releases.
#Warn  							; Enable warnings to assist with detecting common errors.
ListLines, 			Off			; ListLines is disabled to make it harder to determine how script works.
SetWorkingDir, 		% A_ScriptDir	; Ensures a consistent starting directory.

;==================================================

	vDuration 	:= 2000			;[ms] 
,	vFrequency	:= 261.63			;[Hz] 

F_SoundBeep(vFrequency, vDuration)
ExitApp, 0

;==================================================

F_SoundBeep(vFrequency, vDuration)
{
	F_WAV_Header(vDuration)
	F_SubSin(vFrequency)
	F_SavePlay(vFrequency, vDuration)
}

;==================================================

F_WAV_Header(vDuration)
{
;header is 58 bytes long
;e.g. FOURCC identifiers: RIFF, 'fmt ', data, fact

	global	;assume-global mode of operation

	;settings:
	vQual 			:= "22050,16,1" 	;radio quality: PCM 22.050 kHz, 16 Bit, Mono (Sound Recorder default)
	oTemp 			:= StrSplit(vQual, ",")
	vSamplesPerSec 	:= oTemp.1
	vBitsPerSample 	:= oTemp.2
	vChannels 		:= oTemp.3 		;number of channels (mono or stereo), only mono = 1 is currently supported
	vData			:= 0
	vBytesPerSample 	:= vBitsPerSample / 8
	vSamples 			:= vSamplesPerSec * vDuration / 1000		;samples per channel [s] 
	vSizeData 		:= vChannels * vSamples * vBytesPerSample 	;bytes in sample area; e.g. 1 * 22 050 [samp/s] * 10 [s] * 2 [bytes] = 220 500 [bytes]


	VarSetCapacity(vData, 58 + vSizeData, 0)	;Enlarges a variable's holding capacity; vData is enlarged by 58 bytes (to keep header) and fills it with 0

	StrPut("RIFF", &vData + 0, "CP0") 	;chunkID; copies the string "RIFF" to vData with encoding "CP0" (system default ANSI code page) 
	NumPut(50 + vSizeData, vData, 4) 	;chunkSize; stores a number 50 + vSizeData in vData + offset = 4. 
	StrPut("WAVE", &vData + 8, "CP0") 	;(format)
	StrPut("fmt ", &vData + 12, "CP0") 						;chunkID	;'fmt ' uses a WAVEFORMATEX structure
	NumPut(18, vData, 16) 									;chunkSize ;e.g. 18 for PCM
	NumPut(1, vData, 20, "UShort") 							;wFormatTag ;WAVE_FORMAT_PCM := 0x1
	NumPut(vChannels, vData, 22, "UShort") 						;nChannels ;1=mono, 2=stereo (number of channels)
	NumPut(vSamplesPerSec, vData, 24) 							;nSamplesPerSec ;e.g. 22050
	NumPut(vChannels * vSamplesPerSec * vBytesPerSample, vData, 28) 	;nAvgBytesPerSec ;nChannels * nSamplesPerSec * (wBitsPerSample/8)
	NumPut(vChannels * vBytesPerSample, vData, 32, "UShort") 		;nBlockAlign ;nChannels * (wBitsPerSample/8)
	NumPut(vBitsPerSample, vData, 34, "UShort") 					;wBitsPerSample
	NumPut(0, vData, 36, "UShort") 							;cbSize
	StrPut("fact", &vData + 38, "CP0") 						;chunkID
	NumPut(4, vData, 42) 									;chunkSize
	NumPut(vSamples, vData, 46) 								;(number of samples (per channel))
	StrPut("data", &vData + 50, "CP0") 						;chunkID
	NumPut(vSamples * vChannels * vBytesPerSample, vData, 54) 		;chunkSize ;'NumSamples' * nChannels * (wBitsPerSample/8)
}


;==================================================

F_SubSin(vFreq)
{
	global	;assume-global mode of operation

	local	vAddr 			:= &vData + 58				;beginning of wave data, initial value
		,	vEnd 			:= &vData + 58 + vSizeData	;vSizeData [b], vEnd [b] 
		,	vBatchDuration		:= 2 					;seconds
		,	vBatchSamples		:= vSamplesPerSec * vBatchDuration		;e.g. 22 050 [samples/s] * 2 [s] = 44 100 [samples] 
		,	vSPAddr			:= 0						;Single Period Address
		,	vTemp			:= 0
		,	vPtrSize			:= 0						;in bytes

	if (vBytesPerSample = 1)
		vPtrType := "UChar"
	,	vPtrSize := 1
	else if (vBytesPerSample = 2)
		vPtrType := "Short"
	,	vPtrSize := 2
	else
		return

	vSamplesPerWave := Floor(vSamplesPerSec / vFreq)	;or samples per period; e.g. 22 050 [samples/s] / 261.63 [Hz] = 84 [samples]  
	if (vSamplesPerWave <= 0)
		return
	; vEndWave 		:= vAddr + vBatchSamples * vBytesPerSample	;[b] 
	vSPAddr	 	:= F_SinglePeriod(vSamplesPerWave, vBytesPerSample)		;Single Period Address
	Loop
	{
		if (vAddr + vSamplesPerWave * vBytesPerSample > vEnd)
		; || (vAddr + vSamplesPerWave * vBytesPerSample > vEndWave)
			break
		F_BinaryCopy(vSPAddr, vAddr, vSamplesPerWave, vPtrType, vPtrSize)
		vAddr += vSamplesPerWave * vBytesPerSample
	}
}

;==================================================

F_BinaryCopy(vWhat, vWhere, vHowMany, vPtrType, vPtrSize)
{
	global	;assume-global mode of operation
	local	vTemp := 0

	Loop, % vHowMany
	{
		vTemp := NumGet(vWhat + 0, (A_Index - 1) * vPtrSize, vPtrType)
		NumPut(vTemp, vWhere + 0, (A_Index - 1) * vPtrSize, vPtrType)
	}	
}

;==================================================

F_SavePlay(vFreq, vDur)
{
	global	;assume-global mode of operation

	local	vPath 			:= A_ScriptDir . "\" SubStr(A_ScriptName, 1, -4) . "_" . vFreq . "_" . "Hz" . "_" . vDur . "_" . "ms" . ".wav"
		; ,	vPath 			:= A_ScriptDir . "\" . A_Year . A_MM . A_DD . "_" . A_Hour . A_Min . "_" .  vType . "_" . vFreq . "_" . "Hz" . "_" . vDur . "_" . "ms" . ".wav"

	if !FileExist(vPath)
	{
		oFile := FileOpen(vPath, "w")
		oFile.RawWrite(vData, 58 + vSizeData)
		oFile.Close()
	}
	SoundPlay, % vPath, wait
}

;==================================================
F_SinglePeriod(vSamplesPerWave, vBytesPerSample)
{
	local	pi 			:= 3.141592653589793
		,	vPtrType		:= "" 
		,	vNum1		:= 0
		,	vNum2		:= 0
		,	vAdjustNew	:= 0
		,	vTempNew		:= 0
		,	vNumNew		:= 0
		,	GrantCap		:= 0
		
		vWholePeriod		:= 0		;global variable to store samples of the whole period
	
	GrantCap := VarSetCapacity(vWholePeriod, vSamplesPerWave  * vBytesPerSample, 0)	;Enlarges a variable's holding capacityand filled with 0

	if (vBytesPerSample = 1)
		vPtrType := "UChar", vNum1 := 1, vNum2 := 127
	else if (vBytesPerSample = 2)
		vPtrType := "Short", vNum1 := 0, vNum2 := 32767
	else
		return

	vAdjustNew 	:= (2 * pi)  / vSamplesPerWave 	;size of single step in [rad]; actually I suppose the formula should be (2 * pi)  / (vSamplesPerWave - 1), but then it doesn't sound anymore like original file, it is more harsh
,	vTempNew 		:= 0
	Loop, % vSamplesPerWave
	{
		vNumNew 	:= Round((Sin(vTempNew) + vNum1) * vNum2) 
		NumPut(vNumNew, vWholePeriod, (A_Index - 1) * vBytesPerSample, vPtrType)	;Do not pass a variable reference if the variable contains the target address; in that case, pass an expression such as MyVar + 0
		vTempNew 	+= vAdjustNew
		GrantCap := NumGet(vWholePeriod, (A_Index - 1) * vBytesPerSample, vPtrType)
	}
	return &vWholePeriod
}
And finally I've prepared a demo with small GUI, which compares sound of F_SoundBeep and native SoundBeep. For comparison I've used frequencies of piano 🎹 instrument, 1 s long in duration.

Code: Select all

;	Compare sound of .wav file to SoundBeep command. Based on work of jeeswg: https://www.autohotkey.com/boards/viewtopic.php?f=6&t=34584&hilit=WAV
;	Please remember to save this file as UTF-8 with BOM.
;	Ammended by mslonik on 2024-02-04.

#Requires AutoHotkey v1.1.34+ 		; Displays an error and quits if a version requirement is not met.    
#SingleInstance, 		force		; Only one instance of this script may run at a time!
#NoEnv  							; Recommended for performance and compatibility with future AutoHotkey releases.
#Warn  							; Enable warnings to assist with detecting common errors.
ListLines, 			Off			; ListLines is disabled to make it harder to determine how script works.
SetWorkingDir, 		% A_ScriptDir	; Ensures a consistent starting directory.

;==================================================

Gui, PianoNoteDemo: New, 		-Resize +HwndPNDHwnd +Owner -MaximizeBox -MinimizeBox
Gui, PianoNoteDemo: Add, 		Text,    	x0 y0 HwndPND_T1,	% "Please select piano note:`n`n"
Gui, PianoNoteDemo: Add, 		Text,    	x0 y0 HwndPND_T1,	% "The first value is piano key no. left-to-right, next is note code, next is frequency in [Hz]"
Gui, PianoNoteDemo: Add,			ListBox, 	w200 r1 vvFreq,	; piano key frequencies: https://en.wikipedia.org/wiki/Piano_key_frequencies
( Join
1 A0 27.50000|
2 A♯0/B♭0 29.13524|
3 B0 30.86771|
4 C1 Pedal C 32.70320|
5 C♯1/D♭1 34.64783|
6 D1 36.70810|
7 D♯1/E♭1 38.89087|
8 E1 41.20344|
9 F1 43.65353|
10 F♯1/G♭1 46.24930|
11 G1 48.99943|
12 G♯1/A♭1 51.91309|
13 A1 55.00000|
14 A♯1/B♭1 58.27047|
15 B1 61.73541|
16 C2 Deep C 65.40639|
17 C♯2/D♭2 69.29566|
18 D2 73.41619|
19 D♯2/E♭2 77.78175|
20 E2 82.40689|
21 F2 87.30706|
22 F♯2/G♭2 92.49861|
23 G2 97.99886|
24 G♯2/A♭2 103.8262|
25 A2 110.0000|
26 A♯2/B♭2 116.5409|
27 B2 123.4708|
28 C3 130.8128|
29 C♯3/D♭3 138.5913|
30 D3 146.8324|
31 D♯3/E♭3 155.5635|
32 E3 164.8138|
33 F3 174.6141|
34 F♯3/G♭3 184.9972|
35 G3 195.9977|
36 G♯3/A♭3 207.6523|
37 A3 220.0000|
38 A♯3/B♭3 233.0819|
39 B3 246.9417|
40 C4 Middle C 261.6256||
41 C♯4/D♭4 277.1826|
42 D4 293.6648|
43 D♯4/E♭4 311.1270|
44 E4 329.6276|
45 F4 349.2282|
46 F♯4/G♭4 369.9944|
47 G4 391.9954|
48 G♯4/A♭4 415.3047|
49 A4 A440 440.0000|
50 A♯4/B♭4 466.1638|
51 B4 493.8833|
52 C5 Tenor C 523.2511|
53 C♯5/D♭5 554.3653|
54 D5 587.3295|
55 D♯5/E♭5 622.2540|
56 E5 659.2551|
57 F5 698.4565|
58 F♯5/G♭5 739.9888|
59 G5 783.9909|
60 G♯5/A♭5 830.6094|
61 A5 880.0000|
62 A♯5/B♭5 932.3275|
63 B5 987.7666|
64 C6 Soprano C (High C) 1046.502|
65 C♯6/D♭6 1108.731|
66 D6 1174.659|
67 D♯6/E♭6 1244.508|
68 E6 1318.510|
69 F6 1396.913|
70 F♯6/G♭6 1479.978|
71 G6 1567.982|
72 G♯6/A♭6 1661.219|
73 A6 1760.000|
74 A♯6/B♭6 1864.655|
75 B6 1975.533|
76 C7 Double high C 2093.005|
77 C♯7/D♭7 2217.461|
78 D7 2349.318|
79 D♯7/E♭7 2489.016|
80 E7 2637.020|
81 F7 2793.826|
82 F♯7/G♭7 2959.955|
83 G7 3135.963|
84 G♯7/A♭7 3322.438|
85 A7 3520.000|
86 A♯7/B♭7 3729.310|
87 B7 3951.066|
88 C8 Eighth octave 4186.009|
)
Gui, PianoNoteDemo: Add,			Button,	x+20 Default w150 gF_ButtonGenWave, generate 1 s wav
Gui, PianoNoteDemo: Add,			Button,	y+10	w150 gF_SoundBeep, SoundBeep 1 s
Gui, PianoNoteDemo: Show, 		Center AutoSize, % SubStr(A_ScriptName, 1, -4) 
return

;==================================================

PianoNoteDemoGuiClose(GuiHwnd)
{
	ExitApp, 0
}

;==================================================

F_GetFreq()
{
	global	;assume-global mode of operation
	local	vLastSpace 	:= 0
		,	vF			:= 0

	Gui, PianoNoteDemo: Submit, NoHide
	vLastSpace := InStr(vFreq, A_Space, false, 0, 1)
	return SubStr(vFreq, vLastSpace)	
}

;==================================================

F_SoundBeep()
{
	global	;assume-global mode of operation
	local	Frequency := F_GetFreq()
		,	Duration 	:= 1000

	SoundBeep, % Frequency, % Duration
}

;==================================================

F_ButtonGenWave()
{
	global	;assume-global mode of operation
	local	Frequency 	:= F_GetFreq()
		,	WavPattern	:= A_ScriptDir . "\" SubStr(A_ScriptName, 1, -4) . "_" . Frequency . "_" . "Hz" . "_" . 1000 . "_" . "ms" . ".wav"

	if (FileExist(WavPattern)) 
	{
		SoundPlay, % WavPattern
		return
	}

	F_WAV_Header(1000)				;[ms]
	F_SubSin(Frequency)
	F_SavePlay(Frequency, 1000)
}

;==================================================

F_WAV_Header(vDuration)
{
;header is 58 bytes long
;e.g. FOURCC identifiers: RIFF, 'fmt ', data, fact

	global	;assume-global mode of operation

	;settings:
	vQual 			:= "22050,16,1" 	;radio quality: PCM 22.050 kHz, 16 Bit, Mono (Sound Recorder default)
	oTemp 			:= StrSplit(vQual, ",")
	vSamplesPerSec 	:= oTemp.1
	vBitsPerSample 	:= oTemp.2
	vChannels 		:= oTemp.3 		;number of channels (mono or stereo), only mono = 1 is currently supported
	vData			:= 0
	vBytesPerSample 	:= vBitsPerSample / 8
	vSamples 			:= vSamplesPerSec * vDuration / 1000		;samples per channel [s] 
	vSizeData 		:= vChannels * vSamples * vBytesPerSample 	;bytes in sample area; e.g. 1 * 22 050 [samp/s] * 10 [s] * 2 [bytes] = 220 500 [bytes]


	VarSetCapacity(vData, 58 + vSizeData, 0)	;Enlarges a variable's holding capacity; vData is enlarged by 58 bytes (to keep header) and fills it with 0

	StrPut("RIFF", &vData + 0, "CP0") 	;chunkID; copies the string "RIFF" to vData with encoding "CP0" (system default ANSI code page) 
	NumPut(50 + vSizeData, vData, 4) 	;chunkSize; stores a number 50 + vSizeData in vData + offset = 4. 
	StrPut("WAVE", &vData + 8, "CP0") 	;(format)
	StrPut("fmt ", &vData + 12, "CP0") 						;chunkID	;'fmt ' uses a WAVEFORMATEX structure
	NumPut(18, vData, 16) 									;chunkSize ;e.g. 18 for PCM
	NumPut(1, vData, 20, "UShort") 							;wFormatTag ;WAVE_FORMAT_PCM := 0x1
	NumPut(vChannels, vData, 22, "UShort") 						;nChannels ;1=mono, 2=stereo (number of channels)
	NumPut(vSamplesPerSec, vData, 24) 							;nSamplesPerSec ;e.g. 22050
	NumPut(vChannels * vSamplesPerSec * vBytesPerSample, vData, 28) 	;nAvgBytesPerSec ;nChannels * nSamplesPerSec * (wBitsPerSample/8)
	NumPut(vChannels * vBytesPerSample, vData, 32, "UShort") 		;nBlockAlign ;nChannels * (wBitsPerSample/8)
	NumPut(vBitsPerSample, vData, 34, "UShort") 					;wBitsPerSample
	NumPut(0, vData, 36, "UShort") 							;cbSize
	StrPut("fact", &vData + 38, "CP0") 						;chunkID
	NumPut(4, vData, 42) 									;chunkSize
	NumPut(vSamples, vData, 46) 								;(number of samples (per channel))
	StrPut("data", &vData + 50, "CP0") 						;chunkID
	NumPut(vSamples * vChannels * vBytesPerSample, vData, 54) 		;chunkSize ;'NumSamples' * nChannels * (wBitsPerSample/8)
}


;==================================================

F_SubSin(vFreq)
{
	global	;assume-global mode of operation

	local	vAddr 			:= &vData + 58				;beginning of wave data, initial value
		,	vEnd 			:= &vData + 58 + vSizeData	;vSizeData [b], vEnd [b] 
		,	vBatchDuration		:= 2 					;seconds
		,	vBatchSamples		:= vSamplesPerSec * vBatchDuration		;e.g. 22 050 [samples/s] * 2 [s] = 44 100 [samples] 
		,	vSPAddr			:= 0						;Single Period Address
		,	vTemp			:= 0
		,	vPtrSize			:= 0						;in bytes

	if (vBytesPerSample = 1)
		vPtrType := "UChar"
	,	vPtrSize := 1
	else if (vBytesPerSample = 2)
		vPtrType := "Short"
	,	vPtrSize := 2
	else
		return

	vSamplesPerWave := Floor(vSamplesPerSec / vFreq)	;or samples per period; e.g. 22 050 [samples/s] / 261.63 [Hz] = 84 [samples]  
	if (vSamplesPerWave <= 0)
		return
	; vEndWave 		:= vAddr + vBatchSamples * vBytesPerSample	;[b] 
	vSPAddr	 	:= F_SinglePeriod(vSamplesPerWave, vBytesPerSample)		;Single Period Address
	Loop
	{
		if (vAddr + vSamplesPerWave * vBytesPerSample > vEnd)
		; || (vAddr + vSamplesPerWave * vBytesPerSample > vEndWave)
			break
		F_BinaryCopy(vSPAddr, vAddr, vSamplesPerWave, vPtrType, vPtrSize)
		vAddr += vSamplesPerWave * vBytesPerSample
	}
}

;==================================================

F_BinaryCopy(vWhat, vWhere, vHowMany, vPtrType, vPtrSize)
{
	global	;assume-global mode of operation
	local	vTemp := 0

	Loop, % vHowMany
	{
		vTemp := NumGet(vWhat + 0, (A_Index - 1) * vPtrSize, vPtrType)
		NumPut(vTemp, vWhere + 0, (A_Index - 1) * vPtrSize, vPtrType)
	}	
}

;==================================================

F_SavePlay(vFreq, vDur)
{
	global	;assume-global mode of operation

	local	vPath 			:= A_ScriptDir . "\" SubStr(A_ScriptName, 1, -4) . "_" . vFreq . "_" . "Hz" . "_" . vDur . "_" . "ms" . ".wav"
		; ,	vPath 			:= A_ScriptDir . "\" . A_Year . A_MM . A_DD . "_" . A_Hour . A_Min . "_" .  vType . "_" . vFreq . "_" . "Hz" . "_" . vDur . "_" . "ms" . ".wav"

	if !FileExist(vPath)
	{
		oFile := FileOpen(vPath, "w")
		oFile.RawWrite(vData, 58 + vSizeData)
		oFile.Close()
	}
	SoundPlay, % vPath, wait
}

;==================================================
F_SinglePeriod(vSamplesPerWave, vBytesPerSample)
{
	local	pi 			:= 3.141592653589793
		,	vPtrType		:= "" 
		,	vNum1		:= 0
		,	vNum2		:= 0
		,	vAdjustNew	:= 0
		,	vTempNew		:= 0
		,	vNumNew		:= 0
		,	GrantCap		:= 0
		
		vWholePeriod		:= 0		;global variable to store samples of the whole period
	
	GrantCap := VarSetCapacity(vWholePeriod, vSamplesPerWave  * vBytesPerSample, 0)	;Enlarges a variable's holding capacityand filled with 0

	if (vBytesPerSample = 1)
		vPtrType := "UChar", vNum1 := 1, vNum2 := 127
	else if (vBytesPerSample = 2)
		vPtrType := "Short", vNum1 := 0, vNum2 := 32767
	else
		return

	vAdjustNew 	:= (2 * pi)  / vSamplesPerWave 	;size of single step in [rad]; actually I suppose the formula should be (2 * pi)  / (vSamplesPerWave - 1), but then it doesn't sound anymore like original file, it is more harsh
,	vTempNew 		:= 0
	Loop, % vSamplesPerWave
	{
		vNumNew 	:= Round((Sin(vTempNew) + vNum1) * vNum2) 
		NumPut(vNumNew, vWholePeriod, (A_Index - 1) * vBytesPerSample, vPtrType)	;Do not pass a variable reference if the variable contains the target address; in that case, pass an expression such as MyVar + 0
		vTempNew 	+= vAdjustNew
		GrantCap := NumGet(vWholePeriod, (A_Index - 1) * vBytesPerSample, vPtrType)
	}
	return &vWholePeriod
}
Thank you again for your awesome work 👍🙂👏.

Kind regards, mslonik (🐘)

My scripts on this forum: Hotstrings Diacritic O T A G L E
Please become my patreon: Patreon👍
Written in AutoHotkey text replacement tool: Hotstrings.technology
Courses on AutoHotkey :ugeek:

Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: No registered users and 98 guests