Meas formants walkthrough

From Phonlab
Revision as of 10:15, 11 February 2014 by Ronald (talk | contribs) (Created page with " # A sample audiolabel script for formant analysis Here are annotated snippets of a script that reads Praat TextGrids, searches for vowel tokens, and runs a formant analyzer. Fo…")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigationJump to search
  1. A sample audiolabel script for formant analysis

Here are annotated snippets of a script that reads Praat TextGrids, searches for vowel tokens, and runs a formant analyzer. Formant measurements are extracted from the results and written to a text file, along with additional metadata retrieved from the TextGrids.

Let's take a look at this in pseudocode first:

   read in a textgrid
   run the ifcformant system command
   read the ifcformant output
   search the textgrid for vowel tokens (V):
       if V meets size condition:
           get word and context of V from the textgrid
           get regularly-sampled formant measurements for V:
               output measurement with word and context

Our input TextGrids will have three interval tiers each, with tier names 'phone', 'word', and 'context'.

    1. Getting started

First, import the necessary libraries:


   import audiolabel     # library for reading phonetic label files
   import os, subprocess     # access to system commands
   import re             # regular expressions
   import numpy as np    # numeric processing routines
   reload(audiolabel)



   <module 'audiolabel' from 'audiolabel.py'>


    1. Read in a TextGrid

Our script assumes that TextGrids reside in the same directory as the .wav files they annotate, and that they share the same name, except for the extension.


   tg = 'doc/this_is_a_label_file.TextGrid'   # name of Praat TextGrid
   fname = os.path.splitext(tg)[0]  # get filename without extension
   fname



   'doc/this_is_a_label_file'


Now we create a LabelManager by reading in the TextGrid with the hint that the file type is 'praat'. The LabelManager can distinguish 'short' and 'long' Praat formats. If you want to be explicit, you can use 'praat_short' or 'praat_long' as the fromType.


   pm = audiolabel.LabelManager(fromFile=tg, fromType='praat')
   lab = pm.tier('word').labelAt(0.5736)
   lab.center()



   0.621615744181556


    1. Run the ifcformant system command

ifcformant is our formant analysis program. It is a separate executable available on the system.

We will use Python's subprocess module to execute the command and check the return code. To set this up we'll make a list of arguments to pass to subprocess, with the ifcformant command itself included as the first item in the list.


   tempifc = 'doc/__temp.ifc' # output destination of ifcformant command
   speaker = 'male'           # gender of speaker
   ifc_args = ['ifcformant',  # list of arguments
              '--speaker=' + speaker,
              '-e', 'gain -n -3 sinc -t 10 60 contrast',
              '--print-header',
              '--output=' + tempifc,
              fname + '.wav']
   ifc_args



   ['ifcformant',
    '--speaker=male',
    '-e',
    'gain -n -3 sinc -t 10 60 contrast',
    '--print-header',
    '--output=doc/__temp.ifc',
    'doc/this_is_a_label_file.wav']


We use Popen to execute ifcformant and return a handle to the running process. We need to wait() for the process to finish before continuing on with our script. ifcformant can take a while to finish, so this is important!

Any errors reported by ifcformant are connected to subprocess.PIPE so that we can report the errors if ifcformant fails.


   proc = subprocess.Popen(ifc_args, stderr=subprocess.PIPE)
   proc.wait()



   0


Finally, we check the process's return code to see whether ifcformant succeeded, in which case the code is 0. If ifcformant failed, we report the error and raise an exception.


   if proc.returncode != 0:
       for line in proc.stderr:
           sys.stderr.write(line + '\n')
       raise Exception("ifcformant exited with status: {0}".format(proc.returncode))
    1. Read the ifcformant output

ifcformant produces a table of numbers. Since we included the --print-header argument the output contains a header row identifying the fields. Here are the first 10 lines of output:


   !head doc/__temp.ifc
   sec	rms	f1	f2	f3	f4	f0
   0.0050	0.0	0.0	0.0	0.0	0.0	0.0
   0.0150	0.0	0.0	0.0	0.0	0.0	0.0
   0.0250	0.0	0.0	0.0	0.0	0.0	0.0
   0.0350	75.0	354.4	1078.2	2368.6	3173.0	0.0
   0.0450	75.0	386.1	1427.1	2427.6	3291.3	0.0
   0.0550	82.2	473.6	1536.8	2525.5	3369.1	240.0
   0.0650	82.2	450.1	1481.2	2584.4	3366.3	106.2
   0.0750	240.8	526.8	1505.8	2572.2	3333.2	193.5
   0.0850	766.0	500.5	1580.9	2337.8	3358.4	150.0
   

If you think about it, this output is a kind of label file. We have six columns of output for each timepoint, and we can think of these as six label tiers. LabelManager provides a readTable() method for reading in tabular data, and we can invoke it automatically by specifying fromType='table' when creating the LabelManager.

The t1Col='sec' argument tells readTable() that each Label's t1 (see below) is identified by the column labelled 'sec'.


   ifc = audiolabel.LabelManager(fromFile=tempifc, fromType='table', t1Col='sec')
   ifc.tier('f1').labelAt(1.620)



Label( t1=1.6250, text='445.7' )


    1. Search the textgrid for vowel tokens

