├── .gitignore ├── ChangeLog.md ├── LICENSE ├── README.md ├── dftt_timecode ├── __init__.py ├── core │ └── dftt_timecode.py ├── error.py └── pattern.py ├── pytest.ini ├── requirements.txt ├── setup.py └── test ├── __init__.py └── test_dftt_timecode.py /.gitignore: -------------------------------------------------------------------------------- 1 | dftt_timecode/__pycache__ 2 | .vscode 3 | __pycache__ 4 | .venv 5 | .DS_Store 6 | .env 7 | dftt_timecode.egg-info 8 | .pytest_cache 9 | .DS_Store 10 | playground.ipynb -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | ## V0.0.9 3 | First Public Release. 4 | 5 | ## V0.0.10 6 | 7 | 添加 Add: 8 | 9 | - 使用DfttTimecode对象初始化新DfttTimecode对象 10 | 11 | Using a DfttTimecode object to instance a new DfttTimecode object. 12 | 13 | - DfttTimecode类的float和int方法 14 | 15 | class method float() and int() for DfttTimecode class 16 | 17 | - DfttTimecode类的precise_timestamp属性 18 | 19 | attribute precise_timestamp for class DfttTimecode 20 | 21 | 修改 Modify: 22 | 23 | - DfttTimecode运算符在未定义/非法操作时将会报错 24 | 25 | Will raise an error when DfttTimecode operators meet undefined circumstances or illegal operations. 26 | 27 | - DfttTimecode运算符的大小比较规则 28 | 29 | Compare functions of DfttTimecode operators. 30 | 31 | - 使用SMPTE NDF格式字符串新建时码类对象时,若强制drop_frame为True,则新建得到的对象为SMPTE DF格式时码 32 | 33 | ## V0.0.11 34 | 添加 Add: 35 | - `__str__`方法,返回DfttTimecode对象的时间码值 36 | 37 | `__str__`method,return timecode value for DfttTimecode object 38 | 39 | - DfttTimecode单元测试(使用pytest) 40 | 41 | Unit test for DfttTimecode (Using pytest) 42 | 43 | 修改 Modify: 44 | - 对丢帧的检测条件添加有关23.98/23.976的判定 45 | 46 | Add 23.98/23.976FPS to drop frame conditions 47 | 48 | - `+` `-`运算符对相加的两个DfttTimecode对象的strict属性进行或运算 49 | 50 | `+` `-`operators performs an or operation on the strict property of two DfttTimecode objects that are added together 51 | 52 | - 比较运算符,比如`==` `>` `>=`等,在对两个DfttTimecode对象进行比较的时候会先对两个对象的帧率进行判定,若帧率不同抛出异常 53 | 54 | Comparison operators, such as `==`, `>`, `>=`, in the comparison of two DfttTimecode objects will first compare the frame rate of the two objects, if the frame rate is different throw an exception 55 | 56 | - `print(self)` 将会输出基于类型的时间码字符串 57 | 58 | `print(self)` will output a timecode string 59 | 60 | 修复 BugFix: 61 | - `timecode_output(self, dest_type, output_part)` 中`output_part = 3`时错误返回分钟数据的问题 62 | 63 | Addressed a problem when `output_part = 3` in `timecode_output(self, dest_type, output_part)` would return minute value in timecode value 64 | 65 | ## V0.0.12 66 | 修复 BugFix: 67 | - 修复DLP正则表达式错误导致范围在50-99 150-199的DLP tick不能被匹配的问题。 68 | 这一bug会导致使用形如`'00:00:27:183'`的字符串初始化dlp时间码对象时的报错。 69 | 70 | Fix DLP pattern error causing DLP ticks range [50-99] [150-199] cannot be matched. 71 | This bug will cause error when using strings like `'00:00:27:183'` to initilize a dlp timecode object. 72 | 73 | 74 | V0.0.13 changelog 75 | ## V0.0.13 76 | 重构 Refactor: 77 | - 使用`f-string`处理字符串格式输出 78 | 79 | Handling string format output using `f-string` 80 | 81 | - 重构时间码输出函数,减少代码重复 82 | 83 | Refactor the time code output function to reduce code duplication 84 | 85 | 添加 Add: 86 | - 添加`get_audio_sample_count` 方法用于正确输出TC时间戳下的音频采样数, 解决issue [#9](https://github.com/OwenYou/dftt_timecode/issues/9) 87 | 88 | Add `get_audio_sample_count` method for correctly outputting the count of audio samples at TC timestamps,solve issue [#9](https://github.com/OwenYou/dftt_timecode/issues/9) 89 | 90 | 弃用 Deprecate: 91 | - 使用`functools.singledispatchmethod` 代替 `dispatch.InstanceMethodDispatch` 92 | 93 | Use`functools.singledispatchmethod` instead of `dispatch.InstanceMethodDispatch` 94 | 95 | ## V0.0.14 96 | 修复 BugFix: 97 | - 修复v0.0.13打包后不包含core,导致库无法使用的问题。 98 | 99 | Fix v0.0.13 import failure. Caused by missing dftt_timecode.core while packing. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 489 | USA 490 | 491 | Also add information on how to contact you by electronic and paper mail. 492 | 493 | You should also get your employer (if you work as a programmer) or your 494 | school, if any, to sign a "copyright disclaimer" for the library, if 495 | necessary. Here is a sample; alter the names: 496 | 497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 498 | library `Frob' (a library for tweaking knobs) written by James Random 499 | Hacker. 500 | 501 | , 1 April 1990 502 | Ty Coon, President of Vice 503 | 504 | That's all there is to it! 505 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dftt_timecode 2 | 3 | [![pypi](https://img.shields.io/badge/pypi-0.0.14-brightgreen)](https://pypi.org/project/dftt-timecode/) 4 | [![python](https://img.shields.io/badge/python-3-blue)]() 5 | [![GitHub license](https://img.shields.io/badge/license-LGPL2.1-green)](https://github.com/OwenYou/dftt_timecode/blob/main/LICENSE) 6 | 7 | 8 | ## 1. 简介 Introduction 9 | 10 | 为影视行业设计的Python时码库,支持HFR高帧率以及其他丰富的功能。 11 | 12 | Python timecode library for film and TV industry supports HFR and a bunch of cool features. 13 | 14 | DFTT是Department of Film and TV Technology of Beijing Film Academy的简称。 15 | 16 | DFTT stands for the short of Department of Film and Tv Technology of Beijing Film Academy. 17 | 18 | ### 1.1 主要功能 Main Features 19 | 20 | - 支持多种时码格式输入,如SMPTE、SRT、DLP(Cine Canvas)、FFMPEG、FCPX、帧号、现实时间等。 21 | 22 | Multiple timecode format support, including SMPTE, SRT, DLP(Cine Canvas), FFMPEG, FCPX, frame count, time, etc. 23 | 24 | - 支持高帧率,目前支持0.01-999.99fps范围内的帧率。 25 | 26 | High frame rate support, currently supports frame range from 0.01 to 999.99fps. 27 | 28 | - 支持严格的丢帧/非丢帧SMPTE格式。 29 | 30 | Strictly support SMPTE DF/NDF format. 31 | 32 | - 目前支持-99到99小时时间范围。 33 | 34 | Currently support time range from -99 to 99 hours. 35 | 36 | - 支持**严格**模式,在该模式下时码会在0-24小时范围内循环,任意超出该范围的时码会自动转换至范围内。 37 | 38 | **Strict** Mode support, the timecode will circulate from 0 to 24 hours, any timecode outside this range will be automatically converted to a timecode inside it. 39 | 40 | - 内部以高精度时间戳进行存储和计算,各类FPS转换、时码格式转换输出都能保持最高精度。 41 | 42 | Uses high precision timestamp inside for storage and calculation, any FPS conversion or format conversion output can maintain their highest precision. 43 | 44 | - 常用运算符支持,包括时码与时码、时码与数字的各类加减乘除、比较运算。 45 | 46 | Common operator support, including addition, subtraction, multiplication, division, and comparison operator between two timecode objects or a timecode object and a number. 47 | 48 | ## 2. 如何安装 How to install 49 | 50 | ```python 51 | python pip install dftt_timecode 52 | ``` 53 | 54 | ### 2.1 包依赖 Package dependency 55 | 56 | - fractions 57 | - logging 58 | - math 59 | - functools 60 | - re 61 | 62 | ## 3. 使用方法说明 How to use 63 | 64 | ### 3.1 导入 Import 65 | 66 | ```python 67 | from dftt_timecode import DfttTimecode 68 | ``` 69 | 70 | ### 3.2 新建时码类对象 Create timecode objects 71 | 72 | ```Python 73 | a = DfttTimecode('01:00:00:00', 'auto', fps=24, drop_frame=False, strict=True) 74 | #以SMPTE非丢帧时码新建对象 Create object using SMPTE NDF 75 | a = DfttTimecode('1000f', 'auto', fps=119.88, drop_frame=True, strict=True) 76 | #以帧数新建对象 Create object using frame count 77 | a = DfttTimecode('3600.0s', 'auto', fps=Fraction(60000,1001), drop_frame=True, strict=True) 78 | #以时间秒新建对象 Create object using time 79 | a = DfttTimecode(-1200, 'auto', fps=23.976, drop_frame=False, strict=False) 80 | #以int帧数新建对象 Create object using int frame count 81 | ``` 82 | 83 | 对DfttTimecode()相关参数的详细说明,请查阅`4.1 DfttTimecode()参数说明`。 84 | 85 | For detailed parameters descriptions of DfttTimecode(), please refer to chapter `4.1 Parameters Descriptions of DfttTimecode()`. 86 | 87 | ### 3.3 操作时码类对象 Operate DfttTimecode objects 88 | 89 | ```python 90 | a = DfttTimecode('01:00:00:00', 'auto', fps=24, drop_frame=False, strict=True) 91 | assert a.type == 'smpte' 92 | assert a.fps == 24 93 | assert a.framecount == 86400 94 | assert a.timestamp == 3600.0 95 | assert a.is_drop_frame == False 96 | assert a.is_strict == True 97 | assert a.timecode_output('smpte',output_part=0) == '01:00:00:00' 98 | assert a.timecode_output('srt',output_part=1) == '01' 99 | 100 | a = DfttTimecode('25:00:01:103', 'auto', fps=120, drop_frame=False, strict=False) 101 | a.set_fps(24) 102 | assert a.fps == 24 103 | assert a.timecode_output('smpte') == '25:00:01:21' 104 | a.set_strict(strict=True) 105 | assert a.timecode_output('smpte') == '01:00:01:21' 106 | a.set_strict(strict=False) 107 | assert a.is_strict == False 108 | ``` 109 | 对时码类对象操作的详细说明,请查阅`4.2 时码类对象操作说明`。 110 | 111 | For detailed descriptions of DfttTimecode objects' operations, please refer to chapter `4.2 Descriptions of DfttTimecode class operations`. 112 | 113 | ### 3.4 时码类运算符 Operators of DfttTimecode class 114 | 115 | ```python 116 | a = DfttTimecode('01:00:00:00', 'auto', fps=24, drop_frame=False, strict=True) 117 | b = DfttTimecode('01:12:34:12', 'auto', fps=24, drop_frame=False, strict=True) 118 | print(a) # (Timecode:01:00:00:00, Type:smpte, FPS:24.00 NDF, Strict) 119 | print(-a) # (Timecode:23:00:00:00, Type:smpte, FPS:24.00 NDF, Strict) 120 | print(a + b) # (Timecode:02:12:34:12, Type:smpte, FPS:24.00 NDF, Strict) 121 | print(a - b) # (Timecode:23:47:25:12, Type:smpte, FPS:24.00 NDF, Strict) 122 | print(a * 2) # (Timecode:02:00:00:00, Type:smpte, FPS:24.00 NDF, Strict) 123 | print(a / 2) # (Timecode:00:30:00:00, Type:smpte, FPS:24.00 NDF, Strict) 124 | print(a == b) # False 125 | print(a != b) # True 126 | print(a > b) # False 127 | print(a >= b) # False 128 | print(a < b) # True 129 | print(a <= b) # True 130 | ``` 131 | 132 | 对时码类运算符的详细说明,请查阅`4.3 时码类运算符说明` 133 | 134 | For detailed descriptions of DfttTimecode's operators, please refer to chapter `4.3 Descriptions of DfttTimecode class operators`. 135 | 136 | ## 4 参数详细说明 Detailed Parameters Descriptions 137 | 138 | ### 4.1 DfttTimecode()参数说明 Parameters Descriptions of DfttTimecode() 139 | 140 | #### 4.1.1 参数一览 General Descriptions 141 | 142 | ```python 143 | a = DfttTimecode(timecode_value, timecode_type, fps, drop_frame, strict) 144 | ``` 145 | 146 | - **`timecode_value`** 是时码对象的时码值,可以是`str`、`int`、`float`、`tuple`、`list`、`Fraction`类型。 147 | 148 | **`timecode_value`** is the value of a timecode, it can be a `str`, `int`, `float`, `tuple`, `list,` or a `Fraction`. 149 | 150 | - **`timecode_type`** 是时码对象的类型,是`str`类型,目前支持的时码类型包括`auto`、 `smpte`、 `srt`、 `ffmpeg`、 `fcpx`、 `frame`、 `time`。 151 | 152 | **`timecode_type`** must be a `str`, currently supported timecode types include `auto`, `smpte`, `srt`, `ffmpeg`, `fcpx`, `frame`, `time`. 153 | 154 | - **`fps`** 是时码对象的帧率,可以是`int`、`float`、`Fraction`类型。 155 | 156 | **`fps`** is the frame rate of the timecode object, can be an `int`, `float`, or a `Fraction`. 157 | 158 | - **`drop_frame`** 是时码对象的丢帧设置,是`bool`类型,只有当帧率存在丢帧格式时,这一设置才会生效,否则会强制将丢帧设为`False`。**`drop_frame `** 的默认值是`False`。 159 | 160 | **`drop_frame`** must be a `bool`, a timecode object can only be drop-frameable under specific frame rate settings, if not so, **`drop_frame`** will be forced to `False`. The default value of **`drop_frame`** is `False`. 161 | 162 | - **`strict`** 为时码对象设置严格模式,是`bool`类型。设为`True`后,负值和超过24小时的时码都将被转换为0-24小时范围内的值,例如`25:00:00:00`将被转换为`01:00:00:00`, `-01:00:00:00`将被转换为`23:00:00:00`。 **`strict`** 的默认值是`True`。 163 | 164 | **`strict`** will set the strict mode for a timecode object, it must be a `bool`. When set to `True`, negative timecode value and timecode value over 24 hours will be converted to a value inside the range 0 to 24 hours. For example, 25:00:00:00 will be converted to 01:00:00:00, -01:00:00:00 will be converted to 23:00:00:00. The default value of **`strict`** is `True`. 165 | #### 4.1.2 timecode_value 166 | 167 | **`timecode_value`** 决定了时码对象的时间值,DfttTimecode支持以多种类型的数据初始化时间值,且都支持负数。下面详细列出了各个数据类型对应的(可选)初始化方式: 168 | 169 | **`timecode_value`** determines the actual time of a timecode object. DfttTimecode supports initialize time by different data types, including negative numbers. The following table lists different data types and their supported initialization methods. 170 | 171 | | 数据类型
Data type | 支持的初始化方式
Supported initialization methods | 172 | | :---------------------: | :-------------------------------------------------------: | 173 | | `str` | `auto`, `smpte`, `srt`, `ffmpeg`, `fcpx`, `frame`, `time` | 174 | | `int` | `auto`, `frame`, `time` | 175 | | `float` | `auto`, `time` | 176 | | `tuple` | `auto`, `time` | 177 | | `list` | `auto`, `time` | 178 | | `fraction` | `auto`, `time` | 179 | 180 | 目前,DfttTimecode不支持以小数为单位的帧计数方式。 181 | 182 | Currently, DfttTimecode does not support frame count value in decimals. 183 | 184 | #### 4.1.3 timecode_type 185 | 186 | **`timecode_type`** 决定了时码对象的类型。DfttTimecode支持自动判断类型,也支持手动指定类型。在部分场景,如输入值是`int`类时,手动指定类型可以有效地区分以帧计数初始化时码和以时间初始化时码这两种行为。 187 | 188 | **`timecode_type`** determines the timecode type of a timecode object. DfttTimecode supports auto-configure timecode type as well as manual assign a timecode type. Under some circumstances, for example, the input data is `int`, manual assign a timecode type is a sufficient way to clarify whether the input is intended to be a frame or a time value. 189 | 190 | 下表列出了一系列样例 **`timecode_value`** 输入和他们在`'auto'`模式下对应的时码类型: 191 | 192 | The following sheet gives a list of example **`timecode_value`** input and their corresponding timecode type under `'auto'` mode. 193 | 194 | | timecode_value | auto模式下的type
Type under auto mode | 备注
Comment | 195 | | :-------------------------------: | :----------------------------------------: | :----------------------------------------------------------: | 196 | | `'01:00:00:00'` | `smpte` | **`drop_frame`** 将自动设为`False `
**`drop_frame`** will be set to `False` | 197 | | `'01:00:00;00'`, `'01:00:00;000'` | `smpte` | **`drop_frame`** 将自动设为`True`
**`drop_frame`** will be set to `True` | 198 | | `'01:00:00:000'` | `smpte` | 高帧率`smpte`时码,形式与`dlp`相近,如果输入值为`dlp`请强制指认 **`timecode_type`** 为`dlp`
High frame rate timecode, this format is similar to `dlp` timecode, so if your input timecode is actually in `dlp` format, please force **`timecode_type`** to `dlp` | 199 | | `'01:00:00,000'` | `srt` | 最后三位表示毫秒
The last three digits represent milliseconds | 200 | | `'01:00:00.00'` | `ffmpeg` | 最后两位表示秒的小数部分
The last two digits represent the decimal part of a second | 201 | | `'1/24s'`, `'1/24'` | `fcpx` | 可以省略“s”
*s* can be omitted | 202 | | `'1000f`, `'1000'` | `frame` | 可以省略“f”
*f* can be omitted | 203 | | `’1000s'`,`'1000.0'`,`'1000.0s'` | `time` | 可以省略“s”
*s* can be omitted | 204 | | `1000` | `frame` | `int` 数据会自动被认定为`frame`类
`int` data will be considered as a `frame` type | 205 | | `1000.0` | `time` | `float` 数据会自动被认定为`time`类
`float` data will be considered as a `time` type | 206 | | `[1000, 2000]` | `time` | 前者会成为`Fraction`的分子,后者成为分母
the former part will become the numerator of a `Fraction`, and the latter will become the dominator | 207 | | `(1000, 2000)` | `time` | 前者会成为`Fraction`的分子,后者成为分母
the former part will become the numerator of a `Fraction`, and the latter will become the dominator | 208 | | `Fraction(1000, 2000)` | `time` | 也可以直接传入一个`Fraction`对象
Just passing a `Fraction` object is also acceptable | 209 | 210 | 如果输入的时码值与所选择的时码类型不匹配,会抛出错误。 211 | 212 | If the input timecode value does not match the given timecode type, an error will be raised. 213 | 214 | #### 4.1.4 fps 215 | 216 | **`fps`** 是时码对象的帧率,可以是`int`、`float`、`Fraction`类型。 217 | 218 | **`fps`** is the frame rate of the timecode object, can be an `int`, `float` or a `Fraction`. 219 | 220 | #### 4.1.5 drop_frame 221 | 222 | **`drop_frame`** 是时码对象的丢帧设置,是`bool`类型,只有当帧率存在丢帧格式时,这一设置才会生效,否则会强制将丢帧设为`False`。**`drop_frame `** 的默认值是`False`。 223 | 224 | **`drop_frame`** must be a `bool`, a timecode object can only be drop-frameable under specific frame rate settings, if not so, **`drop_frame`** will be forced to `False`. The default value of **`drop_frame`** is `False`. 225 | 226 | 当 **`timecode_type`** 为`auto`时,会根据输入数据的分隔符自动设置 **`drop_frame`** 。 227 | 228 | When **`timecode_type`** is set to `auto`, **`drop_frame`** will be auto-set according to the separator of the input data. 229 | 230 | 当 **`timecode_value`** 在当前 **`drop_frame`** 设置下不合法时(仅当 **`timecode_type`** 为`smpte`时会有这种情况),将会报错。 231 | 232 | When **`timecode_value`** is illegal under the current **`drop_frame`** setting (this should only happen when **`timecode_type`** is `smpte`), there will be an error. 233 | 234 | #### 4.1.6 strict 235 | 236 | **`strict`** 为时码对象设置严格模式,是`bool`类型。设为`True`后,负值和超过24小时的时码都将被转换为0-24小时范围内的值,例如`25:00:00:00`将被转换为`01:00:00:00`, `-01:00:00:00`将被转换为`23:00:00:00`。**`strict`** 的默认值是`True`。 237 | 238 | **`strict`** will set the strict mode for a timecode object, it must be a `bool`. When set to `True`, negative timecode value and timecode value over 24 hours will be converted to a value inside the range 0 to 24 hours. For example, 25:00:00:00 will be converted to 01:00:00:00, -01:00:00:00 will be converted to 23:00:00:00.The default value of **`strict`** is `True`. 239 | 240 | 特别地,对于丢帧时码,由于严格模式的规则是不出现超过24:00:00:00的时码(实际上这个值会被转为00:00:00:00)。因此,在该模式下可容纳的总帧数会小于相同帧率的非丢帧时码。 241 | 242 | In particular, as for a drop-frame timecode, the rule of strict mode does not allow a timecode value greater than 24:00:00:00 (actually, this value will be converted to 00:00:00:00). So, the maximum frame count number a drop-frame timecode can reach under strict mode will be less than a timecode with the same framerate but set to non-drop-frame mode. 243 | 244 | #### 4.1.7 补充说明 Additional info 245 | 246 | 支持使用DfttTimecode对象初始化新DfttTimecode对象 247 | 248 | Using a DfttTimecode object to instance a new DfttTimecode object. 249 | 250 | ```python 251 | tc_a = DfttTimecode('01:00:00:00', 'auto', fps=24, drop_frame=False, strict=True) 252 | tc_b = DfttTimecode(tc_a) 253 | ``` 254 | 255 | ### 4.2 时码类对象操作说明 Descriptions of DfttTimecode class operations 256 | 257 | #### 4.2.1 `self.type` 258 | 259 | ```python 260 | a = DfttTimecode('01:00:00:00', 'auto', fps=24, drop_frame=False, strict=True) 261 | assert a.type == 'smpte' 262 | ``` 263 | 264 | 返回DfttTimecode对象的 **`timecode_type`** 属性,返回类型为`str`。 265 | 266 | Returns the **`timecode_type`** attribute of a DfttTimecode object, returned data type is `str`. 267 | 268 | #### 4.2.2 `self.fps` 269 | 270 | ```python 271 | a = DfttTimecode('01:00:00:00', 'auto', fps=24, drop_frame=False, strict=True) 272 | assert a.fps == 24 273 | ``` 274 | 275 | 返回DfttTimecode对象的 **`fps`** 属性,返回类型取决于设置fps所用的变量类型。 276 | 277 | Returns the **`fps`** attribute of a DfttTimecode object, returned data type is determined by the data type used to set the **`fps`** attribute. 278 | 279 | #### 4.2.3 `self.framecount` 280 | 281 | ```python 282 | a = DfttTimecode('01:00:00:00', 'auto', fps=24, drop_frame=False, strict=True) 283 | assert a.framecount == 86400 284 | ``` 285 | 286 | 返回DfttTimecode对象从0时间起经过的总帧数,返回类型为`int`。 287 | 288 | Returns the total frame count from 0 of a DfttTimecode, returned data type is `int`. 289 | 290 | #### 4.2.4 `self.timestamp` 291 | 292 | ```python 293 | a = DfttTimecode('01:00:00:01', 'auto', fps=24, drop_frame=False, strict=True) 294 | assert a.timestamp == 3600.04167 295 | ``` 296 | 297 | 返回DfttTimecode对象从0时间起经过的总时长,返回类型为`float`,精度为5位小数。 298 | 299 | Returns the total time elapsed from 0 of a DfttTimecode, returned data type is `float`, the precision of the returned value is 5 decimal places. 300 | 301 | #### 4.2.5 `self.is_drop_frame` 302 | 303 | ```python 304 | a = DfttTimecode('01:00:00:00', 'auto', fps=24, drop_frame=False, strict=True) 305 | assert a.is_drop_frame == False 306 | ``` 307 | 308 | 返回DfttTimecode对象的 **`drop_frame`** 属性,返回类型为`bool`. 309 | 310 | Returns the **`drop_frame`** attribute of a DfttTimecode object, returned data type is `bool`. 311 | 312 | #### 4.2.6 `self.is_strict` 313 | 314 | ```python 315 | a = DfttTimecode('01:00:00:00', 'auto', fps=24, drop_frame=False, strict=True) 316 | assert a.is_strict == True 317 | ``` 318 | 319 | 返回DfttTimecode对象的 **`strict`** 属性,返回类型为`bool`. 320 | 321 | Returns the **`strict`** attribute of a DfttTimecode object, returned data type is `bool`. 322 | 323 | #### 4.2.7 `self.precise_timestamp` 324 | 325 | ```python 326 | a = DfttTimecode('01:00:00:00', 'auto', fps=24, drop_frame=False, strict=True) 327 | assert a.precise_timestamp == 3600 328 | ``` 329 | 返回DfttTimecode对象的 **`precise_timestamp`** 属性,返回类型为`Fraction`. 330 | 331 | Returns the **`precise_timestamp`** attribute of a DfttTimecode object, returned data type is `Fraction`. 332 | 333 | 334 | #### 4.2.8 `self.set_fps()` 335 | 336 | ```python 337 | a = DfttTimecode('01:00:00:101', 'auto', fps=120, drop_frame=False, strict=True) 338 | a.set_fps(24, rounding = True) 339 | a.set_fps(120) 340 | assert a.timecode_output('smpte') == '01:00:00:100' 341 | ``` 342 | 343 | 该函数会更改DfttTimecode对象的帧率,并可以选择在更改帧率时是否取整。 344 | 345 | This function will change the frame rate of a DfttTimecode object, you can choose whether or not to round the timecode value while changing the frame rate. 346 | 347 | `self.set_fps()`函数共有两个参数,分别是 **`dest_fps`** 和 **`rounding`** 。 348 | 349 | There are two parameters of `self.set_fps()`, they are **`dest_fps`** and **`rounding`**. 350 | 351 | **`dest_fps`** 是帧率转换的目标帧率,可以是`int`、`float`、`Fraction`类型。 352 | 353 | **`dest_fps`** is the target frame rate of this transform, it can be a,n `int`, `float`, or a `Fraction`. 354 | 355 | **`rounding`** 决定了帧率转换过程中是否舍入时间戳以对齐帧,具体可以参考下面的示例代码。 356 | 357 | **`rounding`** determines whether to round the time stamp to align to the exact frame while converting the frame rate, you can refer to the following example code to see how it works. 358 | 359 | ```python 360 | a = DfttTimecode('01:00:00:101', 'auto', fps=120, drop_frame=False, strict=True) 361 | a.set_fps(24, rounding = False) 362 | a.set_fps(120) 363 | assert a.timecode_output('smpte') == '01:00:00:101' 364 | a.set_fps(24, rounding = True) 365 | a.set_fps(120) 366 | assert a.timecode_output('smpte') == '01:00:00:100' 367 | ``` 368 | 369 | #### 4.2.9 `self.set_type()` 370 | 371 | 该函数会更改DfttTimecode对象的时码类型,并可以选择在更改类型时是否取整。 372 | 373 | This function will change the timecode type of a DfttTimecode object, you can choose whether or not to round the timecode value while changing the timecode type. 374 | 375 | `self.set_type()`函数共有两个参数,分别是 **`dest_type`** 和 **`rounding`** 。 376 | 377 | `self.set_type()` has two parameters, they are **`dest_type`** and **`rounding`** . 378 | 379 | **`dest_type`** 是时码类型转换的目标时码类型,可以是除`'auto'`以外的任何一个支持的时码类型。 380 | 381 | **`dest_type`** is the target timecode type of this transform, it can be any supported timecode type except `'auto'`. 382 | 383 | **`rounding`** 决定了时码类型转换过程中是否舍入时间戳以对齐帧,具体可以参考下面的示例代码。 384 | 385 | **`rounding`** determines whether to round the time stamp to align to the exact frame while converting the timecode type, you can refer to the following example code to see how it works. 386 | 387 | ```python 388 | a = DfttTimecode('01:00:00,123', 'auto', fps=24) 389 | assert a.type == 'srt' 390 | a.set_type('smpte', rounding=True) 391 | assert a.type == 'smpte' 392 | assert a.timecode_output('srt') == '01:00:00,125' 393 | ``` 394 | 395 | #### 4.2.10 `self.set_strict()` 396 | 397 | ```python 398 | a = DfttTimecode('25:01:02:05', 'auto', fps=24, strict=False) 399 | a.set_strict() 400 | assert a.is_strict == True 401 | assert a.timecode_output('smpte') == '01:01:02:05' 402 | a.set_strict(strict=False) 403 | assert a.is_strict == False 404 | ``` 405 | 406 | 该函数会更改DfttTimecode对象的strict模式布尔值。 407 | 408 | This function will change the strict mode bool value of a DfttTimecode object. 409 | 410 | `self.set_strict()` 只有一个参数,即 **`strict`** 。**`strict`** 的类型是`bool`,默认值为`True`。 411 | 412 | `self.set_strict()` has one parameter, which is **`strict`**. The data type of **`strict`** is `bool`, the default value of **`strict`** is `Ture`. 413 | 414 | #### 4.2.11 `self.timecode_output()` 415 | 416 | ```python 417 | a = DfttTimecode('01:02:03:05', 'auto', fps=24) 418 | assert a.timecode_output() == '01:02:03:05' 419 | assert a.timecode_output('srt') == '01:02:03,208' 420 | assert a.timecode_output('srt', output_part=1) == '01' 421 | assert a.timecode_output('srt', output_part=2) == '02' 422 | assert a.timecode_output('srt', output_part=3) == '03' 423 | assert a.timecode_output('srt', output_part=4) == '208' 424 | ``` 425 | 426 | 该函数会以指定类型和部分返回DfttTimecode对象的时码值,返回类型为`str`。 427 | 428 | This function will return the timecode value of a DfttTimecode object in the given timecode type and partition number format, the returned data type is `str`. 429 | 430 | `self.timecode_output()` 有两个参数,分别是 **`dest_type`** 和 **`output_part`** 。 431 | 432 | `self.timecode_output()` has two parameters, they are **`dest_type`** and **`output_part`** . 433 | 434 | **`dest_type`** 是输出时码的类型,可以是任何一个支持的时码类型,它的默认值是`'auto'`,此时会根据DfttTimecode对象自身的时码类型决定输出类型。 435 | 436 | **`dest_type`** is the type of the output timecode value, it can be any supported timecode type, the default value of it is `'auto'`, which means the function will determine the output timecode type according to the timecode type of the DfttTimecode object itself. 437 | 438 | **`output_part`** 是输出的部分,它应是一个`int`值。它的默认值是`0`,即完整输出。`1`到`4`依次代表输出从左至右的每个时码部分。 439 | 440 | **`output_part`** is the partition of the output timecode value, it is an `int`. The default value of it is 0, which means a complete output. Each of 1 to 4 represents an output timecode part from left to right. 441 | 442 | ### 4.3 时码类运算符说明 Descriptions of DfttTimecode class operators 443 | 444 | #### 4.3.1 `print(self)` 445 | 446 | 该运算符会打印DfttTimecode对象相关的时间码值,如下所示。 447 | 448 | This operator will print timecode value of a DfttTimecode object, as the following codes show. 449 | 450 | ```python 451 | a = DfttTimecode('01:00:00,123', 'srt', fps=24, drop_frame=False, strict=True) 452 | print(a) # 01:00:00,123 453 | ``` 454 | 455 | #### 4.3.2 `-self` 456 | 457 | 该运算符会将DfttTimecode对象的时码值取负,且不改变其他属性,如下所示。 458 | 459 | This operator will yield the negation of the timecode value of a DfttTimecode object, and won't affect any of the rest attributes, as the following codes show. 460 | 461 | ```python 462 | a = DfttTimecode('01:00:00,123', 'srt', fps=24, drop_frame=False, strict=True) 463 | print(-a) # 22:59:59,877 464 | ``` 465 | 466 | #### 4.3.3 `+` 467 | 468 | 该运算符可以将两个DfttTimecode对象相加,或将DfttTimecode对象与`int`,`float`或`Fraction`相加。 469 | 470 | This operator can add two DfttTimecode objects together, or add a DfttTimecode object with an `int`, `float`, or a `Fraction`. 471 | 472 | 当DfttTimecode对象与`int`相加时,`int`值将被当作帧计数处理。当DfttTimecode对象与`float`或`Fraction`相加时,后者的值将被当作时间戳处理。 473 | 474 | When adding a DfttTimecode object with an `int`, the `int` will be considered as a frame number. When adding a DfttTimecode object with a `float` or a `Fraction`, the latter will be considered as a time stamp. 475 | 476 | 相加的DfttTimecode对象必须拥有相同的帧率。 477 | 478 | The two DfttTimecode objects to perform the addition must have the same frame rate. 479 | 480 | #### 4.3.4 `-` 481 | 482 | 该运算符可以将两个DfttTimecode对象相减,或将DfttTimecode对象与`int`,`float`或`Fraction`相减。 483 | 484 | This operator can perform a subtraction between two DfttTimecode objects, or perform a subtraction between a DfttTimecode object and an `int`, `float`, or a `Fraction`. 485 | 486 | 当DfttTimecode对象与`int`相减时,`int`值将被当作帧计数处理。当DfttTimecode对象与`float`或`Fraction`相加时,后者的值将被当作时间戳处理。 487 | 488 | When performing a subtraction between a DfttTimecode object and an `int`, the `int` will be considered as a frame number. When performing a subtraction between a DfttTimecode object and a `float` or a `Fraction`, the latter will be considered as a time stamp. 489 | 490 | 相减的DfttTimecode对象必须拥有相同的帧率。 491 | 492 | The two DfttTimecode objects to perform the subtraction must have the same frame rate. 493 | 494 | #### 4.3.5 `*` 495 | 496 | 该运算符可以将一个DfttTimecode对象与一个`int`,`float`或`Fraction`相乘,后者的数学意义是倍数。 497 | 498 | This operator can perform a multiplication between a DfttTimecode object and an `int`, `float`, or a `Fraction`, the mathematical meaning of the latter is a factor. 499 | 500 | #### 4.3.6 `/` 501 | 502 | 该运算符可以将一个DfttTimecode对象与一个`int`,`float`或`Fraction`相除,后者的数学意义是倍数。 503 | 504 | This operator can perform a division between a DfttTimecode object and an `int`, `float`, or a `Fraction`, the mathematical meaning of the latter is a factor. 505 | 506 | 需要注意的是,只有当DfttTimecode对象作为被除数时,除法运算才是有意义的,DfttTimecode对象不能作除数。 507 | 508 | Please be noted, the division operation only makes sense when the DfttTimecode object is used as the dividend, the DfttTimecode object cannot be used as a divisor. 509 | 510 | #### 4.3.7 `==` 511 | 512 | 该运算符可以比较两个DfttTimecode对象是否相等,或比较DfttTimecode对象和`int`,`float`或`Fraction`是否相等。 513 | 514 | This operator can perform a comparison between two DfttTimecode objects, or perform a comparison between a DfttTimecode object and an `int`, `float`, or a `Fraction`, to tell whether they are equal to each other. 515 | 516 | 当两个DfttTimecode对象作比较时,将比较二者的时间戳。当DfttTimecode对象与`int`作比较时,`int`值将被当作帧计数处理。当DfttTimecode对象与`float`或`Fraction`作比较时,后者的值将被当作时间戳处理。 517 | 518 | When performing a comparison between two DfttTimecode objects, a comparison of their timestamp will be performed. When performing a comparison between a DfttTimecode object and an `int`, the `int` will be considered as a frame number. When performing a comparison between a DfttTimecode object and a `float` or a `Fraction`, the latter will be considered as a time stamp. 519 | 520 | #### 4.3.8 `!=` 521 | 522 | 该运算符可以比较两个DfttTimecode对象是否相等,或比较DfttTimecode对象和`int`,`float`或`Fraction`是否相等。 523 | 524 | This operator can perform a comparison between two DfttTimecode objects, or perform a comparison between a DfttTimecode object and an `int`, `float`, or a `Fraction`, to tell whether they are equal to each other. 525 | 526 | 当两个DfttTimecode对象作比较时,将比较二者的时间戳。当DfttTimecode对象与`int`作比较时,`int`值将被当作帧计数处理。当DfttTimecode对象与`float`或`Fraction`作比较时,后者的值将被当作时间戳处理。 527 | 528 | When performing a comparison between two DfttTimecode objects, a comparison of their timestamp will be performed. When performing a comparison between a DfttTimecode object and an `int`, the `int` will be considered as a frame number. When performing a comparison between a DfttTimecode object and a `float` or a `Fraction`, the latter will be considered as a time stamp. 529 | 530 | #### 4.3.9 `>` 531 | 532 | 该运算符可以比较两个DfttTimecode对象的大小,或比较DfttTimecode对象和`int`,`float`或`Fraction`的大小。 533 | 534 | This operator can perform a comparison between two DfttTimecode objects, or perform a comparison between a DfttTimecode object and an `int`, `float`, or a `Fraction`, to tell which one is the greater one. 535 | 536 | 当两个DfttTimecode对象作比较时,将比较二者的时间戳。当DfttTimecode对象与`int`作比较时,`int`值将被当作帧计数处理。当DfttTimecode对象与`float`或`Fraction`作比较时,后者的值将被当作时间戳处理。 537 | 538 | When performing a comparison between two DfttTimecode objects, a comparison of their timestamp will be performed. When performing a comparison between a DfttTimecode object and an `int`, the `int` will be considered as a frame number. When performing a comparison between a DfttTimecode object and a `float` or a `Fraction`, the latter will be considered as a time stamp. 539 | 540 | #### 4.3.10 `>=` 541 | 542 | 该运算符可以比较两个DfttTimecode对象的大小,或比较DfttTimecode对象和`int`,`float`或`Fraction`的大小。 543 | 544 | This operator can perform a comparison between two DfttTimecode objects, or perform a comparison between a DfttTimecode object and an `int`, `float`, or a `Fraction`, to tell which one is the greater one. 545 | 546 | 当两个DfttTimecode对象作比较时,将比较二者的时间戳。当DfttTimecode对象与`int`作比较时,`int`值将被当作帧计数处理。当DfttTimecode对象与`float`或`Fraction`作比较时,后者的值将被当作时间戳处理。 547 | 548 | When performing a comparison between two DfttTimecode objects, a comparison of their timestamp will be performed. When performing a comparison between a DfttTimecode object and an `int`, the `int` will be considered as a frame number. When performing a comparison between a DfttTimecode object and a `float` or a `Fraction`, the latter will be considered as a time stamp. 549 | 550 | #### 4.3.11 `<` 551 | 552 | 该运算符可以比较两个DfttTimecode对象的大小,或比较DfttTimecode对象和`int`,`float`或`Fraction`的大小。 553 | 554 | This operator can perform a comparison between two DfttTimecode objects, or perform a comparison between a DfttTimecode object and an `int`, `float`, or a `Fraction`, to tell which one is the greater one. 555 | 556 | 当两个DfttTimecode对象作比较时,将比较二者的时间戳。当DfttTimecode对象与`int`作比较时,`int`值将被当作帧计数处理。当DfttTimecode对象与`float`或`Fraction`作比较时,后者的值将被当作时间戳处理。 557 | 558 | When performing a comparison between two DfttTimecode objects, a comparison of their timestamp will be performed. When performing a comparison between a DfttTimecode object and an `int`, the `int` will be considered as a frame number. When performing a comparison between a DfttTimecode object and a `float` or a `Fraction`, the latter will be considered as a time stamp. 559 | 560 | #### 4.3.12 `<=` 561 | 562 | 该运算符可以比较两个DfttTimecode对象的大小,或比较DfttTimecode对象和`int`,`float`或`Fraction`的大小。 563 | 564 | This operator can perform a comparison between two DfttTimecode objects, or perform a comparison between a DfttTimecode object and an `int`, `float`, or a `Fraction`, to tell which one is the greater one. 565 | 566 | 当两个DfttTimecode对象作比较时,将比较二者的时间戳。当DfttTimecode对象与`int`作比较时,`int`值将被当作帧计数处理。当DfttTimecode对象与`float`或`Fraction`作比较时,后者的值将被当作时间戳处理。 567 | 568 | When performing a comparison between two DfttTimecode objects, a comparison of their timestamp will be performed. When performing a comparison between a DfttTimecode object and an `int`, the `int` will be considered as a frame number. When performing a comparison between a DfttTimecode object and a `float` or a `Fraction`, the latter will be considered as a time stamp. 569 | 570 | #### 4.3.13 `float(self)` 571 | 572 | 返回 `self.timestamp` 的值.参考[self.timestamp](#424-selftimestamp) 573 | 574 | Return value of `self.timestamp`.Reference to [self.timestamp](#424-selftimestamp) 575 | 576 | ```python 577 | tc_a = DfttTimecode('01:00:00:00', 'auto', fps=24, drop_frame=False, strict=True) 578 | float(tc_a) 579 | #36000.0 580 | ``` 581 | 582 | #### 4.3.14 `int(self)` 583 | 584 | 返回 `self.framecount` 的值.参考[self.framecount](#423-selfframecount) 585 | 586 | Return value of `self.framecount`.Reference to [self.framecount](#423-selfframecount) 587 | 588 | ```python 589 | tc_a = DfttTimecode('01:00:00:00', 'auto', fps=24, drop_frame=False, strict=True) 590 | int(tc_a) 591 | #864000 592 | ``` 593 | 594 | #TODO TimeRange readme -------------------------------------------------------------------------------- /dftt_timecode/__init__.py: -------------------------------------------------------------------------------- 1 | name = 'dftt_timecode' 2 | __author__ = 'You Ziyuan' 3 | __version__ = '0.0.14' 4 | 5 | from dftt_timecode.core.dftt_timecode import DfttTimecode -------------------------------------------------------------------------------- /dftt_timecode/core/dftt_timecode.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from fractions import Fraction 3 | from functools import singledispatchmethod 4 | from math import ceil, floor 5 | from copy import deepcopy 6 | 7 | from typing import Literal, TypeAlias 8 | 9 | from dftt_timecode.error import * 10 | from dftt_timecode.pattern import * 11 | 12 | # logging.basicConfig(filename='dftt_timecode_log.txt', 13 | # filemode='w', 14 | # format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s', 15 | # datefmt='%Y-%m-%d %a %H:%M:%S', 16 | # level=logging.DEBUG) 17 | #set up logger 18 | logger=logging.getLogger(__name__) 19 | logger.setLevel(logging.DEBUG) 20 | formatter=logging.Formatter('%(asctime)s [%(levelname)s] [%(filename)s:%(lineno)d-%(funcName)s()] %(message)s') 21 | 22 | stream_handler=logging.StreamHandler() 23 | stream_handler.setFormatter(formatter) 24 | 25 | # file_handler=logging.FileHandler('dftt_timecode_log.txt',filemode='w') 26 | # file_handler.setFormatter(formatter) 27 | 28 | logger.addHandler(stream_handler) 29 | 30 | TimecodeType : TypeAlias= Literal['smpte', 'srt', 'dlp', 'ffmpeg', 'fcpx', 'frame', 'time','auto'] 31 | 32 | class DfttTimecode: 33 | __type = 'time' 34 | __fps = 24.0 # 帧率 35 | __nominal_fps = 24 # 名义帧率(无小数,进一法取整) 36 | __drop_frame = False # 是否丢帧Dropframe(True为丢帧,False为不丢帧) 37 | __strict = True # 严格模式,默认为真,在该模式下不允许超出24或小于0的时码,将自动平移至0-24范围内,例如-1小时即为23小时,25小时即为1小时 38 | __precise_time = Fraction(0) # 精准时间戳,是所有时码类对象的工作基础 39 | 40 | def __new__(cls, timecode_value=0, timecode_type='auto', fps=24.0, drop_frame=False, strict=True): 41 | if isinstance(timecode_value, DfttTimecode): 42 | return timecode_value 43 | else: 44 | return super(DfttTimecode, cls).__new__(cls) 45 | 46 | def __validate_drop_frame(self, drop_frame: bool, fps: float) -> bool: 47 | if round(fps, 2) % 29.97 == 0: 48 | # FPS为29.97以及倍数时候,尊重drop_frame参数(for 29.97/59.94/119.88 NDF) 49 | return False if drop_frame == False else True 50 | else: 51 | return round(self.fps, 2) % 23.98 == 0 52 | 53 | def __detect_timecode_type(self,timecode_value)->TimecodeType: 54 | if SMPTE_NDF_REGEX.match(timecode_value): # SMPTE NDF 强制DF为False 55 | if self.__drop_frame == True: 56 | raise DFTTTimecodeInitializationError(f'Init Timecode Failed: Timecode value [{timecode_value}] DONOT match drop_frame status [{self.__drop_frame}]! Check input.') 57 | return 'smpte' 58 | 59 | elif SMPTE_DF_REGEX.match(timecode_value): 60 | 61 | # 判断丢帧状态与帧率是否匹配 不匹配则强制转换 62 | if self.__drop_frame == False: 63 | raise DFTTTimecodeInitializationError(f'Init Timecode Failed: Timecode value [{timecode_value}] DONOT match drop_frame status [{self.__drop_frame}]! Check input.') 64 | return 'smpte' 65 | elif SRT_REGEX.match(timecode_value): 66 | return 'srt' 67 | elif FFMPEG_REGEX.match(timecode_value): 68 | return 'ffmpeg' 69 | elif FCPX_REGEX.match(timecode_value): 70 | return 'fcpx' 71 | elif FRAME_REGEX.match(timecode_value): 72 | return 'frame' 73 | elif TIME_REGEX.match(timecode_value): 74 | return 'time' 75 | 76 | def __apply_strict(self) -> None: 77 | """Apply 24h wraparound if strict mode enabled""" 78 | if self.__strict: 79 | self.__precise_time %= 86400 80 | 81 | 82 | def __init_smpte(self, timecode_value: str,minus_flag:bool): 83 | if not SMPTE_REGEX.match(timecode_value): # 判断输入是否符合 84 | logger.error( 85 | 'Timecode type DONOT match input value! Check input.') 86 | raise DFTTTimecodeTypeError 87 | temp_timecode_list = [int(x) if x else 0 for x in SMPTE_REGEX.match( 88 | timecode_value).groups()] # 正则取值 89 | hh,mm,ss,ff = temp_timecode_list 90 | if ff > self.__nominal_fps - 1: # 判断输入帧号在当前帧率下是否合法 91 | logger.error( 92 | 'This timecode is illegal under given params, check your input!') 93 | raise DFTTTimecodeValueError 94 | 95 | if self.__drop_frame == False: # 时码丢帧处理逻辑 96 | frame_index = ff + self.__nominal_fps * \ 97 | (ss + mm * 60 + hh * 3600) 98 | else: 99 | drop_per_min = self.__nominal_fps / 30 * 2 100 | # 检查是否有DF下不合法的帧号 101 | if mm % 10 != 0 and ss == 0 and ff in (0, drop_per_min - 1): 102 | logger.error( 103 | 'This timecode is illegal under given params, check your input!') 104 | raise DFTTTimecodeValueError 105 | else: 106 | total_minutes = 60 * hh + mm 107 | frame_index = (hh * 3600 + mm * 60 + ss) * self.__nominal_fps + ff - ( 108 | self.__nominal_fps / 30) * 2 * ( 109 | # 逢十分钟不丢帧 http://andrewduncan.net/timecodes/ 110 | total_minutes - total_minutes // 10) 111 | if self.__strict == True: # strict输入逻辑 112 | frame_index = frame_index % (self.__fps * 86400) if self.__drop_frame == True else frame_index % ( 113 | self.__nominal_fps * 86400) # 对于DF时码来说,严格处理取真实FPS的模,对于NDF时码,则取名义FPS的模 114 | 115 | if minus_flag == True: 116 | frame_index = -frame_index 117 | self.__precise_time = Fraction( 118 | frame_index / self.__fps) # 时间戳=帧号/帧率 119 | 120 | def __init_srt(self, timecode_value: str,minus_flag:bool): 121 | if not SRT_REGEX.match(timecode_value): # 判断输入是否符合SRT类型 122 | logger.error( 123 | f'Timecode type [srt] DONOT match input value [{timecode_value}]! Check input.') 124 | raise DFTTTimecodeTypeError 125 | 126 | temp_timecode_list = [ 127 | int(x) if x else 0 for x in SRT_REGEX.match(timecode_value).groups()] 128 | # 由于SRT格式本身不存在帧率,将为SRT赋予默认帧率和丢帧状态 129 | logger.info(f'SRT timecode framerate {self.__fps}, DF={self.__drop_frame} assigned') 130 | hh,mm,ss,sub_sec = temp_timecode_list 131 | 132 | self.__precise_time = Fraction(hh * 3600 + mm * 60 + ss + sub_sec / 1000) 133 | if minus_flag: 134 | self.__precise_time = -self.__precise_time 135 | 136 | self.__apply_strict() 137 | 138 | 139 | def __init_dlp(self, timecode_value: str, minus_flag: bool): 140 | if not DLP_REGEX.match(timecode_value): 141 | logger.error( 142 | f'Timecode type [dlp] DONOT match input value [{timecode_value}]! Check input.') 143 | raise DFTTTimecodeTypeError 144 | temp_timecode_list = [ 145 | int(x) if x else 0 for x in DLP_REGEX.match(timecode_value).groups()] 146 | # 由于DLP不存在帧率,将为DLP赋予默认帧率和丢帧状态 147 | logger.info(f'DLP timecode framerate {self.__fps}, DF={self.__drop_frame} assigned') 148 | hh, mm, ss, sub_sec = temp_timecode_list 149 | # dlp每秒共250个子帧 即4ms一个 150 | # 详见https://interop-docs.cinepedia.com/Reference_Documents/CineCanvas(tm)_RevC.pdf 第17页 “TimeIn”部分 151 | 152 | self.__precise_time = Fraction(hh * 3600 + mm * 60 + ss + sub_sec / 250) 153 | if minus_flag: 154 | self.__precise_time = -self.__precise_time 155 | self.__apply_strict() 156 | 157 | 158 | def __init_ffmpeg(self, timecode_value: str,minus_flag:bool): 159 | if not FFMPEG_REGEX.match(timecode_value): 160 | logger.error(f'Timecode type [ffmpeg] DONOT match input value [{timecode_value}]! Check input.') 161 | raise DFTTTimecodeTypeError 162 | temp_timecode_list = [ 163 | int(x) if x else 0 for x in FFMPEG_REGEX.match(timecode_value).groups()] 164 | hh,mm,ss,sub_sec = temp_timecode_list 165 | self.__precise_time = Fraction(hh * 3600 + mm * 60 + ss + float(f'0.{sub_sec}')) 166 | if minus_flag: 167 | self.__precise_time = -self.__precise_time 168 | 169 | self.__apply_strict() 170 | 171 | def __init_fcpx(self, timecode_value: str,minus_flag:bool): 172 | if not FCPX_REGEX.match(timecode_value): 173 | logger.error(f'Timecode type [fcpx] DONOT match input value [{timecode_value}]! Check input.') 174 | raise DFTTTimecodeTypeError 175 | temp_timecode_list = [ 176 | int(x) if x else 0 for x in FCPX_REGEX.match(timecode_value).groups()] 177 | self.__precise_time = Fraction(temp_timecode_list[0], temp_timecode_list[1]) 178 | if minus_flag: 179 | self.__precise_time = -self.__precise_time 180 | 181 | self.__apply_strict() 182 | 183 | def __init_frame(self, timecode_value: str,minus_flag:bool): 184 | if not FRAME_REGEX.match(timecode_value): 185 | logger.error(f'Timecode type [frame] DONOT match input value [{timecode_value}]! Check input.') 186 | raise DFTTTimecodeTypeError 187 | temp_frame_index = int(FRAME_REGEX.match(timecode_value).group(1)) 188 | if self.__strict == True: # 严格模式,对于丢帧时码而言 用实际FPS运算,对于不丢帧时码而言,使用名义FPS运算 189 | temp_frame_index = temp_frame_index % ( 190 | self.__fps * 86400) if self.__drop_frame == True else temp_frame_index % ( 191 | self.__nominal_fps * 86400) 192 | else: 193 | pass 194 | self.__precise_time = Fraction( 195 | temp_frame_index / self.__fps) # 转换为内部精准时间戳 196 | 197 | def __init_time(self, timecode_value: str,minus_flag:bool): 198 | if not TIME_REGEX.match(timecode_value): 199 | logger.error(f'Timecode type [time] DONOT match input value [{timecode_value}]! Check input.') 200 | raise DFTTTimecodeTypeError 201 | temp_timecode_value = TIME_REGEX.match(timecode_value).group(1) 202 | self.__precise_time = Fraction(temp_timecode_value) # 内部时间戳直接等于输入值 203 | 204 | self.__apply_strict() 205 | 206 | def __init_common(self, timecode_type,fps,drop_frame,strict): 207 | self.__type = timecode_type 208 | self.__fps = fps 209 | self.__nominal_fps = ceil(fps) 210 | self.__drop_frame = self.__validate_drop_frame(drop_frame, fps) 211 | self.__strict = strict 212 | 213 | @singledispatchmethod 214 | def __init__(self, timecode_value, timecode_type, fps, drop_frame, strict): # 构造函数 215 | raise TypeError(f"Unsupported timecode value type: {type(timecode_value)}") 216 | 217 | @__init__.register # 若传入的TC值为字符串,则调用此函数 218 | def _(self, timecode_value: str, timecode_type:TimecodeType='auto', fps=24.0, drop_frame=None, strict=True): 219 | # if timecode_value[0] == '-': # 判断首位是否为负,并为flag赋值 220 | # minus_flag = True 221 | # else: 222 | # minus_flag = False 223 | minus_flag= timecode_value.startswith('-') 224 | self.__fps = fps 225 | # 读入帧率取整为名义帧率便于后续计算(包括判断时码是否合法,DF/NDF逻辑等) 用进一法是因为要判断ff值是否大于fps-1 226 | self.__nominal_fps = ceil(fps) 227 | self.__drop_frame = self.__validate_drop_frame(drop_frame, fps) 228 | self.__strict = strict 229 | 230 | timecode_type= timecode_type if timecode_type != 'auto' else self.__detect_timecode_type(timecode_value) 231 | 232 | self.__type = timecode_type 233 | 234 | timecode_type_handler_map={ 235 | 'smpte':self.__init_smpte, 236 | 'srt':self.__init_srt, 237 | 'dlp':self.__init_dlp, 238 | 'ffmpeg':self.__init_ffmpeg, 239 | 'fcpx':self.__init_fcpx, 240 | 'frame':self.__init_frame, 241 | 'time':self.__init_time 242 | } 243 | init_func=timecode_type_handler_map[timecode_type] 244 | if not init_func: 245 | raise DFTTTimecodeTypeError(f'Unknown timecode type :{timecode_type}') 246 | init_func(timecode_value,minus_flag) 247 | 248 | instance_success_log = f'value type {type(timecode_value)} Timecode instance: type={self.__type}, fps={self.__fps}, dropframe={self.__drop_frame}, strict={self.__strict}' 249 | # logging.debug(instance_success_log) 250 | logger.debug(instance_success_log) 251 | 252 | @__init__.register # 输入为Fraction类分数,此时认为输入是时间戳,若不是,则会报错 253 | def _(self, timecode_value: Fraction, timecode_type='time', fps=24.0, drop_frame=False, strict=True): 254 | if timecode_type in ('time', 'auto'): 255 | self.__init_common(timecode_type,fps,drop_frame,strict) 256 | self.__precise_time = timecode_value # 内部时间戳直接等于输入值 257 | self.__apply_strict() 258 | else: 259 | logger.error( 260 | f'Timecode type [{timecode_type}] DONOT match input value [{timecode_value}]! Check input.') 261 | raise DFTTTimecodeTypeError 262 | instance_success_log = f'value type {type(timecode_value)} Timecode instance: type={self.__type}, fps={self.__fps}, dropframe={self.__drop_frame}, strict={self.__strict}' 263 | logger.debug(instance_success_log) 264 | 265 | @__init__.register 266 | def _(self, timecode_value: int, timecode_type='frame', fps=24.0, drop_frame=False, strict=True): 267 | if timecode_type in ('frame', 'auto'): 268 | self.__init_common(timecode_type,fps,drop_frame,strict) 269 | temp_frame_index = timecode_value 270 | if self.__strict == True: 271 | temp_frame_index = temp_frame_index % ( 272 | self.__fps * 86400) if self.__drop_frame == True else temp_frame_index % ( 273 | self.__nominal_fps * 86400) 274 | self.__precise_time = Fraction(temp_frame_index / self.__fps) 275 | 276 | elif timecode_type == 'time': 277 | self.__init_common(timecode_type,fps,drop_frame,strict) 278 | self.__precise_time = timecode_value # 内部时间戳直接等于输入值 279 | self.__apply_strict() 280 | else: 281 | logger.error( 282 | f'Timecode type [{timecode_type}] DONOT match input value [{timecode_value}]! Check input.') 283 | raise DFTTTimecodeTypeError 284 | instance_success_log = f'value type {type(timecode_value)} Timecode instance: type={self.__type}, fps={self.__fps}, dropframe={self.__drop_frame}, strict={self.__strict}' 285 | logger.debug(instance_success_log) 286 | 287 | @__init__.register 288 | def _(self, timecode_value: float, timecode_type='time', fps=24.0, drop_frame=False, strict=True): 289 | if timecode_type in ('time', 'auto'): 290 | self.__init_common(timecode_type,fps,drop_frame,strict) 291 | self.__precise_time = Fraction(timecode_value) # 内部时间戳直接等于输入值 292 | self.__apply_strict() 293 | else: 294 | logger.error( 295 | f'Timecode type [{timecode_type}] DONOT match input value [{timecode_value}]! Check input.') 296 | raise DFTTTimecodeTypeError 297 | instance_success_log = f'value type {type(timecode_value)} Timecode instance: type={self.__type}, fps={self.__fps}, dropframe={self.__drop_frame}, strict={self.__strict}' 298 | logger.debug(instance_success_log) 299 | 300 | @__init__.register 301 | def _(self, timecode_value: tuple, timecode_type='time', fps=24.0, drop_frame=False, strict=True): 302 | if timecode_type in ('time', 'auto'): 303 | self.__init_common(timecode_type,fps,drop_frame,strict) 304 | self.__precise_time = Fraction( 305 | int(timecode_value[0]), int(timecode_value[1])) # 将tuple输入视为分数 306 | self.__apply_strict() 307 | else: 308 | logger.error( 309 | f'Timecode type [{timecode_type}] DONOT match input value [{timecode_value}]! Check input.') 310 | raise DFTTTimecodeTypeError 311 | instance_success_log = f'value type {type(timecode_value)} Timecode instance: type={self.__type}, fps={self.__fps}, dropframe={self.__drop_frame}, strict={self.__strict}' 312 | logger.debug(instance_success_log) 313 | 314 | @__init__.register 315 | def _(self, timecode_value: list, timecode_type='time', fps=24.0, drop_frame=False, strict=True): 316 | if timecode_type in ('time', 'auto'): 317 | self.__init_common(timecode_type,fps,drop_frame,strict) 318 | self.__precise_time = Fraction( 319 | int(timecode_value[0]), int(timecode_value[1])) # 将list输入视为分数 320 | self.__apply_strict() 321 | else: 322 | logger.error( 323 | f'Timecode type [{timecode_type}] DONOT match input value [{timecode_value}]! Check input.') 324 | raise DFTTTimecodeTypeError 325 | instance_success_log = f'value type {type(timecode_value)} Timecode instance: type={self.__type}, fps={self.__fps}, dropframe={self.__drop_frame}, strict={self.__strict}' 326 | logger.debug(instance_success_log) 327 | 328 | @property 329 | def type(self) -> str: 330 | return self.__type 331 | 332 | @property 333 | def fps(self): 334 | return self.__fps 335 | 336 | @property 337 | def is_drop_frame(self) -> bool: 338 | return self.__drop_frame 339 | 340 | @property 341 | def is_strict(self) -> bool: 342 | return self.__strict 343 | 344 | @property 345 | def framecount(self) -> int: 346 | return int(self._convert_to_output_frame()) 347 | 348 | @property 349 | def timestamp(self) -> float: 350 | return float(self._convert_to_output_time()) 351 | 352 | @property 353 | def precise_timestamp(self): 354 | return self.__precise_time 355 | 356 | def _convert_to_output_smpte(self, output_part=0) -> str: 357 | minus_flag = False 358 | frame_index = round(self.__precise_time * self.__fps) # 从内部时间戳计算得帧计数 359 | if frame_index < 0: # 负值时,打上flag,并翻转负号 360 | minus_flag = True 361 | frame_index = -frame_index 362 | 363 | # 计算framecount用于输出smpte时码个部分值 364 | if self.__drop_frame == False: # 不丢帧 365 | # 对于不丢帧时码而言 framecount 为帧计数 366 | _nominal_framecount = frame_index 367 | else: # 丢帧 368 | drop_per_min = self.__nominal_fps / 30 * 2 # 提前计算每分钟丢帧数量 简化后续计算 369 | df_framecount_10min = self.__nominal_fps * 600 - 9 * drop_per_min 370 | 371 | d, m = divmod(frame_index, df_framecount_10min) 372 | drop_frame_frame_number = frame_index + drop_per_min * 9 * d + drop_per_min * ( 373 | # 剩余小于十分钟部分计算丢了多少帧,补偿 374 | ((m - drop_per_min) // (self.__nominal_fps * 60 - drop_per_min)) if m > 2 else 0) 375 | 376 | _nominal_framecount = drop_frame_frame_number 377 | 378 | def _convert_framecount_to_smpte_parts(frame_count: int, fps: int) -> tuple: 379 | hour, r_1 = divmod(frame_count, 60*60*fps) 380 | minute, r_2 = divmod(r_1, 60*fps) 381 | second, frame = divmod(r_2, fps) 382 | return int(hour), int(minute), int(second), round(frame) 383 | 384 | output_hh, output_mm, output_ss, output_ff = _convert_framecount_to_smpte_parts( 385 | _nominal_framecount, self.__nominal_fps) 386 | 387 | output_ff_format = '02d' if self.__fps < 100 else '03d' 388 | output_minus_flag = '' if minus_flag == False else '-' 389 | output_strs = ( 390 | f'{output_minus_flag}{output_hh:02d}', 391 | f'{output_mm:02d}', 392 | f'{output_ss:02d}', 393 | f'{output_ff:{output_ff_format}}') 394 | 395 | if output_part > len(output_strs): 396 | logger.warning( 397 | 'No such part, will return the last part of timecode') 398 | return output_strs[-1] 399 | 400 | # 输出完整时码字符串 401 | if output_part == 0: 402 | main_part = ':'.join(output_strs[:3]) 403 | # 丢帧时码的帧号前应为分号 404 | separator = ';' if self.__drop_frame else ':' 405 | output_str = f'{main_part}{separator}{output_strs[3]}' 406 | return output_str 407 | 408 | elif 1 <= output_part <= len(output_strs): 409 | return output_strs[output_part-1] 410 | 411 | else: 412 | raise IndexError( 413 | 'Negtive output_part is not allowed') 414 | 415 | def _convert_precise_time_to_parts(self, sub_sec_multiplier: int, frame_seperator: str, sub_sec_format: str) -> tuple[str, str, str, str, str]: 416 | minus_flag: bool = self.__precise_time < 0 417 | temp_precise_time = abs(self.__precise_time) 418 | _hh, r_1 = divmod(temp_precise_time, 60*60) 419 | _mm, r_2 = divmod(r_1, 60) 420 | _ss, r_3 = divmod(r_2, 1) 421 | _sub_sec = round(r_3*sub_sec_multiplier) 422 | output_minus_flag = '' if minus_flag == False else '-' 423 | output_hh = f'{output_minus_flag}{_hh:02d}' 424 | outpur_mm = f'{_mm:02d}' 425 | output_ss = f'{_ss:02d}' 426 | output_ff = f'{_sub_sec:{sub_sec_format}}' 427 | 428 | output_full_str = f'{output_hh}:{outpur_mm}:{output_ss}{frame_seperator}{output_ff}' 429 | 430 | return output_full_str, output_hh, outpur_mm, output_ss, output_ff 431 | 432 | def _convert_to_output_srt(self, output_part=0) -> str: 433 | output_strs = self._convert_precise_time_to_parts(sub_sec_multiplier=1000, 434 | frame_seperator=',', 435 | sub_sec_format='03d') 436 | 437 | if output_part > 4: 438 | logger.warning( 439 | 'No such part, will return the last part of timecode') 440 | return output_strs[-1] 441 | 442 | return output_strs[output_part] 443 | ## 444 | 445 | def _convert_to_output_dlp(self, output_part=0) -> str: 446 | output_strs = self._convert_precise_time_to_parts(sub_sec_multiplier=250, 447 | frame_seperator=':', 448 | sub_sec_format='03d') 449 | 450 | if output_part > 4: 451 | logger.warning( 452 | 'No such part, will return the last part of timecode') 453 | return output_strs[-1] 454 | 455 | return output_strs[output_part] 456 | 457 | def _convert_to_output_ffmpeg(self, output_part=0) -> str: 458 | output_strs = self._convert_precise_time_to_parts(sub_sec_multiplier=100, 459 | frame_seperator='.', 460 | sub_sec_format='02d') 461 | 462 | if output_part > 4: 463 | logger.warning( 464 | 'No such part, will return the last part of timecode') 465 | return output_strs[-1] 466 | 467 | return output_strs[output_part] 468 | 469 | def _convert_to_output_fcpx(self, output_part=0) -> str: 470 | if output_part == 0: 471 | pass 472 | else: 473 | logger.warning( 474 | '_convert_to_output_fcpx: This timecode type has only one part.') 475 | output_fcpx_denominator='' if float(self.__precise_time).is_integer() else self.__precise_time.denominator 476 | return f'{self.__precise_time.numerator}{output_fcpx_denominator}s' 477 | 478 | def _convert_to_output_frame(self, output_part=0) -> str: 479 | if output_part == 0: 480 | pass 481 | else: 482 | logger.warning( 483 | 'This timecode type has only one part.') 484 | return str(round(self.__precise_time * self.__fps)) 485 | 486 | def _convert_to_output_time(self, output_part=0) -> str: 487 | if output_part == 0: 488 | pass 489 | else: 490 | logger.warning( 491 | 'This timecode type has only one part.') 492 | output_time = round(float(self.__precise_time), 5) 493 | return str(output_time) 494 | 495 | def timecode_output(self, dest_type='auto', output_part=0): 496 | if dest_type == 'auto': 497 | func = getattr(self, f'_convert_to_output_{self.__type}') 498 | else: 499 | func = getattr(self, f'_convert_to_output_{dest_type}') 500 | if func: 501 | return func(output_part) 502 | else: 503 | logger.warning( 504 | 'CANNOT find such destination type, will return SMPTE type') 505 | func = getattr(self, '_convert_to_output_smpte', None) 506 | return func(output_part) 507 | 508 | def set_fps(self, dest_fps, rounding=True) -> 'DfttTimecode': 509 | self.__fps = dest_fps 510 | self.__nominal_fps = ceil(self.__fps) 511 | if rounding == True: 512 | self.__precise_time = round( 513 | self.__precise_time * self.__fps) / self.__fps 514 | else: 515 | pass 516 | return self 517 | 518 | def set_type(self, dest_type='smpte', rounding=True) -> 'DfttTimecode': 519 | if dest_type in ('smpte', 'srt', 'dlp', 'ffmpeg', 'fcpx', 'frame', 'time'): 520 | self.__type = dest_type 521 | else: 522 | logger.warning('No such type, will remain current type.') 523 | if rounding == True: 524 | temp_str = self.timecode_output(dest_type) 525 | temp_timecode_object = DfttTimecode( 526 | temp_str, dest_type, self.__fps, self.__drop_frame, self.__strict) 527 | self.__precise_time = temp_timecode_object.__precise_time 528 | else: 529 | pass 530 | return self 531 | 532 | def set_strict(self, strict=True) -> 'DfttTimecode': 533 | if strict == self.__strict: 534 | pass 535 | else: 536 | temp_timecode_object = DfttTimecode(self.__precise_time, 'time', self.__fps, self.__drop_frame, 537 | strict) 538 | self.__precise_time = temp_timecode_object.__precise_time 539 | self.__strict = strict 540 | return self 541 | 542 | def get_audio_sample_count(self, sample_rate: int) -> int: 543 | numerator,denominator=self.__precise_time.as_integer_ratio() 544 | return floor(numerator * sample_rate/denominator) 545 | 546 | def __repr__(self): 547 | drop_frame_flag = 'DF' if self.__drop_frame == True else 'NDF' 548 | strict_flag = 'Strict' if self.__strict == True else 'Non-Strict' 549 | return f'(Timecode:{self.timecode_output(self.__type)}, Type:{self.__type},FPS:{float(self.__fps):.02f} {drop_frame_flag}, {strict_flag})' 550 | 551 | def __str__(self): 552 | return self.timecode_output() 553 | 554 | def __add__(self, other): # 运算符重载,加号,加int则认为是帧,加float则认为是时间 555 | temp_sum = self.__precise_time 556 | if isinstance(other, DfttTimecode): 557 | if self.__fps == other.__fps and self.__drop_frame == other.__drop_frame: 558 | temp_sum = self.__precise_time + other.__precise_time 559 | self.__strict = self.__strict or other.__strict 560 | else: # 帧率不同不允许相加,报错 561 | logger.error( 562 | 'Timecode addition requires exact same FPS.') 563 | raise DFTTTimecodeOperatorError 564 | elif isinstance(other, int): # 帧 565 | temp_sum = self.__precise_time + (other / self.__fps) 566 | elif isinstance(other, float): # 时间 567 | temp_sum = self.__precise_time + other 568 | elif isinstance(other, Fraction): # 时间 569 | temp_sum = self.__precise_time + other 570 | else: 571 | logger.error('Undefined addition.') 572 | raise DFTTTimecodeOperatorError 573 | temp_object = DfttTimecode( 574 | temp_sum, 'time', self.__fps, self.__drop_frame, self.__strict) 575 | temp_object.set_type(self.type, rounding=False) 576 | return temp_object 577 | 578 | def __radd__(self, other): # 加法交换律 579 | return self.__add__(other) 580 | 581 | def __sub__(self, other): # 运算符重载,减法,同理,int是帧,float是时间 582 | diff = self.__precise_time 583 | if isinstance(other, DfttTimecode): 584 | if self.__fps == other.__fps and self.__drop_frame == other.__drop_frame: 585 | diff = self.__precise_time - other.__precise_time 586 | self.__strict = self.__strict or other.__strict 587 | else: 588 | logger.error( 589 | 'Timecode subtraction requires exact same FPS.') 590 | raise DFTTTimecodeOperatorError 591 | elif isinstance(other, int): # 帧 592 | diff = self.__precise_time - other / self.__fps 593 | elif isinstance(other, float): # 时间 594 | diff = self.__precise_time - other 595 | elif isinstance(other, Fraction): # 时间 596 | diff = self.__precise_time - other 597 | else: 598 | logger.error(30, 'Undefined subtraction.') 599 | raise DFTTTimecodeOperatorError 600 | temp_object = DfttTimecode( 601 | diff, 'time', self.__fps, self.__drop_frame, self.__strict) 602 | temp_object.set_type(self.type, rounding=False) 603 | return temp_object 604 | 605 | def __rsub__(self, other): # 运算符重载,减法,同理,int是帧,float是时间 606 | diff = self.__precise_time 607 | if isinstance(other, int): # 帧 608 | diff = other / self.__fps - self.__precise_time 609 | elif isinstance(other, float): # 秒 610 | diff = other - self.__precise_time 611 | elif isinstance(other, Fraction): # 时间 612 | diff = other - self.__precise_time 613 | else: 614 | logger.error('Undefined subtraction.') 615 | raise DFTTTimecodeOperatorError 616 | temp_object = DfttTimecode( 617 | diff, 'time', self.__fps, self.__drop_frame, self.__strict) 618 | temp_object.set_type(self.type, rounding=False) 619 | return temp_object 620 | 621 | def __mul__(self, other): # 运算符重载,乘法,int和float都是倍数 622 | prod = self.__precise_time 623 | if isinstance(other, DfttTimecode): 624 | logger.error( 625 | 'Timecode CANNOT multiply with another Timecode.') 626 | raise DFTTTimecodeOperatorError 627 | elif isinstance(other, int): 628 | prod = self.__precise_time * other 629 | elif isinstance(other, float): 630 | prod = self.__precise_time * other 631 | elif isinstance(other, Fraction): 632 | prod = self.__precise_time * other 633 | else: 634 | logger.error('Undefined multiplication.') 635 | raise DFTTTimecodeOperatorError 636 | temp_object = DfttTimecode( 637 | prod, 'time', self.__fps, self.__drop_frame, self.__strict) 638 | temp_object.set_type(self.type, rounding=False) 639 | return temp_object 640 | 641 | def __rmul__(self, other): # 乘法交换律 642 | return self.__mul__(other) 643 | 644 | def __truediv__(self, other): 645 | quo_time = self.__precise_time # quo_time是商(时间戳) 646 | if isinstance(other, DfttTimecode): 647 | logger.error( 648 | 'Timecode CANNOT be devided by another Timecode.') 649 | raise DFTTTimecodeOperatorError 650 | elif isinstance(other, int): # timecode与数相除,得到结果是timecode 651 | quo_time = self.__precise_time / other 652 | elif isinstance(other, float): # timecode与数相除,得到结果是timecode 653 | quo_time = self.__precise_time / other 654 | elif isinstance(other, Fraction): # timecode与数相除,得到结果是timecode 655 | quo_time = self.__precise_time / other 656 | else: 657 | logger.error('Undefined division.') 658 | raise DFTTTimecodeOperatorError 659 | temp_object = DfttTimecode( 660 | quo_time, 'time', self.__fps, self.__drop_frame, self.__strict) 661 | temp_object.set_type(self.type, rounding=False) 662 | return temp_object 663 | 664 | def __rtruediv__(self, other): 665 | if isinstance(other, int) or isinstance(other, float) or isinstance(other, Fraction): 666 | logger.error( 667 | 'Number CANNOT be devided by a Timecode.') 668 | raise DFTTTimecodeOperatorError 669 | else: 670 | logger.error('Undefined division.') 671 | raise DFTTTimecodeOperatorError 672 | 673 | def __eq__(self, other): # 判断相等 674 | if isinstance(other, DfttTimecode): # 与另一个Timecode对象比较 比双方的时间戳 精确到5位小数 675 | if self.fps != other.fps: 676 | raise DFTTTimecodeOperatorError 677 | else: 678 | return round(self.__precise_time, 5) == round(other.__precise_time, 5) 679 | elif isinstance(other, int): # 与int比较 默认int为帧号 比较当前timecode对象的帧号是否与其一致 680 | return int(self.timecode_output('frame')) == other 681 | elif isinstance(other, float): # 与float比较 默认float为时间戳 比较当前timecode对象的时间戳是否与其一致 精确到5位小数 682 | return float(round(self.__precise_time, 5)) == round(other, 5) 683 | # 与Fraction比较 默认Fraction为时间戳 比较当前timecode对象的时间戳是否与其一致 精确到5位小数 684 | elif isinstance(other, Fraction): 685 | return round(self.__precise_time, 5) == round(other, 5) 686 | else: 687 | logger.error('CANNOT compare with such data type.') 688 | raise DFTTTimecodeTypeError 689 | 690 | def __ne__(self, other): 691 | return not self.__eq__(other) 692 | 693 | def __lt__(self, other): # 详见__eq__ 694 | if isinstance(other, DfttTimecode): 695 | if self.fps != other.fps: 696 | raise DFTTTimecodeOperatorError 697 | else: 698 | return round(self.__precise_time, 5) < round(other.__precise_time, 5) 699 | elif isinstance(other, int): 700 | return int(self.timecode_output('frame')) < other 701 | elif isinstance(other, float): 702 | return float(round(self.__precise_time, 5)) < round(other, 5) 703 | elif isinstance(other, Fraction): 704 | return round(self.__precise_time, 5) < round(other, 5) 705 | else: 706 | logger.error('CANNOT compare with such data type.') 707 | raise DFTTTimecodeTypeError 708 | 709 | def __le__(self, other): # 详见__eq__ 710 | if isinstance(other, DfttTimecode): 711 | if self.fps != other.fps: 712 | raise DFTTTimecodeOperatorError 713 | else: 714 | return round(self.__precise_time, 5) <= round(other.__precise_time, 5) 715 | elif isinstance(other, int): 716 | return int(self.timecode_output('frame')) <= other 717 | elif isinstance(other, float): 718 | return float(round(self.__precise_time, 5)) <= round(other, 5) 719 | elif isinstance(other, Fraction): 720 | return round(self.__precise_time, 5) <= round(other, 5) 721 | else: 722 | logger.error('CANNOT compare with such data type.') 723 | raise DFTTTimecodeTypeError 724 | 725 | def __gt__(self, other): # 详见__eq__ 726 | if isinstance(other, DfttTimecode): 727 | if self.fps != other.fps: 728 | raise DFTTTimecodeOperatorError 729 | else: 730 | return round(self.__precise_time, 5) > round(other.__precise_time, 5) 731 | elif isinstance(other, int): 732 | return int(self.timecode_output('frame')) > other 733 | elif isinstance(other, float): 734 | return float(round(self.__precise_time, 5)) > round(other, 5) 735 | elif isinstance(other, Fraction): 736 | return round(self.__precise_time, 5) > round(other, 5) 737 | else: 738 | logger.error('CANNOT compare with such data type.') 739 | raise DFTTTimecodeTypeError 740 | 741 | def __ge__(self, other): # 详见__eq__ 742 | if isinstance(other, DfttTimecode): 743 | if self.fps != other.fps: 744 | raise DFTTTimecodeOperatorError 745 | else: 746 | return round(self.__precise_time, 5) >= round(other.__precise_time, 5) 747 | elif isinstance(other, int): 748 | return int(self.timecode_output('frame')) >= other 749 | elif isinstance(other, float): 750 | return float(round(self.__precise_time, 5)) >= round(other, 5) 751 | elif isinstance(other, Fraction): 752 | return round(self.__precise_time, 5) >= round(other, 5) 753 | else: 754 | logger.error('CANNOT compare with such data type.') 755 | raise DFTTTimecodeTypeError 756 | 757 | def __neg__(self): # 取负操作 返回时间戳取负的Timecode对象(strict规则照常应用 例如01:00:00:00 strict的对象 取负后为23:00:00:00) 758 | temp_object = DfttTimecode(-self.__precise_time, 'time', 759 | self.__fps, self.__drop_frame, self.__strict) 760 | temp_object.set_type(self.type, rounding=False) 761 | return temp_object 762 | 763 | def __float__(self): 764 | return self.timestamp 765 | 766 | def __int__(self): 767 | return self.framecount 768 | -------------------------------------------------------------------------------- /dftt_timecode/error.py: -------------------------------------------------------------------------------- 1 | class DFTTError(Exception): 2 | pass 3 | 4 | 5 | class DFTTTimecodeValueError(DFTTError): 6 | pass 7 | 8 | 9 | class DFTTTimecodeTypeError(DFTTError): 10 | pass 11 | 12 | class DFTTTimecodeOperatorError(DFTTError): 13 | pass 14 | 15 | class DFTTTimecodeInitializationError(DFTTError): 16 | pass 17 | -------------------------------------------------------------------------------- /dftt_timecode/pattern.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | SMPTE_NDF_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d):){1}(\d?\d\d{1}){1}$') 4 | # SMPTE NDF 形如01:23:45:12或-01:23:45:12 对于高帧率(>=100fps) 形如01:01:23:45:102或-01:01:23:45:102 5 | SMPTE_DF_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d);){1}(\d?\d\d{1}){1}$') 6 | # SMPTE DF 形如01:23:45;12或-01:23:45;12 对于高帧率(>=100fps) 形如01:00:00;102或-01:00:00;102 7 | SMPTE_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d);?:?){1}(\d?\d\d{1}){1}$') 8 | # SMPTE全匹配 即SMPTE NDF与SMPTE DF的并集 9 | SRT_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d),){1}(\d\d\d){1}$') 10 | # SRT 形如01:23:45,678或-01:23:45,678 11 | FFMPEG_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d)\.){1}(\d?\d+){1}$') 12 | # FFMPEG 形如01:23:45.67或-01:23:45.67 13 | DLP_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d):){1}([01][0-9][0-9]|2[0-4][0-9]|25[0]){1}$') 14 | # DLP 形如01:23:45:102或-01:23:45:102(末三位取值范围是0-249) 15 | FCPX_REGEX = re.compile(r'^(?:-)?(\d+)[/](\d+)?s$') 16 | # FCPX 形如1/24s或-1/24s s可有可无 17 | FRAME_REGEX = re.compile(r'^(-?\d+?)f?$') 18 | # 帧号 形如1234f或-1234f f可有可无 19 | TIME_REGEX = re.compile(r'^(-?\d+?(\.{1})\d+?|-?\d+?)s?$') 20 | # 时间戳 形如1234s或-1234.5s s可有可无 21 | 22 | 23 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -v -s 3 | testpaths = test -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | iniconfig==2.0.0 2 | packaging==24.2 3 | pluggy==1.5.0 4 | pytest==8.3.4 5 | setuptools==75.8.0 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | import dftt_timecode as package 3 | with open("README.md", "r",encoding='UTF-8') as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name=package.name, 8 | version=package.__version__, 9 | author="You Ziyuan", 10 | author_email="hikaridragon0216@gmail.com", 11 | description="Timecode library for film and TV industry, supports HFR and a bunch of cool features", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/OwenYou/dftt_timecode", 15 | packages=['dftt_timecode','dftt_timecode.core'], 16 | python_requires=">=3.10", 17 | classifiers=[ 18 | "Programming Language :: Python :: 3", 19 | "Development Status :: 2 - Pre-Alpha", 20 | "License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)", 21 | "Natural Language :: Chinese (Simplified)", 22 | "Operating System :: OS Independent", 23 | ], 24 | ) 25 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OwenYou/dftt_timecode/51d96854fc9f16469ce7fac74321f417624329c9/test/__init__.py -------------------------------------------------------------------------------- /test/test_dftt_timecode.py: -------------------------------------------------------------------------------- 1 | from fractions import Fraction 2 | import pytest 3 | from dftt_timecode.error import * 4 | from dftt_timecode import DfttTimecode as TC 5 | 6 | 7 | @pytest.fixture( 8 | params=[ 9 | ("00:00:01:00", "auto", 24, False, True), 10 | ("1000", "auto", 119.88, True, True), 11 | ("1.0", "auto", Fraction(60000, 1001), True, True), 12 | ("00:01:00;02", "auto", 29.97, True, True), 13 | ("01:00:00,123", "auto", 24, False, True), 14 | ], 15 | ids=["smpte", "frame", "time", "smpte_df", "srt"], 16 | ) 17 | def tc_data(request): 18 | return request.param 19 | 20 | 21 | def test_tc_instance(tc_data): 22 | assert isinstance(TC(*tc_data), TC) 23 | 24 | 25 | @pytest.mark.parametrize( 26 | "timecode_value, timecode_type, fps, drop_frame, strict, result_type", 27 | [ 28 | ("01:00:00:00", "smpte", 24, False, True, "smpte"), 29 | ("1000f", "frame", 119.88, True, True, "frame"), 30 | ("3600.0s", "time", Fraction(60000, 1001), True, True, "time"), 31 | ], 32 | ids=["smpte", "frame", "time"], 33 | ) 34 | def test_type(timecode_value, timecode_type, fps, drop_frame, strict, result_type): 35 | assert ( 36 | TC(timecode_value, timecode_type, fps, drop_frame, strict).type == result_type 37 | ) 38 | 39 | 40 | @pytest.mark.parametrize( 41 | "timecode_value, timecode_type, fps, drop_frame, strict, result_type", 42 | [ 43 | ("01:00:00:00", "auto", 24, False, True, "smpte"), 44 | ("1000f", "auto", 119.88, True, True, "frame"), 45 | ("3600.0s", "auto", Fraction(60000, 1001), True, True, "time"), 46 | ], 47 | ids=["smpte", "frame", "time"], 48 | ) 49 | def test_auto_type(timecode_value, timecode_type, fps, drop_frame, strict, result_type): 50 | assert TC(timecode_value, "auto", fps, drop_frame, strict).type == result_type 51 | 52 | 53 | @pytest.mark.parametrize( 54 | "timecode_value, timecode_type, fps, drop_frame, strict", 55 | [ 56 | ("01:00:00:23", "smpte", 20, False, True), 57 | ], 58 | ids=["smpte"], 59 | ) 60 | def test_invalid_timecode(timecode_value, timecode_type, fps, drop_frame, strict): 61 | from dftt_timecode.error import DFTTTimecodeValueError 62 | 63 | with pytest.raises(DFTTTimecodeValueError): 64 | tc = TC(timecode_value, timecode_type, fps, drop_frame, strict) 65 | 66 | 67 | @pytest.mark.parametrize( 68 | "timecode_value, timecode_type, fps, drop_frame, strict, result_fps", 69 | [ 70 | ("01:00:00:00", "auto", 24, False, True, 24), 71 | ("1000f", "auto", 119.88, True, True, 119.88), 72 | ("3600.0s", "auto", Fraction(60000, 1001), True, True, Fraction(60000, 1001)), 73 | ], 74 | ids=["smpte", "frame", "time"], 75 | ) 76 | def test_fps(timecode_value, timecode_type, fps, drop_frame, strict, result_fps): 77 | assert TC(timecode_value, timecode_type, fps, drop_frame, strict).fps == result_fps 78 | 79 | 80 | @pytest.mark.parametrize( 81 | "timecode_value, timecode_type, fps, drop_frame, strict, result_framecount", 82 | [ 83 | ("00:00:01:00", "auto", 24, False, True, 24), 84 | ("1000f", "auto", 119.88, True, True, 1000), 85 | ("1.0s", "auto", Fraction(60000, 1001), True, True, 60), 86 | ("00:01:00;02", "auto", 29.97, True, True, 1800), 87 | ], 88 | ids=["smpte", "frame", "time", "smpte_nf"], 89 | ) 90 | def test_framecount( 91 | timecode_value, timecode_type, fps, drop_frame, strict, result_framecount 92 | ): 93 | assert ( 94 | TC(timecode_value, timecode_type, fps, drop_frame, strict).framecount 95 | == result_framecount 96 | ) 97 | 98 | 99 | @pytest.mark.parametrize( 100 | "timecode_value, timecode_type, fps, drop_frame, strict", 101 | [ 102 | ("00:00:01:00", "auto", 24, False, True), 103 | ("1000f", "auto", 119.88, True, True), 104 | ("1.0s", "auto", Fraction(60000, 1001), True, True), 105 | ("00:01:00;02", "auto", 29.97, True, True), 106 | pytest.param("00:01:00:02", "auto", 29.97, False, True), 107 | ], 108 | ids=["smpte", "frame", "time", "smpte_df", "smpte_ndf"], 109 | ) 110 | def test_dropframe_strict(timecode_value, timecode_type, fps, drop_frame, strict): 111 | assert ( 112 | TC(timecode_value, timecode_type, fps, drop_frame, strict).is_drop_frame 113 | is drop_frame 114 | ) 115 | assert ( 116 | TC(timecode_value, timecode_type, fps, drop_frame, strict).is_strict is strict 117 | ) 118 | 119 | 120 | @pytest.fixture( 121 | params=[ 122 | ("00:01:01:01", "auto", 24, False, True, 61.04167, Fraction(1465 / 24)), 123 | ("1000f", "auto", 119.88, True, True, 8.34168, Fraction(1000 / 119.88)), 124 | ("1.0s", "auto", Fraction(60000 / 1001), True, True, 1, 1), 125 | ("00:01:00;02", "auto", 29.97, True, True, 60.06006, Fraction(1800 / 29.97)), 126 | ], 127 | ids=["smpte", "frame", "time", "smpte_nf"], 128 | ) 129 | def timestamp_data(request): 130 | return request.param 131 | 132 | 133 | def test_timestamp(timestamp_data): 134 | assert TC(*timestamp_data[:-2]).timestamp == pytest.approx(timestamp_data[-2]) 135 | 136 | 137 | def test_precise_timestamp(timestamp_data): 138 | assert TC(*timestamp_data[:-2]).precise_timestamp == timestamp_data[-1] 139 | 140 | 141 | @pytest.fixture( 142 | params=[ 143 | ("01:00:00:101", "auto", 120, False, True, 24, True, "01:00:00:100"), 144 | ("01:00:00:101", "auto", 120, False, True, 24, False, "01:00:00:101"), 145 | ], 146 | ids=["120_24_Round", "120_24_NRound"], 147 | ) 148 | def set_fps_data(request): 149 | yield request.param 150 | 151 | 152 | def test_set_fps(set_fps_data): 153 | tc = TC(*set_fps_data[:-3]) 154 | tc.set_fps(set_fps_data[-3], rounding=set_fps_data[-2]) 155 | assert tc.fps == set_fps_data[-3] 156 | tc.set_fps(set_fps_data[2]) 157 | assert tc.timecode_output("smpte") == set_fps_data[-1] 158 | 159 | 160 | @pytest.fixture( 161 | params=[ 162 | ("00:00:01:101", "auto", 120, False, True, "frame", True, "221"), 163 | ("00:00:01,123", "auto", 120, False, True, "smpte", True, "00:00:01:015"), 164 | ], 165 | ids=["smpte_frame", "srt_smpte_round"], 166 | ) 167 | def set_type_data(request): 168 | yield request.param 169 | 170 | 171 | def test_set_type(set_type_data): 172 | tc = TC(*set_type_data[:-3]) 173 | tc.set_type(set_type_data[-3], rounding=set_type_data[-2]) 174 | assert tc.type == set_type_data[-3] 175 | assert tc.timecode_output(set_type_data[-3]) == set_type_data[-1] 176 | 177 | 178 | @pytest.fixture( 179 | params=[ 180 | ("25:00:01:101", "auto", 120, False, False, True, "01:00:01:101"), 181 | ("24:00:00:01", "auto", 24, False, False, True, "00:00:00:01"), 182 | ], 183 | ids=["120FPS", "24FPS"], 184 | ) 185 | def set_strict_data(request): 186 | yield request.param 187 | 188 | 189 | def test_set_strict(set_strict_data): 190 | tc = TC(*set_strict_data[:-2]) 191 | assert tc.is_strict == set_strict_data[-3] 192 | tc.set_strict() 193 | assert tc.is_strict == set_strict_data[-2] 194 | assert tc.timecode_output("smpte") == set_strict_data[-1] 195 | 196 | 197 | @pytest.mark.parametrize( 198 | argnames="timecode_value, timecode_type, fps, drop_frame, strict,output_smpte,output_frame,output_time,output_srt,output_fcpx,output_ffmpeg", 199 | argvalues=[ 200 | ( 201 | "00:00:01:00", 202 | "auto", 203 | 24, 204 | False, 205 | True, 206 | "00:00:01:00", 207 | "24", 208 | "1.0", 209 | "00:00:01,000", 210 | "1s", 211 | "00:00:01.00", 212 | ), 213 | ( 214 | "00:10:00;00", 215 | "auto", 216 | 29.97, 217 | True, 218 | True, 219 | "00:10:00;00", 220 | "17982", 221 | "600.0", 222 | "00:10:00,000", 223 | "600s", 224 | "00:10:00.00", 225 | ), 226 | ], 227 | ids=["NDF", "DF"], 228 | ) 229 | def test_timecode_output( 230 | timecode_value, 231 | timecode_type, 232 | fps, 233 | drop_frame, 234 | strict, 235 | output_smpte, 236 | output_frame, 237 | output_time, 238 | output_srt, 239 | output_fcpx, 240 | output_ffmpeg, 241 | ): 242 | tc = TC(timecode_value, timecode_type, fps, drop_frame, strict) 243 | assert tc.timecode_output() == timecode_value 244 | assert tc.timecode_output("smpte") == output_smpte 245 | assert tc.timecode_output("frame") == output_frame 246 | assert tc.timecode_output("time") == output_time 247 | assert tc.timecode_output("srt") == output_srt 248 | assert tc.timecode_output("fcpx") == output_fcpx 249 | assert tc.timecode_output("ffmpeg") == output_ffmpeg 250 | 251 | 252 | @pytest.mark.parametrize( 253 | argnames="timecode_value, timecode_type, fps, drop_frame, strict,output_type,output_str,part_1,part_2,part_3,part_4", 254 | argvalues=[ 255 | ( 256 | "11:22:33:13", 257 | "auto", 258 | 24, 259 | False, 260 | True, 261 | "smpte", 262 | "11:22:33:13", 263 | "11", 264 | "22", 265 | "33", 266 | "13", 267 | ), 268 | ( 269 | "11:22:33,456", 270 | "auto", 271 | 24, 272 | False, 273 | True, 274 | "srt", 275 | "11:22:33,456", 276 | "11", 277 | "22", 278 | "33", 279 | "456", 280 | ), 281 | ], 282 | ids=["smpte", "srt"], 283 | ) 284 | def test_timecode_output_part( 285 | timecode_value, 286 | timecode_type, 287 | fps, 288 | drop_frame, 289 | strict, 290 | output_type, 291 | output_str, 292 | part_1, 293 | part_2, 294 | part_3, 295 | part_4, 296 | ): 297 | tc = TC(timecode_value, timecode_type, fps, drop_frame, strict) 298 | assert tc.timecode_output(output_type) == output_str 299 | assert tc.timecode_output(output_type, output_part=1) == part_1 300 | assert tc.timecode_output(output_type, output_part=2) == part_2 301 | assert tc.timecode_output(output_type, output_part=3) == part_3 302 | assert tc.timecode_output(output_type, output_part=4) == part_4 303 | 304 | 305 | def test_print(tc_data, capsys): 306 | tc = TC(*tc_data) 307 | print(tc, end="") 308 | print_output = capsys.readouterr() 309 | assert print_output.out == tc_data[0] 310 | 311 | 312 | @pytest.fixture() 313 | def neg_result(request): 314 | neg_result_data = { 315 | "00:00:01:00": "23:59:59:00", 316 | "1000": "10356632", 317 | "1.0": "86399.0", 318 | "00:01:00;02": "23:58:59;28", 319 | "01:00:00,123": "22:59:59,877", 320 | } 321 | tc_value = request.node.funcargs["tc_data"][0] 322 | print(tc_value) 323 | return neg_result_data.get(tc_value) 324 | 325 | 326 | def test_neg(tc_data, neg_result): 327 | tc = TC(*tc_data) 328 | tc = -tc 329 | assert tc.timecode_output() == neg_result 330 | 331 | 332 | @pytest.fixture( 333 | params=[ 334 | ( 335 | ("00:00:01:00", "auto", 24, False, True), 336 | ("23:59:59:00", "auto", 24, False, False), 337 | ("00:00:00:00", "auto", 24, False, True), 338 | ), 339 | ( 340 | ("1000", "auto", 119.88, True, True), 341 | ("120", "auto", 119.88, True, True), 342 | ("1120", "auto", 119.88, True, True), 343 | ), 344 | pytest.param( 345 | ( 346 | ("1000", "auto", 119.88, True, True), 347 | ("1s", "auto", 119.88, True, True), 348 | ("1120", "auto", 119.88, True, True), 349 | ), 350 | marks=pytest.mark.xfail, 351 | ), 352 | ( 353 | ("1s", "auto", Fraction(60000, 1001), True, True), 354 | ("1.0", "auto", Fraction(60000, 1001), True, True), 355 | ("2s", "auto", Fraction(60000, 1001), True, True), 356 | ), 357 | ( 358 | ("00:00:59;29", "auto", 29.97, True, True), 359 | ("00:00:00;01", "auto", 29.97, True, True), 360 | ("00:01:00;02", "auto", 29.97, True, True), 361 | ), 362 | ( 363 | ("01:00:00,123", "auto", 24, False, True), 364 | ("01:00:00,878", "auto", 24, False, True), 365 | ("02:00:01,001", "auto", 24, False, True), 366 | ), 367 | ], 368 | ids=["smpte", "frame", "frame_xfail", "time", "smpte_df", "srt"], 369 | ) 370 | def plus_tc_data(request): 371 | return [TC(*request.param[i]) for i in range(3)] 372 | 373 | 374 | def test_plus_tc(plus_tc_data): 375 | tc_plus = plus_tc_data[0] + plus_tc_data[1] 376 | assert tc_plus == plus_tc_data[2] 377 | 378 | 379 | @pytest.fixture( 380 | params=[ 381 | ( 382 | ("00:00:00:23", "auto", 24, False, True), 383 | 1, 384 | ("00:00:01:00", "auto", 24, False, True), 385 | ), 386 | ( 387 | ("1000", "auto", 119.88, True, True), 388 | 111, 389 | ("1111", "auto", 119.88, True, True), 390 | ), 391 | pytest.param( 392 | ( 393 | ("1s", "auto", Fraction(60000, 1001), True, True), 394 | 60, 395 | ("2s", "auto", Fraction(60000, 1001), True, True), 396 | ), 397 | marks=pytest.mark.xfail, 398 | ), 399 | ( 400 | ("00:00:59;29", "auto", 29.97, True, True), 401 | 1, 402 | ("00:01:00;02", "auto", 29.97, True, True), 403 | ), 404 | ( 405 | ("01:00:00,123", "auto", 24, False, True), 406 | 24, 407 | ("01:00:01,123", "auto", 24, False, True), 408 | ), 409 | ( 410 | ("00:00:00:23", "auto", 24, False, True), 411 | 1.0, 412 | ("00:00:01:23", "auto", 24, False, True), 413 | ), 414 | pytest.param( 415 | ( 416 | ("1000", "auto", 119.88, True, True), 417 | 1.0, 418 | ("1120", "auto", 119.88, True, True), 419 | ), 420 | marks=pytest.mark.xfail, 421 | ), 422 | ( 423 | ("1s", "auto", Fraction(60000, 1001), True, True), 424 | 60.0, 425 | ("61s", "auto", Fraction(60000, 1001), True, True), 426 | ), 427 | pytest.param( 428 | ( 429 | ("00:00:59;29", "auto", 29.97, True, True), 430 | 1.0, 431 | ("00:01:01;01", "auto", 29.97, True, True), 432 | ), 433 | marks=pytest.mark.xfail, 434 | ), 435 | ( 436 | ("01:00:00,123", "auto", 24, False, True), 437 | 0.877, 438 | ("01:00:01,000", "auto", 24, False, True), 439 | ), 440 | pytest.param( 441 | ( 442 | ("00:09:59;00", "auto", 29.97, True, True), 443 | Fraction(1000, 1001), 444 | ("00:10:00;00", "auto", 29.97, True, True), 445 | ), 446 | marks=pytest.mark.xfail, 447 | ), 448 | ], 449 | ids=[ 450 | "smpte_int", 451 | "frame_int", 452 | "time_int_xfail", 453 | "smpte_df_int", 454 | "srt_int", 455 | "smpte_float", 456 | "frame_float_xfail", 457 | "time_float", 458 | "smpte_df_float", 459 | "srt_float", 460 | "smpte_df_fraction_xfail", 461 | ], 462 | ) 463 | def plus_num_data(request): 464 | return [TC(*request.param[0]), request.param[1], TC(*request.param[2])] 465 | 466 | 467 | def test_plus_num(plus_num_data): 468 | tc_plus = plus_num_data[0] + plus_num_data[1] 469 | assert tc_plus == plus_num_data[2] 470 | 471 | 472 | @pytest.fixture( 473 | params=[ 474 | ( 475 | ("00:00:01:00", "auto", 24, False, True), 476 | ("00:00:02:00", "auto", 24, False, False), 477 | ("23:59:59:00", "auto", 24, False, True), 478 | ), 479 | ( 480 | ("1000", "auto", 119.88, True, False), 481 | ("1001", "auto", 119.88, True, False), 482 | ("-1", "auto", 119.88, True, False), 483 | ), 484 | ( 485 | ("2s", "auto", Fraction(60000, 1001), True, True), 486 | ("1.0", "auto", Fraction(60000, 1001), True, True), 487 | ("1s", "auto", Fraction(60000, 1001), True, True), 488 | ), 489 | ( 490 | ("00:00:59;29", "auto", 29.97, True, True), 491 | ("1", "auto", 29.97, True, True), 492 | ("00:00:59;28", "auto", 29.97, True, True), 493 | ), 494 | ( 495 | ("00:00:00,100", "auto", 24, False, True), 496 | ("00:00:01,000", "auto", 24, False, True), 497 | ("23:59:59,100", "auto", 24, False, True), 498 | ), 499 | ], 500 | ids=["smpte", "frame", "time", "smpte_df", "srt"], 501 | ) 502 | def sub_tc_data(request): 503 | return [TC(*request.param[i]) for i in range(3)] 504 | 505 | 506 | def test_sub_tc(sub_tc_data): 507 | tc_sub = sub_tc_data[0] - sub_tc_data[1] 508 | assert tc_sub == sub_tc_data[2] 509 | 510 | 511 | @pytest.fixture( 512 | params=[ 513 | ( 514 | ("00:00:00:23", "auto", 24, False, True), 515 | 23, 516 | ("00:00:00:00", "auto", 24, False, True), 517 | ), 518 | ( 519 | ("1000", "auto", 119.88, True, False), 520 | 1001, 521 | ("-1", "auto", 119.88, True, False), 522 | ), 523 | pytest.param( 524 | ( 525 | ("1s", "auto", Fraction(60000, 1001), True, True), 526 | 60, 527 | ("0s", "auto", Fraction(60000, 1001), True, True), 528 | ), 529 | marks=pytest.mark.xfail, 530 | ), 531 | ( 532 | ("00:00:59;29", "auto", 29.97, True, True), 533 | 1, 534 | ("00:00:59;28", "auto", 29.97, True, True), 535 | ), 536 | ( 537 | ("01:00:00,123", "auto", 24, False, True), 538 | 24, 539 | ("00:59:59,123", "auto", 24, False, True), 540 | ), 541 | ( 542 | ("00:00:00:23", "auto", 24, False, True), 543 | 1.0, 544 | ("23:59:59:23", "auto", 24, False, True), 545 | ), 546 | (("1000", "auto", 120, True, True), 1.0, ("880", "auto", 120, True, True)), 547 | (("1s", "auto", 60, True, True), 60.0, ("86341s", "auto", 60, True, True)), 548 | pytest.param( 549 | ( 550 | ("00:01:01;02", "auto", 29.97, True, True), 551 | 1.0, 552 | ("00:01:00;00", "auto", 29.97, True, True), 553 | ), 554 | marks=pytest.mark.xfail, 555 | ), 556 | ( 557 | ("01:00:00,123", "auto", 24, False, True), 558 | -0.123, 559 | ("01:00:00,246", "auto", 24, False, True), 560 | ), 561 | ], 562 | ids=[ 563 | "smpte_int", 564 | "frame_int", 565 | "time_int_xfail", 566 | "smpte_df_int", 567 | "srt_int", 568 | "smpte_float", 569 | "frame_float_xfail", 570 | "time_float", 571 | "smpte_df_float_xfail", 572 | "srt_float", 573 | ], 574 | ) 575 | def sub_num_data(request): 576 | return [TC(*request.param[0]), request.param[1], TC(*request.param[2])] 577 | 578 | 579 | def test_sub_num(sub_num_data): 580 | tc_sub = sub_num_data[0] - sub_num_data[1] 581 | assert tc_sub == sub_num_data[2] 582 | 583 | 584 | @pytest.fixture( 585 | params=[ 586 | ( 587 | 23, 588 | ("00:00:00:23", "auto", 24, False, True), 589 | ("00:00:00:00", "auto", 24, False, True), 590 | ), 591 | ( 592 | 1001, 593 | ("1002", "auto", 119.88, True, False), 594 | ("-1", "auto", 119.88, True, False), 595 | ), 596 | pytest.param( 597 | ( 598 | 60, 599 | ("1s", "auto", Fraction(60000, 1001), True, True), 600 | ("0s", "auto", Fraction(60000, 1001), True, True), 601 | ), 602 | marks=pytest.mark.xfail, 603 | ), 604 | ( 605 | 1800, 606 | ("00:00:59;29", "auto", 29.97, True, True), 607 | ("00:00:00;01", "auto", 29.97, True, True), 608 | ), 609 | ( 610 | 24, 611 | ("00:00:00,123", "auto", 24, False, True), 612 | ("00:00:00,877", "auto", 24, False, True), 613 | ), 614 | ( 615 | 1.0, 616 | ("00:00:00:23", "auto", 24, False, True), 617 | ("00:00:00:01", "auto", 24, False, True), 618 | ), 619 | (10.0, ("1000", "auto", 120, True, True), ("200", "auto", 120, True, True)), 620 | (1.0, ("60s", "auto", 60, True, True), ("86341s", "auto", 60, True, True)), 621 | ( 622 | 0.123, 623 | ("01:00:00,123", "auto", 24, False, True), 624 | ("23:00:00,000", "auto", 24, False, True), 625 | ), 626 | ], 627 | ids=[ 628 | "smpte_int", 629 | "frame_int", 630 | "time_int_xfail", 631 | "smpte_df_int", 632 | "srt_int", 633 | "smpte_float", 634 | "frame_float_xfail", 635 | "time_float", 636 | "srt_float", 637 | ], 638 | ) 639 | def rsub_num_data(request): 640 | return [request.param[0], TC(*request.param[1]), TC(*request.param[2])] 641 | 642 | 643 | def test_rsub_num(rsub_num_data): 644 | tc_sub = rsub_num_data[0] - rsub_num_data[1] 645 | assert tc_sub == rsub_num_data[2] 646 | 647 | 648 | @pytest.fixture( 649 | params=[ 650 | ( 651 | ("00:00:00:23", "auto", 24, False, True), 652 | 2, 653 | ("00:00:01:22", "auto", 24, False, True), 654 | ), 655 | ( 656 | ("1002", "auto", 119.88, True, False), 657 | 2, 658 | ("2004", "auto", 119.88, True, False), 659 | ), 660 | ( 661 | ("1s", "auto", Fraction(60000, 1001), True, True), 662 | 60, 663 | ("60s", "auto", Fraction(60000, 1001), True, True), 664 | ), 665 | ( 666 | ("00:01:00;02", "auto", 29.97, True, True), 667 | 10, 668 | ("00:10:00;18", "auto", 29.97, True, True), 669 | ), 670 | ( 671 | ("00:00:00,123", "auto", 24, False, True), 672 | 10, 673 | ("00:00:01,230", "auto", 24, False, True), 674 | ), 675 | ( 676 | ("00:00:00:00", "auto", 24, False, True), 677 | 10000.11, 678 | ("00:00:00:00", "auto", 24, False, True), 679 | ), 680 | (("1000", "auto", 120, True, True), 1.5, ("1500", "auto", 120, True, True)), 681 | (("60s", "auto", 60, True, True), 1.5, ("90s", "auto", 60, True, True)), 682 | ( 683 | ("01:00:00,000", "auto", 24, False, True), 684 | 1.5, 685 | ("01:30:00,000", "auto", 24, False, True), 686 | ), 687 | ], 688 | ids=[ 689 | "smpte_int", 690 | "frame_int", 691 | "time_int", 692 | "smpte_df_int", 693 | "srt_int", 694 | "smpte_float", 695 | "frame_float", 696 | "time_float", 697 | "srt_float", 698 | ], 699 | ) 700 | def mul_num_data(request): 701 | return [TC(*request.param[0]), request.param[1], TC(*request.param[2])] 702 | 703 | 704 | def test_mul(mul_num_data): 705 | tc_mul = mul_num_data[0] * mul_num_data[1] 706 | tc_rmul = mul_num_data[1] * mul_num_data[0] 707 | assert tc_mul == mul_num_data[2] 708 | assert tc_rmul == mul_num_data[2] 709 | 710 | 711 | def test_mul_xfail(): 712 | from dftt_timecode.error import DFTTTimecodeOperatorError 713 | 714 | tc_1 = TC("00:00:00:23", "auto", 24, False, True) 715 | tc_2 = TC("00:11:45:14", "auto", 24, False, True) 716 | with pytest.raises(DFTTTimecodeOperatorError): 717 | tc_mul_xfail = tc_1 * tc_2 718 | 719 | 720 | @pytest.fixture( 721 | params=[ 722 | ( 723 | ("00:00:01:00", "auto", 24, False, True), 724 | 2, 725 | ("00:00:00:12", "auto", 24, False, True), 726 | ), 727 | ( 728 | ("114514", "auto", 119.88, True, False), 729 | 2, 730 | ("57257", "auto", 119.88, True, False), 731 | ), 732 | ( 733 | ("60s", "auto", Fraction(60000, 1001), True, True), 734 | 60, 735 | ("1s", "auto", Fraction(60000, 1001), True, True), 736 | ), 737 | ( 738 | ("00:01:00;02", "auto", 29.97, True, True), 739 | 0.1, 740 | ("00:10:00;18", "auto", 29.97, True, True), 741 | ), 742 | ( 743 | ("00:00:01,234", "auto", 24, False, True), 744 | Fraction(1, 2), 745 | ("00:00:02,468", "auto", 24, False, True), 746 | ), 747 | ( 748 | ("00:00:00:00", "auto", 24, False, True), 749 | 10000.11, 750 | ("00:00:00:00", "auto", 24, False, True), 751 | ), 752 | (("1000", "auto", 120, True, True), 2.5, ("400", "auto", 120, True, True)), 753 | (("60s", "auto", 60, True, True), 1.5, ("40s", "auto", 60, True, True)), 754 | ( 755 | ("01:00:00,000", "auto", 24, False, True), 756 | 0.5, 757 | ("02:00:00,000", "auto", 24, False, True), 758 | ), 759 | ], 760 | ids=[ 761 | "smpte_int", 762 | "frame_int", 763 | "time_int", 764 | "smpte_df_int", 765 | "srt_int", 766 | "smpte_float", 767 | "frame_float", 768 | "time_float", 769 | "srt_float", 770 | ], 771 | ) 772 | def div_num_data(request): 773 | return [TC(*request.param[0]), request.param[1], TC(*request.param[2])] 774 | 775 | 776 | def test_div(div_num_data): 777 | tc_div = div_num_data[0] / div_num_data[1] 778 | assert tc_div == div_num_data[2] 779 | from dftt_timecode.error import DFTTTimecodeOperatorError 780 | 781 | with pytest.raises(DFTTTimecodeOperatorError): 782 | div_num_data[1] / div_num_data[0] 783 | 784 | 785 | @pytest.mark.parametrize( 786 | argnames="tc_value,compare_tc_value", 787 | argvalues=[ 788 | pytest.param( 789 | ("00:00:01:00", "auto", 24, False, True), 790 | ("00:00:01:00", "auto", 25, False, True), 791 | marks=pytest.mark.xfail(raises=DFTTTimecodeOperatorError), 792 | ), 793 | (("00:00:01:00", "auto", 24, False, True), ("24", "auto", 24, False, True)), 794 | ( 795 | ("00:10:00;00", "auto", 29.97, True, True), 796 | ("600.0", "auto", 29.97, True, True), 797 | ), 798 | ( 799 | ("00:00:01:60", "auto", 120, False, True), 800 | ("00:00:01,500", "auto", 120, False, True), 801 | ), 802 | ], 803 | ids=["smpte_smpte_xfail", "smpte_frame", "smpte_time", "smpte_srt"], 804 | ) 805 | def test_eq_tc(tc_value, compare_tc_value): 806 | assert TC(*tc_value) == TC(*compare_tc_value) 807 | 808 | 809 | @pytest.mark.parametrize( 810 | argnames="tc_value,compare_num", 811 | argvalues=[ 812 | (("00:00:01:00", "auto", 24, False, True), 24), 813 | (("114514", "auto", 24, False, True), 114514), 814 | pytest.param(("60s", "auto", 29.97, True, True), 1799, marks=pytest.mark.xfail), 815 | (("00:00:01,500", "auto", 120, False, True), 180), 816 | (("00:00:01:00", "auto", 24, False, True), 1.0), 817 | (("2400", "auto", 24, False, True), 100.0), 818 | (("60s", "auto", 29.97, True, True), 60.0), 819 | (("00:00:01,500", "auto", 120, False, True), 1.5), 820 | ], 821 | ids=[ 822 | "smpte_int", 823 | "frame_int", 824 | "time_int_xfail", 825 | "srt_int", 826 | "smpte_float", 827 | "frame_float", 828 | "time_float", 829 | "srt_float", 830 | ], 831 | ) 832 | def test_eq_num(tc_value, compare_num): 833 | assert TC(*tc_value) == compare_num 834 | 835 | 836 | @pytest.mark.parametrize( 837 | argnames="tc_value,compare_value,xvalue", 838 | argvalues=[ 839 | ( 840 | ("00:00:01:00", "auto", 24, False, True), 841 | ("00:00:00:22", "auto", 24, False, True), 842 | True, 843 | ), 844 | ( 845 | ("00:00:01:00", "auto", 24, False, True), 846 | ("15", "auto", 24, False, True), 847 | True, 848 | ), 849 | ( 850 | ("00:00:01:00", "auto", 24, False, True), 851 | ("1.0", "auto", 24, False, True), 852 | False, 853 | ), 854 | ( 855 | ("00:00:01:00", "auto", 24, False, True), 856 | ("00:00:00,500", "auto", 24, False, True), 857 | True, 858 | ), 859 | (("00:00:01:00", "auto", 24, False, True), 22, True), 860 | (("1919", "auto", 24, False, True), 114514, False), 861 | (("2s", "auto", 24, False, True), 24, True), 862 | (("00:00:01,500", "auto", 24, False, True), 24, True), 863 | (("00:00:01:00", "auto", 24, False, True), 0.5, True), 864 | (("24", "auto", 24, False, True), 24, False), 865 | (("5s", "auto", 24, False, True), 0.0, True), 866 | (("00:00:01,233", "auto", 24, False, True), 1.0, True), 867 | ], 868 | ids=[ 869 | "smpte_smpte", 870 | "smpte_frame", 871 | "smpte_time", 872 | "smpte_srt", 873 | "smpte_int", 874 | "frame_int", 875 | "time_int", 876 | "srt_int", 877 | "smpte_float", 878 | "frame_float", 879 | "time_float", 880 | "srt_float", 881 | ], 882 | ) 883 | def test_gt(tc_value, compare_value, xvalue): 884 | tc = TC(*tc_value) 885 | from numbers import Number 886 | 887 | if isinstance(compare_value, Number): 888 | assert (tc > compare_value) == xvalue 889 | elif isinstance(compare_value, tuple): 890 | compare_tc = TC(*compare_value) 891 | assert (tc > compare_tc) == xvalue 892 | 893 | 894 | @pytest.mark.parametrize( 895 | argnames="tc_value,compare_value,xvalue", 896 | argvalues=[ 897 | ( 898 | ("00:00:01:00", "auto", 24, False, True), 899 | ("00:00:00:22", "auto", 24, False, True), 900 | True, 901 | ), 902 | ( 903 | ("00:00:01:00", "auto", 24, False, True), 904 | ("15", "auto", 24, False, True), 905 | True, 906 | ), 907 | ( 908 | ("00:00:01:00", "auto", 24, False, True), 909 | ("1.0", "auto", 24, False, True), 910 | True, 911 | ), 912 | ( 913 | ("00:00:01:00", "auto", 24, False, True), 914 | ("00:00:00,500", "auto", 24, False, True), 915 | True, 916 | ), 917 | (("00:00:01:00", "auto", 24, False, True), 22, True), 918 | (("1919", "auto", 24, False, True), 114514, False), 919 | (("2s", "auto", 24, False, True), 24, True), 920 | (("00:00:01,500", "auto", 24, False, True), 24, True), 921 | (("00:00:01:00", "auto", 24, False, True), 0.5, True), 922 | (("24", "auto", 24, False, True), 24, True), 923 | (("5s", "auto", 24, False, True), 0.0, True), 924 | (("00:00:01,233", "auto", 24, False, True), 1.0, True), 925 | ], 926 | ids=[ 927 | "smpte_smpte", 928 | "smpte_frame", 929 | "smpte_time", 930 | "smpte_srt", 931 | "smpte_int", 932 | "frame_int", 933 | "time_int", 934 | "srt_int", 935 | "smpte_float", 936 | "frame_float", 937 | "time_float", 938 | "srt_float", 939 | ], 940 | ) 941 | def test_ge(tc_value, compare_value, xvalue): 942 | tc = TC(*tc_value) 943 | from numbers import Number 944 | 945 | if isinstance(compare_value, Number): 946 | assert (tc >= compare_value) == xvalue 947 | elif isinstance(compare_value, tuple): 948 | compare_tc = TC(*compare_value) 949 | assert (tc >= compare_tc) == xvalue 950 | 951 | 952 | @pytest.mark.parametrize( 953 | argnames="tc_value,compare_value,xvalue", 954 | argvalues=[ 955 | ( 956 | ("00:00:01:00", "auto", 24, False, True), 957 | ("00:00:00:22", "auto", 24, False, True), 958 | False, 959 | ), 960 | ( 961 | ("00:00:01:00", "auto", 24, False, True), 962 | ("15", "auto", 24, False, True), 963 | False, 964 | ), 965 | ( 966 | ("00:00:01:00", "auto", 24, False, True), 967 | ("1.0", "auto", 24, False, True), 968 | False, 969 | ), 970 | ( 971 | ("00:00:01:00", "auto", 24, False, True), 972 | ("00:00:00,500", "auto", 24, False, True), 973 | False, 974 | ), 975 | (("00:00:01:00", "auto", 24, False, True), 22, False), 976 | (("1919", "auto", 24, False, True), 114514, True), 977 | (("2s", "auto", 24, False, True), 999, True), 978 | (("00:00:01,500", "auto", 24, False, True), 48, True), 979 | (("00:00:01:00", "auto", 24, False, True), 2.0, True), 980 | (("24", "auto", 24, False, True), 1.0, False), 981 | (("0.0", "auto", 24, False, True), 5.0, True), 982 | (("00:00:01,233", "auto", 24, False, True), 2.0, True), 983 | ], 984 | ids=[ 985 | "smpte_smpte", 986 | "smpte_frame", 987 | "smpte_time", 988 | "smpte_srt", 989 | "smpte_int", 990 | "frame_int", 991 | "time_int", 992 | "srt_int", 993 | "smpte_float", 994 | "frame_float", 995 | "time_float", 996 | "srt_float", 997 | ], 998 | ) 999 | def test_lt(tc_value, compare_value, xvalue): 1000 | tc = TC(*tc_value) 1001 | from numbers import Number 1002 | 1003 | if isinstance(compare_value, Number): 1004 | assert (tc < compare_value) == xvalue 1005 | elif isinstance(compare_value, tuple): 1006 | compare_tc = TC(*compare_value) 1007 | assert (tc < compare_tc) == xvalue 1008 | 1009 | 1010 | @pytest.mark.parametrize( 1011 | argnames="tc_value,compare_value,xvalue", 1012 | argvalues=[ 1013 | ( 1014 | ("00:00:01:00", "auto", 24, False, True), 1015 | ("00:00:00:22", "auto", 24, False, True), 1016 | False, 1017 | ), 1018 | ( 1019 | ("00:00:01:00", "auto", 24, False, True), 1020 | ("15", "auto", 24, False, True), 1021 | False, 1022 | ), 1023 | pytest.param( 1024 | ("00:00:01:00", "auto", 24, False, True), 1025 | ("1.0", "auto", 25, False, True), 1026 | True, 1027 | marks=pytest.mark.xfail(raises=DFTTTimecodeOperatorError), 1028 | ), 1029 | ( 1030 | ("00:00:01:00", "auto", 24, False, True), 1031 | ("00:00:00,500", "auto", 24, False, True), 1032 | False, 1033 | ), 1034 | (("00:00:01:00", "auto", 24, False, True), 22, False), 1035 | (("1919", "auto", 24, False, True), 114514, True), 1036 | (("2s", "auto", 24, False, True), 999, True), 1037 | (("00:00:01,500", "auto", 24, False, True), 48, True), 1038 | (("00:00:01:00", "auto", 24, False, True), 2.0, True), 1039 | (("24", "auto", 24, False, True), 1.0, True), 1040 | (("0.0", "auto", 24, False, True), 5.0, True), 1041 | (("00:00:01,233", "auto", 24, False, True), 2.0, True), 1042 | ], 1043 | ids=[ 1044 | "smpte_smpte", 1045 | "smpte_frame", 1046 | "smpte_time", 1047 | "smpte_srt", 1048 | "smpte_int", 1049 | "frame_int", 1050 | "time_int", 1051 | "srt_int", 1052 | "smpte_float", 1053 | "frame_float", 1054 | "time_float", 1055 | "srt_float", 1056 | ], 1057 | ) 1058 | def test_lt(tc_value, compare_value, xvalue): 1059 | tc = TC(*tc_value) 1060 | from numbers import Number 1061 | 1062 | if isinstance(compare_value, Number): 1063 | assert (tc <= compare_value) == xvalue 1064 | elif isinstance(compare_value, tuple): 1065 | compare_tc = TC(*compare_value) 1066 | assert (tc <= compare_tc) == xvalue 1067 | 1068 | 1069 | @pytest.mark.parametrize( 1070 | argnames="tc_value,xvalue", 1071 | argvalues=[ 1072 | (("00:00:01:00", "auto", 24, False, True), 1.0), 1073 | (("00:01:00;02", "auto", 29.97, True, True), float(Fraction(1800 / 29.97))), 1074 | (("48", "auto", 24, False, True), 2.0), 1075 | (("114514s", "auto", 24, False, True), 114514.0), 1076 | (("00:00:01,500", "auto", 24, False, True), 1.5), 1077 | ], 1078 | ids=["smpte", "smpte_df", "frame", "time", "srt"], 1079 | ) 1080 | def test_float(tc_value, xvalue): 1081 | tc = TC(*tc_value) 1082 | assert float(tc) == pytest.approx(xvalue, 5) 1083 | 1084 | 1085 | @pytest.mark.parametrize( 1086 | argnames="tc_value,xvalue", 1087 | argvalues=[ 1088 | (("00:00:01:00", "auto", 24, False, True), 24), 1089 | (("00:01:00;02", "auto", 29.97, True, True), 1800), 1090 | (("114514", "auto", 24, False, True), 114514), 1091 | (("2.0s", "auto", 24, False, True), 48), 1092 | (("00:00:01,500", "auto", 24, False, True), 36), 1093 | ], 1094 | ids=["smpte", "smpte_df", "frame", "time", "srt"], 1095 | ) 1096 | def test_int(tc_value, xvalue): 1097 | tc = TC(*tc_value) 1098 | assert int(tc) == xvalue 1099 | 1100 | 1101 | @pytest.mark.parametrize( 1102 | argnames="tc_value,sample_rate,xvalue", 1103 | argvalues=[ 1104 | (("00:00:01:00", "auto", 24, False, True), 48000, 48000), 1105 | (("00:00:01:01", "auto", 24, False, True), 48000, 50000), 1106 | (("00:00:01:01", "auto", 24, False, True), 44100, 45937), 1107 | ], 1108 | ids=["ideal", "single_frame", "24fps_44100"], 1109 | ) 1110 | def test_audio_sample_count(tc_value, sample_rate, xvalue): 1111 | tc = TC(*tc_value) 1112 | assert tc.get_audio_sample_count(sample_rate) == xvalue 1113 | --------------------------------------------------------------------------------