Praat: Manipulate formants

This Praat script gradually shifts a formant track (here: first formant, F1) to create a phonetic continuum from high-to-low (e.g., from “bet” to “bit”). It takes a list of word pairs as input, reads the first member, and applies the formant shift while controlling for intensity and f0.

You can also download the script as a .praat file.

################################################################################
### Hans Rutger Bosker, Radboud University
### HansRutger.Bosker@donders.ru.nl
### based on an earlier script created by Matthias Sjerps
### Date: 20 July 2023, run in Praat 6.3.08 on Windows 10
### License: CC BY-NC 4.0
################################################################################


	###>> This script takes pairs of items (e.g., bet and bit) from a directory
	###>>	that critically only differ in their F1 ("bet" has higher F1 than "bit").
	###>>	The script then creates a phonetic continuum from high F1 (bet) to low F1 (bit)
	###>>	using Burg's LPC method.
	
	###>> IMPORTANT: Please provide a list of word pairs in a .txt file in the
	###>>	input directory. This file should contain two tab-separated columns
	###>>	with the labels "word1" and "word2". Then script then loops over the
	###>>	rows in this list of pairs. For example:
	
	###>>	word1	word2
	###>>	bet	bit
	###>>	get	git
	###>>	set	sit

	###>> IMPORTANT: Every individual sound file should already have an accompanying
	###>>	.TextGrid file containing one tier. This tier should have 3 intervals:
	###>>	1. The interval preceding the critical vowel
	###>>	2. The interval of the critical vowel
	###>>	3. The interval following the critical vowel

	###>> The script includes transplantation of the intensity contour of the original
	###>>	word1 to the manipulated output sound. The script can also set the
	###>>	f0 of the manipulated vowel to a fixed average value calculated
	###>>	across the two members of an item pair. This is done by setting
	###>>	the variable control_f0$ to the value "yes".

	
################################################################################
### Variables you will definitely need to customize:
################################################################################

### Where can the files be found?

dir_in$ = "C:\Users\hanbos\Desktop\mysounds"

### Where should the output files be saved?

dir_out$ = "C:\Users\hanbos\Desktop\mysounds\continua"

### What is the name of the .txt file containing the list of word pairs?
### 	NOTE: Do **NOT** include the .txt extension; just the name!

table_name$ = "list_of_pairs"

### Next to the formant manipulations, do you also want to control the f0 and duration
### 	of the output sounds by setting them to the average value between the two words?

control_f0$ = "yes"

### Number of estimated formants (default: 5)
nformants = 5
### Provide the max. frequency for filtering (default for F1: 3000)
### 	Manipulations will be performed below this frequency only;
### 	the original signal is used above this frequency.
###		This helps to create natural-sounding (i.e., convincing) output speech.
filter = 3000
### Provide cutoff frequency for formant detection (default: 11000)
cutoff = 11000
### Provide the min and max pitch settings.
###		For female talker: 100 - 400
###		For male talker: 75 - 300
minpitch = 100
maxpitch = 400
### Define how many steps should be on your continua.
###		It helps if this value is an odd number so you have a 'true' average
###		in the middle. Default is 11 because this means the steps go up/down
###		by 10% every time. This means that step 1 has the mean F1 of the original
###		high-F1 member (e.g., bet) and step 11 has the mean F1 of the original
###		low-F1 member (e.g., bit).
nSteps = 11
### Define your manipulation method (default: burg)
methodLPC$ = "burg"
### Define your fade-in/fade-out overlap window in seconds (default: 0.01)
window = 0.01





################################################################################
### Let's check whether the directories specified above exist...
################################################################################

### Let's check if the input directory exists.
### This script will throw an error if the directory doesn't exist
### (i.e., it won't write to a mysterious temp directory).

### First check whether the input directory ends in a backslash (if so, removed)

if right$(dir_in$,1)="/"
	dir_in$ = left$(dir_in$,length(dir_in$)-1)
elsif right$(dir_in$,1)="\"
	dir_in$ = left$(dir_in$,length(dir_in$)-1)
endif

### Then create a temporary txt file in the folder
### and try to write it to the input folder.

### NOTE: The "nocheck" below asks Praat not to complain if the folder
### does *not* exist. We'll manually check whether the saving of this
### temp txt file has succeeded or not further down below.

temp_filename$ = dir_in$ + "/" + "my_temporary_Praat_file.txt"
nocheck writeFileLine: temp_filename$, "This is just to check if the directory exists"

### Can the file be found?

file_exists_yesno = fileReadable(temp_filename$)

