├── .coveragerc ├── .gitattributes ├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── CHANGELOG.md ├── DEVELOP.md ├── LICENSE ├── README.rst ├── docs ├── index.html ├── promo.html ├── promo │ ├── duration_morph.html │ ├── f0_morph.html │ ├── morph_utils.html │ └── morph_utils │ │ ├── audio_scripts.html │ │ ├── interpolation.html │ │ ├── modify_pitch_accent.html │ │ ├── morph_sequence.html │ │ ├── plot_morphed_data.html │ │ └── utils.html └── search.js ├── examples ├── __init__.py ├── duration_manipulation_example.py ├── files │ ├── carrots1.TextGrid │ ├── carrots1.wav │ ├── carrots2.TextGrid │ ├── carrots2.wav │ ├── mary1.TextGrid │ ├── mary1.txt │ ├── mary1.wav │ ├── mary1_double_length.wav │ ├── mary1_mary2_dur_morph.png │ ├── mary1_mary2_f0_morph.png │ ├── mary1_mary2_f0_morph_compare.png │ ├── mary1_stylized.PitchTier │ ├── mary2.TextGrid │ ├── mary2.txt │ ├── mary2.wav │ ├── modify_pitch_accent │ │ ├── mary1_accented.pitch │ │ └── mary1_accented.wav │ ├── oranges.TextGrid │ ├── oranges.wav │ ├── pickles.TextGrid │ └── pickles.wav ├── modify_pitch_accent_example.py ├── pitch_morph_example.py ├── pitch_morph_to_pitch_contour.py └── test │ └── __init__.py ├── promo ├── __init__.py ├── duration_morph.py ├── f0_morph.py └── morph_utils │ ├── __init__.py │ ├── audio_scripts.py │ ├── interpolation.py │ ├── modify_pitch_accent.py │ ├── morph_sequence.py │ ├── plot_morphed_data.py │ └── utils.py ├── setup.py ├── tests ├── __init__.py └── integration │ ├── __init__.py │ └── test_integration.py └── tutorials ├── tutorial1_1_intro_to_promo.ipynb └── tutorial1_2_pitch_manipulations.ipynb /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | */python?.?/* 4 | */site-packages/nose/* 5 | */examples/* 6 | */test/* 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | tutorials/* linguist-vendored -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: ProMo test runner 2 | 3 | on: [push] 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | python-version: ["3.7", "3.8", "3.9", "3.10"] 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up Python ${{ matrix.python-version }} 13 | uses: actions/setup-python@v3 14 | with: 15 | python-version: ${{ matrix.python-version }} 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install -U pip setuptools 20 | pip install "importlib-metadata>=4.12" coveralls pytest pytest-cov 21 | pip install matplotlib scipy 22 | python setup.py install 23 | - name: Test with pytest 24 | run: | 25 | pytest --cov=promo --cov-report=xml tests/ 26 | - name: Upload coverage to Codecov 27 | uses: codecov/codecov-action@v3 28 | with: 29 | files: ./coverage.xml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | eggs/ 15 | lib/ 16 | lib64/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | 24 | # PyInstaller 25 | # Usually these files are written by a python script from a template 26 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 27 | *.manifest 28 | *.spec 29 | 30 | *.DS_Store 31 | 32 | *.project 33 | *.pydevproject 34 | 35 | examples/files/*/ 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Major revisions 3 | 4 | *ProMo uses semantic versioning (Major.Minor.Patch)* 5 | 6 | Ver 2.0 (July 15, 2023) 7 | - drop support for python 2.x 8 | 9 | Ver 1.3 (May 29, 2017) 10 | - added tutorials 11 | - f0Morph() can now exclude certain regions from the morph process if desired 12 | 13 | Ver 1.2 (January 27, 2017) 14 | - added code for reshaping pitch accents (shift alignment, add plateau, or change height) 15 | 16 | Ver 1.1 (February 22, 2016) 17 | - f0 morph code for modifying speaker pitch range and average pitch 18 | - (October 20, 2016) Added integration tests with travis CI and coveralls support. 19 | 20 | Ver 1.0 (January 19, 2016) 21 | - first public release. 22 | 23 | Beta (July 1, 2013) 24 | - first version which was utilized in my dissertation work 25 | -------------------------------------------------------------------------------- /DEVELOP.md: -------------------------------------------------------------------------------- 1 | 2 | These are development notes for myself 3 | 4 | ## Documentation 5 | 6 | Documentation is generated with the following command: 7 | `pdoc promo -d google -o docs` 8 | 9 | A live version can be seen with 10 | `pdoc promo -d google` 11 | 12 | pdoc will read from promo, as installed on the computer, so you may need to run `pip install .` if you want to generate documentation from a locally edited version of promo. 13 | 14 | ## Tests 15 | 16 | Tests are run with 17 | 18 | `pytest --cov=promo tests/` 19 | 20 | ## Release 21 | 22 | Releases are built and deployed with: 23 | 24 | `python setup.py bdist_wheel sdist` 25 | 26 | `twine upload dist/*` 27 | 28 | Don't forget to tag the release. 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tim Mahrt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | ----------------------- 3 | ProMo (Prosody Morph) 4 | ----------------------- 5 | 6 | .. image:: https://travis-ci.org/timmahrt/ProMo.svg?branch=main 7 | :target: https://travis-ci.org/timmahrt/ProMo 8 | 9 | .. image:: https://coveralls.io/repos/github/timmahrt/ProMo/badge.svg?branch=main 10 | :target: https://coveralls.io/github/timmahrt/ProMo?branch=main 11 | 12 | .. image:: https://img.shields.io/badge/license-MIT-blue.svg? 13 | :target: http://opensource.org/licenses/MIT 14 | 15 | *Questions? Comments? Feedback? Chat with us on gitter!* 16 | 17 | .. image:: https://badges.gitter.im/pythonProMo/Lobby.svg? 18 | :alt: Join the chat at https://gitter.im/pythonProMo/Lobby 19 | :target: https://gitter.im/pythonProMo/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge 20 | 21 | ----- 22 | 23 | A library for manipulating pitch and duration in an algorithmic way, for 24 | resynthesizing speech. 25 | 26 | This library can be used to resynthesize pitch in natural speech using pitch 27 | contours taken from other speech samples, generated pitch contours, 28 | or through algorithmic manipulations of the source pitch contour. 29 | 30 | .. sectnum:: 31 | .. contents:: 32 | 33 | Documentation 34 | ============= 35 | 36 | Automatically generated pdocs can be found here: 37 | 38 | http://timmahrt.github.io/ProMo/ 39 | 40 | 41 | Common Use Cases 42 | ================ 43 | 44 | What can you do with this library? 45 | 46 | Apply the pitch or duration from one speech sample to another. 47 | 48 | - alignment happens both in time and in hertz 49 | 50 | - after the morph process, the source pitch points will be at the same 51 | absolute pitch and relative time as they are in the target file 52 | 53 | - time is relative to the start and stop time of the interval being 54 | considered (e.g. the pitch value at 80% of the duration of the interval). 55 | Relative time is used so that the source and target files don't have to 56 | be the same length. 57 | 58 | - temporal morphing is a minor effect if the sampling frequency is high 59 | but it can be significant when, for example, using a stylized pitch 60 | contour with few pitch samples. 61 | 62 | - modifications can be done between entire wav files or between 63 | corresponding intervals as specified in a textgrid or other annotation 64 | (indicating the boundaries of words, stressed vowels, etc.) 65 | 66 | - the larger the file, the less useful the results are likely to be 67 | without using a transcript of some sort 68 | 69 | - the transcripts do not have to match in lexical content, only in the 70 | number of intervals (same number of words or phones, etc.) 71 | 72 | - modifications can be scaled (it is possible to generate a wav file with 73 | a pitch contour that is 30% or 60% between the source and target contours). 74 | 75 | - can also morph the pitch range and average pitch independently. 76 | 77 | - resynthesis is performed by Praat. 78 | 79 | - pitch can be obtained from praat (such as by using praatio) 80 | or from other sources (e.g. ESPS getF0) 81 | 82 | - plots of the resynthesis (such as the ones below) can be generated 83 | 84 | 85 | Illustrative example 86 | ====================== 87 | 88 | Consider the phrase "Mary rolled the barrel". In the first recording 89 | (examples/mary1.wav), "Mary rolled the barrel" was said in response 90 | to a question such as "Did John roll the barrel?". On the other hand, 91 | in the second recording (examples/mary2.wav) the utterance was said 92 | in response to a question such as "What happened yesterday". 93 | 94 | "Mary" in "mary1.wav" is produced with more emphasis than in "mary2.wav". 95 | It is longer and carries a more drammatic pitch excursion. Using 96 | ProMo, we can make mary1.wav spoken similar to mary2.wav, even 97 | though they were spoken in a different way and by different speakers. 98 | 99 | Duration and pitch carry meaning. Change these, and you can change the 100 | meaning being conveyed. 101 | 102 | ``Note that modifying pitch and duration too much can introduce artifacts. 103 | Such artifacts can be heard even in pitch morphing mary1.wav to mary2.wav.`` 104 | 105 | Pitch morphing (examples/pitch_morph_example.py): 106 | 107 | The following image shows morphing of pitch from mary1.wav to mary2.wav 108 | on a word-by-word level 109 | in increments of 33% (33%, 66%, 100%). Note that the morph adjusts the 110 | temporal dimension of the target signal to fit the duration of the source 111 | signal (the source and generated contours are equally shorter 112 | than the target contour). This occurs at the level of the file unless 113 | the user specifies an equal number of segments to align in time 114 | (e.g. using word-level transcriptions, as done here, or phone-level 115 | transcriptions, etc.) 116 | 117 | .. image:: examples/files/mary1_mary2_f0_morph.png 118 | :width: 500px 119 | 120 | With the ability to morph pitch range and average pitch, it becomes easier 121 | to morph contours produced by different speakers: 122 | 123 | The following image shows four different pitch manipulations. On the 124 | **upper left** is the raw morph. Notice that final output (black line) is 125 | very close to the target. Differences stem from duration differences. 126 | 127 | However, the average pitch and pitch range are qualities of speech that 128 | can signify differences in gender in addition to other aspects of 129 | speaker identity. By resetting the average pitch and pitch range to 130 | that of the source, it is possible to morph the contour while maintaining 131 | aspects of the source speaker's identity. 132 | 133 | The image in the **upper right** contains a morph 134 | followed by a reset of the average pitch to the source speaker's average 135 | pitch. In the **bottom right** a morph followed by a reset of the speaker's 136 | pitch range. In the **bottom right** pitch range was reset and then the 137 | speaker's average pitch was reset. 138 | 139 | The longer the speech sample, the more representative the pitch range and 140 | mean pitch will be of the speaker. In this example both are skewed higher 141 | by the pitch accent on the first word. 142 | 143 | Here the average pitch of the source (a female speaker) is much higher 144 | than the target (a male speaker) and the resulting morph sounds like it 145 | comes from a different speaker than the source or target speakers. 146 | The three recordings that involve resetting pitch range and/or average 147 | pitch sound much more natural. 148 | 149 | .. image:: examples/files/mary1_mary2_f0_morph_compare.png 150 | :width: 500px 151 | 152 | Duration morphing (examples/duration_manipulation_example.py): 153 | 154 | The following image shows morphing of duration from mary1.wav to mary2.wav 155 | on a word-by-word basis in increments of 33% (33%, 66%, 100%). 156 | This process can operate over an entire file or, similar to pitch morphing, 157 | with annotated segments, as done in this example. 158 | 159 | .. image:: examples/files/mary1_mary2_dur_morph.png 160 | :width: 500px 161 | 162 | 163 | Tutorials 164 | ================ 165 | 166 | Tutorials for learning about prosody manipulation and how to use ProMo are available. 167 | 168 | `Tutorial 1.1: Intro to ProMo `_ 169 | 170 | `Tutorial 1.2: Pitch manipulation tutorial `_ 171 | 172 | 173 | Version History 174 | ================ 175 | 176 | *ProMo uses semantic versioning (Major.Minor.Patch)* 177 | 178 | Please view `CHANGELOG.md ` for version history. 179 | 180 | 181 | Requirements 182 | ============== 183 | 184 | ``Python 3.3.*`` or above (or below, probably) 185 | 186 | My praatIO library is required and can be downloaded 187 | `here `_ 188 | 189 | Matplotlib is required if you want to plot graphs. 190 | `Matplotlib website `_ 191 | 192 | Scipy is required if you want to use interpolation--typically if you have stylized 193 | pitch contours (in praat PitchTier format, for example) that you want to use in 194 | your morphing). 195 | `Scipy website `_ 196 | 197 | Matplotlib and SciPy are non-trivial to install, as they depends on several large 198 | packages. You can 199 | visit their websites for more information. **I recommend the following instructions to 200 | install matplotlib** which uses *python wheels*. These will install all required 201 | libraries in one fell swoop. 202 | 203 | On Mac, open a terminal and type: 204 | 205 | python -m pip install matplotlib 206 | 207 | python -m pip install scipy 208 | 209 | On Windows, open a cmd or powershell window and type: 210 | 211 | <> -m pip install matplotlib 212 | 213 | <> -m pip install scipy 214 | 215 | e.g. C:\\python27\\python.exe -m install matplotlib 216 | 217 | Otherwise, to manually install, after downloading the source from github, from a command-line shell, navigate to the directory containing setup.py and type:: 218 | 219 | python setup.py install 220 | 221 | If python is not in your path, you'll need to enter the full path e.g.:: 222 | 223 | C:\Python27\python.exe setup.py install 224 | 225 | If you are using ``Python 2.x`` or ``Python < 3.7``, you can use `Promo 1.x`. 226 | 227 | Usage 228 | ========= 229 | 230 | See /examples for example usages 231 | 232 | 233 | Installation 234 | ================ 235 | 236 | If you on Windows, you can use the installer found here (check that it is up to date though) 237 | `Windows installer `_ 238 | 239 | Promo is on pypi and can be installed or upgraded from the command-line shell with pip like so:: 240 | 241 | python -m pip install promo --upgrade 242 | 243 | Otherwise, to manually install, after downloading the source from github, from a command-line shell, navigate to the directory containing setup.py and type:: 244 | 245 | python setup.py install 246 | 247 | If python is not in your path, you'll need to enter the full path e.g.:: 248 | 249 | C:\Python36\python.exe setup.py install 250 | 251 | 252 | Citing ProMo 253 | =============== 254 | 255 | If you use ProMo in your research, please cite it like so: 256 | 257 | Tim Mahrt. ProMo: The Prosody-Morphing Library. 258 | https://github.com/timmahrt/ProMo, 2016. 259 | 260 | 261 | Acknowledgements 262 | ================ 263 | 264 | Development of ProMo was possible thanks to NSF grant **BCS 12-51343** to 265 | Jennifer Cole, José I. Hualde, and Caroline Smith and to the A*MIDEX project 266 | (n° **ANR-11-IDEX-0001-02**) to James Sneed German funded by the 267 | Investissements d'Avenir French Government program, 268 | managed by the French National Research Agency (ANR). 269 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/promo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | promo API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 41 |
42 |
43 |

44 | promo

45 | 46 | 47 | 48 | 49 | 50 |
51 |
52 | 234 | -------------------------------------------------------------------------------- /docs/promo/morph_utils.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | promo.morph_utils API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 49 |
50 |
51 |

