Last active
March 19, 2025 16:44
-
-
Save Metal-666/ca319f03ea0de8e8367e01a068f70437 to your computer and use it in GitHub Desktop.
My Reaper script for inserting playthrough footage into the timeline. Check the comments for usage info.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import os | |
| import sys | |
| from datetime import datetime, date, time, timedelta | |
| # Config | |
| FOOTAGE_DIRECTORY = "D:\\Files\\PlaythroughFootage" # <- this directory will be searched for the video clips | |
| FOOTAGE_TRACK_NAME = "FOOTAGE" # <- the full footage track name will be: <SOURCE_TRACK_NAME> <FOOTAGE_TRACK_NAME> | |
| FOOTAGE_FILE_EXTENSION = ".mkv" # <- used to filter the files found in the footage directory | |
| FOOTAGE_LATENCY_COMPENSATION = 0.25 # <- shifts the inserted footage to the left by this amount (in seconds) | |
| CURRENT_PROJECT = 0 # <- all operations will be done inside the first project tab | |
| def main(): # <- most of the code is inside a function for easy script exiting using 'return' | |
| # Setting up important variables | |
| trackCount = RPR_GetNumTracks() | |
| # Validating things | |
| if not os.path.isdir(FOOTAGE_DIRECTORY): | |
| RPR_ShowMessageBox(f"Path '{FOOTAGE_DIRECTORY}' does not point to a valid directory!", "Footage directory not found!", 0) | |
| return | |
| if trackCount == 0: | |
| RPR_ShowMessageBox(f"The project does not contain any tracks!", "No source tracks!", 0) | |
| return | |
| # Using user input to choose the source track | |
| userInput = RPR_GetUserInputs("Insert footage for track:", 1, "Track #", "", 512) | |
| if not userInput[0]: # <- the first value tells us whether the input dialog was cancelled or not | |
| return | |
| sourceTrackNumber = int(userInput[4]) # <- the fifth value is the actual input | |
| sourceTrackIndex = sourceTrackNumber - 1 # <- for convenience, we ask the user to enter the track number (they start at 1), then we convert it to the 0-based index | |
| # Validating user input | |
| if sourceTrackIndex < 0 or sourceTrackIndex >= trackCount: | |
| RPR_ShowMessageBox(f"Track index {sourceTrackIndex} is out of range [0, {trackCount - 1}]!", "Track not found!", 0) | |
| return | |
| sourceTrack = RPR_GetTrack(CURRENT_PROJECT, sourceTrackIndex) | |
| sourceTrackName = RPR_GetSetMediaTrackInfo_String(sourceTrack, "P_NAME", "", False)[3] | |
| footageTrackIndex = trackCount # <- the footage track will be inserted at the end of the track list | |
| # Creating the footage track | |
| RPR_InsertTrackInProject(CURRENT_PROJECT, footageTrackIndex, 0) | |
| footageTrack = RPR_GetTrack(CURRENT_PROJECT, footageTrackIndex) | |
| RPR_GetSetMediaTrackInfo_String(footageTrack, "P_NAME", f"{sourceTrackName} {FOOTAGE_TRACK_NAME}", True) | |
| RPR_SetMediaTrackInfo_Value(footageTrack, "B_MAINSEND", False) # <- effectively disables the audio on the track | |
| RPR_SetOnlyTrackSelected(footageTrack) # <- selecting the footage track is required to insert media items into it | |
| # Discovering the footage files | |
| footageFiles = [] # <- will contain (filePath, fileCreationDatetime) tuples | |
| for fileName in os.listdir(FOOTAGE_DIRECTORY): | |
| if not fileName.endswith(FOOTAGE_FILE_EXTENSION): | |
| continue | |
| filePath = os.path.join(FOOTAGE_DIRECTORY, fileName) | |
| creationDatetime = datetime.fromtimestamp(os.path.getctime(filePath)) | |
| footageFiles.append((filePath, creationDatetime)) | |
| footageFiles.sort(key = lambda x: x[1]) # <- files are sorted by date | |
| footageFiles.reverse() # <- puts newest files at the front | |
| errors = [] # <- a list of errors which occured during the footage insertion | |
| # Iterating over the media items in the source track and inserting the corresponding footage clips into the footage track | |
| for i in range(RPR_GetTrackNumMediaItems(sourceTrack)): | |
| errorDetected = False | |
| # First, let's determine the key parameters of each media item. | |
| # These are Length, Position and Start Offset | |
| sourceMediaItem = RPR_GetTrackMediaItem(sourceTrack, i) | |
| sourceTakeIndex = int(RPR_GetMediaItemInfo_Value(sourceMediaItem, "I_CURTAKE")) | |
| sourceTake = RPR_GetMediaItemTake(sourceMediaItem, sourceTakeIndex) | |
| sourceTakeName = RPR_GetSetMediaItemTakeInfo_String(sourceTake, 'P_NAME', '', False)[3] | |
| source = RPR_GetMediaItemTake_Source(sourceTake) | |
| position = RPR_GetMediaItemInfo_Value(sourceMediaItem, "D_POSITION") | |
| length = RPR_GetMediaItemInfo_Value(sourceMediaItem, "D_LENGTH") | |
| startOffset = RPR_GetMediaItemTakeInfo_Value(sourceTake, "D_STARTOFFS") | |
| sourceDate = RPR_GetMediaFileMetadata(source, "BWF:OriginationDate", "", 128)[3] | |
| sourceTime = RPR_GetMediaFileMetadata(source, "BWF:OriginationTime", "", 128)[3] | |
| sourceTrueButImpreciseDatetime = datetime.combine(date.fromisoformat(sourceDate), time.fromisoformat(sourceTime)) | |
| sourceFilePath = RPR_GetMediaSourceFileName(source, "", 512)[1] | |
| sourceDatetime = datetime.fromtimestamp(os.path.getctime(sourceFilePath)) | |
| timeMismatchDetected = False | |
| # Ideally we would want to use th embedded time metadata of the source file to figure out the offsets. Unfortunately, the BWF time tag only | |
| # goes down to second precision, which is not good enough for our use case. Therefore we rely on the filesystem timestamp instead. | |
| # The problem is, the file creation time reported by the filesystem can be incorrect (it can be changed manually; also lookup "filesystem tunneling") | |
| # so we compare it with the embedded timestamp tag first. If the difference between two timestamps is greater than 1 second, | |
| # we can assume something went wrong with the file creation time, so we'll fallback to the embedded timestamp | |
| if (sourceTrueButImpreciseDatetime - sourceDatetime).total_seconds() > 1: | |
| sourceDatetime = sourceTrueButImpreciseDatetime | |
| timeMismatchDetected = True | |
| errorDetected = True | |
| # Now we search for the footage file, which contains the current media item. Remember, the files are sorted by date backwards, | |
| # so we are effectively going back in time in search of the first video clip that was created before the media item (therefore will contain it). | |
| # It is possible that the video file was cut short before the media item was recorded, so it would be good to add more checks here | |
| # (video creation time + video duration < media creation time = video was cut short and does not contain the media item) | |
| relevantFootageFile = None | |
| for footageFile in footageFiles: | |
| if footageFile[1] < sourceDatetime: | |
| relevantFootageFile = footageFile | |
| break | |
| if relevantFootageFile is None: | |
| errorDetected = True # <- technically unnecessary, but I'd still like to keep this here | |
| errors.append(f"Failed to find video file for the media item #{i} ({sourceTakeName})") | |
| continue | |
| # How far into the video does the recorded media item start? | |
| timeDifference = sourceDatetime - footageFile[1] | |
| timeDifferenceSeconds = timeDifference.total_seconds() # <- this far | |
| # Now that we have all the necessary data, let's insert the footage clip for the current media item | |
| RPR_InsertMedia(relevantFootageFile[0], 0) | |
| footageMediaItem = RPR_GetTrackMediaItem(footageTrack, RPR_GetTrackNumMediaItems(footageTrack) - 1) | |
| footageTake = RPR_GetMediaItemTake(footageMediaItem, 0) | |
| RPR_SetMediaItemPosition(footageMediaItem, position, True) | |
| RPR_SetMediaItemLength(footageMediaItem, length, True) | |
| RPR_SetMediaItemTakeInfo_Value(footageTake, "D_STARTOFFS", startOffset + timeDifferenceSeconds + FOOTAGE_LATENCY_COMPENSATION) | |
| # If there was a time mismatch, we don't want to lock the media item, as the user will have to adjust it manually | |
| if not timeMismatchDetected: | |
| RPR_SetMediaItemInfo_Value(footageMediaItem, "C_LOCK", 1) | |
| else: | |
| errors.append(f"File creation time discrepancy detected in the media item #{i} ({sourceTakeName}). You'll have to manually adjust this item!") | |
| # Finally, let's set the footage media item's color | |
| mediaItemColor = None | |
| if errorDetected: | |
| mediaItemColor = RPR_ColorToNative(243, 139, 168) # <- if there was an error, we'll color it red | |
| else: | |
| mediaItemColor = RPR_ColorToNative(166, 227, 161) # <- if there wasn't one, we'll color it green | |
| RPR_SetMediaItemInfo_Value(footageMediaItem, "I_CUSTOMCOLOR", mediaItemColor|0x1000000) | |
| # Script execution report | |
| if len(errors) > 0: | |
| RPR_ShowMessageBox(f"The following errors occured while inserting the footage:\n- {'\n- '.join(errors)}", "Done!", 0) | |
| else: | |
| RPR_ShowMessageBox("All footage inserted successfully!", "Done!", 0) | |
| RPR_SetEditCurPos2(CURRENT_PROJECT, 0, True, True) # <- moves the playhead to the beginning of the project | |
| main() |
Author
Author
Usage:
- Make sure you have Python installed.
- Download this script.
- In Reaper, open the Actions window (Actions > Show action list), click "New action", then "Load ReaScript...". Find and select the downloaded script.
- After the script has been added, edit the config values. To do this, either open it in a text editor (Notepad, Notepad++, VS Code etc) or find it in the Actions list, right click, and select "Edit...". The config values are located at the top of the file; use the default values as a reference.
- In Reaper preferences, go to Audio > Recording and add
$secondat the end of the "Recorded filenames" field. This is necessary to avoid filesystem tunneling. - Before each recording session, start OBS/Camera and have it roll in the background, recording yourself through your webcam or whatever.
- When you want the footage to be inserted into the project (for example after the song is finished, or when you need to remember how a certain section is played), open the Actions window and double-click my script.
- In the window that pops up, enter the track number of the track with your recorded media items (the "source track").
- The relevant footage will be discovered and inserted into a new track. If the process is successful, you will see that each of the source media items now has a corresponding video clip of the same length and at the same position. The color of each footage item will indicate if it was inserted correctly (green) or if there was an issue and you'll have to manually adjust it (red).
- Open the Video window to watch your playthrough (View > Video) during the project playback.
- If you make changes to the items in the source track, delete the footage track and re-run the script.
Author
Notes:
- At the moment, the script mostly relies on file metadata (reported by the filesystem) to determine the time offsets. This is not ideal since files can be moved around and copied and stuff, but I don't know of any better way. OBS does not write any time metadata to the video files and you cannot tell it to specify milliseconds in filenames (seconds won't give us enough accuracy). Reaper does write some metadata to the .wav files but the precision ends at seconds, so we can't rely on that either. It is however used as a fallback in case the difference between the embedded timestamp and filesystem timestamp is too large (could indicate that the file metadata was altered, or filesystem tunneling occured)
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment


The solution:
Ideally I would want the video footage from my webcam to be recorded inside Reaper, alongside the audio. This might be possible but I'm not smart enough to actually make that happen. So instead we are gonna do this: have OBS (with my webcam as the primary source) rolling in the background every time I'm working on a project; after the entire song is finished, a script would go over all the footage and insert bits of it into the project, so that it matches the recorded media items.