将gif同步到音乐的节奏比预期的持续时间短

时间:2020-06-01 15:58:14

标签: python audio gif

我正在尝试将gif同步到Spotify上播放的音乐的节拍中,但与此同时遇到速度问题。我一定要疯了,因为我找不到为什么它不起作用的原因。以下是我的方法:

  • 进行初始BPM(例如:150)并找到Beats / Second(BPS
    • BPS = BPM / 60
  • 从节拍/秒(SPB)中找到秒/节拍(BPS
    • SPB = 1 / BPS
  • 通过乘以.gif的节拍数/循环数(SPL)来找到秒数/循环数(BPL)。
    • SPL = SPB * BPL
  • 将秒/循环(SPL)转换为毫秒/循环(MSPL
    • MSPL = SPL * 1000
  • 将.gif中的帧数(MSPL除以毫秒/循环(num_frames),以找到一帧(frame_time)所需的时间,四舍五入为最接近的偶数,因为.gif帧时间仅精确到整毫秒
    • frame_time = MSPL / num_frames
  • 合计总帧时间(actual_duration)并循环遍历所有帧,增加或减少1毫秒,直到actual_durationceil(MSPL)匹配(始终将较长的实际持续时间优先于较短的持续时间)
    difference = MSPL - actual_duration
    if not math.isclose(0, difference):
        # Add the difference and always prioritize longer duration compared to real duration value
        correction = int(math.ceil(difference))
        for i in range(0, abs(correction)):
            # Add/subtract corrections as necessary to get actual duration as close as possible to calculated duration
            frame_times[i % len(frame_times)] += math.copysign(1, correction)
    

现在,gif的实际毫秒/循环应该始终等于MSLP或大于MSLP。但是,当我以指定的帧时间保存.gif时,如果校正值不为0,则.gif总是以比预期更快的速度播放。我注意到在使用其他提供相同的“将gif同步到音乐”功能的在线服务时,情况也是如此。所以我想不仅仅是疯了。

以下是用于获取帧时间的实际代码:

def get_frame_times(tempo: float, beats_per_loop: int, num_frames: int):
    # Calculate the number of seconds per beat in order to get number of milliseconds per loop
    beats_per_sec = tempo / 60
    secs_per_beat = 1 / beats_per_sec
    duration = math.ceil(secs_per_beat * beats_per_loop * 1000)
    frame_times = []
    # Try to make frame times as even as possible by dividing duration by number of frames and rounding
    actual_duration = 0
    for _ in range(0, num_frames):
        # Rounding method: Bankers Rounding (round to the nearest even number)
        frame_time = round(duration / num_frames)
        frame_times.append(frame_time)
        actual_duration += frame_time
    # Add the difference and always prioritize longer duration compared to real duration value
    difference = duration - actual_duration
    if not math.isclose(0, difference):
        correction = int(math.ceil(difference))
        for i in range(0, abs(correction)):
            # Add/subtract corrections as necessary to get actual duration as close as possible to calculated duration
            frame_times[i % len(frame_times)] += math.copysign(1, correction)
    return frame_times

我通过使用PIL(枕头)的“图像”模块来保存gif:

frame_times = get_frame_times(tempo, beats_per_loop, num_frames)
frames = []
for i in range(0, num_frames):
  # Frames are appended to frames list here
# disposal=2 used since the frames may be transparent
frames[0].save(
    output_file, 
    save_all=True, 
    append_images=frames[1:], 
    loop=0, 
    duration=frame_times, 
    disposal=2)

我在这里做错什么了吗?我似乎无法找出为什么这行不通,以及为什么gif的实际持续时间比指定的帧时间短得多。提供该功能的其他站点/服务最终获得相同的结果使我感觉稍微好一点,但与此同时,我感觉这绝对应该可行。

1 个答案:

答案 0 :(得分:0)

解决了!我不知道这是.gif格式的限制还是PIL中Image模块的限制,但是看来帧时间最多只能精确到10毫秒的倍数。在检查了修改后的图像的实际帧时间后,它们被下限到最接近的10的倍数,从而导致整体回放速度比预期的快。

要解决此问题,我修改了代码,以10为增量选择帧时间(如果需要,可以再次设置更长的实际持续时间),并在整个列表中尽可能均匀地分散帧时间调整:

def get_frame_times(tempo: float, beats_per_loop: int, num_frames: int):
    # Calculate the number of seconds per beat in order to get number of milliseconds per loop
    beats_per_sec = tempo / 60
    secs_per_beat = 1 / beats_per_sec
    duration = round_tens(secs_per_beat * beats_per_loop * 1000)
    frame_times = []
    # Try to make frame times as even as possible by dividing duration by number of frames
    actual_duration = 0
    for _ in range(0, num_frames):
        frame_time = round_tens(duration / num_frames)
        frame_times.append(frame_time)
        actual_duration += frame_time
    # Adjust frame times to match as closely as possible to the actual duration, rounded to multiple of 10
    # Keep track of which indexes we've added to already and attempt to split corrections as evenly as possible
    # throughout the frame times
    correction = duration - actual_duration
    adjust_val = int(math.copysign(10, correction))
    i = 0
    seen_i = {i}
    while actual_duration != duration:
        frame_times[i % num_frames] += adjust_val
        actual_duration += adjust_val
        if i not in seen_i:
            seen_i.add(i)
        elif len(seen_i) == num_frames:
            seen_i.clear()
            i = 0
        else:
            i += 1
        i += num_frames // abs(correction // 10)
    return frame_times