52 | promo.morph_utils

53 | 54 | 55 | 56 | 57 | 58 |
59 |
60 | 242 | -------------------------------------------------------------------------------- /docs/promo/morph_utils/audio_scripts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | promo.morph_utils.audio_scripts API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 47 |
48 |
49 |

50 | promo.morph_utils.audio_scripts

51 | 52 |

Created on Aug 23, 2014

53 | 54 |

@author: tmahrt

55 |
56 | 57 | 58 | 59 | 60 | 61 |
 1"""
 62 |  2Created on Aug 23, 2014
 63 |  3
 64 |  4@author: tmahrt
 65 |  5"""
 66 |  6
 67 |  7import wave
 68 |  8
 69 |  9
 70 | 10def getSoundFileDuration(fn):
 71 | 11    """
 72 | 12    Returns the duration of a wav file (in seconds)
 73 | 13    """
 74 | 14    audiofile = wave.open(fn, "r")
 75 | 15
 76 | 16    params = audiofile.getparams()
 77 | 17    framerate = params[2]
 78 | 18    nframes = params[3]
 79 | 19
 80 | 20    duration = float(nframes) / framerate
 81 | 21    return duration
 82 | 
83 | 84 | 85 |
86 |
87 | 88 |
89 | 90 | def 91 | getSoundFileDuration(fn): 92 | 93 | 94 | 95 |
96 | 97 |
11def getSoundFileDuration(fn):
 98 | 12    """
 99 | 13    Returns the duration of a wav file (in seconds)
100 | 14    """
101 | 15    audiofile = wave.open(fn, "r")
102 | 16
103 | 17    params = audiofile.getparams()
104 | 18    framerate = params[2]
105 | 19    nframes = params[3]
106 | 20
107 | 21    duration = float(nframes) / framerate
108 | 22    return duration
109 | 
110 | 111 | 112 |

Returns the duration of a wav file (in seconds)