We want to extract formant measurements during sections of the audio file that are identified as vowels. We set up a regular expression that matches vowel tokens in the TextGrid's 'phone' tier. This regex contains two named capture groups, 'vowel' and 'stress', that will be returned when a matching label is found.


   vre = re.compile(
            "(?P<vowel>AA|AE|AH|AO|AW|AX|AXR|AY|EH|ER|EY|IH|IX|IY|OW|OY|UH|UW|UX)(?P<stress>\d)?"
         )
   vre



   re.compile(r'(?P<vowel>AA|AE|AH|AO|AW|AX|AXR|AY|EH|ER|EY|IH|IX|IY|OW|OY|UH|UW|UX)(?P<stress>\d)?')


A LabelManager tier is a Sequence, which allows for easy access to labels in a loop:


   for lab in pm.tier('context'):
       print lab.text


   1
   2
   

A LabelManager tier has a search() method that applies a regex and returns every Label that matches.


   tokens = pm.tier('phone').search(vre)
   tokens



   [Label( t1=0.1850, t2=0.3058, text='IH2' ),
    Label( t1=0.4208, t2=0.5183, text='IH0' ),
    Label( t1=0.5736, t2=0.6696, text='AH1' )]


In our script we also want to return the named captures from our match. We change the return values of search() with the returnMatch parameter.


   tokens = pm.tier('phone').search(vre, returnMatch=True)
   tokens



   [(Label( t1=0.1850, t2=0.3058, text='IH2' ), <_sre.SRE_Match at 0x5c00828>),
    (Label( t1=0.4208, t2=0.5183, text='IH0' ), <_sre.SRE_Match at 0x5c00d78>),
    (Label( t1=0.5736, t2=0.6696, text='AH1' ), <_sre.SRE_Match at 0x5f96140>)]


It is simple to set up a loop to access each matching Label and its associated match data.


   for v, m in pm.tier('phone').search(vre, returnMatch=True):
       print v.t1(), '-', v.t2()
       print v.text
       print m.group('vowel')
       print m.group('stress')
       print
   0.185027889005 - 0.305817195604
   IH2
   IH
   2
   
   0.4207853308 - 0.51828995179
   IH0
   IH
   0
   
   0.573591080112 - 0.669640408251
   AH1
   AH
   1
   
   

A conditional expression restricts the results to other criteria:


   for v, m in pm.tier('phone').search(vre, returnMatch=True):
       if v.duration() > 0.1:
           print v.t1(), '-', v.t2()
           print v.text
           print m.group('vowel')
           print m.group('stress')
           print 
   0.185027889005 - 0.305817195604
   IH2
   IH
   2
   
   

The 'word' and 'context' tiers can be queried to return the Label that occurs at the time of the vowel token with the labelAt method.


   for v, m in pm.tier('phone').search(vre, returnMatch=True):
       if v.duration() > 0.1:
           print v.text
           word = pm.tier('word').labelAt(v.center())
           print word.text
           context = pm.tier('context').labelAt(v.center())
           print context.text
           
   IH2
   This
   1
   
    1. Get regularly-sampled formant measurements

For this script we want formant measurments at the start and end of each vowel token, plus five equally-spaced timepoints in between. The linspace function from numpy simplifies the calculation of these timepoints:


   for v, m in pm.tier('phone').search(vre, returnMatch=True):
       if v.duration() > 0.1:
           points = np.linspace(v.t1(), v.t2(), num=7)
           print points
   [ 0.18502789  0.20515944  0.22529099  0.24542254  0.26555409  0.28568564
     0.3058172 ]
   

The LabelManager has a labelsAt method that calls the labelAt method on every tier managed by the LabelManager and returns a tuple of the results. If the tiers are named, the elements of the tuple can be accessed with the corresponding name.


   for v, m in pm.tier('phone').search(vre, returnMatch=True):
       if v.duration() > 0.1:
           for t in np.linspace(v.t1(), v.t2(), num=7):
               meas = ifc.labelsAt(t)
               print meas.f1.text
   237.8
   425.9
   416.8
   476.5
   503.4
   510.5
   474.9
   

This is a format string that is used to output results. The field names are in curly brackets, and the portion after the colon specifies formatting details for numeric formats. Output is tab-separated.

    1. Get word and context from the TextGrid



   fmt = '\t'.join(["{t1:0.4f}", "{t2:0.4f}", "{lintime:0.4f}", "{ifctime:0.4f}",
                   "{idx:d}", "{vowel}", "{stress}", "{rms}", "{f1}", "{f2}",
                   "{f3}", "{f4}", "{f0}", "{word}", "{context}\n"
                  ])
   fmt



   '{t1:0.4f}\t{t2:0.4f}\t{lintime:0.4f}\t{ifctime:0.4f}\t{idx:d}\t{vowel}\t{stress}\t{rms}\t{f1}\t{f2}\t{f3}\t{f4}\t{f0}\t{word}\t{context}\n'


And this creates our header line. As with the fmt assignment, we join the fields with a tab. Tabs could be inserted directly in to the string instead of using join() and split(), but the extra syntax makes the field names easier to pick out for a human reader.


   head = '\t'.join(('t1 t2 lintime ifctime idx vowel stress \
                      rms f1 f2 f3 f4 f0 word context'
                    ).split()) + '\n'
   head
   



   't1\tt2\tlintime\tifctime\tidx\tvowel\tstress\trms\tf1\tf2\tf3\tf4\tf0\tword\tcontext\n'



   head2 = 't1\tt2\tlintime\tifctime\tidx\tvowel\tstress\trms\tf1\tf2\tf3\tf4\tf0\tword\tcontext\n'
   head2



   't1\tt2\tlintime\tifctime\tidx\tvowel\tstress\trms\tf1\tf2\tf3\tf4\tf0\tword\tcontext\n'