if file_exists_yesno = 1
	# if you *could* read that temp txt file,
	# this confirms that the directory is valid.
	# Then you can delete it.
	deleteFile: temp_filename$
else
	# if that file wasn't readable, that means that the directory wasn't valid. 
	printline The folder 'dir_in$' was not found
	exit Your input directory doesn't exist. Check spelling. The directory must *already* exist.
endif

## Now re-do this for the output directory:

if right$(dir_out$,1)="/"
	dir_out$ = left$(dir_out$,length(dir_out$)-1)
elsif right$(dir_out$,1)="\"
	dir_out$ = left$(dir_out$,length(dir_out$)-1)
endif

### Then create a temporary txt file in the folder
### and try to write it to the input folder.

### NOTE: The "nocheck" below asks Praat not to complain if the folder
### does *not* exist. We'll manually check whether the saving of this
### temp txt file has succeeded or not further down below.

temp_filename$ = dir_out$ + "/" + "my_temporary_Praat_file.txt"
nocheck writeFileLine: temp_filename$, "This is just to check if the directory exists"

### Can the file be found?

file_exists_yesno = fileReadable(temp_filename$)

if file_exists_yesno = 1
	# if you *could* read that temp txt file,
	# this confirms that the directory is valid.
	# Then you can delete that temp txt file.
	deleteFile: temp_filename$
else
	# if that file wasn't readable, that means that the directory wasn't valid. 
	printline The folder 'dir_out$' was not found
	exit Your output directory doesn't exist. Check spelling. The directory must *already* exist.
endif





################################################################################
################################################################################
#################################    SCRIPT    #################################
################################################################################
################################################################################

## Let's clear the Info window so we can print fresh new log data of our stimuli.
## This log prints for each pair of items ("item"), for each created step,
##		the original word duration, vowel duration, mean f0, f1, f2, f3 for the 1st member of the item pair
##		the original word duration, vowel duration, mean f0, f1, f2, f3 for the 2nd member of the item pair
##		the word duration, vowel duration, mean f0, f1, f2, f3 for the manipulated output sound.
clearinfo
echo item	step	dur_1	voweldur_1	f0_1	f1_1	f2_1	f3_1	dur_2	voweldur_2	f0_2	f1_2	f2_2	f3_2	dur_manip	voweldur_manip	f0_manip	f1_manip	f2_manip	f3_manip





Read from file... 'dir_in$'\'table_name$'.txt
nItems = Get number of rows

