Repository: Zulko/moviepy

Issue:
Floating point precision errors lead to blank frames at end of composite clips
Example:
```
clip duration = 0.56
fps = 12.5
```
When this gets written out to frames, we use (in Clip.py's iterframes):

```
        for t in np.arange(0, self.duration, 1.0/fps):
```

But numpy gives:

```
(Pdb) np.arange(0, .56, 1.0/12.5)
array([ 0.  ,  0.08,  0.16,  0.24,  0.32,  0.4 ,  0.48,  0.56])
```

This means that a composite video clip will try to generate an 8th frame at t=0.56.  However, since the individual clips that make up the composite all have end=0.56, none of them is considered to be playing at that time.  This results in a black final frame (just the background color, with no clips composited over it).

I don't think this is really a bug in the compositor so much as a result of numerical precision issues resulting in the extra t=0.56 frame being generated.  Specifically, numpy says that arange generates a half-open interval [start, end), so in theory the arange array should end at t=0.48, but it doesn't, because the numbers 0.56 and 0.8 cannot be represented perfectly by floating point.

One quick hacky workaround to this specific problem is to change the upper limit of the arange to:

```
self.duration - 1.0/(2*fps)
```

This will prevent the spurious final frame from being generated.


Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/30800434/badge)](https://coveralls.io/builds/30800434)

Coverage increased (+0.02%) to 67.36% when pulling **7a16274df592f6e2bc0dc34354cea08c9bf9816a on tburrows13:fix-concatenate-indexerror** into **b2ed1f6514cfbc5620ef0ad6460b6feffc37b85a on Zulko:master**.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65103a9..99c6deb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -53,6 +53,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - `IndexError` in `vfx.freeze`, `vfx.time_mirror` and `vfx.time_symmetrize` [#1124]
 - Using `rotate()` with a `ColorClip` no longer crashes [#1139]
 - `AudioFileClip` would not generate audio identical to the original file [#1108]
+- Several issues resulting from incorrect time values due to floating point errors [#1195], for example:
+    - Blank frames at the end of clips [#210]
+    - Sometimes getting `IndexError: list index out of range` when using `concatenate_videoclips` [#646] 
 
 
 ## [v1.0.3](https://github.com/zulko/moviepy/tree/v1.0.3) (2020-05-07)
diff --git a/moviepy/Clip.py b/moviepy/Clip.py
index e742728..1036dde 100644
--- a/moviepy/Clip.py
+++ b/moviepy/Clip.py
@@ -466,7 +466,11 @@ class Clip:
                      for frame in myclip.iter_frames()])
         """
         logger = proglog.default_bar_logger(logger)
-        for t in logger.iter_bar(t=np.arange(0, self.duration, 1.0 / fps)):
+        for frame_index in logger.iter_bar(t=np.arange(0, int(self.duration * fps))):
+            # int is used to ensure that floating point errors are rounded
+            # down to the nearest integer
+            t = frame_index / fps
+
             frame = self.get_frame(t)
             if (dtype is not None) and (frame.dtype != dtype):
                 frame = frame.astype(dtype)


Test patch:
diff --git a/tests/test_compositing.py b/tests/test_compositing.py
index 2f04954..0bb900c 100644
--- a/tests/test_compositing.py
+++ b/tests/test_compositing.py
@@ -38,3 +38,26 @@ def test_clips_array_duration():
     video = clips_array([[red, green, blue]]).set_duration(5)
     video.write_videofile(join(TMP_DIR, "test_clips_array.mp4"))
     close_all_clips(locals())
+
+
+def test_concatenate_self():
+    clip = BitmapClip([["AAA", "BBB"], ["CCC", "DDD"]]).set_fps(1)
+    target = BitmapClip([["AAA", "BBB"], ["CCC", "DDD"]]).set_fps(1)
+
+    concatenated = concatenate_videoclips([clip])
+
+    concatenated.write_videofile(join(TMP_DIR, "test_concatenate_self.mp4"))
+    assert concatenated == target
+
+
+def test_concatenate_floating_point():
+    """
+    >>> print("{0:.20f}".format(1.12))
+    1.12000000000000010658
+
+    This test uses duration=1.12 to check that it still works when the clip duration is
+    represented as being bigger than it actually is. Fixed in #1195.
+    """
+    clip = ColorClip([100, 50], color=[255, 128, 64], duration=1.12).set_fps(25.0)
+    concat = concatenate_videoclips([clip])
+    concat.write_videofile("concat.mp4", preset="ultrafast")