-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmidisync.applescript
More file actions
164 lines (131 loc) · 5.77 KB
/
Copy pathmidisync.applescript
File metadata and controls
164 lines (131 loc) · 5.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
(*
Copyright (c) 2025 Ethan Griffith
Licensed under the MIT License. See LICENSE file for details.
*)
-- provides a gui for selecting files and running the python script midisync.py
-- set python interpreter path
set pythonInterpreter to "/Library/Frameworks/Python.framework/Versions/3.14/bin/python3"
-- python script embedded as text
set pythonCode to "import sys
import mido
import numpy
from moviepy.editor import VideoFileClip, concatenate_videoclips, ColorClip
def extractNotes(filePath, chordThreshold=0.1):
mid = mido.MidiFile(filePath)
noteEvents = []
currentTime = 0.0
activeNotes = {}
for msg in mid:
currentTime += msg.time
if msg.type == 'note_on' and msg.velocity > 0:
activeNotes[msg.note] = currentTime
elif msg.type == 'note_off' or (msg.type == 'note_on' and msg.velocity == 0):
if msg.note in activeNotes:
startTime = activeNotes[msg.note]
endTime = currentTime
noteEvents.append({'pitch': msg.note, 'start': startTime, 'end': endTime})
del activeNotes[msg.note]
noteEvents.sort(key=lambda x: x['start'])
noteNum = 0
lastStartTime = None
groupedEvents = []
for event in noteEvents:
if lastStartTime is None or (event['start'] - lastStartTime) > chordThreshold:
noteNum += 1
lastStartTime = event['start']
groupedEvents.append({'noteNum': noteNum, 'start': event['start'], 'end': event['end']})
return groupedEvents
def createVideo(midiPath, videoClipPath, outputPath):
noteEvents = extractNotes(midiPath, chordThreshold=0.1)
if not noteEvents:
return
sourceClip = VideoFileClip(videoClipPath)
videoClips = []
currentPos = 0.0
for event in noteEvents:
noteNum = event['noteNum']
startTime = event['start']
endTime = event['end']
duration = endTime - startTime
if startTime > currentPos:
gapDuration = startTime - currentPos
greenClip = ColorClip(size=(sourceClip.w, sourceClip.h), color=(0, 255, 0), duration=gapDuration)
videoClips.append(greenClip)
if sourceClip.duration < duration:
clips = []
remainingDuration = duration
forward = True
while remainingDuration > 0:
clipDuration = min(remainingDuration, sourceClip.duration)
if forward:
clipToAdd = sourceClip.subclip(0, clipDuration)
else:
# Create reversed clip by reversing frame indices
clipToAdd = sourceClip.subclip(0, clipDuration).fx(
lambda clip: clip.fl_time(lambda t: clipDuration - t - 1.0/clip.fps, apply_to=['video', 'audio'])
).set_duration(clipDuration)
clips.append(clipToAdd)
remainingDuration -= clipDuration
forward = not forward
noteClip = concatenate_videoclips(clips)
else:
noteClip = sourceClip.subclip(0, duration)
if noteNum % 2 == 0:
noteClip = noteClip.fx(lambda clip: clip.fl_image(lambda img: numpy.fliplr(img)))
videoClips.append(noteClip)
currentPos = endTime
finalVideo = concatenate_videoclips(videoClips, method='compose')
finalVideo.write_videofile(outputPath, fps=30, codec='libx264', audio=False)
sourceClip.close()
finalVideo.close()
if __name__ == '__main__':
if len(sys.argv) == 4:
midiFile = sys.argv[1]
videoClip = sys.argv[2]
outputVideo = sys.argv[3]
else:
sys.exit(1)
createVideo(midiFile, videoClip, outputVideo)
"
try
-- select MIDI file
set midiFile to choose file with prompt "Select the MIDI file:" of type {"mid", "midi", "public.midi-audio"}
set midiPath to POSIX path of midiFile
-- select video clip file
set videoFile to choose file with prompt "Select the video clip:" of type {"mov", "mp4", "avi", "public.movie", "public.mpeg-4"}
set videoPath to POSIX path of videoFile
-- choose output location and name
set outputFile to choose file name with prompt "Save output video as:" default name "output.mp4"
set outputPath to POSIX path of outputFile
set msgString to "Processing video..." & return & "You will be notified when it is completed."
display notification msgString with title "MIDISync"
-- create temporary python script file
set tempScript to (do shell script "echo $HOME") & "/.midisync_temp.py"
-- write python code to file using cat with heredoc to avoid quote issues
do shell script "cat > " & quoted form of tempScript & " << 'ENDOFPYTHON'
" & pythonCode & "
ENDOFPYTHON"
-- build and execute python command
set pythonCommand to pythonInterpreter & " " & quoted form of tempScript & " " & quoted form of midiPath & " " & quoted form of videoPath & " " & quoted form of outputPath
-- run the script
set scriptOutput to do shell script pythonCommand
-- clean up temp file
do shell script "rm " & quoted form of tempScript
-- show completion message
display dialog "Video created successfully!" & return & return & "Output: " & outputPath buttons {"OK"} default button 1 with icon note with title "MIDISync"
-- reveal in finder, delay to ensure file is registered
delay 0.5
try
tell application "Finder"
reveal POSIX file outputPath
activate
end tell
on error
-- if reveal fails, open the containing folder
do shell script "open " & quoted form of (do shell script "dirname " & quoted form of outputPath)
end try
on error errMsg number errNum
if errNum is not -128 then
display dialog "Error: " & errMsg buttons {"OK"} default button 1 with icon stop
end if
end try