for ItemCnt from 1 to nItems
	## Create leading and trailing silence of 0.5 sec to append to the words to
	## improve spectral measurements at the onset/offset of the words.
	Create Sound from formula... silence1 Mono 0 0.5 cutoff  0

	#######################################
	## Read and measure acoustics of word1
	#######################################

	select Table 'table_name$'
	word1$ = Get value... 'ItemCnt' word1
	word2$ = Get value... 'ItemCnt' word2
	
	Read from file... 'dir_in$'\'word1$'.wav
	Rename... word1
	sampFreq_1 = Get sampling frequency
	dur_1 = Get total duration
	int_1 = Get intensity (dB)
	To Pitch... 0 'minpitch' 'maxpitch'
	f0_1 = Get mean... 0 0 Hertz

	select Sound word1
	Resample: cutoff, 50
	Rename... word1_belowcutoff
	select Sound word1
	Remove

	Create Sound from formula... silence2 Mono 0 0.5 cutoff  0
	select Sound silence1
	plus Sound word1_belowcutoff
	plus Sound silence2
	Concatenate
	Rename... word1_pad

	Filter (pass Hann band)... 0 filter 10
	Rename... Low_Part_1
	lowInt_1 = Get intensity (dB)
	To Intensity... 100 0 no
	Down to IntensityTier
	select Intensity Low_Part_1
	Remove

	select Sound word1_pad
	To LPC (burg): ('nformants'*2), 0.025, 0.005, 50
	select Sound word1_pad
	plus LPC word1_pad
	Filter (inverse)
	Rename... Source_1
	select LPC word1_pad
	Remove
	
	Read from file... 'dir_in$'\'word1$'.TextGrid
	Rename... word1
	Shift times by... 0.5
	vowelonset_1 = Get start time of interval... 1 2
	voweloffset_1 = Get end time of interval... 1 2
	voweldur_1 = voweloffset_1 - vowelonset_1

	select Sound word1_pad
	To Formant ('methodLPC$')... 0 nformants (cutoff/2) 0.025 50
	maxForm = Get maximum number of formants
	for formCnt from 1 to maxForm
		origFormantsArray_1 ['formCnt'] = Get mean... 'formCnt' 'vowelonset_1' 'voweloffset_1' hertz
		f'formCnt'_1 = Get mean... 'formCnt' 'vowelonset_1' 'voweloffset_1' hertz
	endfor
	Remove
	
	
	
	
	
	#######################################
	## Read and measure acoustics of word2
	#######################################

	Read from file... 'dir_in$'\'word2$'.wav
	Rename... word2
	sampFreq_2 = Get sampling frequency
	dur_2 = Get total duration
	int_2 = Get intensity (dB)
	To Pitch... 0 'minpitch' 'maxpitch'
	f0_2 = Get mean... 0 0 Hertz

	select Sound word2
	Resample: cutoff, 50
	Rename... word2_belowcutoff
	select Sound word2
	Remove

	Create Sound from formula... silence2 Mono 0 0.5 cutoff  0
	select Sound silence1
	plus Sound word2_belowcutoff
	plus Sound silence2
	Concatenate
	Rename... word2_pad

	Filter (pass Hann band)... 0 filter 10
	Rename... Low_Part_2
	lowInt_2 = Get intensity (dB)
	To Intensity... 100 0 no
	Down to IntensityTier
	select Intensity Low_Part_2
	Remove

	select Sound word2_pad
	To LPC (burg): ('nformants'*2), 0.025, 0.005, 50
	select Sound word2_pad
	plus LPC word2_pad
	Filter (inverse)
	Rename... Source_2
	select LPC word2_pad
	Remove

	Read from file... 'dir_in$'\'word2$'.TextGrid
	Rename... word2
	Shift times by... 0.5
	vowelonset_2 = Get start time of interval... 1 2
	voweloffset_2 = Get end time of interval... 1 2
	voweldur_2 = voweloffset_2 - vowelonset_2

	select Sound word2_pad
	To Formant ('methodLPC$')... 0 nformants (cutoff/2) 0.025 50
	maxForm = Get maximum number of formants
	for formCnt from 1 to maxForm
		origFormantsArray_2 ['formCnt'] = Get mean... 'formCnt' 'vowelonset_2' 'voweloffset_2' hertz
		f'formCnt'_2 = Get mean... 'formCnt' 'vowelonset_2' 'voweloffset_2' hertz
	endfor
	Remove
	
	
	

	
	#######################################
	## Now loop over steps
	#######################################

	for stepCtr from 1 to nSteps

		# NOTE: we take word1 (here: the high-F1 member [bet]) as the basis for the manipulations.

		select Sound word1_pad
		To Formant ('methodLPC$')... 0 nformants (cutoff/2) 0.025 50
		maxForm = Get maximum number of formants
		nFrames = Get number of frames
		Rename: "New_Form_'stepCtr'"

		###############################################################################################################
		# if some items need custom formant shifts, you can define an item-specific constant here
		###############################################################################################################
		constant = 0
		#	if word1$ = "bet"
		#		constant = 30
		#	elsif word1$ = "get"
		#		constant = 50
		#	elsif word1$ = "set"
		#		constant = 40
		#	endif

		## NOTE: we create a continuum starting at the high-F1 member (on step 1)
		##	gradually going do to the low-F1 member.
		stepsize = (f1_1 - f1_2)/(nSteps-1)
		Formula (frequencies): "if row = 1 then (self-(((stepCtr-1)*stepsize)+constant)) else self fi"
		
		## You can also control other formants, for instance by setting the F2 to the average of the two vowels
		## thus creating an ambiguous F2 in between the two vowels that is fixed/constant at every step.
		## For instance:

		#ambF2 = (f2_1 + f2_2)/2
		#distanceToAmbF2 = f2_1 - ambF2
		#Formula (frequencies): "if row = 2 then (self-distanceToAmbF2) else self fi"
		
		## Filter the original source with the new formant filter
		select Formant New_Form_'stepCtr'
		plus Sound Source_1
		Filter

		## Restrict to lower-freq content only
		Filter (pass Hann band)... 0 filter 10
		Rename... word1_'stepCtr'_Low

		## Transplant the original intensity contour back onto the manipulated speech
		To Intensity... 100 0 no
		Down to IntensityTier
		Formula... self*-1
		plus Sound word1_'stepCtr'_Low
		Multiply... yes
		plus IntensityTier Low_Part_1
		Multiply... yes
		Scale intensity... lowInt_1

		## Remove leading and trailing silences
		Extract part: 0.5, (dur_1+0.5), "rectangular", 1, "no"
		Resample: sampFreq_1, 50
		Rename... word1_'stepCtr'_Low_nonpad

		## Combine with the original high-freq content
		Read from file... 'dir_in$'\'word1$'.wav
		Rename... word1
		Filter (pass Hann band)... filter sampFreq_1 10
		Rename... word1_High
		plus Sound word1_'stepCtr'_Low_nonpad
		Combine to stereo
		Convert to mono
		Rename... word1_'stepCtr'_manip
		Scale intensity... int_1
		selectObject: "Sound word1"
		Scale intensity... int_1

		select Sound word1_'stepCtr'_manip
		dur_manip = Get total duration
		To Manipulation... 0.01 'minpitch' 'maxpitch'
		Extract pitch tier
		f0_manip = Get mean (curve)... 0 0

		## Set the F0 to the average values across word1 & word2
		if control_f0$ = "yes"
			newWordF0 = (f0_1+f0_2)/2
			f0ScalingFactor = newWordF0/f0_manip

			select Manipulation word1_'stepCtr'_manip
			Extract pitch tier
			Multiply frequencies... 0 1000 'f0ScalingFactor'
			f0_manip = Get mean (curve)... 0 0
			plus Manipulation word1_'stepCtr'_manip
			Replace pitch tier
			select Manipulation word1_'stepCtr'_manip
			Get resynthesis (overlap-add)
			Rename... word1_'stepCtr'_manip
		endif
		
		## We've shifted the formants in the entire word but we really only want to keep
		## the critical manipulated vowel only. So here we combine:
		## 1. the original pre-vowel interval
		## 2. the manipulated vowel interval
		## 3. the original post-vowel interval
		## using a switch (man1Orig0Switch) that starts at 0 (set to original unmanipulated)
		## and switches back and forth to 1 (manipulated), then 0, etc.

		Read from file... 'dir_in$'\'word1$'.TextGrid
		Rename... word1
		nInterv = Get number of intervals: 1
		man1Orig0Switch = 0
		for intervCnt from 1 to nInterv
			selectObject: "TextGrid word1"
			
			intSta = Get start point: 1, intervCnt
			intEnd = Get end point: 1, intervCnt
			if intervCnt = 1
				intSta2 = intSta
				intEnd2 = intEnd + (window/2)
			elsif intervCnt = nInterv
				intSta2 = intSta - (window/2)
				intEnd2 = intEnd
			elsif intervCnt = 2
				intSta2 = intSta - (window/2)
				intEnd2 = intEnd + (window/2)
				vowelonset = intSta
				voweloffset = intEnd
				voweldur_manip = intEnd-intSta
			endif

			if man1Orig0Switch = 0
				selectObject: "Sound word1"
				Extract part: intSta2, intEnd2, "rectangular", 1, "no"
				Rename... 'intervCnt'
				man1Orig0Switch = 1
			elsif man1Orig0Switch = 1
				selectObject: "Sound word1_'stepCtr'_manip"
				Extract part: intSta2, intEnd2, "rectangular", 1, "no"
				Rename... 'intervCnt'
				man1Orig0Switch = 0
			endif
		endfor

		selectObject: "Sound 1"
		for intervCnt_2 from 2 to nInterv
			plusObject: "Sound 'intervCnt_2'"
		endfor
		Concatenate with overlap... 'window'
		Rename... Resynth____'stepCtr'

		## Save the final result sound
		Save as WAV file: "'dir_out$'\'word1$'_step'stepCtr'.wav"
		selectObject: "TextGrid word1"
		Save as text file: "'dir_out$'\'word1$'_step'stepCtr'.TextGrid"
		
		# Let's query the F1, F2, F3, F4 for the manipulated vowel interval
		select Formant New_Form_'stepCtr'
		f1_manip = Get mean... 1 (vowelonset+0.5) ((voweloffset)+0.5) hertz
		f2_manip = Get mean... 2 (vowelonset+0.5) ((voweloffset)+0.5) hertz
		f3_manip = Get mean... 3 (vowelonset+0.5) ((voweloffset)+0.5) hertz
		f4_manip = Get mean... 4 (vowelonset+0.5) ((voweloffset)+0.5) hertz
		
		select Sound Resynth____'stepCtr'
		dur_manip = Get total duration
			
		printline 'word1$'	'stepCtr'	'dur_1'	'voweldur_1'	'f0_1'	'f1_1'	'f2_1'	'f3_1'	'dur_2'	'voweldur_2'	'f0_2'	'f1_2'	'f2_2'	'f3_2'	'dur_manip'	'voweldur_manip'	'f0_manip'	'f1_manip'	'f2_manip'	'f3_manip'
	endfor

	select all
	minus Table 'table_name$'
	Remove
endfor



################################################################################
# End of script
################################################################################
Previous
Next