113 |
114 | 115 | 116 |
117 |
118 | 300 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmahrt/ProMo/c1d8252453c28592ce4c09dc1d690a4e3b8550cc/examples/__init__.py -------------------------------------------------------------------------------- /examples/duration_manipulation_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jan 18, 2016 3 | 4 | @author: Tim 5 | """ 6 | 7 | import os 8 | from os.path import join 9 | 10 | from promo import duration_morph 11 | from promo.morph_utils import utils 12 | 13 | # Define the arguments for the code 14 | root = os.path.abspath(join(".", "files")) 15 | # praatEXE = r"C:\Praat.exe" # Windows 16 | praatEXE = "/Applications/Praat.app/Contents/MacOS/Praat" # Mac 17 | 18 | minPitch = 50 19 | maxPitch = 350 20 | stepList = utils.generateStepList(3) 21 | 22 | fromName = "mary1" 23 | toName = "mary2" 24 | fromWavFN = join(root, fromName + ".wav") 25 | toWavFN = join(root, toName + ".wav") 26 | 27 | fromPitchFN = fromName + ".txt" 28 | toPitchFN = toName + ".txt" 29 | 30 | tierName = "words" 31 | 32 | fromTGFN = join(root, fromName + ".TextGrid") 33 | toTGFN = join(root, toName + ".TextGrid") 34 | 35 | outputPath = join(root, "duration_morph") 36 | utils.makeDir(outputPath) 37 | outputName = "%s_%s_dur_morph" % (fromName, toName) 38 | outputTG = join(outputPath, "%s.TextGrid" % outputName) 39 | outputImageFN = join(outputPath, "%s.png" % outputName) 40 | filterFunc = None 41 | includeUnlabeledRegions = False 42 | 43 | # Morph the duration from one file to another 44 | durationParams = duration_morph.getMorphParameters( 45 | fromTGFN, toTGFN, tierName, filterFunc, includeUnlabeledRegions 46 | ) 47 | morphedTG = duration_morph.textgridMorphDuration(fromTGFN, toTGFN) 48 | morphedTG.save(outputTG, format="short_textgrid", includeBlankSpaces=True) 49 | duration_morph.changeDuration( 50 | fromWavFN, 51 | durationParams, 52 | stepList, 53 | outputName, 54 | outputMinPitch=minPitch, 55 | outputMaxPitch=maxPitch, 56 | praatEXE=praatEXE, 57 | ) 58 | 59 | duration_morph.outputMorphPlot( 60 | fromTGFN, toTGFN, tierName, durationParams, stepList, outputImageFN 61 | ) 62 | 63 | # Increase duration of all segments by 20 percent 64 | twentyPercentMore = lambda x: (x * 1.20) 65 | outputName = "%s_20_percent_more" % fromName 66 | outputTG = join(outputPath, "%s.TextGrid" % outputName) 67 | outputImageFN = join(outputPath, "%s.png" % outputName) 68 | filterFunc = None 69 | includeUnlabeledRegions = True 70 | durationParams = duration_morph.getManipulatedParamaters( 71 | fromTGFN, tierName, twentyPercentMore, filterFunc, includeUnlabeledRegions 72 | ) 73 | 74 | duration_morph.changeDuration( 75 | fromWavFN, 76 | durationParams, 77 | stepList, 78 | outputName, 79 | outputMinPitch=minPitch, 80 | outputMaxPitch=maxPitch, 81 | praatEXE=praatEXE, 82 | ) 83 | -------------------------------------------------------------------------------- /examples/files/carrots1.TextGrid: -------------------------------------------------------------------------------- 1 | File type = "ooTextFile" 2 | Object class = "TextGrid" 3 | 4 | 0 5 | 1.8589342403628117 6 | 7 | 2 8 | "IntervalTier" 9 | "utterances" 10 | 0 11 | 1.8589342403628117 12 | 3 13 | 0 14 | 0.10066011955624378 15 | "" 16 | 0.10066011955624378 17 | 1.7829654912323427 18 | "carrots" 19 | 1.7829654912323427 20 | 1.8589342403628117 21 | "" 22 | "IntervalTier" 23 | "words" 24 | 0 25 | 1.8589342403628117 26 | 9 27 | 0 28 | 0.10066011955624378 29 | "" 30 | 0.10066011955624378 31 | 0.5911953453482961 32 | "carrots" 33 | 0.5911953453482961 34 | 0.6965451924982671 35 | "are" 36 | 0.6965451924982671 37 | 0.7278209283709147 38 | "a" 39 | 0.7278209283709147 40 | 0.8479855977763502 41 | "good" 42 | 0.8479855977763502 43 | 1.1113602156512776 44 | "source" 45 | 1.1113602156512776 46 | 1.2150639714395304 47 | "of" 48 | 1.2150639714395304 49 | 1.7829654912323427 50 | "vitamins" 51 | 1.7829654912323427 52 | 1.8589342403628117 53 | "" 54 | -------------------------------------------------------------------------------- /examples/files/carrots1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmahrt/ProMo/c1d8252453c28592ce4c09dc1d690a4e3b8550cc/examples/files/carrots1.wav -------------------------------------------------------------------------------- /examples/files/carrots2.TextGrid: -------------------------------------------------------------------------------- 1 | File type = "ooTextFile" 2 | Object class = "TextGrid" 3 | 4 | 0 5 | 1.6989115646258504 6 | 7 | 2 8 | "IntervalTier" 9 | "utterances" 10 | 0 11 | 1.6989115646258504 12 | 3 13 | 0 14 | 0.11756963669278822 15 | "" 16 | 0.11756963669278822 17 | 1.599394618061411 18 | "utterance" 19 | 1.599394618061411 20 | 1.6989115646258504 21 | "" 22 | "IntervalTier" 23 | "words" 24 | 0 25 | 1.6989115646258504 26 | 9 27 | 0 28 | 0.11756963669278822 29 | "" 30 | 0.11756963669278822 31 | 0.42596975971874523 32 | "carrots" 33 | 0.42596975971874523 34 | 0.5192419920485469 35 | "are" 36 | 0.5192419920485469 37 | 0.5433122455530118 38 | "a" 39 | 0.5433122455530118 40 | 0.651628386323104 41 | "good" 42 | 0.651628386323104 43 | 0.883304576303579 44 | "source" 45 | 0.883304576303579 46 | 1.0186997522661942 47 | "of" 48 | 1.0186997522661942 49 | 1.599394618061411 50 | "vitamins" 51 | 1.599394618061411 52 | 1.6989115646258504 53 | "" 54 | -------------------------------------------------------------------------------- /examples/files/carrots2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmahrt/ProMo/c1d8252453c28592ce4c09dc1d690a4e3b8550cc/examples/files/carrots2.wav -------------------------------------------------------------------------------- /examples/files/mary1.TextGrid: -------------------------------------------------------------------------------- 1 | File type = "ooTextFile" 2 | Object class = "TextGrid" 3 | 4 | 0 5 | 1.14997732426 6 | 7 | 4 8 | "IntervalTier" 9 | "utterances" 10 | 0 11 | 1.149977 12 | 1 13 | 0 14 | 1.14997732426 15 | "mary rolled the barrel" 16 | "IntervalTier" 17 | "phones" 18 | 0 19 | 1.149977 20 | 18 21 | 0 22 | 0.1372039584151871 23 | "m" 24 | 0.1372039584151871 25 | 0.285 26 | "E" 27 | 0.285 28 | 0.35692440239815754 29 | "r" 30 | 0.35692440239815754 31 | 0.4032975614297392 32 | "i:" 33 | 0.4032975614297392 34 | 0.505 35 | "r" 36 | 0.505 37 | 0.585 38 | "@U" 39 | 0.585 40 | 0.615 41 | "l" 42 | 0.615 43 | 0.6340592337535624 44 | "d" 45 | 0.6340592337535624 46 | 0.6804323927851441 47 | "" 48 | 0.6804323927851441 49 | 0.7190766919781289 50 | "D" 51 | 0.7190766919781289 52 | 0.7345344116553227 53 | "@" 54 | 0.7345344116553227 55 | 0.7864281848573308 56 | "" 57 | 0.7864281848573308 58 | 0.805 59 | "b" 60 | 0.805 61 | 0.895 62 | "{" 63 | 0.895 64 | 0.985 65 | "r" 66 | 0.985 67 | 1.0635630162127356 68 | "@" 69 | 1.0635630162127356 70 | 1.1499 71 | "l" 72 | 1.1499 73 | 1.14997732426 74 | "" 75 | "IntervalTier" 76 | "words" 77 | 0 78 | 1.149977 79 | 7 80 | 0 81 | 0.4032975614297392 82 | "mary" 83 | 0.4032975614297392 84 | 0.6340592337535624 85 | "rolled" 86 | 0.6340592337535624 87 | 0.6804323927851441 88 | "" 89 | 0.6804323927851441 90 | 0.7345344116553227 91 | "the" 92 | 0.7345344116553227 93 | 0.7864281848573308 94 | "" 95 | 0.7864281848573308 96 | 1.1499 97 | "barrel" 98 | 1.1499 99 | 1.14997732426 100 | "" 101 | "IntervalTier" 102 | "Information" 103 | 0 104 | 1.149977 105 | 2 106 | 0 107 | 0.57498866213 108 | "This annotation was produced by SPPAS 1.7.5 on 2016-01-08" 109 | 0.57498866213 110 | 1.14997732426 111 | "Copyright (C) 2011-2015 Brigitte Bigi" 112 | -------------------------------------------------------------------------------- /examples/files/mary1.txt: -------------------------------------------------------------------------------- 1 | 0.01,--undefined--,--undefined-- 2 | 0.02,--undefined--,--undefined-- 3 | 0.03,--undefined--,--undefined-- 4 | 0.04,--undefined--,--undefined-- 5 | 0.05,--undefined--,--undefined-- 6 | 0.06,170.986,--undefined-- 7 | 0.07,173.067,56.037 8 | 0.08,176.342,57.811 9 | 0.09,180.614,59.401 10 | 0.10,185.649,61.168 11 | 0.11,191.170,63.542 12 | 0.12,201.529,66.193 13 | 0.13,203.294,68.424 14 | 0.14,204.991,69.972 15 | 0.15,208.985,70.945 16 | 0.16,213.659,71.565 17 | 0.17,216.392,72.041 18 | 0.18,221.247,72.518 19 | 0.19,226.599,73.022 20 | 0.20,230.384,73.443 21 | 0.21,234.289,73.657 22 | 0.22,237.601,73.624 23 | 0.23,239.740,73.378 24 | 0.24,241.624,72.992 25 | 0.25,243.502,72.563 26 | 0.26,244.468,72.157 27 | 0.27,244.515,71.797 28 | 0.28,243.953,71.508 29 | 0.29,242.808,71.322 30 | 0.30,240.727,71.246 31 | 0.31,237.365,71.246 32 | 0.32,233.299,71.247 33 | 0.33,228.504,71.174 34 | 0.34,222.956,71.023 35 | 0.35,216.147,70.838 36 | 0.36,210.496,70.578 37 | 0.37,204.430,70.066 38 | 0.38,198.331,69.111 39 | 0.39,188.476,67.624 40 | 0.40,181.630,65.646 41 | 0.41,175.039,63.381 42 | 0.42,166.024,61.259 43 | 0.43,158.138,59.899 44 | 0.44,153.066,59.655 45 | 0.45,150.864,60.299 46 | 0.46,149.748,61.380 47 | 0.47,149.111,62.526 48 | 0.48,148.334,63.509 49 | 0.49,147.703,64.264 50 | 0.50,147.228,64.830 51 | 0.51,146.104,65.216 52 | 0.52,144.290,65.364 53 | 0.53,142.425,65.223 54 | 0.54,140.676,64.794 55 | 0.55,138.768,64.118 56 | 0.56,137.086,63.298 57 | 0.57,134.911,62.493 58 | 0.58,132.870,61.803 59 | 0.59,131.776,61.102 60 | 0.60,131.357,60.048 61 | 0.61,131.705,58.232 62 | 0.62,131.954,55.325 63 | 0.63,--undefined--,51.243 64 | 0.64,--undefined--,46.762 65 | 0.65,--undefined--,44.052 66 | 0.66,--undefined--,44.173 67 | 0.67,--undefined--,47.026 68 | 0.68,--undefined--,50.744 69 | 0.69,--undefined--,53.410 70 | 0.70,135.412,54.510 71 | 0.71,133.476,54.010 72 | 0.72,128.999,51.987 73 | 0.73,--undefined--,48.678 74 | 0.74,--undefined--,44.682 75 | 0.75,--undefined--,41.127 76 | 0.76,--undefined--,41.715 77 | 0.77,--undefined--,46.866 78 | 0.78,--undefined--,51.492 79 | 0.79,--undefined--,54.932 80 | 0.80,--undefined--,57.438 81 | 0.81,138.202,59.092 82 | 0.82,138.564,59.946 83 | 0.83,138.709,60.232 84 | 0.84,131.130,60.311 85 | 0.85,130.596,60.353 86 | 0.86,129.043,60.290 87 | 0.87,127.297,60.082 88 | 0.88,122.797,59.816 89 | 0.89,121.618,59.505 90 | 0.90,--undefined--,58.945 91 | 0.91,60.264,57.944 92 | 0.92,60.070,56.593 93 | 0.93,59.692,55.250 94 | 0.94,59.028,54.447 95 | 0.95,--undefined--,54.617 96 | 0.96,--undefined--,55.270 97 | 0.97,--undefined--,55.616 98 | 0.98,--undefined--,55.544 99 | 0.99,--undefined--,55.430 100 | 1.00,75.843,55.355 101 | 1.01,71.184,54.981 102 | 1.02,71.229,54.173 103 | 1.03,61.328,53.208 104 | 1.04,71.540,52.355 105 | 1.05,71.481,51.490 106 | 1.06,81.928,50.375 107 | 1.07,--undefined--,48.864 108 | 1.08,--undefined--,46.912 109 | 1.09,--undefined--,--undefined-- 110 | 1.10,--undefined--,--undefined-- 111 | 1.11,--undefined--,--undefined-- 112 | 1.12,--undefined--,--undefined-- 113 | 1.13,--undefined--,--undefined-- 114 | 1.14,--undefined--,--undefined-- 115 | -------------------------------------------------------------------------------- /examples/files/mary1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmahrt/ProMo/c1d8252453c28592ce4c09dc1d690a4e3b8550cc/examples/files/mary1.wav -------------------------------------------------------------------------------- /examples/files/mary1_double_length.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmahrt/ProMo/c1d8252453c28592ce4c09dc1d690a4e3b8550cc/examples/files/mary1_double_length.wav -------------------------------------------------------------------------------- /examples/files/mary1_mary2_dur_morph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmahrt/ProMo/c1d8252453c28592ce4c09dc1d690a4e3b8550cc/examples/files/mary1_mary2_dur_morph.png -------------------------------------------------------------------------------- /examples/files/mary1_mary2_f0_morph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmahrt/ProMo/c1d8252453c28592ce4c09dc1d690a4e3b8550cc/examples/files/mary1_mary2_f0_morph.png -------------------------------------------------------------------------------- /examples/files/mary1_mary2_f0_morph_compare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmahrt/ProMo/c1d8252453c28592ce4c09dc1d690a4e3b8550cc/examples/files/mary1_mary2_f0_morph_compare.png -------------------------------------------------------------------------------- /examples/files/mary1_stylized.PitchTier: -------------------------------------------------------------------------------- 1 | File type = "ooTextFile" 2 | Object class = "PitchTier" 3 | 4 | 0 5 | 1.15 6 | 5 7 | 0.056680468728010795 8 | 246.1883065964818 9 | 0.29475247524752474 10 | 236.306587141354 11 | 0.6444572809305424 12 | 67.74244086921922 13 | 0.7358388402911984 14 | 67.4955754895304 15 | 1.0168460934641401 16 | 142.23123722561377 17 | -------------------------------------------------------------------------------- /examples/files/mary2.TextGrid: -------------------------------------------------------------------------------- 1 | File type = "ooTextFile" 2 | Object class = "TextGrid" 3 | 4 | 0 5 | 1.2759375 6 | 7 | 4 8 | "IntervalTier" 9 | "utterances" 10 | 0 11 | 1.275937 12 | 1 13 | 0 14 | 1.2759375 15 | "mary rolled the barrel" 16 | "IntervalTier" 17 | "phones" 18 | 0 19 | 1.2759 20 | 15 21 | 0 22 | 0.09220432329519228 23 | "m" 24 | 0.09220432329519228 25 | 0.20245976303353727 26 | "E" 27 | 0.20245976303353727 28 | 0.27596338952576727 29 | "r" 30 | 0.27596338952576727 31 | 0.36416774131644325 32 | "i:" 33 | 0.36416774131644325 34 | 0.4719730601717139 35 | "r" 36 | 0.4719730601717139 37 | 0.545 38 | "@U" 39 | 0.545 40 | 0.585 41 | "l" 42 | 0.585 43 | 0.625 44 | "d" 45 | 0.625 46 | 0.705 47 | "D" 48 | 0.705 49 | 0.7427114177514277 50 | "@" 51 | 0.7427114177514277 52 | 0.825 53 | "b" 54 | 0.825 55 | 0.895 56 | "{" 57 | 0.895 58 | 0.9766979620850265 59 | "r" 60 | 0.9766979620850265 61 | 1.0489765281357193 62 | "@" 63 | 1.0489765281357193 64 | 1.2759 65 | "l" 66 | "IntervalTier" 67 | "words" 68 | 0 69 | 1.2759 70 | 4 71 | 0 72 | 0.36416774131644325 73 | "mary" 74 | 0.36416774131644325 75 | 0.625 76 | "rolled" 77 | 0.625 78 | 0.7427114177514277 79 | "the" 80 | 0.7427114177514277 81 | 1.2759 82 | "barrel" 83 | "IntervalTier" 84 | "Information" 85 | 0 86 | 1.275937 87 | 2 88 | 0 89 | 0.63796875 90 | "This annotation was produced by SPPAS 1.7.5 on 2016-01-08" 91 | 0.63796875 92 | 1.2759375 93 | "Copyright (C) 2011-2015 Brigitte Bigi" 94 | -------------------------------------------------------------------------------- /examples/files/mary2.txt: -------------------------------------------------------------------------------- 1 | 0.01,--undefined--,--undefined-- 2 | 0.02,--undefined--,--undefined-- 3 | 0.03,--undefined--,--undefined-- 4 | 0.04,--undefined--,--undefined-- 5 | 0.05,--undefined--,--undefined-- 6 | 0.06,--undefined--,--undefined-- 7 | 0.07,105.168,69.581 8 | 0.08,105.994,70.602 9 | 0.09,105.467,71.091 10 | 0.10,100.507,71.052 11 | 0.11,99.803,70.667 12 | 0.12,99.073,70.253 13 | 0.13,98.807,70.071 14 | 0.14,98.789,70.150 15 | 0.15,98.814,70.379 16 | 0.16,99.243,70.687 17 | 0.17,100.012,71.075 18 | 0.18,100.905,71.559 19 | 0.19,103.630,72.095 20 | 0.20,105.387,72.578 21 | 0.21,106.820,72.905 22 | 0.22,108.396,73.068 23 | 0.23,110.889,73.162 24 | 0.24,113.217,73.308 25 | 0.25,115.133,73.565 26 | 0.26,116.976,73.909 27 | 0.27,118.502,74.267 28 | 0.28,119.120,74.567 29 | 0.29,119.318,74.781 30 | 0.30,119.560,74.915 31 | 0.31,119.565,74.980 32 | 0.32,119.328,74.964 33 | 0.33,118.746,74.843 34 | 0.34,117.578,74.602 35 | 0.35,116.471,74.246 36 | 0.36,115.342,73.788 37 | 0.37,113.924,73.227 38 | 0.38,111.872,72.542 39 | 0.39,109.104,71.738 40 | 0.40,105.887,70.924 41 | 0.41,102.953,70.290 42 | 0.42,100.314,69.945 43 | 0.43,97.894,69.803 44 | 0.44,95.623,69.711 45 | 0.45,93.360,69.644 46 | 0.46,92.277,69.609 47 | 0.47,91.396,69.503 48 | 0.48,89.573,69.184 49 | 0.49,88.631,68.681 50 | 0.50,88.321,68.195 51 | 0.51,87.731,67.878 52 | 0.52,87.318,67.678 53 | 0.53,87.000,67.472 54 | 0.54,87.088,67.215 55 | 0.55,87.531,66.909 56 | 0.56,87.835,66.510 57 | 0.57,88.094,65.912 58 | 0.58,88.259,64.893 59 | 0.59,87.764,63.143 60 | 0.60,87.139,60.443 61 | 0.61,87.562,57.010 62 | 0.62,91.236,53.958 63 | 0.63,92.169,52.410 64 | 0.64,92.890,51.843 65 | 0.65,93.391,51.731 66 | 0.66,93.813,52.043 67 | 0.67,94.831,52.951 68 | 0.68,95.893,55.156 69 | 0.69,94.627,59.037 70 | 0.70,98.024,62.958 71 | 0.71,98.763,65.713 72 | 0.72,98.151,67.030 73 | 0.73,97.713,66.924 74 | 0.74,96.395,65.499 75 | 0.75,92.721,63.022 76 | 0.76,91.602,60.175 77 | 0.77,90.466,58.115 78 | 0.78,90.036,57.585 79 | 0.79,92.319,59.484 80 | 0.80,100.911,63.419 81 | 0.81,102.462,67.109 82 | 0.82,103.555,69.703 83 | 0.83,103.852,71.259 84 | 0.84,103.563,72.059 85 | 0.85,103.528,72.391 86 | 0.86,104.482,72.466 87 | 0.87,105.166,72.407 88 | 0.88,105.139,72.283 89 | 0.89,105.088,72.122 90 | 0.90,104.781,71.905 91 | 0.91,104.082,71.612 92 | 0.92,103.095,71.272 93 | 0.93,101.440,70.914 94 | 0.94,99.139,70.499 95 | 0.95,96.181,69.983 96 | 0.96,94.019,69.405 97 | 0.97,92.070,68.862 98 | 0.98,90.347,68.414 99 | 0.99,89.321,68.045 100 | 1.00,88.491,67.711 101 | 1.01,88.000,67.385 102 | 1.02,84.705,67.028 103 | 1.03,83.113,66.592 104 | 1.04,81.469,66.109 105 | 1.05,81.320,65.624 106 | 1.06,74.971,65.010 107 | 1.07,67.664,64.133 108 | 1.08,68.579,63.185 109 | 1.09,69.225,62.617 110 | 1.10,--undefined--,62.606 111 | 1.11,--undefined--,62.854 112 | 1.12,--undefined--,62.925 113 | 1.13,--undefined--,62.800 114 | 1.14,82.945,62.844 115 | 1.15,81.734,63.154 116 | 1.16,88.694,63.423 117 | 1.17,94.396,63.422 118 | 1.18,92.992,63.095 119 | 1.19,92.357,62.380 120 | 1.20,92.525,61.127 121 | 1.21,85.585,59.156 122 | 1.22,85.571,--undefined-- 123 | 1.23,--undefined--,--undefined-- 124 | 1.24,--undefined--,--undefined-- 125 | 1.25,--undefined--,--undefined-- 126 | 1.26,--undefined--,--undefined-- 127 | 1.27,--undefined--,--undefined-- 128 | -------------------------------------------------------------------------------- /examples/files/mary2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmahrt/ProMo/c1d8252453c28592ce4c09dc1d690a4e3b8550cc/examples/files/mary2.wav -------------------------------------------------------------------------------- /examples/files/modify_pitch_accent/mary1_accented.pitch: -------------------------------------------------------------------------------- 1 | File type = "ooTextFile" 2 | Object class = "PitchTier" 3 | 4 | 0 5 | 1.15 6 | 66 7 | 0.03 8 | 169.26350933562304 9 | 0.04000000000000001 10 | 175.06096798269488 11 | 0.05 12 | 180.53079796661905 13 | 0.06 14 | 188.25739596057423 15 | 0.07 16 | 197.53691311756558 17 | 0.08 18 | 207.10006884871126 19 | 0.09 20 | 223.64738903823564 21 | 0.1 22 | 229.8010298362463 23 | 0.11000000000000001 24 | 231.40982881787465 25 | 0.12 26 | 238.6083838193913 27 | 0.13 28 | 248.1358399056958 29 | 0.14 30 | 253.18214362638437 31 | 0.15 32 | 261.4748348468869 33 | 0.16 34 | 270.96728418331526 35 | 0.17 36 | 278.36736292449467 37 | 0.18 38 | 286.1290781125609 39 | 0.19 40 | 292.54527051625945 41 | 0.2 42 | 296.133188492755 43 | 0.21 44 | 299.570472680903 45 | 0.22 46 | 303.01826331268296 47 | 0.23 48 | 304.7328929784146 49 | 0.28 50 | 304.7328929784146 51 | 0.29000000000000004 52 | 304.39722652065234 53 | 0.30000000000000004 54 | 303.45444221438146 55 | 0.30999999999999994 56 | 301.4291453601235 57 | 0.31999999999999995 58 | 297.76365238511437 59 | 0.32999999999999996 60 | 291.6494807070254 61 | 0.33999999999999997 62 | 284.5106433927805 63 | 0.35 64 | 275.5188899307223 65 | 0.36 66 | 265.0122689068457 67 | 0.37 68 | 253.0537574400704 69 | 0.38 70 | 242.76258796205013 71 | 0.39 72 | 230.76298417942525 73 | 0.4 74 | 217.54105606172152 75 | 0.41000000000000003 76 | 200.86578483368507 77 | 0.42000000000000004 78 | 188.87778127167607 79 | 0.48 80 | 148.48339283291168 81 | 0.49 82 | 147.62849987067128 83 | 0.5 84 | 147.44126693321846 85 | 0.51 86 | 146.299388586239 87 | 0.52 88 | 144.30957581969855 89 | 0.53 90 | 142.26295165048498 91 | 0.54 92 | 140.70056454801684 93 | 0.55 94 | 138.29956903954752 95 | 0.56 96 | 137.06859378073722 97 | 0.5700000000000001 98 | 134.75766046429203 99 | 0.58 100 | 132.59293694710988 101 | 0.59 102 | 131.70283167599132 103 | 0.6 104 | 131.03308082671123 105 | 0.61 106 | 132.49628448641104 107 | 0.7000000000000001 108 | 136.06721859037773 109 | 0.71 110 | 132.1814038308358 111 | 0.72 112 | 125.6790313807359 113 | 0.81 114 | 138.1515824219021 115 | 0.8200000000000001 116 | 138.37795517656892 117 | 0.85 118 | 131.0388028478471 119 | 0.86 120 | 128.96345766138313 121 | 0.87 122 | 127.01905473434924 123 | 0.88 124 | 122.61241851069539 125 | 0.89 126 | 121.36410143817102 127 | 0.9 128 | 121.61151844972076 129 | 0.91 130 | 120.33926554766303 131 | 0.99 132 | 101.38254446154208 133 | 1.0 134 | 75.83188227490534 135 | 1.05 136 | 81.41340806441394 137 | 1.06 138 | 82.11906328949101 139 | -------------------------------------------------------------------------------- /examples/files/modify_pitch_accent/mary1_accented.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmahrt/ProMo/c1d8252453c28592ce4c09dc1d690a4e3b8550cc/examples/files/modify_pitch_accent/mary1_accented.wav -------------------------------------------------------------------------------- /examples/files/oranges.TextGrid: -------------------------------------------------------------------------------- 1 | File type = "ooTextFile" 2 | Object class = "TextGrid" 3 | 4 | 0 5 | 2.065011337868481 6 | 7 | 2 8 | "IntervalTier" 9 | "utterances" 10 | 0 11 | 2.065011337868481 12 | 3 13 | 0 14 | 0.09719047916329368 15 | "" 16 | 0.09719047916329368 17 | 1.481420388560374 18 | "utterance" 19 | 1.481420388560374 20 | 2.065011337868481 21 | "" 22 | "IntervalTier" 23 | "words" 24 | 0 25 | 2.065011337868481 26 | 9 27 | 0 28 | 0.09719047916329368 29 | "" 30 | 0.09719047916329368 31 | 0.5159337410548808 32 | "she" 33 | 0.5159337410548808 34 | 0.7902197204598506 35 | "likes" 36 | 0.7902197204598506 37 | 0.8487340627329107 38 | "to" 39 | 0.8487340627329107 40 | 1.0242770895520912 41 | "eat" 42 | 1.0242770895520912 43 | 1.481420388560374 44 | "oranges" 45 | 1.481420388560374 46 | 1.6002776463025277 47 | "he" 48 | 1.6002776463025277 49 | 1.9440494071567567 50 | "said" 51 | 1.9440494071567567 52 | 2.065011337868481 53 | "" 54 | -------------------------------------------------------------------------------- /examples/files/oranges.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmahrt/ProMo/c1d8252453c28592ce4c09dc1d690a4e3b8550cc/examples/files/oranges.wav -------------------------------------------------------------------------------- /examples/files/pickles.TextGrid: -------------------------------------------------------------------------------- 1 | File type = "ooTextFile" 2 | Object class = "TextGrid" 3 | 4 | 0 5 | 1.3504535147392291 6 | 7 | 2 8 | "IntervalTier" 9 | "utterances" 10 | 0 11 | 1.3504535147392291 12 | 3 13 | 0 14 | 0.046817941877007156 15 | "" 16 | 0.046817941877007156 17 | 1.24264822825685 18 | "utterance" 19 | 1.24264822825685 20 | 1.3504535147392291 21 | "" 22 | "IntervalTier" 23 | "words" 24 | 0 25 | 1.3504535147392291 26 | 7 27 | 0 28 | 0.046817941877007156 29 | "" 30 | 0.046817941877007156 31 | 0.16879263108775114 32 | "he" 33 | 0.16879263108775114 34 | 0.4414419363823553 35 | "likes" 36 | 0.4414419363823553 37 | 0.4868834872647893 38 | "to" 39 | 0.4868834872647893 40 | 0.6650621999353858 41 | "eat" 42 | 0.6650621999353858 43 | 1.24264822825685 44 | "pickles" 45 | 1.24264822825685 46 | 1.3504535147392291 47 | "" 48 | -------------------------------------------------------------------------------- /examples/files/pickles.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmahrt/ProMo/c1d8252453c28592ce4c09dc1d690a4e3b8550cc/examples/files/pickles.wav -------------------------------------------------------------------------------- /examples/modify_pitch_accent_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jan 26, 2017 3 | 4 | This file contains two examples demonstrating the use 5 | of the module modify_pitch_accent 6 | 7 | @author: Tim 8 | """ 9 | 10 | import os 11 | from os.path import join 12 | 13 | from praatio import textgrid 14 | from praatio import data_points 15 | from praatio import audio 16 | from praatio import pitch_and_intensity 17 | from praatio import praat_scripts 18 | from praatio import praatio_scripts 19 | from praatio.utilities import constants 20 | 21 | from promo.morph_utils import modify_pitch_accent 22 | 23 | 24 | def toStr(inputNum): 25 | if inputNum < 0: 26 | retStr = "%02d" % inputNum 27 | else: 28 | retStr = "+%02d" % inputNum 29 | 30 | return retStr 31 | 32 | 33 | root = os.path.abspath(join(".", "files")) 34 | rootOutputPath = join(root, "modify_pitch_accent") 35 | praatEXE = r"C:\Praat.exe" # Windows path 36 | 37 | 38 | #################################### 39 | # 1st example - a simple example 40 | # Create more emphasis on the subject 41 | #################################### 42 | 43 | tgFN = "mary1.TextGrid" 44 | wavFN = "mary1.wav" 45 | pitchIntensityFN = "mary1.txt" 46 | originalPitchFN = "mary1.pitch" 47 | outputWavFN = "mary1_accented.wav" 48 | outputPitchFN = "mary1_accented.pitch" 49 | minPitch = 75 50 | maxPitch = 450 51 | 52 | if not os.path.exists(rootOutputPath): 53 | os.mkdir(rootOutputPath) 54 | 55 | # 1st - get pitch 56 | piList = pitch_and_intensity.extractPI( 57 | join(root, wavFN), 58 | join(rootOutputPath, pitchIntensityFN), 59 | praatEXE, 60 | minPitch, 61 | maxPitch, 62 | ) 63 | pitchList = [(timeV, pitchV) for timeV, pitchV, _ in piList] 64 | 65 | dur = audio.WavQueryObj(join(root, wavFN)).getDuration() 66 | pointObj = data_points.PointObject2D(pitchList, constants.DataPointTypes.PITCH, 0, dur) 67 | pointObj.save(join(rootOutputPath, originalPitchFN)) 68 | 69 | 70 | # 2nd - get region to manipulate. Let's make the subject more emphatic! 71 | tg = textgrid.openTextgrid(join(root, "mary1.TextGrid"), includeEmptyIntervals=False) 72 | tier = tg.getTier("words") 73 | start, stop, _ = tier.entries[0] # Getting info for the first word 74 | 75 | targetPitchList = [ 76 | (timeV, pitchV) for timeV, pitchV in pitchList if timeV >= start and timeV <= stop 77 | ] 78 | 79 | # 3rd - make manipulation 80 | accent = modify_pitch_accent.PitchAccent(targetPitchList) 81 | accent.addPlateau(0.05) # Peak is dragged out for 0.05 seconds 82 | accent.adjustPeakHeight(60) # Plateau is raised by 60 hz 83 | accent.shiftAccent(-0.03) # Recenter the plateau around the original peak 84 | 85 | # 4th - integrate results 86 | moddedPitchList = accent.reintegrate(pitchList) 87 | 88 | # 5th - resynthesize 89 | praat_scripts.resynthesizePitch( 90 | praatEXE, 91 | join(root, wavFN), 92 | join(rootOutputPath, outputPitchFN), 93 | join(rootOutputPath, outputWavFN), 94 | minPitch, 95 | maxPitch, 96 | pointList=moddedPitchList, 97 | ) 98 | 99 | 100 | #################################### 101 | # 2nd example - a more complicated example 102 | # Incrementally create more (or less) emphasis on the subject 103 | # with a 3 by 3 by 3 matrix of different changes 104 | #################################### 105 | 106 | # 1st and 2nd -- already done above 107 | 108 | # 3rd - make manipulation 109 | 110 | outputFN = join(rootOutputPath, os.path.splitext(outputWavFN)[0] + "_s%s_h%s_p%s") 111 | 112 | for plateauAmount in [0.0, 0.04, 0.08]: 113 | for heightAmount in [0, 40, 80]: 114 | for shiftAmount in [0.0, 0.04, 0.08]: 115 | accent = modify_pitch_accent.PitchAccent(targetPitchList) 116 | accent.addPlateau(plateauAmount) 117 | accent.adjustPeakHeight(heightAmount) 118 | accent.shiftAccent(shiftAmount) 119 | 120 | # 4th - integrate results 121 | moddedPitchList = accent.reintegrate(pitchList) 122 | 123 | # 5th - resynthesize the wav file 124 | outputName = outputFN % ( 125 | toStr(shiftAmount * 1000), 126 | toStr(heightAmount), 127 | toStr(plateauAmount * 1000), 128 | ) 129 | wavOutputFN = outputName + ".wav" 130 | pitchOutputFN = outputName + ".pitch" 131 | praat_scripts.resynthesizePitch( 132 | praatEXE, 133 | join(root, wavFN), 134 | pitchOutputFN, 135 | wavOutputFN, 136 | minPitch, 137 | maxPitch, 138 | pointList=moddedPitchList, 139 | ) 140 | -------------------------------------------------------------------------------- /examples/pitch_morph_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | 4 | An example of morphing the pitch in one file to that in another 5 | """ 6 | import os 7 | from os.path import join 8 | 9 | from praatio import pitch_and_intensity 10 | 11 | from promo import f0_morph 12 | from promo.morph_utils import utils 13 | 14 | # Define the arguments for the code 15 | root = os.path.abspath(join(".", "files")) 16 | praatEXE = r"C:\Praat.exe" # Windows path 17 | # praatEXE = "/Applications/Praat.app/Contents/MacOS/Praat" # Mac path 18 | 19 | minPitch = 50 20 | maxPitch = 350 21 | stepList = utils.generateStepList(3) 22 | 23 | fromName = "mary1" 24 | toName = "mary2" 25 | fromWavFN = fromName + ".wav" 26 | toWavFN = toName + ".wav" 27 | 28 | fromPitchFN = fromName + ".txt" 29 | toPitchFN = toName + ".txt" 30 | 31 | fromTGFN = join(root, os.path.splitext(fromWavFN)[0] + ".TextGrid") 32 | toTGFN = join(root, os.path.splitext(toWavFN)[0] + ".TextGrid") 33 | 34 | # Prepare the data for morphing 35 | # 1ST load it into memory 36 | fromPitch = pitch_and_intensity.extractPI( 37 | join(root, fromWavFN), 38 | join(root, fromPitchFN), 39 | praatEXE, 40 | minPitch, 41 | maxPitch, 42 | forceRegenerate=False, 43 | ) 44 | toPitch = pitch_and_intensity.extractPI( 45 | join(root, toWavFN), 46 | join(root, toPitchFN), 47 | praatEXE, 48 | minPitch, 49 | maxPitch, 50 | forceRegenerate=False, 51 | ) 52 | 53 | # 2ND remove intensity values 54 | fromPitch = [(time, pitch) for time, pitch, _ in fromPitch] 55 | toPitch = [(time, pitch) for time, pitch, _ in toPitch] 56 | 57 | # 3RD select which sections to align. 58 | # We'll use textgrids for this purpose. 59 | tierName = "words" 60 | fromPitch = f0_morph.getPitchForIntervals(fromPitch, fromTGFN, tierName) 61 | toPitch = f0_morph.getPitchForIntervals(toPitch, toTGFN, tierName) 62 | 63 | pitchTier = pitch_and_intensity.extractPitchTier( 64 | join(root, fromWavFN), 65 | join(root, "mary1.PitchTier"), 66 | praatEXE, 67 | minPitch, 68 | maxPitch, 69 | forceRegenerate=True, 70 | ) 71 | # FINALLY: Run the morph process 72 | f0_morph.f0Morph( 73 | fromWavFN=join(root, fromWavFN), 74 | pitchPath=root, 75 | stepList=stepList, 76 | outputName="%s_%s_f0_morph" % (fromName, toName), 77 | doPlotPitchSteps=True, 78 | fromPitchData=fromPitch, 79 | toPitchData=toPitch, 80 | outputMinPitch=minPitch, 81 | outputMaxPitch=maxPitch, 82 | praatEXE=praatEXE, 83 | sourcePitchDataList=pitchTier.pointList, 84 | ) 85 | 86 | #################### 87 | # The remaining examples below demonstrate the use of 88 | # f0_morph.f0Morph() with different arguments. It may 89 | # be helpful for your work. However, you can safely comment 90 | # out or delete the code below to keep things simple. 91 | #################### 92 | 93 | 94 | # Or for more control over the steps: 95 | stepList = [ 96 | 0.10, 97 | ] # 10% morph 98 | # Run the morph process 99 | f0_morph.f0Morph( 100 | fromWavFN=join(root, fromWavFN), 101 | pitchPath=root, 102 | stepList=stepList, 103 | outputName="%s_%s_f0_morph" % (fromName, toName), 104 | doPlotPitchSteps=True, 105 | fromPitchData=fromPitch, 106 | toPitchData=toPitch, 107 | outputMinPitch=minPitch, 108 | outputMaxPitch=maxPitch, 109 | praatEXE=praatEXE, 110 | ) 111 | 112 | # And, as shown in the next four examples, 113 | # we can reset the speaker's pitch range and mean pitch back to their own 114 | stepList = [ 115 | 1.0, 116 | ] 117 | 118 | # Source's mean pitch, target's pitch range 119 | f0_morph.f0Morph( 120 | fromWavFN=join(root, fromWavFN), 121 | pitchPath=root, 122 | stepList=stepList, 123 | outputName="%s_%s_f0_morph_w_average" % (fromName, toName), 124 | doPlotPitchSteps=True, 125 | fromPitchData=fromPitch, 126 | toPitchData=toPitch, 127 | outputMinPitch=minPitch, 128 | outputMaxPitch=maxPitch, 129 | praatEXE=praatEXE, 130 | keepPitchRange=False, 131 | keepAveragePitch=True, 132 | ) 133 | 134 | # Target's mean pitch, source's pitch range 135 | f0_morph.f0Morph( 136 | fromWavFN=join(root, fromWavFN), 137 | pitchPath=root, 138 | stepList=stepList, 139 | outputName="%s_%s_f0_morph_w_range" % (fromName, toName), 140 | doPlotPitchSteps=True, 141 | fromPitchData=fromPitch, 142 | toPitchData=toPitch, 143 | outputMinPitch=minPitch, 144 | outputMaxPitch=maxPitch, 145 | praatEXE=praatEXE, 146 | keepPitchRange=True, 147 | keepAveragePitch=False, 148 | ) 149 | 150 | # Source's mean pitch, source's pitch range 151 | outputName = "%s_%s_f0_morph_w_range_and_average" % (fromName, toName) 152 | f0_morph.f0Morph( 153 | fromWavFN=join(root, fromWavFN), 154 | pitchPath=root, 155 | stepList=stepList, 156 | outputName=outputName, 157 | doPlotPitchSteps=True, 158 | fromPitchData=fromPitch, 159 | toPitchData=toPitch, 160 | outputMinPitch=minPitch, 161 | outputMaxPitch=maxPitch, 162 | praatEXE=praatEXE, 163 | keepPitchRange=True, 164 | keepAveragePitch=True, 165 | ) 166 | 167 | # Target's mean pitch, target's pitch range (default behavior) 168 | f0_morph.f0Morph( 169 | fromWavFN=join(root, fromWavFN), 170 | pitchPath=root, 171 | stepList=stepList, 172 | outputName="%s_%s_f0_morph_w_regular" % (fromName, toName), 173 | doPlotPitchSteps=True, 174 | fromPitchData=fromPitch, 175 | toPitchData=toPitch, 176 | outputMinPitch=minPitch, 177 | outputMaxPitch=maxPitch, 178 | praatEXE=praatEXE, 179 | keepPitchRange=False, 180 | keepAveragePitch=False, 181 | ) 182 | -------------------------------------------------------------------------------- /examples/pitch_morph_to_pitch_contour.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jun 29, 2016 3 | 4 | This file shows an example of morphing to a pitch tier. 5 | In f0_morph.py, the target pitch contour is extracted in the 6 | script from another file. In this example, the pitch tier 7 | could come from any source (hand sculpted or generated). 8 | 9 | WARNING: If you attempt to morph to a pitch track that has 10 | few sample points, the morph process will fail. 11 | 12 | @author: Tim 13 | """ 14 | 15 | import os 16 | from os.path import join 17 | 18 | from praatio import pitch_and_intensity 19 | from praatio import data_points 20 | 21 | from promo import f0_morph 22 | from promo.morph_utils import utils 23 | from promo.morph_utils import interpolation 24 | 25 | 26 | # Define the arguments for the code 27 | 28 | root = os.path.abspath(join(".", "files")) 29 | praatEXE = r"C:\Praat.exe" # Windows paths 30 | praatEXE = "/Applications/Praat.app/Contents/MacOS/Praat" # Mac paths 31 | 32 | minPitch = 50 33 | maxPitch = 350 34 | stepList = utils.generateStepList(3) 35 | 36 | fromName = "mary1" 37 | fromWavFN = fromName + ".wav" 38 | fromPitchFN = fromName + ".txt" 39 | fromTGFN = join(root, fromName + ".TextGrid") 40 | 41 | toName = "mary1_stylized" 42 | toPitchFN = toName + ".PitchTier" 43 | 44 | # Prepare the data for morphing 45 | # 1st load it into memory 46 | fromPitchList = pitch_and_intensity.extractPI( 47 | join(root, fromWavFN), 48 | join(root, fromPitchFN), 49 | praatEXE, 50 | minPitch, 51 | maxPitch, 52 | forceRegenerate=False, 53 | ) 54 | fromPitchList = [(time, pitch) for time, pitch, _ in fromPitchList] 55 | 56 | # Load in the target pitch contour 57 | pitchTier = data_points.open2DPointObject(join(root, toPitchFN)) 58 | toPitchList = [(time, pitch) for time, pitch in pitchTier.pointList] 59 | 60 | # The target contour doesn't contain enough sample points, so interpolate 61 | # over the provided samples 62 | # (this step can be skipped if there are enough sample points--a warning 63 | # will be issued if there are any potential problems) 64 | toPitchList = interpolation.quadraticInterpolation(toPitchList, 4, 1000, 0) 65 | 66 | # 3rd select which sections to align. 67 | # We'll use textgrids for this purpose. 68 | tierName = "words" 69 | fromPitch = f0_morph.getPitchForIntervals(fromPitchList, fromTGFN, tierName) 70 | toPitch = f0_morph.getPitchForIntervals(toPitchList, fromTGFN, tierName) 71 | 72 | # Run the morph process 73 | f0_morph.f0Morph( 74 | fromWavFN=join(root, fromWavFN), 75 | pitchPath=root, 76 | stepList=stepList, 77 | outputName="%s_%s_f0_morph" % (fromName, toName), 78 | doPlotPitchSteps=True, 79 | fromPitchData=fromPitch, 80 | toPitchData=toPitch, 81 | outputMinPitch=minPitch, 82 | outputMaxPitch=maxPitch, 83 | praatEXE=praatEXE, 84 | ) 85 | -------------------------------------------------------------------------------- /examples/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmahrt/ProMo/c1d8252453c28592ce4c09dc1d690a4e3b8550cc/examples/test/__init__.py -------------------------------------------------------------------------------- /promo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmahrt/ProMo/c1d8252453c28592ce4c09dc1d690a4e3b8550cc/promo/__init__.py -------------------------------------------------------------------------------- /promo/duration_morph.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jun 5, 2013 3 | 4 | @author: timmahrt 5 | """ 6 | 7 | import os 8 | from os.path import join 9 | import copy 10 | 11 | from praatio import textgrid 12 | from praatio import praat_scripts 13 | from praatio import data_points 14 | from praatio.utilities import constants 15 | 16 | from promo.morph_utils import utils 17 | from promo.morph_utils import audio_scripts 18 | from promo.morph_utils import plot_morphed_data 19 | 20 | # This value is used to differentiate a praat interval boundary that marks 21 | # the start of one region and the end of another. 22 | PRAAT_TIME_DIFF = 0.000001 23 | 24 | 25 | class NoLabeledRegionFoundException(Exception): 26 | def __init__(self, tgFN): 27 | super(NoLabeledRegionFoundException, self).__init__() 28 | self.tgFN = tgFN 29 | 30 | def __str__(self): 31 | return "No labeled region fitting the specified criteria for tg: " + self.tgFN 32 | 33 | 34 | def changeDuration( 35 | fromWavFN, 36 | durationParameters, 37 | stepList, 38 | outputName, 39 | outputMinPitch, 40 | outputMaxPitch, 41 | praatEXE, 42 | ): 43 | """ 44 | Uses praat to morph duration in one file to duration in another 45 | 46 | Praat uses the PSOLA algorithm 47 | """ 48 | 49 | rootPath = os.path.split(fromWavFN)[0] 50 | 51 | # Prep output directories 52 | outputPath = join(rootPath, "duration_resynthesized_wavs") 53 | utils.makeDir(outputPath) 54 | 55 | durationTierPath = join(rootPath, "duration_tiers") 56 | utils.makeDir(durationTierPath) 57 | 58 | fromWavDuration = audio_scripts.getSoundFileDuration(fromWavFN) 59 | 60 | durationParameters = copy.deepcopy(durationParameters) 61 | # Pad any gaps with values of 1 (no change in duration) 62 | 63 | # No need to stretch out any pauses at the beginning 64 | if durationParameters[0][0] != 0: 65 | tmpVar = (0, durationParameters[0][0] - PRAAT_TIME_DIFF, 1) 66 | durationParameters.insert(0, tmpVar) 67 | 68 | # Or the end 69 | if durationParameters[-1][1] < fromWavDuration: 70 | durationParameters.append( 71 | (durationParameters[-1][1] + PRAAT_TIME_DIFF, fromWavDuration, 1) 72 | ) 73 | 74 | # Create the praat script for doing duration manipulation 75 | for stepAmount in stepList: 76 | durationPointList = [] 77 | for start, end, ratio in durationParameters: 78 | percentChange = 1 + (ratio - 1) * stepAmount 79 | durationPointList.append((start, percentChange)) 80 | durationPointList.append((end, percentChange)) 81 | 82 | outputPrefix = "%s_%0.3g" % (outputName, stepAmount) 83 | durationTierFN = join(durationTierPath, "%s.DurationTier" % outputPrefix) 84 | outputWavFN = join(outputPath, "%s.wav" % outputPrefix) 85 | durationTier = data_points.PointObject2D( 86 | durationPointList, constants.DataPointTypes.DURATION, 0, fromWavDuration 87 | ) 88 | durationTier.save(durationTierFN) 89 | 90 | praat_scripts.resynthesizeDuration( 91 | praatEXE, 92 | fromWavFN, 93 | durationTierFN, 94 | outputWavFN, 95 | outputMinPitch, 96 | outputMaxPitch, 97 | ) 98 | 99 | 100 | def getBareParameters(wavFN): 101 | wavDuration = audio_scripts.getSoundFileDuration(wavFN) 102 | return [ 103 | (0, wavDuration, ""), 104 | ] 105 | 106 | 107 | def getMorphParameters(fromTGFN, toTGFN, tierName, filterFunc=None, useBlanks=False): 108 | """ 109 | Get intervals for source and target audio files 110 | 111 | Use this information to find out how much to stretch/shrink each source 112 | interval. 113 | 114 | The target values are based on the contents of toTGFN. 115 | """ 116 | 117 | if filterFunc is None: 118 | filterFunc = lambda entry: True # Everything is accepted 119 | 120 | fromEntryList = utils.getIntervals( 121 | fromTGFN, tierName, includeUnlabeledRegions=useBlanks 122 | ) 123 | toEntryList = utils.getIntervals( 124 | toTGFN, tierName, includeUnlabeledRegions=useBlanks 125 | ) 126 | 127 | fromEntryList = [entry for entry in fromEntryList if filterFunc(entry)] 128 | toEntryList = [entry for entry in toEntryList if filterFunc(entry)] 129 | 130 | assert len(fromEntryList) == len(toEntryList) 131 | 132 | durationParameters = [] 133 | for fromEntry, toEntry in zip(fromEntryList, toEntryList): 134 | fromStart, fromEnd = fromEntry[:2] 135 | toStart, toEnd = toEntry[:2] 136 | 137 | # Praat will ignore a second value appearing at the same time as 138 | # another so we give each start a tiny offset to distinguish intervals 139 | # that start and end at the same point 140 | toStart += PRAAT_TIME_DIFF 141 | fromStart += PRAAT_TIME_DIFF 142 | 143 | ratio = (toEnd - toStart) / float((fromEnd - fromStart)) 144 | durationParameters.append((fromStart, fromEnd, ratio)) 145 | 146 | return durationParameters 147 | 148 | 149 | def getManipulatedParamaters(tgFN, tierName, modFunc, filterFunc=None, useBlanks=False): 150 | """ 151 | Get intervals for source and target audio files 152 | 153 | Use this information to find out how much to stretch/shrink each source 154 | interval. 155 | 156 | The target values are based on modfunc. 157 | """ 158 | 159 | fromExtractInfo = utils.getIntervals(tgFN, tierName, filterFunc, useBlanks) 160 | 161 | durationParameters = [] 162 | for fromInfoTuple in fromExtractInfo: 163 | fromStart, fromEnd = fromInfoTuple[:2] 164 | toStart, toEnd = modFunc(fromStart), modFunc(fromEnd) 165 | 166 | # Praat will ignore a second value appearing at the same time as 167 | # another so we give each start a tiny offset to distinguish intervals 168 | # that start and end at the same point 169 | toStart += PRAAT_TIME_DIFF 170 | fromStart += PRAAT_TIME_DIFF 171 | 172 | ratio = (toEnd - toStart) / float((fromEnd - fromStart)) 173 | 174 | ratioTuple = (fromStart, fromEnd, ratio) 175 | durationParameters.append(ratioTuple) 176 | 177 | return durationParameters 178 | 179 | 180 | def outputMorphTextgrids(fromTGFN, durationParameters, stepList, outputTGName): 181 | if outputTGName is not None: 182 | utils.makeDir(os.path.split(outputTGName)[0]) 183 | 184 | # Create the adjusted textgrids 185 | if outputTGName is not None: 186 | for stepFactor in stepList: 187 | stepDurationParameters = [ 188 | (start, end, 1 + (ratio - 1) * stepFactor) 189 | for start, end, ratio in durationParameters 190 | ] 191 | adjustedTG = textgridManipulateDuration(fromTGFN, stepDurationParameters) 192 | 193 | outputTGFN = "%s_%0.3g.TextGrid" % (outputTGName, stepFactor) 194 | adjustedTG.save( 195 | outputTGFN, format="short_textgrid", includeBlankSpaces=True 196 | ) 197 | 198 | 199 | def outputMorphPlot( 200 | fromTGFN, toTGFN, tierName, durationParameters, stepList, outputImageFN 201 | ): 202 | if outputImageFN is not None: 203 | utils.makeDir(os.path.split(outputImageFN)[0]) 204 | 205 | # Create the plot of the morph 206 | if outputImageFN is not None: 207 | _plotResults( 208 | durationParameters, 209 | fromTGFN, 210 | toTGFN, 211 | tierName, 212 | stepList, 213 | outputImageFN, 214 | None, 215 | False, 216 | ) 217 | 218 | 219 | def _plotResults( 220 | durationParameters, 221 | fromTGFN, 222 | toTGFN, 223 | tierName, 224 | stepList, 225 | outputPNGFN, 226 | filterFunc, 227 | includeUnlabeledRegions, 228 | ): 229 | # Containers 230 | fromDurList = [] 231 | toDurList = [] 232 | actDurList = [] 233 | labelList = [] 234 | 235 | fromExtractInfo = utils.getIntervals( 236 | fromTGFN, tierName, filterFunc, includeUnlabeledRegions 237 | ) 238 | toExtractInfo = utils.getIntervals( 239 | toTGFN, tierName, filterFunc, includeUnlabeledRegions 240 | ) 241 | 242 | # Get durations 243 | for fromInfoTuple, toInfoTuple in zip(fromExtractInfo, toExtractInfo): 244 | fromStart, fromEnd = fromInfoTuple[:2] 245 | toStart, toEnd = toInfoTuple[:2] 246 | 247 | labelList.append(fromInfoTuple[2]) 248 | fromDurList.append(fromEnd - fromStart) 249 | toDurList.append(toEnd - toStart) 250 | 251 | # Get iterpolated values 252 | for stepAmount in stepList: 253 | tmpDurList = [] 254 | for fromStart, fromEnd, ratio in durationParameters: 255 | dur = fromEnd - fromStart 256 | percentChange = 1 + (ratio - 1) * stepAmount 257 | tmpDurList.append(dur * percentChange) 258 | 259 | actDurList.append(tmpDurList) 260 | 261 | # Plot data 262 | plot_morphed_data.plotDuration( 263 | fromDurList, toDurList, actDurList, labelList, outputPNGFN 264 | ) 265 | 266 | 267 | def textgridMorphDuration(fromTGFN, toTGFN): 268 | """ 269 | A convenience function. Morphs interval durations of one tg to another. 270 | 271 | This assumes the two textgrids have the same number of segments. 272 | """ 273 | fromTG = textgrid.openTextgrid(fromTGFN, includeEmptyIntervals=False) 274 | toTG = textgrid.openTextgrid(toTGFN, includeEmptyIntervals=False) 275 | adjustedTG = textgrid.Textgrid() 276 | 277 | for tierName in fromTG.tierNames: 278 | fromTier = fromTG.getTier(tierName) 279 | toTier = toTG.getTier(tierName) 280 | adjustedTier = fromTier.morph(toTier) 281 | adjustedTG.addTier(adjustedTier) 282 | 283 | return adjustedTG 284 | 285 | 286 | def textgridManipulateDuration(tgFN, ratioList): 287 | tg = textgrid.openTextgrid(tgFN, includeEmptyIntervals=False) 288 | 289 | adjustedTG = textgrid.Textgrid() 290 | 291 | for tierName in tg.tierNames: 292 | fromTier = tg.getTier(tierName) 293 | 294 | adjustedTier = None 295 | if isinstance(fromTier, textgrid.IntervalTier): 296 | adjustedTier = _morphIntervalTier(fromTier, ratioList) 297 | elif isinstance(fromTier, textgrid.PointTier): 298 | adjustedTier = _morphPointTier(fromTier, ratioList) 299 | 300 | assert adjustedTier is not None 301 | adjustedTG.addTier(adjustedTier) 302 | 303 | return adjustedTG 304 | 305 | 306 | def _getTimeDiff(start, stop, ratio): 307 | """Returns the time difference between interval and interval*ratio""" 308 | return (ratio - 1) * (stop - start) 309 | 310 | 311 | def _morphPointTier(tier, ratioList): 312 | cumulativeAdjustAmount = 0 313 | i = 0 314 | newEntryList = [] 315 | for timestamp, label in tier.entries: 316 | # Advance to the manipulation interval that coincides with the 317 | # current point or appears after it 318 | while i < len(ratioList) and timestamp > ratioList[i][1]: 319 | rStart, rStop, ratio = ratioList[i] 320 | cumulativeAdjustAmount += _getTimeDiff(rStart, rStop, ratio) 321 | i += 1 322 | 323 | newTime = timestamp + cumulativeAdjustAmount 324 | 325 | # Alter the time if the point is within a manipulation interval 326 | if i < len(ratioList): 327 | rStart, rStop, ratio = ratioList[i] 328 | if timestamp > rStart and timestamp <= rStop: 329 | newTime += _getTimeDiff(rStart, timestamp, ratio) 330 | 331 | newEntryList.append((newTime, label)) 332 | 333 | maxT = tier.maxTimestamp + cumulativeAdjustAmount 334 | return tier.new(entries=newEntryList, maxTimestamp=maxT) 335 | 336 | 337 | def _morphIntervalTier(tier, ratioList): 338 | cumulativeAdjustAmount = 0 339 | i = 0 340 | newEntryList = [] 341 | for start, stop, label in tier.entries: 342 | # Syncronize the manipulation and data intervals so that they 343 | # are either overlapping or the manipulation interval is farther 344 | # in the future. This accumulates the effect of past 345 | # manipulations so we know how much to offset timestamps 346 | # for the current interval. 347 | while ( 348 | i < len(ratioList) and start > ratioList[i][0] and start >= ratioList[i][1] 349 | ): 350 | rStart, rStop, ratio = ratioList[i] 351 | cumulativeAdjustAmount = _getTimeDiff(rStart, rStop, ratio) 352 | i += 1 353 | 354 | newStart = start + cumulativeAdjustAmount 355 | newStop = stop + cumulativeAdjustAmount 356 | 357 | # Manipulate the interval further if there is overlap with a 358 | # manipulation interval 359 | while i < len(ratioList): 360 | rStart, rStop, ratio = ratioList[i] 361 | 362 | # currAdjustAmount is the ratio modified by percent change 363 | # e.g. if the ratio is a boost of 1.2 but percent change is 364 | # 0.5, ratio = 1.1 (it loses half of its effect) 365 | 366 | # Adjusting the start position based on overlap with the 367 | # current adjust interval 368 | if start <= rStart: 369 | pass 370 | elif start > rStart and start <= rStop: 371 | newStart += _getTimeDiff(rStart, start, ratio) 372 | 373 | # Adjusting the stop position based on overlap with the 374 | # current adjust interval 375 | if stop >= rStop: 376 | newStop += _getTimeDiff(rStart, rStop, ratio) 377 | elif stop < rStop and stop >= rStart: 378 | newStop += _getTimeDiff(rStart, stop, ratio) 379 | 380 | # If we are beyond the current manipulation interval, 381 | # then we need to move to the next one 382 | if stop >= rStop: 383 | cumulativeAdjustAmount += _getTimeDiff(rStart, rStop, ratio) 384 | i += 1 385 | # Otherwise, we are done manipulating the current interval 386 | elif stop < rStop: 387 | break 388 | 389 | newEntryList.append((newStart, newStop, label)) 390 | 391 | newMax = tier.maxTimestamp + cumulativeAdjustAmount 392 | return tier.new(entries=newEntryList, maxTimestamp=newMax) 393 | -------------------------------------------------------------------------------- /promo/f0_morph.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on May 31, 2013 3 | 4 | @author: timmahrt 5 | 6 | Contains utilities for extracting, creating, and manipulating pitch files in 7 | praat. 8 | """ 9 | 10 | from os.path import join 11 | 12 | from praatio import textgrid 13 | from praatio import data_points 14 | from praatio import praat_scripts 15 | from praatio.utilities import utils as praatio_utils 16 | from praatio.utilities import constants 17 | 18 | from promo.morph_utils import utils 19 | from promo.morph_utils import audio_scripts 20 | from promo.morph_utils import plot_morphed_data 21 | from promo.morph_utils import morph_sequence 22 | 23 | 24 | class MissingPitchDataException(Exception): 25 | def __str__(self): 26 | txt = ( 27 | "\n\nNo data points available in a region for morphing.\n" 28 | "Two data points are needed in each region to do the morph\n" 29 | "Regions with fewer than two samples are skipped, which " 30 | "should be fine for some cases (e.g. unvoiced segments).\n" 31 | "If you need more data points, see " 32 | "promo.morph_utils.interpolation" 33 | ) 34 | return txt 35 | 36 | 37 | def getPitchForIntervals(data, tgFN, tierName): 38 | """ 39 | Preps data for use in f0Morph 40 | """ 41 | tg = textgrid.openTextgrid(tgFN, includeEmptyIntervals=False) 42 | data = tg.getTier(tierName).getValuesInIntervals(data) 43 | data = [dataList for _, dataList in data] 44 | 45 | return data 46 | 47 | 48 | def f0Morph( 49 | fromWavFN, 50 | pitchPath, 51 | stepList, 52 | outputName, 53 | doPlotPitchSteps, 54 | fromPitchData, 55 | toPitchData, 56 | outputMinPitch, 57 | outputMaxPitch, 58 | praatEXE, 59 | keepPitchRange=False, 60 | keepAveragePitch=False, 61 | sourcePitchDataList=None, 62 | minIntervalLength=0.3, 63 | ): 64 | """ 65 | Resynthesizes the pitch track from a source to a target wav file 66 | 67 | fromPitchData and toPitchData should be segmented according to the 68 | portions that you want to morph. The two lists must have the same 69 | number of sublists. 70 | 71 | Occurs over a three-step process. 72 | 73 | This function can act as a template for how to use the function 74 | morph_sequence.morphChunkedDataLists to morph pitch contours or 75 | other data. 76 | 77 | By default, everything is morphed, but it is possible to maintain elements 78 | of the original speaker's pitch (average pitch and pitch range) by setting 79 | the appropriate flag) 80 | 81 | sourcePitchDataList: if passed in, any regions unspecified by 82 | fromPitchData will be sampled from this list. In 83 | essence, this allows one to leave segments of 84 | the original pitch contour untouched by the 85 | morph process. 86 | """ 87 | 88 | fromDuration = audio_scripts.getSoundFileDuration(fromWavFN) 89 | 90 | # Find source pitch samples that will be mixed in with the target 91 | # pitch samples later 92 | nonMorphPitchData = [] 93 | if sourcePitchDataList is not None: 94 | timeList = sorted(fromPitchData) 95 | timeList = [(row[0][0], row[-1][0]) for row in timeList] 96 | endTime = sourcePitchDataList[-1][0] 97 | invertedTimeList = praatio_utils.invertIntervalList(timeList, endTime) 98 | invertedTimeList = [ 99 | (start, stop) 100 | for start, stop in invertedTimeList 101 | if stop - start > minIntervalLength 102 | ] 103 | 104 | for start, stop in invertedTimeList: 105 | pitchList = praatio_utils.getValuesInInterval( 106 | sourcePitchDataList, start, stop 107 | ) 108 | nonMorphPitchData.extend(pitchList) 109 | 110 | # Iterative pitch tier data path 111 | pitchTierPath = join(pitchPath, "pitchTiers") 112 | resynthesizedPath = join(pitchPath, "f0_resynthesized_wavs") 113 | for tmpPath in [pitchTierPath, resynthesizedPath]: 114 | utils.makeDir(tmpPath) 115 | 116 | # 1. Prepare the data for morphing - acquire the segments to merge 117 | # (Done elsewhere, with the input fed into this function) 118 | 119 | # 2. Morph the fromData to the toData 120 | try: 121 | finalOutputList = morph_sequence.morphChunkedDataLists( 122 | fromPitchData, toPitchData, stepList 123 | ) 124 | except IndexError: 125 | raise MissingPitchDataException() 126 | 127 | fromPitchData = [row for subList in fromPitchData for row in subList] 128 | toPitchData = [row for subList in toPitchData for row in subList] 129 | 130 | # 3. Save the pitch data and resynthesize the pitch 131 | mergedDataList = [] 132 | for i in range(0, len(finalOutputList)): 133 | outputDataList = finalOutputList[i] 134 | 135 | if keepPitchRange is True: 136 | outputDataList = morph_sequence.morphRange(outputDataList, fromPitchData) 137 | 138 | if keepAveragePitch is True: 139 | outputDataList = morph_sequence.morphAveragePitch( 140 | outputDataList, fromPitchData 141 | ) 142 | 143 | if sourcePitchDataList is not None: 144 | outputDataList.extend(nonMorphPitchData) 145 | outputDataList.sort() 146 | 147 | stepOutputName = "%s_%0.3g" % (outputName, stepList[i]) 148 | pitchFNFullPath = join(pitchTierPath, "%s.PitchTier" % stepOutputName) 149 | outputFN = join(resynthesizedPath, "%s.wav" % stepOutputName) 150 | pointObj = data_points.PointObject2D( 151 | outputDataList, constants.DataPointTypes.PITCH, 0, fromDuration 152 | ) 153 | pointObj.save(pitchFNFullPath) 154 | 155 | outputTime, outputVals = zip(*outputDataList) 156 | mergedDataList.append((outputTime, outputVals)) 157 | 158 | praat_scripts.resynthesizePitch( 159 | praatEXE, 160 | fromWavFN, 161 | pitchFNFullPath, 162 | outputFN, 163 | outputMinPitch, 164 | outputMaxPitch, 165 | ) 166 | 167 | # 4. (Optional) Plot the generated contours 168 | if doPlotPitchSteps: 169 | fromTime, fromVals = zip(*fromPitchData) 170 | toTime, toVals = zip(*toPitchData) 171 | 172 | plot_morphed_data.plotF0( 173 | (fromTime, fromVals), 174 | (toTime, toVals), 175 | mergedDataList, 176 | join(pitchTierPath, "%s.png" % outputName), 177 | ) 178 | -------------------------------------------------------------------------------- /promo/morph_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmahrt/ProMo/c1d8252453c28592ce4c09dc1d690a4e3b8550cc/promo/morph_utils/__init__.py -------------------------------------------------------------------------------- /promo/morph_utils/audio_scripts.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Aug 23, 2014 3 | 4 | @author: tmahrt 5 | """ 6 | 7 | import wave 8 | 9 | 10 | def getSoundFileDuration(fn): 11 | """ 12 | Returns the duration of a wav file (in seconds) 13 | """ 14 | audiofile = wave.open(fn, "r") 15 | 16 | params = audiofile.getparams() 17 | framerate = params[2] 18 | nframes = params[3] 19 | 20 | duration = float(nframes) / framerate 21 | return duration 22 | -------------------------------------------------------------------------------- /promo/morph_utils/interpolation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jun 29, 2016 3 | 4 | @author: Tim 5 | """ 6 | 7 | try: 8 | import numpy as np 9 | except ImportError: 10 | hasNumpy = False 11 | else: 12 | hasNumpy = True 13 | 14 | 15 | def _numpyCheck(): 16 | if not hasNumpy: 17 | raise ImportError( 18 | "Numpy required to do data interpolation. " 19 | "Install numpy or don't use data interpolation" 20 | ) 21 | 22 | 23 | def quadraticInterpolation(valueList2d, numDegrees, n, startTime=None, endTime=None): 24 | """ 25 | Generates a series of points on a smooth curve that cross the given points 26 | 27 | numDegrees - the degrees of the fitted polynomial 28 | - the curve gets weird if this value is too high for the input 29 | n - number of points to output 30 | startTime/endTime/n - n points will be generated at evenly spaced 31 | intervals between startTime and endTime 32 | """ 33 | _numpyCheck() 34 | 35 | x, y = zip(*valueList2d) 36 | 37 | if startTime is None: 38 | startTime = x[0] 39 | if endTime is None: 40 | endTime = x[-1] 41 | 42 | polyFunc = np.poly1d(np.polyfit(x, y, numDegrees)) 43 | 44 | newX = np.linspace(startTime, endTime, n) 45 | 46 | retList = [(n, polyFunc(n)) for n in newX] 47 | 48 | return retList 49 | -------------------------------------------------------------------------------- /promo/morph_utils/modify_pitch_accent.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jan 26, 2017 3 | 4 | This convenience code that makes it easy to manipulate 5 | characteristics of a pitch accent. It is flexible in how 6 | it is used (on a stylized pitch contour or raw values and 7 | as a single step or in a piecewise manner). 8 | 9 | See examples/modify_pitch_accent_example.py for example usage. 10 | 11 | NOTE: By pitch accent, I mean a single hill or peak, surrounded 12 | by less intense values to the left and right. Some of the 13 | functions make reference to the max value in the dataset, so if you 14 | have multiple peaks or a downward slope, etc. you may get strange 15 | results. 16 | 17 | @author: Tim 18 | """ 19 | import copy 20 | 21 | 22 | def _deletePoints(f0List, start, end): 23 | return [(timeV, f0V) for timeV, f0V in f0List if timeV < start or timeV > end] 24 | 25 | 26 | class PitchAccent(object): 27 | def __init__(self, pointList): 28 | 29 | self.pointList = copy.deepcopy(pointList) 30 | 31 | self.netLeftShift = 0 32 | self.netRightShift = 0 33 | 34 | pitchList = [f0V for _, f0V in self.pointList] 35 | minV = min(pitchList) 36 | maxV = max(pitchList) 37 | 38 | self.peakI = pitchList.index(maxV) 39 | 40 | timeList = [timeV for timeV, _ in self.pointList] 41 | self.minT = min(timeList) 42 | self.maxT = max(timeList) 43 | 44 | def adjustPeakHeight(self, heightAmount): 45 | """ 46 | Adjust peak height 47 | 48 | The foot of the accent is left unchanged and intermediate 49 | values are linearly scaled 50 | """ 51 | if heightAmount == 0: 52 | return 53 | 54 | pitchList = [f0V for _, f0V in self.pointList] 55 | minV = min(pitchList) 56 | maxV = max(pitchList) 57 | scale = lambda x, y: x + y * (x - minV) / float(maxV - minV) 58 | 59 | self.pointList = [ 60 | (timeV, scale(f0V, heightAmount)) for timeV, f0V in self.pointList 61 | ] 62 | 63 | def addPlateau(self, plateauAmount, pitchSampFreq=None): 64 | """ 65 | Add a plateau 66 | 67 | A negative plateauAmount will move the peak backwards. 68 | A positive plateauAmount will move the peak forwards. 69 | 70 | All points on the side of the peak growth will also get moved. 71 | i.e. the slope of the peak does not change. The accent gets 72 | wider instead. 73 | 74 | If pitchSampFreq=None, the plateau will only be specified by 75 | the start and end points of the plateau 76 | """ 77 | if plateauAmount == 0: 78 | return 79 | 80 | maxPoint = self.pointList[self.peakI] 81 | 82 | # Define the plateau 83 | if pitchSampFreq is not None: 84 | numSteps = abs(int(plateauAmount / pitchSampFreq)) 85 | timeChangeList = [stepV * pitchSampFreq for stepV in range(0, numSteps + 1)] 86 | else: 87 | timeChangeList = [ 88 | plateauAmount, 89 | ] 90 | 91 | # Shift the side being pushed by the plateau 92 | if plateauAmount < 0: # Plateau moves left of the peak 93 | leftSide = self.pointList[: self.peakI] 94 | rightSide = self.pointList[self.peakI :] 95 | 96 | plateauPoints = [ 97 | (maxPoint[0] + timeChange, maxPoint[1]) for timeChange in timeChangeList 98 | ] 99 | leftSide = [(timeV + plateauAmount, f0V) for timeV, f0V in leftSide] 100 | self.netLeftShift += plateauAmount 101 | 102 | elif plateauAmount > 0: # Plateau moves right of the peak 103 | leftSide = self.pointList[: self.peakI + 1] 104 | rightSide = self.pointList[self.peakI + 1 :] 105 | 106 | plateauPoints = [ 107 | (maxPoint[0] + timeChange, maxPoint[1]) for timeChange in timeChangeList 108 | ] 109 | rightSide = [(timeV + plateauAmount, f0V) for timeV, f0V in rightSide] 110 | self.netRightShift += plateauAmount 111 | 112 | self.pointList = leftSide + plateauPoints + rightSide 113 | 114 | def shiftAccent(self, shiftAmount): 115 | """ 116 | Move the whole accent earlier or later 117 | """ 118 | if shiftAmount == 0: 119 | return 120 | 121 | self.pointList = [(time + shiftAmount, pitch) for time, pitch in self.pointList] 122 | 123 | # Update shift amounts 124 | if shiftAmount < 0: 125 | self.netLeftShift += shiftAmount 126 | elif shiftAmount >= 0: 127 | self.netRightShift += shiftAmount 128 | 129 | def deleteOverlapping(self, targetList): 130 | """ 131 | Erase points from another list that overlap with points in this list 132 | """ 133 | start = self.pointList[0][0] 134 | stop = self.pointList[-1][0] 135 | 136 | if self.netLeftShift < 0: 137 | start += self.netLeftShift 138 | 139 | if self.netRightShift > 0: 140 | stop += self.netRightShift 141 | 142 | targetList = _deletePoints(targetList, start, stop) 143 | 144 | return targetList 145 | 146 | def reintegrate(self, fullPointList): 147 | """ 148 | Integrates the pitch values of the accent into a larger pitch contour 149 | """ 150 | # Erase the original region of the accent 151 | fullPointList = _deletePoints(fullPointList, self.minT, self.maxT) 152 | 153 | # Erase the new region of the accent 154 | fullPointList = self.deleteOverlapping(fullPointList) 155 | 156 | # Add the accent into the full pitch list 157 | outputPointList = fullPointList + self.pointList 158 | outputPointList.sort() 159 | 160 | return outputPointList 161 | -------------------------------------------------------------------------------- /promo/morph_utils/morph_sequence.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Sep 18, 2013 3 | 4 | @author: timmahrt 5 | 6 | Given two lists of tuples of the form [(value, time), (value, time)], morph 7 | can iteratively transform the values in one list to the values in the other 8 | while maintaining the times in the first list. 9 | 10 | Both time scales are placed on a relative scale. This assumes that the times 11 | may be different and the number of samples may be different but the 'events' 12 | occur at the same relative location (half way through, at the end, etc.). 13 | 14 | Both dynamic time warping and morph, align two data lists in time. However, 15 | dynamic time warping does this by analyzing the event structure and aligning 16 | events in the two signals as best it can 17 | (i.e. it changes when events happen in relative time while morph preserves 18 | when events happen in relative time). 19 | """ 20 | 21 | 22 | class RelativizeSequenceException(Exception): 23 | def __init__(self, dist): 24 | super(RelativizeSequenceException, self).__init__() 25 | self.dist = dist 26 | 27 | def __str__(self): 28 | return ( 29 | "You need at least two unique values to make " 30 | + "a sequence relative. Input: %s" % repr(self.dist) 31 | ) 32 | 33 | 34 | def makeSequenceRelative(absVSequence): 35 | """ 36 | Puts every value in a list on a continuum between 0 and 1 37 | 38 | Also returns the min and max values (to reverse the process) 39 | """ 40 | 41 | if len(absVSequence) < 2 or len(set(absVSequence)) == 1: 42 | raise RelativizeSequenceException(absVSequence) 43 | 44 | minV = min(absVSequence) 45 | maxV = max(absVSequence) 46 | relativeSeq = [(value - minV) / (maxV - minV) for value in absVSequence] 47 | 48 | return relativeSeq, minV, maxV 49 | 50 | 51 | def makeSequenceAbsolute(relVSequence, minV, maxV): 52 | """ 53 | Makes every value in a sequence absolute 54 | """ 55 | 56 | return [(value * (maxV - minV)) + minV for value in relVSequence] 57 | 58 | 59 | def _makeTimingRelative(absoluteDataList): 60 | """ 61 | Given normal pitch tier data, puts the times on a scale from 0 to 1 62 | 63 | Input is a list of tuples of the form 64 | ([(time1, pitch1), (time2, pitch2),...] 65 | 66 | Also returns the start and end time so that the process can be reversed 67 | """ 68 | 69 | timingSeq = [row[0] for row in absoluteDataList] 70 | valueSeq = [list(row[1:]) for row in absoluteDataList] 71 | 72 | relTimingSeq, startTime, endTime = makeSequenceRelative(timingSeq) 73 | 74 | relDataList = [ 75 | tuple( 76 | [ 77 | time, 78 | ] 79 | + row 80 | ) 81 | for time, row in zip(relTimingSeq, valueSeq) 82 | ] 83 | 84 | return relDataList, startTime, endTime 85 | 86 | 87 | def _makeTimingAbsolute(relativeDataList, startTime, endTime): 88 | """ 89 | Maps values from 0 to 1 to the provided start and end time 90 | 91 | Input is a list of tuples of the form 92 | ([(time1, pitch1), (time2, pitch2),...] 93 | """ 94 | 95 | timingSeq = [row[0] for row in relativeDataList] 96 | valueSeq = [list(row[1:]) for row in relativeDataList] 97 | 98 | absTimingSeq = makeSequenceAbsolute(timingSeq, startTime, endTime) 99 | 100 | absDataList = [ 101 | tuple( 102 | [ 103 | time, 104 | ] 105 | + row 106 | ) 107 | for time, row in zip(absTimingSeq, valueSeq) 108 | ] 109 | 110 | return absDataList 111 | 112 | 113 | def _getSmallestDifference(inputList, targetVal): 114 | """ 115 | Returns the value in inputList that is closest to targetVal 116 | 117 | Iteratively splits the dataset in two, so it should be pretty fast 118 | """ 119 | targetList = inputList[:] 120 | retVal = None 121 | while True: 122 | # If we're down to one value, stop iterating 123 | if len(targetList) == 1: 124 | retVal = targetList[0] 125 | break 126 | halfPoint = int(len(targetList) / 2.0) - 1 127 | a = targetList[halfPoint] 128 | b = targetList[halfPoint + 1] 129 | 130 | leftDiff = abs(targetVal - a) 131 | rightDiff = abs(targetVal - b) 132 | 133 | # If the distance is 0, stop iterating, the targetVal is present 134 | # in the inputList 135 | if leftDiff == 0 or rightDiff == 0: 136 | retVal = targetVal 137 | break 138 | 139 | # Look at left half or right half 140 | if leftDiff < rightDiff: 141 | targetList = targetList[: halfPoint + 1] 142 | else: 143 | targetList = targetList[halfPoint + 1 :] 144 | 145 | return retVal 146 | 147 | 148 | def _getNearestMappingIndexList(fromValList, toValList): 149 | """ 150 | Finds the indicies for data points that are closest to each other. 151 | 152 | The inputs should be in relative time, scaled from 0 to 1 153 | e.g. if you have [0, .1, .5., .9] and [0, .1, .2, 1] 154 | will output [0, 1, 1, 2] 155 | """ 156 | 157 | indexList = [] 158 | for fromTimestamp in fromValList: 159 | smallestDiff = _getSmallestDifference(toValList, fromTimestamp) 160 | i = toValList.index(smallestDiff) 161 | indexList.append(i) 162 | 163 | return indexList 164 | 165 | 166 | def morphDataLists(fromList, toList, stepList): 167 | """ 168 | Iteratively morph fromList into toList using the values 0 to 1 in stepList 169 | 170 | stepList: a value of 0 means no change and a value of 1 means a complete 171 | change to the other value 172 | """ 173 | 174 | # If there are more than 1 pitch value, then we align the data in 175 | # relative time. 176 | # Each data point comes with a timestamp. The earliest timestamp is 0 177 | # and the latest timestamp is 1. Using this method, for each relative 178 | # timestamp in the source list, we find the closest relative timestamp 179 | # in the target list. Just because two pitch values have the same index 180 | # in the source and target lists does not mean that they correspond to 181 | # the same speech event. 182 | fromListRel, fromStartTime, fromEndTime = _makeTimingRelative(fromList) 183 | toListRel = _makeTimingRelative(toList)[0] 184 | 185 | # If fromList has more points, we'll have flat areas 186 | # If toList has more points, we'll might miss peaks or valleys 187 | fromTimeList = [dataTuple[0] for dataTuple in fromListRel] 188 | toTimeList = [dataTuple[0] for dataTuple in toListRel] 189 | indexList = _getNearestMappingIndexList(fromTimeList, toTimeList) 190 | alignedToPitchRel = [toListRel[i] for i in indexList] 191 | 192 | for stepAmount in stepList: 193 | newPitchList = [] 194 | 195 | # Perform the interpolation 196 | for fromTuple, toTuple in zip(fromListRel, alignedToPitchRel): 197 | fromTime, fromValue = fromTuple 198 | toTime, toValue = toTuple 199 | 200 | # i + 1 b/c i_0 = 0 = no change 201 | newValue = fromValue + (stepAmount * (toValue - fromValue)) 202 | newTime = fromTime + (stepAmount * (toTime - fromTime)) 203 | 204 | newPitchList.append((newTime, newValue)) 205 | 206 | newPitchList = _makeTimingAbsolute(newPitchList, fromStartTime, fromEndTime) 207 | 208 | yield stepAmount, newPitchList 209 | 210 | 211 | def morphChunkedDataLists(fromDataList, toDataList, stepList): 212 | """ 213 | Morph one set of data into another, in a stepwise fashion 214 | 215 | A convenience function. Given a set of paired data lists, 216 | this will morph each one individually. 217 | 218 | Returns a single list with all data combined together. 219 | """ 220 | 221 | assert len(fromDataList) == len(toDataList) 222 | 223 | # Morph the fromDataList into the toDataList 224 | outputList = [] 225 | for x, y in zip(fromDataList, toDataList): 226 | 227 | # We cannot morph a region if there is no data or only 228 | # a single data point for either side 229 | if (len(x) < 2) or (len(y) < 2): 230 | continue 231 | 232 | tmpList = [ 233 | outputPitchList for _, outputPitchList in morphDataLists(x, y, stepList) 234 | ] 235 | outputList.append(tmpList) 236 | 237 | # Transpose list 238 | finalOutputList = outputList.pop(0) 239 | for subList in outputList: 240 | for i, subsubList in enumerate(subList): 241 | finalOutputList[i].extend(subsubList) 242 | 243 | return finalOutputList 244 | 245 | 246 | def morphAveragePitch(fromDataList, toDataList): 247 | """ 248 | Adjusts the values in fromPitchList to have the same average as toPitchList 249 | 250 | Because other manipulations can alter the average pitch, morphing the pitch 251 | is the last pitch manipulation that should be done 252 | 253 | After the morphing, the code removes any values below zero, thus the 254 | final average might not match the target average. 255 | """ 256 | 257 | timeList, fromPitchList = zip(*fromDataList) 258 | toPitchList = [pitchVal for _, pitchVal in toDataList] 259 | 260 | # Zero pitch values aren't meaningful, so filter them out if they are 261 | # in the dataset 262 | fromListNoZeroes = [val for val in fromPitchList if val > 0] 263 | fromAverage = sum(fromListNoZeroes) / float(len(fromListNoZeroes)) 264 | 265 | toListNoZeroes = [val for val in toPitchList if val > 0] 266 | toAverage = sum(toListNoZeroes) / float(len(toListNoZeroes)) 267 | 268 | newPitchList = [val - fromAverage + toAverage for val in fromPitchList] 269 | 270 | # finalAverage = sum(newPitchList) / float(len(newPitchList)) 271 | 272 | # Removing zeroes and negative pitch values 273 | retDataList = [ 274 | (time, pitchVal) 275 | for time, pitchVal in zip(timeList, newPitchList) 276 | if pitchVal > 0 277 | ] 278 | 279 | return retDataList 280 | 281 | 282 | def morphRange(fromDataList, toDataList): 283 | """ 284 | Changes the scale of values in one distribution to that of another 285 | 286 | ie The maximum value in fromDataList will be set to the maximum value in 287 | toDataList. The 75% largest value in fromDataList will be set to the 288 | 75% largest value in toDataList, etc. 289 | 290 | Small sample sizes will yield results that are not very meaningful 291 | """ 292 | 293 | # Isolate and sort pitch values 294 | fromPitchList = [dataTuple[1] for dataTuple in fromDataList] 295 | toPitchList = [dataTuple[1] for dataTuple in toDataList] 296 | 297 | fromPitchListSorted = sorted(fromPitchList) 298 | toPitchListSorted = sorted(toPitchList) 299 | 300 | # Bin pitch values between 0 and 1 301 | fromListRel = makeSequenceRelative(fromPitchListSorted)[0] 302 | toListRel = makeSequenceRelative(toPitchListSorted)[0] 303 | 304 | # Find each values closest equivalent in the other list 305 | indexList = _getNearestMappingIndexList(fromListRel, toListRel) 306 | 307 | # Map the source pitch to the target pitch value 308 | # Pitch value -> get sorted position -> get corresponding position in 309 | # target list -> get corresponding pitch value = the new pitch value 310 | retList = [] 311 | for time, pitch in fromDataList: 312 | fromI = fromPitchListSorted.index(pitch) 313 | toI = indexList[fromI] 314 | newPitch = toPitchListSorted[toI] 315 | 316 | retList.append((time, newPitch)) 317 | 318 | return retList 319 | -------------------------------------------------------------------------------- /promo/morph_utils/plot_morphed_data.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Sep 18, 2013 3 | 4 | @author: timmahrt 5 | """ 6 | 7 | import math 8 | 9 | try: 10 | import matplotlib.pyplot as plt 11 | import matplotlib.cm as cm 12 | except ImportError: 13 | hasMatplotlib = False 14 | else: 15 | hasMatplotlib = True 16 | 17 | 18 | def _matplotlibCheck(): 19 | if not hasMatplotlib: 20 | raise ImportError( 21 | "Matplotlib required to generate plots. " 22 | "Install matplotlib or disable plotting" 23 | ) 24 | 25 | 26 | def plotSinglePitchTrack(fromTuple, fnFullPath): 27 | _matplotlibCheck() 28 | 29 | fig, (ax0) = plt.subplots(nrows=1) 30 | 31 | # Old data 32 | plot1 = ax0.plot( 33 | fromTuple, 34 | color="red", 35 | linewidth=2, 36 | ) 37 | 38 | plt.ylabel("Pitch (Hz)") 39 | plt.xlabel("Sample number") 40 | 41 | plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.0) 42 | # plt.legend([plot1, plot2, plot3], ["From", "To", "Merged line"]) 43 | 44 | plt.savefig(fnFullPath, dpi=300, bbox_inches="tight") 45 | plt.close(fig) 46 | 47 | 48 | def plotTwoPitchTracks(fromTuple, toTuple, fnFullPath): 49 | _matplotlibCheck() 50 | 51 | fig, (ax0, ax1) = plt.subplots(nrows=2) 52 | 53 | # Old data 54 | plot1 = ax0.plot( 55 | fromTuple, 56 | color="red", 57 | linewidth=2, 58 | # label="From" 59 | ) 60 | ax0.set_title("Mary is going to the mall (statement)") 61 | 62 | plot2 = ax1.plot( 63 | toTuple, 64 | color="red", 65 | linewidth=2, 66 | # label="From" 67 | ) 68 | 69 | ax1.set_title("Mary is going to the mall (question)") 70 | 71 | plt.ylabel("Pitch (Hz)") 72 | plt.xlabel("Sample number") 73 | 74 | plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.0) 75 | # plt.legend([plot1, plot2, plot3], ["From", "To", "Merged line"]) 76 | 77 | plt.savefig(fnFullPath, dpi=300, bbox_inches="tight") 78 | plt.close(fig) 79 | 80 | 81 | def plotF0(fromTuple, toTuple, mergeTupleList, fnFullPath): 82 | """ 83 | Plots the original data in a graph above the plot of the dtw'ed data 84 | """ 85 | _matplotlibCheck() 86 | 87 | fig, (ax0) = plt.subplots(nrows=1) 88 | 89 | # Old data 90 | plot1 = ax0.plot(fromTuple[0], fromTuple[1], color="red", linewidth=2, label="From") 91 | plot2 = ax0.plot(toTuple[0], toTuple[1], color="blue", linewidth=2, label="To") 92 | ax0.set_title("Plot of F0 Morph") 93 | plt.ylabel("Pitch (hz)") 94 | plt.xlabel("Time (s)") 95 | 96 | # Merge data 97 | colorValue = 0 98 | colorStep = 255.0 / len(mergeTupleList) 99 | for timeList, valueList in mergeTupleList: 100 | colorValue += colorStep 101 | hexValue = "#%02x0000" % int(255 - colorValue) 102 | if int(colorValue) == 255: 103 | ax0.plot( 104 | timeList, 105 | valueList, 106 | color=hexValue, 107 | linewidth=1, 108 | label="Merged line, final iteration", 109 | ) 110 | else: 111 | ax0.plot(timeList, valueList, color=hexValue, linewidth=1) 112 | 113 | plt.legend(loc=1, borderaxespad=0.0) 114 | # plt.legend([plot1, plot2, plot3], ["From", "To", "Merged line"]) 115 | 116 | plt.savefig(fnFullPath, dpi=300, bbox_inches="tight") 117 | plt.close(fig) 118 | 119 | 120 | def plotIntensity(fromDataList, toDataList, mergeTupleList, controlPoints, fnFullPath): 121 | def mag2DB(valueList): 122 | stepSize = 500 123 | 124 | returnList = [] 125 | for i in range(int(math.ceil(len(valueList) / float(stepSize)))): 126 | subList = valueList[i * stepSize : (i + 1) * stepSize] 127 | value = math.sqrt(sum([val ** 2 for val in subList])) 128 | 129 | returnList.append(20.0 * math.log10(value)) 130 | 131 | return returnList 132 | 133 | _matplotlibCheck() 134 | 135 | fromDataList = mag2DB(fromDataList) 136 | toDataList = mag2DB(toDataList) 137 | 138 | print(max(fromDataList)) 139 | print(max(toDataList)) 140 | 141 | fig, ax0 = plt.subplots(nrows=1) 142 | # fig, (ax0, ax1, ax2) = plt.subplots(nrows=3) 143 | 144 | # Old data 145 | plot1 = ax0.plot(fromDataList, color="red", linewidth=2, label="From") 146 | # plot1.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.) 147 | 148 | plot2 = ax0.plot(toDataList, color="blue", linewidth=2, label="To") 149 | # plot2.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.) 150 | 151 | ax0.set_title("Plot of Intensity Morph") 152 | plt.ylabel("RMS intensity (db)") 153 | plt.xlabel("Time (s)") 154 | 155 | # Merge data 156 | 157 | colorValue = 0 158 | colorStep = 255.0 / len(mergeTupleList) 159 | for valueList in mergeTupleList: 160 | 161 | valueList = mag2DB(valueList) 162 | 163 | print(max(valueList)) 164 | 165 | colorValue += colorStep 166 | hexValue = "#%02x0000" % int(255 - colorValue) 167 | xValues = [i for i in range(len(valueList))] 168 | if colorValue == 255.0: 169 | plot3 = ax0.plot( 170 | xValues, valueList, color=hexValue, linewidth=1, label="Merged line" 171 | ) 172 | else: 173 | plot3 = ax0.plot(xValues, valueList, color=hexValue, linewidth=1) 174 | 175 | controlPoints = mag2DB(controlPoints) 176 | xValues = [i for i in range(len(controlPoints))] 177 | ax0.scatter(xValues, controlPoints, color="pink", label="Control points") 178 | 179 | plt.legend( 180 | loc=1, 181 | # bbox_to_anchor=(1.05, 1), loc=2, 182 | borderaxespad=0.0, 183 | ) 184 | 185 | plt.savefig(fnFullPath, dpi=300, bbox_inches="tight") 186 | plt.close(fig) 187 | 188 | 189 | def plotDuration( 190 | fromDurationList, toDurationList, resultDataList, labelList, fnFullPath 191 | ): 192 | 193 | _matplotlibCheck() 194 | 195 | dataList = ( 196 | [ 197 | fromDurationList, 198 | ] 199 | + resultDataList 200 | + [ 201 | toDurationList, 202 | ] 203 | ) 204 | 205 | # Sanity check 206 | for subList in dataList: 207 | assert len(subList) == len(fromDurationList) 208 | 209 | # Constants 210 | colorTuple = ["red", "yellow", "blue"] 211 | width = 0.2 212 | n = len(dataList) 213 | iterN = range(n) 214 | 215 | # Pre-plotting work 216 | fig, ax0 = plt.subplots(nrows=1) 217 | 218 | # Labels 219 | ax0.set_title("Plot of Duration Morph") 220 | plt.ylabel("Duration (s)") 221 | plt.xlabel("Iterations") 222 | plt.title("Duration morph") 223 | 224 | # Draw x ticks (iteration numbers) 225 | xLabelList = [] 226 | for i in range(len(resultDataList)): 227 | xLabelList.append(str(i + 1)) # 1 based 228 | 229 | xLabelList = ( 230 | [ 231 | "From", 232 | ] 233 | + xLabelList 234 | + [ 235 | "To", 236 | ] 237 | ) 238 | iterN2 = [val + width / 2.0 for val in iterN] 239 | plt.xticks(iterN2, xLabelList) 240 | 241 | # Plot the data 242 | transposedList = zip(*dataList) 243 | bottom = [0 for i in iterN] 244 | for i, row in enumerate(transposedList): 245 | 246 | # Horizontal line that sits on the tallest column 247 | ax0.axhline(bottom[0], color="black") 248 | 249 | # Word label that appears after the final column 250 | bottomAppend = max(row) 251 | ax0.text(n - 1.0 + 0.5, (bottom[0] + (bottomAppend / 3.0)), labelList[i]) 252 | 253 | # The column data 254 | ax0.bar( 255 | x=iterN, 256 | height=row, 257 | bottom=bottom, 258 | width=width, 259 | alpha=0.6, 260 | color=colorTuple[i % 3], 261 | ) 262 | 263 | # Update the start positions for the next iteration 264 | 265 | bottom = [origBottom + bottomAppend for origBottom in bottom] 266 | 267 | # Draw a final horizontal line over the highest column 268 | ax0.axhline(bottom[0], color="black") 269 | 270 | # Save and cleanup 271 | plt.savefig(fnFullPath, dpi=300, bbox_inches="tight") 272 | plt.close(fig) 273 | -------------------------------------------------------------------------------- /promo/morph_utils/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Apr 2, 2015 3 | 4 | @author: tmahrt 5 | """ 6 | 7 | import os 8 | 9 | from praatio import textgrid 10 | 11 | 12 | def getIntervals(fn, tierName, filterFunc=None, includeUnlabeledRegions=False): 13 | """ 14 | Get information about the 'extract' tier, used by several merge scripts 15 | """ 16 | 17 | tg = textgrid.openTextgrid(fn, includeEmptyIntervals=includeUnlabeledRegions) 18 | 19 | tier = tg.getTier(tierName) 20 | 21 | entries = tier.entries 22 | if filterFunc is not None: 23 | entries = [entry for entry in entries if filterFunc(entry)] 24 | 25 | return entries 26 | 27 | 28 | def makeDir(path): 29 | if not os.path.exists(path): 30 | os.mkdir(path) 31 | 32 | 33 | def generateStepList(numSteps, includeZero=False): 34 | assert numSteps > 0 35 | 36 | stepList = [] 37 | if includeZero: 38 | stepList.append(0) 39 | 40 | for i in range(numSteps): 41 | stepList.append((i + 1) / float((numSteps))) 42 | 43 | return stepList 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | """ 4 | Created on Aug 29, 2014 5 | 6 | @author: tmahrt 7 | """ 8 | from setuptools import setup 9 | import io 10 | 11 | setup( 12 | name="promo", 13 | python_requires=">3.6.0", 14 | version="2.0.0", 15 | author="Tim Mahrt", 16 | author_email="timmahrt@gmail.com", 17 | url="https://github.com/timmahrt/ProMo", 18 | package_dir={"promo": "promo"}, 19 | packages=["promo", "promo.morph_utils"], 20 | license="LICENSE", 21 | install_requires=["praatio ~= 6.0", "typing_extensions"], 22 | description=( 23 | "Library for manipulating pitch and duration in an " 24 | "algorithmic way, for resynthesizing speech" 25 | ), 26 | long_description=io.open("README.rst", "r", encoding="utf-8").read(), 27 | # install_requires=[], # No requirements! # requires 'from setuptools import setup' 28 | ) 29 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmahrt/ProMo/c1d8252453c28592ce4c09dc1d690a4e3b8550cc/tests/__init__.py -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmahrt/ProMo/c1d8252453c28592ce4c09dc1d690a4e3b8550cc/tests/integration/__init__.py -------------------------------------------------------------------------------- /tests/integration/test_integration.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Oct 20, 2016 3 | 4 | @author: tmahrt 5 | 6 | Runs integration tests 7 | 8 | The examples were all written as scripts. They weren't meant to be 9 | imported or run from other code. So here, the integration test is just 10 | importing the scripts, which causes them to execute. If the code completes 11 | with no errors, then the code is at least able to complete. 12 | 13 | Testing whether or not the code actually did what it is supposed to is 14 | another issue and will require some refactoring. 15 | 16 | prosody morph requires praat to do the final manipulation. For the 17 | integration tests run without access to praat, that last step 18 | will be skipped (and unverified) however the test will still pass. 19 | """ 20 | 21 | import unittest 22 | import os 23 | from os.path import join 24 | import sys 25 | from pathlib import Path 26 | 27 | from praatio.utilities import errors 28 | from promo import duration_morph 29 | from promo.morph_utils import utils as morph_utils 30 | 31 | _root = os.path.join(Path(__file__).parents[2], "examples") 32 | sys.path.append(_root) 33 | 34 | 35 | class TestIntegration(unittest.TestCase): 36 | """Integration tests""" 37 | 38 | def test_modify_pitch_accent(self): 39 | """Running 'modify_pitch_accent_example.py'""" 40 | print("\nmodify_pitch_accent_example.py" + "\n" + "-" * 10) 41 | try: 42 | import modify_pitch_accent_example 43 | except errors.FileNotFound: 44 | pass 45 | 46 | def test_duration_manipulation(self): 47 | """Running 'duration_manipulation_example.py'""" 48 | print("\nduration_manipulation_example.py" + "\n" + "-" * 10) 49 | try: 50 | import duration_manipulation_example 51 | except errors.FileNotFound: 52 | pass 53 | 54 | def test_pitch_morph(self): 55 | """Running 'pitch_morph_example.py'""" 56 | print("\npitch_morph_example.py" + "\n" + "-" * 10) 57 | try: 58 | import pitch_morph_example 59 | except errors.FileNotFound: 60 | pass 61 | 62 | def test_pitch_morph_to_contour(self): 63 | """Running 'pitch_morph_to_pitch_contour.py'""" 64 | print("\npitch_morph_to_pitch_contour.py" + "\n" + "-" * 10) 65 | try: 66 | import pitch_morph_to_pitch_contour 67 | except errors.FileNotFound: 68 | pass 69 | 70 | def test_getMorphParameters(self): 71 | filterFunc = None 72 | includeUnlabeledRegions = False 73 | duration_morph.getMorphParameters( 74 | self.fromTGFN, 75 | self.toTGFN, 76 | self.tierName, 77 | filterFunc, 78 | includeUnlabeledRegions, 79 | ) 80 | 81 | def test_getManipulatedParameters(self): 82 | twentyPercentMore = lambda x: (x * 1.20) 83 | filterFunc = None 84 | includeUnlabeledRegions = True 85 | duration_morph.getManipulatedParamaters( 86 | self.fromTGFN, 87 | self.tierName, 88 | twentyPercentMore, 89 | filterFunc, 90 | includeUnlabeledRegions, 91 | ) 92 | 93 | def test_changeDuration(self): 94 | filterFunc = None 95 | includeUnlabeledRegions = False 96 | durationParams = duration_morph.getMorphParameters( 97 | self.fromTGFN, 98 | self.toTGFN, 99 | self.tierName, 100 | filterFunc, 101 | includeUnlabeledRegions, 102 | ) 103 | 104 | stepList = morph_utils.generateStepList(3) 105 | outputName = "mary1_dur_morph" 106 | # praatEXE = r"C:\Praat.exe" # Windows 107 | praatEXE = "/Applications/Praat.app/Contents/MacOS/Praat" # Mac 108 | try: 109 | duration_morph.changeDuration( 110 | self.fromWavFN, 111 | durationParams, 112 | stepList, 113 | outputName, 114 | outputMinPitch=self.minPitch, 115 | outputMaxPitch=self.maxPitch, 116 | praatEXE=praatEXE, 117 | ) 118 | except errors.FileNotFound: 119 | pass 120 | 121 | def test_textgridMorphDuration(self): 122 | duration_morph.textgridMorphDuration(self.fromTGFN, self.toTGFN) 123 | 124 | def test_textgridManipulateDuration(self): 125 | filterFunc = None 126 | includeUnlabeledRegions = False 127 | durationParams = duration_morph.getMorphParameters( 128 | self.fromTGFN, 129 | self.toTGFN, 130 | self.tierName, 131 | filterFunc, 132 | includeUnlabeledRegions, 133 | ) 134 | 135 | duration_morph.outputMorphTextgrids( 136 | self.fromTGFN, 137 | durationParams, 138 | [ 139 | 1, 140 | ], 141 | join(self.root, "outputName.TextGrid"), 142 | ) 143 | 144 | def setUp(self): 145 | unittest.TestCase.setUp(self) 146 | 147 | root = os.path.join(_root, "files") 148 | self.oldRoot = os.getcwd() 149 | os.chdir(_root) 150 | self.startingList = os.listdir(root) 151 | self.startingDir = os.getcwd() 152 | 153 | self.root = root 154 | 155 | self.fromWavFN = join(self.root, "mary1.wav") 156 | self.fromTGFN = join(self.root, "mary1.TextGrid") 157 | self.toTGFN = join(self.root, "mary2.TextGrid") 158 | self.tierName = "words" 159 | self.minPitch = 50 160 | self.maxPitch = 350 161 | 162 | def tearDown(self): 163 | """Remove any files generated during the test""" 164 | # unittest.TestCase.tearDown(self) 165 | 166 | root = os.path.join(".", "files") 167 | endingList = os.listdir(root) 168 | endingDir = os.getcwd() 169 | rmList = [fn for fn in endingList if fn not in self.startingList] 170 | 171 | if self.oldRoot == root: 172 | for fn in rmList: 173 | fnFullPath = os.path.join(root, fn) 174 | if os.path.isdir(fnFullPath): 175 | os.rmdir(fnFullPath) 176 | else: 177 | os.remove(fnFullPath) 178 | 179 | os.chdir(self.oldRoot) 180 | 181 | 182 | if __name__ == "__main__": 183 | unittest.main() 184 | --------------------------------------------------------------------------------