Praat: Dynamic Time Warping

This Praat script takes pairs of items (e.g., bet and bit) from a directory and matches the duration of individual speech sounds in word1 to the duration of individual speech sounds in word2 (…a kind of simplistic Dynamic Time Warping).

In order for this script to work, you’ll need to have a TextGrid for each individual .wav file that contains segmentations of each individual speech sound. Moreover, the TextGrids for the two members of a pair will need to have the same number of intervals segmented.

Note that it helps if the intervals in the TextGrids are segmented in a relatively fine-grained manner. For instance, segmenting pauses, stop closures, stop bursts, etc. as separate intervals will make the output sound more natural. Also note that the duration scaling is linear: if interval1 is 200 ms in word1 but 250 ms in word2, then the duration of interval1 is expanded by a factor of 1.25.

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

################################################################################
### Hans Rutger Bosker, Radboud University
### HansRutger.Bosker@donders.ru.nl
### 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
	###>>	and matches the duration of individual speech sounds in word1
	###>>	to the duration of individual speech sounds in word2.
	
	###>> 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".For example:
	
	###>>	word1	word2
	###>>	bet	bit
	###>>	get	git
	###>>	set	sit

	###>> IMPORTANT: Each sound file should have an accompanying TextGrid with
	###>>	one interval tier. This tier should contain segmentations for each
	###>>	individual speech sound. It's best to segment as many sound segments
	###>>	as possible. For instance, for stop consonants, segmenting the closure
	###>>	separately from the burst avoids misaligned output.
	###>>	The TextGrids for the two members of a word pair should contain an
	###>>	identical number of (matching) intervals.

	
################################################################################
### 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\output"

### 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"

### Provide the min and max pitch settings.
###		For female talker: 100 - 400
###		For male talker: 75 - 300
minpitch = 100
maxpitch = 400




################################################################################
### 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 keeps track of the individual word durations and interval durations.
clearinfo
echo item	word1_dur	word2_dur	interval_nr	interval_dur_1	interval_dur_2	interval_dur_dtw	scaleFactor	word_dtw_dur



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

for ItemCnt from 1 to nItems
	select Table 'table_name$'
	word1$ = Get value... 'ItemCnt' word1
	word2$ = Get value... 'ItemCnt' word2
	
	Read from file... 'dir_in$'\'word1$'.TextGrid
	Rename... word1
	word1_nInts = Get number of intervals... 1
	Read from file... 'dir_in$'\'word1$'.wav
	Rename... word1
	word1_dur = Get total duration
	word1_int = Get intensity (dB)
	
	Read from file... 'dir_in$'\'word2$'.TextGrid
	Rename... word2
	word2_nInts = Get number of intervals... 1
	Read from file... 'dir_in$'\'word2$'.wav
	Rename... word2
	word2_dur = Get total duration
	word2_int = Get intensity (dB)

	
	## Check if the number of intervals in the two TextGrids match
	if word1_nInts <> word2_nInts
		# if that file wasn't readable, that means that the directory wasn't valid. 
		printline 'word1$' has 'word1_nInts' intervals but 'word2$' has 'word2_nInts' intervals.
		exit The number of intervals in the two input TextGrids do not match. Adjust the TextGrids.
	endif


	for thisInt to word1_nInts
		select TextGrid word1
		thisStart = Get start time of interval... 1 'thisInt'
		thisEnd = Get end time of interval... 1 'thisInt'
		word1_thisInt_startArray ['thisInt'] = thisStart
		word1_thisInt_endArray ['thisInt'] = thisEnd
		thisDur = thisEnd - thisStart
		word1_thisInt_durArray ['thisInt'] = thisDur

		select TextGrid word2
		thisStart = Get start time of interval... 1 'thisInt'
		thisEnd = Get end time of interval... 1 'thisInt'
		word2_thisInt_startArray ['thisInt'] = thisStart
		word2_thisInt_endArray ['thisInt'] = thisEnd
		thisDur = thisEnd - thisStart
		word2_thisInt_durArray ['thisInt'] = thisDur
	endfor


	select Sound word1
	To Manipulation... 0.01 'minpitch' 'maxpitch'
	Extract duration tier
	for thisIntNumber to word1_nInts
		start_of_1 = word1_thisInt_startArray ['thisIntNumber']
		end_of_1 = word1_thisInt_endArray ['thisIntNumber']
		dur_of_1 = word1_thisInt_durArray ['thisIntNumber']
		dur_of_2 = word2_thisInt_durArray ['thisIntNumber']
		scaledDur = dur_of_2
		word1_thisInt_scaledDurArray ['thisIntNumber'] = scaledDur
		durScaleFactor = scaledDur/dur_of_1
		word1_thisInt_scaleFactorArray ['thisIntNumber'] = durScaleFactor
		Add point: ('start_of_1' + 0.001), durScaleFactor
		Add point: ('end_of_1' - 0.001), durScaleFactor
	endfor
	plus Manipulation word1
	Replace duration tier
	select Manipulation word1
	Get resynthesis (overlap-add)
	Scale intensity... 'word1_int'
	dtw_dur = Get total duration
	Rename... word_dtw
	
	To TextGrid: "intervals", ""
	
	boundarytimepoint = 0
	for thisIntNumber to word1_nInts
		start_of_1 = word1_thisInt_startArray ['thisIntNumber']
		end_of_1 = word1_thisInt_endArray ['thisIntNumber']
		dur_of_1 = word1_thisInt_durArray ['thisIntNumber']
		dur_of_2 = word2_thisInt_durArray ['thisIntNumber']
		scaledDur = word1_thisInt_scaledDurArray ['thisIntNumber']
		durScaleFactor = word1_thisInt_scaleFactorArray ['thisIntNumber']
		
		if thisIntNumber <> word1_nInts
			boundarytimepoint = boundarytimepoint + scaledDur
			select TextGrid word_dtw
			Insert boundary... 1 'boundarytimepoint'
		endif

		printline 'word1$'	'word1_dur'	'word2_dur'	'thisIntNumber'	'dur_of_1'	'dur_of_2'	'scaledDur'	'durScaleFactor'	'dtw_dur'
	endfor

	#######################################
	## Save the final result sound
	#######################################
	select Sound word_dtw
	Save as WAV file: "'dir_out$'\'word1$'_dtw.wav"
	select TextGrid word_dtw
	Save as text file: "'dir_out$'\'word1$'_dtw.TextGrid"

	select all
	minus Table 'table_name$'
	Remove
endfor



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