├── .github └── CONTRIBUTING.md ├── .gitignore ├── LICENSE ├── README.md ├── go.mod └── timecode ├── rate.go ├── rate_test.go ├── timecode.go └── timecode_test.go /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [Open an issue](https://github.com/trimmer-io/go-timecode/issues/new) to discuss your 2 | plans before doing any work on go-timecode. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files 2 | *.slo 3 | *.lo 4 | *.o 5 | 6 | # Compiled Dynamic libraries 7 | *.so 8 | 9 | # Compiled Static libraries 10 | *.lai 11 | *.la 12 | *.a 13 | 14 | # Key files 15 | *.key 16 | *.csr 17 | *.crt 18 | 19 | # OSX specific files 20 | .DS_Store 21 | 22 | # generated files and directories 23 | build/* 24 | *.log 25 | *.pid 26 | *~ 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Alexander Eichhorn 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-timecode 2 | =========== 3 | 4 | [![GoDoc](https://godoc.org/github.com/trimmer-io/go-timecode/timecode?status.svg)](https://godoc.org/github.com/trimmer-io/go-timecode/timecode) 5 | 6 | 7 | go-timecode is a [Go](http://golang.org/) library for SMPTE ST 12-1-2014 timecodes. 8 | 9 | Features 10 | -------- 11 | - correct drop-frame and non-drop-frame support 12 | - all standard film, video and TV edit rates 23.976, 24, 25, 29.97, 30, 48, 50, 59.94, 60, 96, 100, 120 13 | - arbitrary user-defined edit rates down to 1ns precision with a timecode runtime of ~9 years 14 | - conversion between timecode, frame number and realtime 15 | - timecode and frame calculations 16 | - timecode & rate fit into a single 64bit integer for efficient binary storage 17 | - parses and outputs SMPTE ST 12-1 timecode with DF flag 18 | - different output methods to include and parse edit rate with timecode strings 19 | 20 | 21 | Many timecode libraries treat timecode values as literal wall-clock time which they are not. Instead, timecodes are mere address labels for edit units in a sequence of video frames or audio samples. With a different edit rate, the same frame in a sequence has a different timecode address. 22 | 23 | It's probably the reason why drop-frame timecodes are often misunderstood. I guess it's better to call them skip-timecodes because that's what's happening. At 29.97fps each edit unit has a duration of 33.3666ms, 30 such edit units last for 1.001s. Hence, a 29.97DF timecode of `00:00:01;00` actually means 1.001s instead of 1s. Now this makes it obvious that the time-part of the timecode runs a bit slower than realtime. To compensate for this speed issue, a drop-frame timecode *skips some address labels* now and then. To be precise, it skips frame labels `??:??:??;00` and `??:??:??;01` every minute but not every 10th minute. That's all. 24 | 25 | 26 | Documentation 27 | ------------- 28 | 29 | - [API Reference](http://godoc.org/github.com/trimmer-io/go-timecode/timecode) 30 | - [FAQ](https://github.com/trimmer-io/go-timecode/wiki/FAQ) 31 | 32 | Installation 33 | ------------ 34 | 35 | Install go-timecode using the "go get" command: 36 | 37 | go get github.com/trimmer-io/go-timecode/timecode 38 | 39 | The Go distribution is go-timecode's only dependency. 40 | 41 | Examples 42 | -------- 43 | 44 | ``` 45 | import "github.com/trimmer-io/go-timecode/timecode" 46 | 47 | tc := timecode.Parse("00:00:59;29") 48 | tc.SetRate(timecode.Rate30DF) 49 | tc.Add(2*time.Minute) 50 | fmt.Println("Frame number at TC", tc, "is", tc.Frame()) 51 | 52 | ``` 53 | 54 | 55 | Contributing 56 | ------------ 57 | 58 | See [CONTRIBUTING.md](https://github.com/trimmer-io/go-timecode/blob/master/.github/CONTRIBUTING.md). 59 | 60 | 61 | License 62 | ------- 63 | 64 | go-timecode is available under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). 65 | 66 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/trimmer-io/go-timecode 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /timecode/rate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Alexander Eichhorn 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package timecode 16 | 17 | import ( 18 | "fmt" 19 | "math" 20 | "strconv" 21 | "strings" 22 | "time" 23 | ) 24 | 25 | type Rate struct { 26 | // Id for standard edit rates. This value is set to 0 when a rate 27 | // is unknown and to R_MAX when the rate is user-defined. 28 | enum int 29 | // Nominal frame count per second by which the timecode using this rate will 30 | // assign timecode address labels. 31 | fps int 32 | // Numerator value of the effective edit rate at which the data stream 33 | // will advance in real-time (e.g. 25 for 25fps). 34 | rateNum int 35 | // Denominator value of the effective edit rate at which the data stream 36 | // will advance in real-time. For most edit rates this value will be 1, 37 | // drop-frame Television rates like 29.97, 59.94 and the special camera 38 | // capture rate 23.976 use 1001. 39 | rateDen int 40 | // Number of timecode address labels that will be dropped once per minute. 41 | dropFrames int 42 | // Effective number of actual frames per 10 minute time interval. This is 43 | // the same number as valid timecode address labels during that duration. 44 | framesPer10Min int 45 | } 46 | 47 | // Standard edit rates for non-drop-frame timecodes. 48 | const ( 49 | _ = iota // special: treat nanosecond component as frame number 50 | R_23976 // 24000,1001 (Note: this is NOT a drop-frame edit rate) 51 | R_24 // 24,1 52 | R_25 // 25,1 53 | R_30 // 30,1 54 | R_48 // 48,1 55 | R_50 // 50,1 56 | R_60 // 60,1 57 | R_96 // 96,1 58 | R_100 // 100,1 59 | R_120 // 120,1 60 | R_MAX = 15 // special case: requires rateNum and rateDen to be set 61 | ) 62 | 63 | // Standard edit rates for drop-frame timecodes. 64 | const ( 65 | df = 16 + iota 66 | _ // undefined 67 | _ // undefined 68 | _ // undefined 69 | R_30DF // 30000,1001 70 | _ // undefined 71 | _ // undefined 72 | R_60DF // 60000,1001 73 | ) 74 | 75 | // Common edit rate configurations you should use in your code when calling New() 76 | var ( 77 | InvalidRate Rate = Rate{R_MAX, 0, 0, 0, 0, 0} 78 | OneFpsRate Rate = Rate{0, 1, 1, 1, 0, 1 * 600} // == 1fps 79 | IdentityRate Rate = Rate{0, 1000000000, 1000000000, 1, 0, 1000000000 * 600} // == 1ns 80 | IdentityRateDF Rate = Rate{df, 1000000000, 1000000000, 1, 0, 1000000000 * 600} // == 1ns 81 | Rate23976 Rate = Rate{R_23976, 24, 24000, 1001, 0, 24 * 600} 82 | Rate24 Rate = Rate{R_24, 24, 24, 1, 0, 24 * 600} 83 | Rate25 Rate = Rate{R_25, 25, 25, 1, 0, 25 * 600} 84 | Rate30 Rate = Rate{R_30, 30, 30, 1, 0, 30 * 600} 85 | Rate30DF Rate = Rate{R_30DF, 30, 30000, 1001, 2, 17982} 86 | Rate48 Rate = Rate{R_48, 48, 48, 1, 0, 48 * 600} 87 | Rate50 Rate = Rate{R_50, 50, 50, 1, 0, 50 * 600} 88 | Rate60 Rate = Rate{R_60, 60, 60, 1, 0, 60 * 600} 89 | Rate60DF Rate = Rate{R_60DF, 60, 60000, 1001, 4, 35964} 90 | Rate96 Rate = Rate{R_96, 96, 96, 1, 0, 96 * 600} 91 | Rate100 Rate = Rate{R_100, 100, 100, 1, 0, 100 * 600} 92 | Rate120 Rate = Rate{R_120, 120, 120, 1, 0, 120 * 600} 93 | ) 94 | 95 | var rates map[int]Rate = map[int]Rate{ 96 | 0: IdentityRate, 97 | df: IdentityRateDF, 98 | R_23976: Rate23976, 99 | R_24: Rate24, 100 | R_25: Rate25, 101 | R_30: Rate30, 102 | R_30DF: Rate30DF, 103 | R_48: Rate48, 104 | R_50: Rate50, 105 | R_60: Rate60, 106 | R_60DF: Rate60DF, 107 | R_96: Rate96, 108 | R_100: Rate100, 109 | R_120: Rate120, 110 | } 111 | 112 | // NewRate creates a user-defined rate from rate numerator n and denominator d. 113 | // If the rate is approximately close to a pre-defined standard rate, the 114 | // standard rate's configuration including the appropriate enum id will be used. 115 | func NewRate(n, d int) Rate { 116 | if n == 0 { 117 | n = 1 118 | } 119 | if d == 0 { 120 | d = 1 121 | } 122 | fps := float32(n) / float32(d) 123 | r := NewFloatRate(fps) 124 | if r.enum == R_MAX { 125 | return Rate{R_MAX, int(math.Ceil(float64(fps))), n, d, 0, int(fps * 600)} 126 | } 127 | return r 128 | } 129 | 130 | // NewFloatRate converts the float32 f to a rate. If the rate is approximately 131 | // close to a pre-defined standard rate, the standard rate's configuration 132 | // including the appropriate enum id will be used. 133 | func NewFloatRate(f float32) Rate { 134 | switch { 135 | case 23.975 <= f && f < 23.997: 136 | return rates[R_23976] 137 | case f == 24: 138 | return rates[R_24] 139 | case f == 25: 140 | return rates[R_25] 141 | case 29.96 < f && f < 29.98: 142 | return rates[R_30DF] 143 | case f == 30: 144 | return rates[R_30] 145 | case f == 48: 146 | return rates[R_48] 147 | case f == 50: 148 | return rates[R_50] 149 | case 59.93 < f && f < 59.95: 150 | return rates[R_60DF] 151 | case f == 60: 152 | return rates[R_60] 153 | case f == 96: 154 | return rates[R_96] 155 | case f == 100: 156 | return rates[R_100] 157 | case f == 120: 158 | return rates[R_120] 159 | default: 160 | return Rate{R_MAX, int(f), int(f * 1000), 1000, 0, int(f) * 600} 161 | } 162 | } 163 | 164 | // ParseRate converts the string s to a rate. The string is treated as a 165 | // rate enumeration index when its value is an integer, as floating point 166 | // rate when s parses as float32 or as rational rate otherwise. 167 | // 168 | // If the pased float or rational rate is approximately close to a pre-defined 169 | // standard rate, the standard rate's configuration including the appropriate 170 | // enum id will be used. 171 | func ParseRate(s string) (Rate, error) { 172 | // try parsing as index 173 | if i, err := strconv.Atoi(s); err == nil { 174 | switch { 175 | case i <= R_MAX: 176 | fallthrough 177 | case i == R_30DF || i == R_60DF: 178 | return rates[i], nil 179 | default: 180 | return NewFloatRate(float32(i)), nil 181 | } 182 | } 183 | 184 | // try parsing as float 185 | if f, err := strconv.ParseFloat(s, 32); err == nil { 186 | return NewFloatRate(float32(f)), nil 187 | } 188 | 189 | // try parsing as rational 190 | if fields := strings.Split(s, "/"); len(fields) == 2 { 191 | a, _ := strconv.Atoi(fields[0]) 192 | b, err := strconv.Atoi(fields[1]) 193 | if err == nil && b > 0 { 194 | return NewFloatRate(float32(a) / float32(b)), nil 195 | } 196 | } 197 | 198 | return InvalidRate, fmt.Errorf("timecode: parsing rate \"%s\": invalid syntax", s) 199 | } 200 | 201 | // IsZero indicates if the rate equals IdentityRate. This may be used to check if 202 | // a timecode has no associated rate using Timecode.Rate().IsZero(). 203 | func (r Rate) IsZero() bool { 204 | return r.IsEqual(IdentityRate) 205 | } 206 | 207 | // IsValid indicates if a rate may be used in calculations. Rates with a denominator 208 | // of zero would lead to division by zero panics, rates with a numerator of zero 209 | // are undefined. 210 | func (r Rate) IsValid() bool { 211 | return r.rateNum > 0 && r.rateDen > 0 212 | } 213 | 214 | // IsDrop indicates if the rate refers to a drop-frame timecode. 215 | func (r Rate) IsDrop() bool { 216 | return r.enum&0x10 > 0 217 | } 218 | 219 | // IndexString returns the enumeration for a standard timecode as string. 220 | func (r Rate) IndexString() string { 221 | return strconv.Itoa(r.enum) 222 | } 223 | 224 | // Fraction returns the rate's numerator and denominator. 225 | func (r Rate) Fraction() (int, int) { 226 | return r.rateNum, r.rateDen 227 | } 228 | 229 | // RationalString returns the rate as rational string of form 'numerator/denominator'. 230 | func (r Rate) RationalString() string { 231 | return strings.Join([]string{ 232 | strconv.Itoa(r.rateNum), 233 | strconv.Itoa(r.rateDen), 234 | }, "/") 235 | } 236 | 237 | // Float returns the rate as floating point number. 238 | func (r Rate) Float() float32 { 239 | switch r.rateDen { 240 | case 0: 241 | return 0 242 | case 1: 243 | return float32(r.rateNum) 244 | default: 245 | return float32(r.rateNum) / float32(r.rateDen) 246 | } 247 | } 248 | 249 | // FloatString returns the rate as floating point string with maximum precision 250 | // of 3 digits. 251 | func (r Rate) FloatString() string { 252 | switch r.rateDen { 253 | case 0: 254 | return "0.0" 255 | case 1: 256 | return strconv.FormatFloat(float64(r.rateNum), 'f', 1, 32) 257 | default: 258 | 259 | return strconv.FormatFloat(float64(r.rateNum)/float64(r.rateDen), 'f', 3, 32) 260 | } 261 | } 262 | 263 | func (r Rate) MarshalText() ([]byte, error) { 264 | return []byte(r.FloatString()), nil 265 | } 266 | 267 | func (r *Rate) UnmarshalText(data []byte) error { 268 | d := string(data) 269 | switch d { 270 | case "", "-", "--", "NaN", "unknown": 271 | *r = IdentityRate 272 | return nil 273 | default: 274 | if rr, err := ParseRate(d); err != nil { 275 | return err 276 | } else { 277 | *r = rr 278 | return nil 279 | } 280 | } 281 | } 282 | 283 | // FrameDuration returns the duration of a single frame at the edit rate. 284 | func (r Rate) FrameDuration() time.Duration { 285 | if r.rateNum == 0 { 286 | return time.Nanosecond 287 | } 288 | return time.Duration(1000000000 * float64(r.rateDen) / float64(r.rateNum)) 289 | } 290 | 291 | // Duration returns the duration of f frames at the edit rate. 292 | func (r Rate) Duration(f int64) time.Duration { 293 | if r.rateNum == 0 { 294 | return 0 295 | } 296 | d := time.Duration(float64(f) * 1000000000 * float64(r.rateDen) / float64(r.rateNum)) 297 | return r.Truncate(d, 2) 298 | } 299 | 300 | // Frames returns the number of frames matching duration d at the edit rate. 301 | func (r Rate) Frames(d time.Duration) int64 { 302 | return int64(d / r.FrameDuration()) 303 | } 304 | 305 | // Truncate clips duration d to the edit rate's interval length, while internally 306 | // rounding to precision digits. 307 | func (r Rate) Truncate(d time.Duration, precision int) time.Duration { 308 | i := int64(d) 309 | n := int64(r.FrameDuration()) 310 | if x := i % n; x > n/2 { 311 | return time.Duration(i + n - x) 312 | } else if x > n/int64(precision) { 313 | return time.Duration(i - x) 314 | } else { 315 | return d 316 | } 317 | } 318 | 319 | func (r Rate) TruncateFloat(d float64, precision int) time.Duration { 320 | pow := math.Pow(10, float64(precision)) 321 | rd := r.FrameDuration() 322 | val := pow * float64(d) / float64(rd) 323 | _, div := math.Modf(val) 324 | var round float64 325 | if d > 0 { 326 | if div >= 0.5 { 327 | round = math.Ceil(val) 328 | } else { 329 | round = math.Floor(val) 330 | } 331 | } else { 332 | if div >= 0.5 { 333 | round = math.Floor(val) 334 | } else { 335 | round = math.Ceil(val) 336 | } 337 | } 338 | return time.Duration(round/pow) * rd 339 | } 340 | 341 | // MinRate returns the rate with smaller frame duration. 342 | func MinRate(a, b Rate) Rate { 343 | if a.FrameDuration() > b.FrameDuration() { 344 | return a 345 | } 346 | return b 347 | } 348 | 349 | // MaxRate returns the rate with larger frame duration. 350 | func MaxRate(a, b Rate) Rate { 351 | if a.FrameDuration() < b.FrameDuration() { 352 | return a 353 | } 354 | return b 355 | } 356 | 357 | // IsSmaller returns true if rate b has a larger frame duration than rate r. 358 | func (r Rate) IsSmaller(b Rate) bool { 359 | return r.FrameDuration() < b.FrameDuration() 360 | } 361 | 362 | // IsEqual returns true when rate r and b have equal frame duration. 363 | func (r Rate) IsEqual(b Rate) bool { 364 | return r.FrameDuration() == b.FrameDuration() 365 | } 366 | -------------------------------------------------------------------------------- /timecode/rate_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Alexander Eichhorn 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package timecode 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | ) 21 | 22 | type RateTestcase struct { 23 | Id int 24 | rateNumA int 25 | RateDenA int 26 | rateNumB int 27 | RateDenB int 28 | AisLower bool 29 | } 30 | 31 | var ( 32 | RateMinMaxTestcases []RateTestcase = []RateTestcase{ 33 | RateTestcase{1, 25, 1, 24, 1, false}, 34 | RateTestcase{2, 25, 1, 1, 1, false}, 35 | RateTestcase{3, 25, 1, 1000, 1000, false}, 36 | RateTestcase{4, 25, 1, 30000, 1001, true}, 37 | RateTestcase{5, 30000, 1001, 25, 1, false}, 38 | RateTestcase{6, 60000, 1001, 30000, 1001, false}, 39 | RateTestcase{7, 30000, 1001, 60000, 1001, true}, 40 | } 41 | ) 42 | 43 | type RateDurationTestcase struct { 44 | Rate Rate 45 | } 46 | 47 | var ( 48 | RateDurationTestcases []Rate = []Rate{ 49 | OneFpsRate, 50 | // IdentityRate, 51 | // IdentityRateDF, 52 | Rate23976, 53 | Rate24, 54 | Rate25, 55 | Rate30, 56 | Rate30DF, 57 | Rate48, 58 | Rate50, 59 | Rate60, 60 | Rate60DF, 61 | Rate96, 62 | Rate100, 63 | Rate120, 64 | } 65 | ) 66 | 67 | func TestTimecodeRateMin(t *testing.T) { 68 | for _, v := range RateMinMaxTestcases { 69 | a := NewRate(v.rateNumA, v.RateDenA) 70 | b := NewRate(v.rateNumB, v.RateDenB) 71 | c := MinRate(a, b) 72 | if v.AisLower { 73 | if !a.IsEqual(c) { 74 | t.Errorf("[Case #%.2d] Failed min test %d/%d != %d/%d", v.Id, c.rateNum, c.rateDen, a.rateNum, a.rateDen) 75 | } 76 | } else { 77 | if !b.IsEqual(c) { 78 | t.Errorf("[Case #%.2d] Failed min test %d/%d != %d/%d", v.Id, c.rateNum, c.rateDen, b.rateNum, b.rateDen) 79 | } 80 | } 81 | } 82 | } 83 | 84 | func TestTimecodeRateMax(t *testing.T) { 85 | for _, v := range RateMinMaxTestcases { 86 | a := NewRate(v.rateNumA, v.RateDenA) 87 | b := NewRate(v.rateNumB, v.RateDenB) 88 | c := MaxRate(a, b) 89 | if v.AisLower { 90 | if !b.IsEqual(c) { 91 | t.Errorf("[Case #%.2d] Failed max test %s != %s", v.Id, c.RationalString(), b.RationalString()) 92 | } 93 | } else { 94 | if !a.IsEqual(c) { 95 | t.Errorf("[Case #%.2d] Failed max test %s != %s", v.Id, c.RationalString(), a.RationalString()) 96 | } 97 | } 98 | } 99 | } 100 | 101 | func TestTimecodeRateDuration(t *testing.T) { 102 | for i, v := range RateDurationTestcases { 103 | num, den := v.Fraction() 104 | real := time.Duration(float64(den) * 1000000000 / float64(num)) 105 | if d := v.FrameDuration(); d != real { 106 | t.Errorf("[Case #%.2d] Wrong frame duration %d != %d (%d)", i, d, real, real-d) 107 | } 108 | } 109 | } 110 | 111 | func TestTimecodeDuration(t *testing.T) { 112 | for i, v := range RateDurationTestcases { 113 | num, den := v.Fraction() 114 | for fps := v.Frames(time.Second) + 1; fps > 0; fps-- { 115 | real := time.Duration(float64(fps) * 1000000000 * float64(den) / float64(num)) 116 | if d := v.Duration(fps); d != real { 117 | t.Errorf("[Case #%.2d] Wrong duration rate %s for %d frames %d != %d (%d)", i, v.RationalString(), fps, d, real, real-d) 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /timecode/timecode.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Alexander Eichhorn 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | // SMPTE ST 12-1-2014 16 | // SMPTE ST 331-2011, value 81h 17 | // 18 | // see also 19 | // http://andrewduncan.net/timecodes/ 20 | // http://www.bodenzord.com/archives/79 21 | // https://documentation.apple.com/en/finalcutpro/usermanual/index.html#chapter=D%26section=6 22 | // 23 | // TODO 24 | // - support SMPTE 24h bit 25 | 26 | // Package timecode provides types and primitives to work with SMPTE ST 12-1 27 | // timecodes at standard and user-defined edit rates. Currently only the DF 28 | // flag is supported and timecodes cannot be negative. 29 | // 30 | // The package supports functions to convert between timecode, frame number 31 | // and realtime durations as well as functions for timecode calculations. 32 | // Drop-frame and non-drop-frame timecodes are correctly handled and all 33 | // standard film, video and Television edit rates are supported. You may 34 | // also use arbitrary user-defined edit rates down to 1ns precision with 35 | // a timecode runtime of ~9 years. 36 | // 37 | // Timecode and edit rate are stored as a single 64bit integer for efficient 38 | // timecode handling and comparisons. 39 | package timecode 40 | 41 | import ( 42 | "database/sql/driver" 43 | "fmt" 44 | "math" 45 | "reflect" 46 | "strconv" 47 | "strings" 48 | "time" 49 | ) 50 | 51 | // Timecode represents a duration at nanosecond precision similar to Golang's 52 | // time.Duration. Unlike time.Duration Timecode cannot be negative. 53 | // 54 | // The 6 most significant bits are used to store an edit rate identifier required for 55 | // offset calculations. The timecode's duration value occupies the 59 least significant 56 | // bits which allows for expressing ~9 years of runtime. 57 | type Timecode uint64 58 | 59 | const ( 60 | Invalid Timecode = math.MaxUint64 61 | Zero Timecode = 0 62 | Origin string = "00:00:00:00" 63 | rate_bits uint64 = 5 // = 16+16 frame rates (NDF+DF) 64 | time_bits uint64 = 59 // ~9 years at 1 ns granularity (needs 30bit) 65 | time_mask uint64 = (1<>time_bits)] 121 | if !ok { 122 | rate = rates[0] 123 | } 124 | return rate 125 | } 126 | 127 | // Parse converts the string s to a timecode with optional rate. Without 128 | // rate, the frame number is stored as raw nanosecond value. To reflect its 129 | // actual duration you must call SetRate before calling any calculation 130 | // functions. It's legal to parse and print timecodes without a rate. 131 | // 132 | // Well-formed timecodes must contain exactly four numeric segements of format 133 | // 'hh:mm:ss:ff' where 'hh' denotes hours, 'mm' minutes, 'ss' seconds and 'ff' 134 | // frames. Drop-frame timecodes use a semicolon ';' as the last separator 135 | // between seconds and frame number. 136 | // 137 | // If s contains a '@' character, Parse treats the following substring as rate 138 | // expression and uses ParseRate() to read it. 139 | func Parse(s string) (Timecode, error) { 140 | 141 | if s == "" { 142 | s = Origin 143 | } 144 | 145 | isDF := strings.Contains(s, ";") 146 | hasRate := strings.Contains(s, "@") 147 | r := IdentityRate 148 | 149 | // treat DN and non-DF the same way (rate must be set after parsing) 150 | if isDF { 151 | s = strings.Replace(s, ";", ":", -1) 152 | r = IdentityRateDF 153 | } 154 | 155 | // strip and parse rate 156 | if hasRate { 157 | idx := strings.Index(s, "@") 158 | var err error 159 | r, err = ParseRate(s[idx+1:]) 160 | if err != nil { 161 | return Invalid, err 162 | } 163 | s = s[:idx] 164 | 165 | // timecode is a frame counter, don't treat it as literal time! 166 | var frames int64 167 | for i, v := range strings.Split(s, ":") { 168 | t, err := strconv.ParseUint(v, 10, 64) 169 | if err != nil { 170 | // reject timecodes with invalid numbers 171 | return Invalid, fmt.Errorf("timecode: parsing timecode \"%s\": invalid syntax", s) 172 | } 173 | switch i { 174 | case 0: 175 | frames += int64(t) * 3600 * int64(r.fps) 176 | case 1: 177 | frames += int64(t) * 60 * int64(r.fps) 178 | case 2: 179 | frames += int64(t) * int64(r.fps) 180 | case 3: 181 | frames += int64(t) 182 | default: 183 | // reject timecodes longer than 4 segements 184 | return Invalid, fmt.Errorf("timecode: parsing timecode \"%s\": invalid syntax", s) 185 | } 186 | } 187 | 188 | // reverse the adjustment for drop frame timecodes 189 | if isDF { 190 | d := frames / int64(r.framesPer10Min) 191 | m := frames % int64(r.framesPer10Min) 192 | df := int64(r.dropFrames) 193 | frames = frames - 9*df*d - df*((m-df)/int64(r.framesPer10Min/10)) 194 | } 195 | 196 | return New(r.Duration(frames), r), nil 197 | } 198 | 199 | // without rate we keep the frame number as nanosec part until a rate is set 200 | var d time.Duration 201 | for i, v := range strings.Split(s, ":") { 202 | t, err := strconv.ParseUint(v, 10, 64) 203 | if err != nil { 204 | // reject timecodes with invalid numbers 205 | return Invalid, fmt.Errorf("timecode: parsing timecode \"%s\": invalid syntax", s) 206 | } 207 | switch i { 208 | case 0: 209 | d += time.Duration(t) * time.Hour 210 | case 1: 211 | d += time.Duration(t) * time.Minute 212 | case 2: 213 | d += time.Duration(t) * time.Second 214 | case 3: 215 | d += time.Duration(t) 216 | default: 217 | // reject timecodes longer than 4 segements 218 | return Invalid, fmt.Errorf("timecode: parsing timecode \"%s\": invalid syntax", s) 219 | } 220 | } 221 | 222 | return New(d, r), nil 223 | } 224 | 225 | // FromSMPTE unpacks the SMPTE timecode from tc and also considers the 226 | // drop-frame bit. User bits are ignored right now. 227 | func FromSMPTE(tc uint32, bits uint32) Timecode { 228 | h := uint64((tc>>28&0x03)*10 + (tc >> 24 & 0x0F)) 229 | m := uint64((tc>>20&0x07)*10 + (tc >> 16 & 0x0F)) 230 | s := uint64((tc>>12&0x07)*10 + (tc >> 8 & 0x0F)) 231 | f := uint64((tc>>4&0x03)*10 + (tc & 0x0F)) 232 | d := h*uint64(time.Hour) + m*uint64(time.Minute) + s*uint64(time.Second) + f 233 | t := Timecode(d & time_mask) 234 | if tc&0x40 > 0 { 235 | t |= df << time_bits 236 | } 237 | return t 238 | } 239 | 240 | // FromSMPTEwithRate unpacks the SMPTE timecode from tc, considering the 241 | // drop-frame bit and uses rate as initial timecode rate. 242 | func FromSMPTEwithRate(tc, bits uint32, rate float32) Timecode { 243 | t := FromSMPTE(tc, bits) 244 | if rate != 0 { 245 | t.SetRate(NewFloatRate(rate)) 246 | } 247 | return t 248 | } 249 | 250 | // SMPTE returns a packed SMPTE timecode and user bits from the current 251 | // timecode value. 252 | func (t Timecode) SMPTE() (uint32, uint32) { 253 | rate := t.Rate() 254 | fps := int64(rate.fps) 255 | frame := t.adjustedFrame(rate) 256 | ff := frame % fps 257 | ss := frame / fps % 60 258 | mm := frame / (fps * 60) % 60 259 | hh := frame / (fps * 3600) 260 | tc := (hh/10)<<28 + hh%10<<24 + mm/10<<20 + mm%10<<16 + ss/10<<12 + ss%10<<8 + ff/10<<4 + ff%10 261 | if rate.IsDrop() { 262 | tc |= 0x40 263 | } 264 | return uint32(tc), 0 265 | } 266 | 267 | // String returns a string representation of the timecode as `hh:mm:ss:ff`. 268 | // If the timecode uses a drop-frame edit rate, the last separator in the 269 | // string is a semicolon `;`. 270 | func (t Timecode) String() string { 271 | rate := t.Rate() 272 | frame := t.adjustedFrame(rate) 273 | fps := int64(rate.fps) 274 | ff := frame % fps 275 | ss := frame / fps % 60 276 | mm := frame / (fps * 60) % 60 277 | hh := frame / (fps * 3600) 278 | sep := ':' 279 | if t.Rate().IsDrop() { 280 | sep = ';' 281 | } 282 | return fmt.Sprintf("%02d:%02d:%02d%c%02d", hh, mm, ss, sep, ff) 283 | } 284 | 285 | // StringWithRate returns the timecode as string appended with the current 286 | // rate after a separating `@` character. 287 | func (t Timecode) StringWithRate() string { 288 | if t.Rate().enum == IdentityRate.enum { 289 | return t.String() 290 | } 291 | return fmt.Sprintf("%s@%s", t.String(), t.Rate().FloatString()) 292 | } 293 | 294 | // Uint64 returns the raw timecode value as unsigned 64bit integer. 295 | func (t Timecode) Uint64() uint64 { 296 | return uint64(t) 297 | } 298 | 299 | // Duration returns the duration part of the timecode. 300 | func (t Timecode) Duration() time.Duration { 301 | return time.Duration((uint64(t) & time_mask)) 302 | } 303 | 304 | // Second returns a properly rounded number of seconds covered by the 305 | // timecode. 306 | func (t Timecode) Second() int64 { 307 | // adjust for small rounding errors from periodic fractions 308 | // as found with almost all frame rate durations 309 | // 310 | // 24fps 41.666666ms 311 | // 30fps DF 33.366666ms 312 | // 30fps 33.333333ms 313 | // 48fps 20.833333ms 314 | // 60fps DF 16.683333ms 315 | // 60fps 16.666666ms 316 | // 120fps 8.333333ms 317 | // 318 | // return int64(t.Duration() / time.Second) 319 | return int64(math.Floor(float64(t.Duration())/float64(time.Second) + 0.001)) 320 | } 321 | 322 | // Second returns the number of milliseconds covered by the timecode. 323 | func (t Timecode) Millisecond() int64 { 324 | return int64(t.Duration() / time.Millisecond) 325 | } 326 | 327 | // Frame returns the frame sequence counter value corresponding to the 328 | // timecode's duration at the timecode's edit rate. Note that this value 329 | // will be wrong when the edit rate is unknown or unset, as is the case 330 | // after parsing a timecode from string without setting the rate. 331 | func (t Timecode) Frame() int64 { 332 | rate, ok := rates[int(uint64(t)>>time_bits)] 333 | if !ok { 334 | rate = rates[0] 335 | } 336 | return t.FrameAtRate(rate) 337 | } 338 | 339 | // FrameAtRate returns the frame sequence counter value corresponding to the 340 | // timecode's duration at edit rate r. 341 | func (t Timecode) FrameAtRate(r Rate) int64 { 342 | // when rate id is 0 the frame number within the current second 343 | // is stored as nanosecond value 344 | if r.enum == 0 || r.enum == df { 345 | f := int64(r.fps) * t.Second() 346 | f += int64(t.Duration() % time.Second) 347 | return f 348 | } 349 | 350 | // all other cases use nanosecond as time base for duration 351 | return int64(t.Duration() / r.FrameDuration()) 352 | } 353 | 354 | func (t Timecode) adjustedFrame(r Rate) int64 { 355 | f := t.FrameAtRate(r) 356 | if !r.IsDrop() { 357 | return f 358 | } 359 | 360 | // for 29.97DF skip timecodes 0 and 1 of the first second 361 | // of every minute, except when the number of minutes 362 | // is divisible by ten (same for 59.97DF except skip 4 timecodes) 363 | d := f / int64(r.framesPer10Min) 364 | m := f % int64(r.framesPer10Min) 365 | df := int64(r.dropFrames) 366 | return f + 9*df*d + df*((m-df)/int64(r.framesPer10Min/10)) 367 | } 368 | 369 | // Sub returns the difference between timecodes t and t2 in nanoseconds as 370 | // time.Duration. 371 | func (t Timecode) Sub(t2 Timecode) time.Duration { 372 | return t.Duration() - t2.Duration() 373 | } 374 | 375 | // Add returns a new timecode with current rate and duration d added to the 376 | // current duration. Any negative result will be clipped to zero. 377 | func (t Timecode) Add(d time.Duration) Timecode { 378 | d = t.Duration() + d 379 | if d < 0 { 380 | return New(0, t.Rate()) 381 | } 382 | return New(d, t.Rate()) 383 | } 384 | 385 | // AddFrames returns a new timecode adjusted by f frames relative to the 386 | // edit rate. If f is positive, the new timecode is larger than the 387 | // current one, if negative it will be smaller. Any negative result after 388 | // adding will be clipped to zero. 389 | func (t Timecode) AddFrames(f int64) Timecode { 390 | if f > t.Frame() { 391 | return New(0, t.Rate()) 392 | } 393 | return New(t.Duration()+t.Rate().Duration(f), t.Rate()) 394 | } 395 | 396 | // MarshalText implements the encoding.TextMarshaler interface for 397 | // converting a timecode value to string. This implementation preserves 398 | // the rate 399 | func (t Timecode) MarshalText() ([]byte, error) { 400 | if t.IsValid() { 401 | return []byte(t.StringWithRate()), nil 402 | } else { 403 | return []byte{}, nil 404 | } 405 | } 406 | 407 | // UnmarshalText implements the encoding.TextMarshaler interface for 408 | // reading a timecode values. 409 | func (t *Timecode) UnmarshalText(data []byte) error { 410 | x, err := Parse(string(data)) 411 | if err != nil { 412 | return err 413 | } 414 | *t = x 415 | return nil 416 | } 417 | 418 | // Scan implements sql.Scanner interface for converting database values 419 | // to timecode so you can use type timecode.Timecode directly with ORMs 420 | // or the sql package. 421 | func (t *Timecode) Scan(value interface{}) error { 422 | var x Timecode 423 | var err error 424 | switch v := value.(type) { 425 | case int64: 426 | x = Timecode(v) 427 | case string: 428 | x, err = Parse(v) 429 | case []byte: 430 | x, err = Parse(string(v)) 431 | case nil: 432 | x = Zero 433 | } 434 | if err != nil { 435 | return err 436 | } 437 | *t = x 438 | return nil 439 | } 440 | 441 | // Value implements sql driver.Valuer interface for converting timecodes 442 | // to a database driver compatible type. 443 | func (t Timecode) Value() (driver.Value, error) { 444 | return int64(t), nil 445 | } 446 | 447 | // ConvertTimecode implements schema.Converter function defined by the 448 | // Gorilla schema package. To use this converter you need to register it 449 | // via 450 | // 451 | // dec := schema.NewDecoder() 452 | // dec.RegisterConverter(timecode.Timecode(0), timecode.ConvertTimecode) 453 | // 454 | // This will eventually becomes unnecessary once https://github.com/gorilla/schema/issues/57 455 | // is fixed. 456 | func ConvertTimecode(value string) reflect.Value { 457 | if t, err := Parse(value); err != nil { 458 | return reflect.ValueOf(t) 459 | } 460 | return reflect.Value{} 461 | } 462 | -------------------------------------------------------------------------------- /timecode/timecode_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Alexander Eichhorn 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package timecode 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "testing" 21 | "time" 22 | ) 23 | 24 | func s(v int64) time.Duration { 25 | return time.Duration(v) * time.Second 26 | } 27 | 28 | func ms(v int64) time.Duration { 29 | return time.Duration(v) * time.Millisecond 30 | } 31 | 32 | func us(v int64) time.Duration { 33 | return time.Duration(v) * time.Microsecond 34 | } 35 | 36 | func ns(v int64) time.Duration { 37 | return time.Duration(v) * time.Nanosecond 38 | } 39 | 40 | type TimecodeTestcase struct { 41 | Id string 42 | RateNum int 43 | RateDen int 44 | Time time.Duration 45 | Offset time.Duration 46 | Second int64 47 | Frame int64 48 | AsString string 49 | } 50 | 51 | func (tc *TimecodeTestcase) Check(t *testing.T, code Timecode) { 52 | if !code.IsValid() { 53 | t.Errorf("[Case #%s] Failed generating valid timecode", tc.Id) 54 | } 55 | r := NewRate(tc.RateNum, tc.RateDen) 56 | ms := code.Millisecond() 57 | expectedMs := int64((tc.Time + tc.Offset) / time.Millisecond) 58 | if cr := code.Rate(); !r.IsEqual(cr) { 59 | t.Errorf("[Case #%s] Wrong rate: expected=%s got=%s", tc.Id, r.RationalString(), cr.RationalString()) 60 | } 61 | if ms != expectedMs { 62 | t.Errorf("[Case #%s] Wrong millisecond: expected=%d got=%d", tc.Id, expectedMs, ms) 63 | } 64 | s := code.Second() 65 | if s != tc.Second { 66 | t.Errorf("[Case #%s] Wrong second: expected=%d got=%d", tc.Id, tc.Second, s) 67 | } 68 | f := code.Frame() 69 | if f != tc.Frame { 70 | t.Errorf("[Case #%s] Wrong frame: expected=%d got=%d", tc.Id, tc.Frame, f) 71 | } 72 | fr := code.FrameAtRate(r) 73 | if fr != tc.Frame { 74 | t.Errorf("[Case #%s] Wrong frame with rate: expected=%d got=%d", tc.Id, tc.Frame, fr) 75 | } 76 | str := code.String() 77 | if str != tc.AsString { 78 | t.Errorf("[Case #%s] Wrong string: expected=%s got=%s", tc.Id, tc.AsString, str) 79 | } 80 | } 81 | 82 | var ( 83 | TimecodeCreateTestcases []TimecodeTestcase = []TimecodeTestcase{ 84 | // 23.976 fps (note: this is non-drop frame, meaning the that there are 85 | // actually 24 frames counted in time code, but the time code 86 | // runs slower than wall-clock time) 87 | // 88 | TimecodeTestcase{"23_1", 24000, 1001, 0, 0, 0, 0, "00:00:00:00"}, 89 | TimecodeTestcase{"23_2", 24000, 1001, Rate23976.Duration(1), 0, 0, 1, "00:00:00:01"}, 90 | TimecodeTestcase{"23_3", 24000, 1001, Rate23976.Duration(6), 0, 0, 6, "00:00:00:06"}, 91 | TimecodeTestcase{"23_4", 24000, 1001, Rate23976.Duration(24), 0, 1, 24, "00:00:01:00"}, 92 | TimecodeTestcase{"23_5", 24000, 1001, Rate23976.Duration(60 * 24), 0, 60, 1440, "00:01:00:00"}, 93 | TimecodeTestcase{"23_6", 24000, 1001, Rate23976.Duration(600 * 24), 0, 600, 14400, "00:10:00:00"}, 94 | TimecodeTestcase{"23_7", 24000, 1001, Rate23976.Duration(14401), 0, 600, 14401, "00:10:00:01"}, 95 | 96 | // 24fps 97 | TimecodeTestcase{"24_1", 24, 1, ms(0), 0, 0, 0, "00:00:00:00"}, 98 | TimecodeTestcase{"24_2", 24, 1, Rate24.Duration(1), 0, 0, 1, "00:00:00:01"}, 99 | TimecodeTestcase{"24_3", 24, 1, Rate24.Duration(6), 0, 0, 6, "00:00:00:06"}, 100 | TimecodeTestcase{"24_4", 24, 1, Rate24.Duration(24), 0, 1, 24, "00:00:01:00"}, 101 | TimecodeTestcase{"24_5", 24, 1, Rate24.Duration(60*24 + 1), 0, 60, 1441, "00:01:00:01"}, 102 | TimecodeTestcase{"24_6", 24, 1, Rate24.Duration(600*24 + 1), 0, 600, 14401, "00:10:00:01"}, 103 | 104 | // 25fps 105 | TimecodeTestcase{"25_1", 25, 1, ms(0), 0, 0, 0, "00:00:00:00"}, 106 | TimecodeTestcase{"25_2", 25, 1, ms(40), 0, 0, 1, "00:00:00:01"}, 107 | TimecodeTestcase{"25_3", 25, 1, ms(240), 0, 0, 6, "00:00:00:06"}, 108 | TimecodeTestcase{"25_4", 25, 1, s(1), 0, 1, 25, "00:00:01:00"}, 109 | TimecodeTestcase{"25_5", 25, 1, s(60) + ms(40), 0, 60, 1501, "00:01:00:01"}, 110 | TimecodeTestcase{"25_6", 25, 1, s(600) + ms(40), 0, 600, 15001, "00:10:00:01"}, 111 | 112 | // 29.97 fps (note: this is a drop-frame timecode with 29.97 actual frames per 113 | // wall-clock second; to repair the resulting difference between 114 | // timecode display and wall clock, the first 2 timecode values 115 | // are skipped every minute, but not every tenth minute) 116 | // 117 | // so 00:01:00:00 and 00:01:00:01 don't exist, 118 | // but 00:10:00:00 and 00:10:00:01 do 119 | // 120 | TimecodeTestcase{"29_1", 30000, 1001, ms(0), 0, 0, 0, "00:00:00;00"}, 121 | TimecodeTestcase{"29_2", 30000, 1001, Rate30DF.Duration(1), 0, 0, 1, "00:00:00;01"}, 122 | TimecodeTestcase{"29_3", 30000, 1001, Rate30DF.Duration(6), 0, 0, 6, "00:00:00;06"}, 123 | TimecodeTestcase{"29_4", 30000, 1001, Rate30DF.Duration(30), 0, 1, 30, "00:00:01;00"}, 124 | TimecodeTestcase{"29_5", 30000, 1001, Rate30DF.Duration(1799), 0, 60, 1799, "00:00:59;29"}, 125 | TimecodeTestcase{"29_6", 30000, 1001, Rate30DF.Duration(1800), 0, 60, 1800, "00:01:00;02"}, 126 | TimecodeTestcase{"29_7", 30000, 1001, Rate30DF.Duration(17982), 0, 600, 17982, "00:10:00;00"}, 127 | 128 | // 30 fps 129 | TimecodeTestcase{"30_1", 30, 1, ms(0), 0, 0, 0, "00:00:00:00"}, 130 | TimecodeTestcase{"30_2", 30, 1, Rate30.Duration(1), 0, 0, 1, "00:00:00:01"}, 131 | TimecodeTestcase{"30_3", 30, 1, Rate30.Duration(6), 0, 0, 6, "00:00:00:06"}, 132 | TimecodeTestcase{"30_4", 30, 1, Rate30.Duration(30), 0, 1, 30, "00:00:01:00"}, 133 | TimecodeTestcase{"30_5", 30, 1, Rate30.Duration(60*30 + 1), 0, 60, 1801, "00:01:00:01"}, 134 | TimecodeTestcase{"30_6", 30, 1, Rate30.Duration(600*30 + 1), 0, 600, 18001, "00:10:00:01"}, 135 | 136 | // 48 fps 137 | TimecodeTestcase{"48_1", 48, 1, ms(0), 0, 0, 0, "00:00:00:00"}, 138 | TimecodeTestcase{"48_2", 48, 1, Rate48.Duration(1), 0, 0, 1, "00:00:00:01"}, 139 | TimecodeTestcase{"48_3", 48, 1, Rate48.Duration(6), 0, 0, 6, "00:00:00:06"}, 140 | TimecodeTestcase{"48_4", 48, 1, Rate48.Duration(48), 0, 1, 48, "00:00:01:00"}, 141 | TimecodeTestcase{"48_5", 48, 1, Rate48.Duration(60*48 + 1), 0, 60, 2881, "00:01:00:01"}, 142 | TimecodeTestcase{"48_6", 48, 1, Rate48.Duration(600*48 + 1), 0, 600, 28801, "00:10:00:01"}, 143 | 144 | // 50 fps 145 | TimecodeTestcase{"50_1", 50, 1, ms(0), 0, 0, 0, "00:00:00:00"}, 146 | TimecodeTestcase{"50_2", 50, 1, Rate50.Duration(1), 0, 0, 1, "00:00:00:01"}, 147 | TimecodeTestcase{"50_3", 50, 1, Rate50.Duration(6), 0, 0, 6, "00:00:00:06"}, 148 | TimecodeTestcase{"50_4", 50, 1, Rate50.Duration(50), 0, 1, 50, "00:00:01:00"}, 149 | TimecodeTestcase{"50_5", 50, 1, Rate50.Duration(60*50 + 1), 0, 60, 3001, "00:01:00:01"}, 150 | TimecodeTestcase{"50_6", 50, 1, Rate50.Duration(600*50 + 1), 0, 600, 30001, "00:10:00:01"}, 151 | 152 | // 59.97 fps drop frame 153 | TimecodeTestcase{"59_1", 60000, 1001, ms(0), 0, 0, 0, "00:00:00;00"}, 154 | TimecodeTestcase{"59_2", 60000, 1001, Rate60DF.Duration(1), 0, 0, 1, "00:00:00;01"}, 155 | TimecodeTestcase{"59_3", 60000, 1001, Rate60DF.Duration(6), 0, 0, 6, "00:00:00;06"}, 156 | TimecodeTestcase{"59_4", 60000, 1001, Rate60DF.Duration(60), 0, 1, 60, "00:00:01;00"}, 157 | TimecodeTestcase{"59_5", 60000, 1001, Rate60DF.Duration(3599), 0, 60, 3599, "00:00:59;59"}, 158 | TimecodeTestcase{"59_6", 60000, 1001, Rate60DF.Duration(3600), 0, 60, 3600, "00:01:00;04"}, 159 | TimecodeTestcase{"59_7", 60000, 1001, Rate60DF.Duration(35964), 0, 600, 35964, "00:10:00;00"}, 160 | 161 | // 60 fps 162 | TimecodeTestcase{"60_1", 60, 1, ms(0), 0, 0, 0, "00:00:00:00"}, 163 | TimecodeTestcase{"60_2", 60, 1, Rate60.Duration(1), 0, 0, 1, "00:00:00:01"}, 164 | TimecodeTestcase{"60_3", 60, 1, Rate60.Duration(6), 0, 0, 6, "00:00:00:06"}, 165 | TimecodeTestcase{"60_4", 60, 1, Rate60.Duration(60), 0, 1, 60, "00:00:01:00"}, 166 | TimecodeTestcase{"60_5", 60, 1, Rate60.Duration(60*60 + 1), 0, 60, 3601, "00:01:00:01"}, 167 | TimecodeTestcase{"60_6", 60, 1, Rate60.Duration(600*60 + 1), 0, 600, 36001, "00:10:00:01"}, 168 | 169 | // 100 fps 170 | TimecodeTestcase{"100_1", 100, 1, ms(0), 0, 0, 0, "00:00:00:00"}, 171 | TimecodeTestcase{"100_2", 100, 1, Rate100.Duration(1), 0, 0, 1, "00:00:00:01"}, 172 | TimecodeTestcase{"100_3", 100, 1, Rate100.Duration(6), 0, 0, 6, "00:00:00:06"}, 173 | TimecodeTestcase{"100_4", 100, 1, Rate100.Duration(100), 0, 1, 100, "00:00:01:00"}, 174 | TimecodeTestcase{"100_5", 100, 1, Rate100.Duration(60*100 + 1), 0, 60, 6001, "00:01:00:01"}, 175 | TimecodeTestcase{"100_6", 100, 1, Rate100.Duration(600*100 + 1), 0, 600, 60001, "00:10:00:01"}, 176 | 177 | // 120 fps 178 | TimecodeTestcase{"120_1", 120, 1, ms(0), 0, 0, 0, "00:00:00:00"}, 179 | TimecodeTestcase{"120_2", 120, 1, Rate120.Duration(1), 0, 0, 1, "00:00:00:01"}, 180 | TimecodeTestcase{"120_3", 120, 1, Rate120.Duration(6), 0, 0, 6, "00:00:00:06"}, 181 | TimecodeTestcase{"120_4", 120, 1, Rate120.Duration(120), 0, 1, 120, "00:00:01:00"}, 182 | TimecodeTestcase{"120_5", 120, 1, Rate120.Duration(60*120 + 1), 0, 60, 7201, "00:01:00:01"}, 183 | TimecodeTestcase{"120_6", 120, 1, Rate120.Duration(600*120 + 1), 0, 600, 72001, "00:10:00:01"}, 184 | } 185 | ) 186 | 187 | func TestCreate(t *testing.T) { 188 | for _, v := range TimecodeCreateTestcases { 189 | tt := New(v.Time, NewRate(v.RateNum, v.RateDen)) 190 | v.Check(t, tt) 191 | } 192 | } 193 | 194 | func TestParse(t *testing.T) { 195 | for _, v := range TimecodeCreateTestcases { 196 | tt, err := Parse(v.AsString) 197 | if err != nil { 198 | t.Errorf("[Case #%s] unexpected error: %v", v.Id, err) 199 | } 200 | tt.SetRate(NewRate(v.RateNum, v.RateDen)) 201 | v.Check(t, tt) 202 | } 203 | } 204 | 205 | func TestParseWithFloatRate(t *testing.T) { 206 | for _, v := range TimecodeCreateTestcases { 207 | tt := New(v.Time, NewRate(v.RateNum, v.RateDen)) 208 | s := tt.StringWithRate() 209 | expected := fmt.Sprintf("%s@%s", v.AsString, NewRate(v.RateNum, v.RateDen).FloatString()) 210 | if s != expected { 211 | t.Errorf("[Case #%s] Wrong string with rate: expected=%s got=%s", v.Id, expected, s) 212 | } 213 | t2, err := Parse(s) 214 | if err != nil { 215 | t.Errorf("[Case #%s] unexpected error: %v", v.Id, err) 216 | } 217 | v.Check(t, t2) 218 | } 219 | } 220 | 221 | func TestParseWithRationalRate(t *testing.T) { 222 | for _, v := range TimecodeCreateTestcases { 223 | s := fmt.Sprintf("%s@%s", v.AsString, NewRate(v.RateNum, v.RateDen).RationalString()) 224 | tt, err := Parse(s) 225 | if err != nil { 226 | t.Errorf("[Case #%s] unexpected error: %v", v.Id, err) 227 | } 228 | v.Check(t, tt) 229 | } 230 | } 231 | 232 | func TestParseWithIndexRate(t *testing.T) { 233 | for _, v := range TimecodeCreateTestcases { 234 | s := fmt.Sprintf("%s@%s", v.AsString, NewRate(v.RateNum, v.RateDen).IndexString()) 235 | tt, err := Parse(s) 236 | if err != nil { 237 | t.Errorf("[Case #%s] unexpected error: %v", v.Id, err) 238 | } 239 | v.Check(t, tt) 240 | } 241 | } 242 | 243 | func TestPassthrough(t *testing.T) { 244 | for _, v := range TimecodeCreateTestcases { 245 | tt, err := Parse(v.AsString) 246 | if err != nil { 247 | t.Errorf("[Case #%s] unexpected error: %v", v.Id, err) 248 | } 249 | s := tt.String() 250 | if v.AsString != s { 251 | t.Errorf("[Case #%s] Wrong passthrough: %s/%s", v.Id, v.AsString, s) 252 | } 253 | } 254 | } 255 | 256 | type TimecodeMarshal struct { 257 | T Timecode `json:"timecode"` 258 | } 259 | 260 | func TestMarshal(t *testing.T) { 261 | for _, v := range TimecodeCreateTestcases { 262 | m := TimecodeMarshal{ 263 | T: New(v.Time, NewRate(v.RateNum, v.RateDen)), 264 | } 265 | b, err := json.Marshal(m) 266 | if err != nil { 267 | t.Errorf("[Case #%s] Marshal failed: %s", v.Id, err) 268 | } 269 | c := TimecodeMarshal{} 270 | if err = json.Unmarshal(b, &c); err != nil { 271 | t.Errorf("[Case #%s] Unmarshal failed: %s", v.Id, err) 272 | } 273 | c.T.SetRate(m.T.Rate()) 274 | v.Check(t, c.T) 275 | } 276 | } 277 | 278 | var ( 279 | TimecodeOffsetTestcases []TimecodeTestcase = []TimecodeTestcase{ 280 | TimecodeTestcase{"25_1", 25, 1, ms(40), ms(40), 0, 2, "00:00:00:02"}, // 40ms = 1 frame 281 | TimecodeTestcase{"25_2", 25, 1, ms(1000), ms(40), 1, 26, "00:00:01:01"}, // 25 ms < 1 frame 282 | TimecodeTestcase{"25_3", 25, 1, ms(240), ms(1000), 1, 31, "00:00:01:06"}, // 1s 283 | TimecodeTestcase{"25_4", 24, 1, Rate24.Duration(78471), Rate24.Duration(9), 3270, 78480, "00:54:30:00"}, // 00:54:29:15 + 9f 284 | } 285 | ) 286 | 287 | func TestOffset(t *testing.T) { 288 | for _, v := range TimecodeOffsetTestcases { 289 | r := NewRate(v.RateNum, v.RateDen) 290 | tt := New(v.Time, r).Add(v.Offset) 291 | v.Check(t, tt) 292 | } 293 | } 294 | --------------------------------------------------------------------------------