Compare commits

...

3 Commits

Author SHA1 Message Date
d4ad426378 Implementation experiments
I seem to be stuck, Pro Tools isn't letting me write pan automation with
the message "Command Error: ErrType 126: PT_InvalidParameter (Invalid pan
control ID - incompatible pan space/parameter/channel values?)"
2025-11-07 21:53:53 -08:00
c11524c596 More implementation of Pro Tools 2025-11-07 15:30:15 -08:00
b506fd83de Implementing for ptsl in progress 2025-11-06 16:09:03 -08:00
5 changed files with 180 additions and 117 deletions

View File

@@ -1,19 +1,20 @@
import bpy import bpy
from .operator_protools_export import ProToolsExport from .operator_protools_export import SendToProTools
def export_pt_menu_callback(self, _): def export_pt_menu_callback(self, _):
self.layout.operator(ProToolsExport.bl_idname, text="Send to Pro Tools") layout = self.layout
layout.separator()
layout.operator(SendToProTools.bl_idname, text="Send to Pro Tools")
def register(): def register():
bpy.utils.register_class(ProToolsExport) bpy.utils.register_class(SendToProTools)
bpy.types.TOPBAR_MT_file_export.append(export_pt_menu_callback) bpy.types.VIEW3D_MT_object.append(export_pt_menu_callback)
def unregister(): def unregister():
bpy.utils.unregister_class(ProToolsExport) bpy.utils.unregister_class(SendToProTools)
bpy.types.VIEW3D_MT_object.remove(export_pt_menu_callback)
bpy.types.TOPBAR_MT_file_export.remove(export_pt_menu_callback)

View File

@@ -14,10 +14,23 @@ license = [
] ]
website = "https://git.squad51.us/jamie/soundobjects_blender_addon" website = "https://git.squad51.us/jamie/soundobjects_blender_addon"
platforms = ["macos-arm64", "windows-x64"] platforms = ["macos-arm64", "windows-x64"]
wheels = [
'./wheels/grpcio-1.74.0-cp311-cp311-macosx_11_0_universal2.whl',
'./wheels/grpcio-1.74.0-cp311-cp311-win_amd64.whl',
'./wheels/iniconfig-2.3.0-py3-none-any.whl',
'./wheels/packaging-25.0-py3-none-any.whl',
'./wheels/pluggy-1.6.0-py3-none-any.whl',
'./wheels/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl',
'./wheels/protobuf-6.31.1-cp310-abi3-win_amd64.whl',
'./wheels/py_ptsl-600.1.0-py3-none-any.whl',
'./wheels/pygments-2.19.2-py3-none-any.whl',
'./wheels/pytest-8.4.2-py3-none-any.whl'
]
[permissions] [permissions]
network = "Communicates with Pro Tools via RPC, only localhost" network = "Communicates with Pro Tools via RPC, only localhost"
[build] [build]
paths_exclude_pattern = [ paths_exclude_pattern = [
"__pycache__/", "__pycache__/",

131
intern/pro_tools.py Normal file
View File

@@ -0,0 +1,131 @@
import pprint
import bpy
import ptsl
from ptsl import PTSL_pb2 as pt
from .geom_utils import compute_relative_vector, room_norm_vector, speaker_active_time_range
def mixdown_speaker(speaker: bpy.types.Object, scene: bpy.types.Scene) -> str:
return ""
def create_clip(audio_file_path: str, client: ptsl.Client) -> str:
return ""
def spot_clip(speaker: bpy.types.Object, clip_id: str, track_id: str,
client: ptsl.Client):
pass
def create_track(speaker: bpy.types.Object, client: ptsl.Client):
assert speaker.data
result = client.run_command(pt.CId_CreateNewTracks,
{'numuer_of_tracks': 1,
'track_name': speaker.data.name,
'track_format': "TFormat_Mono",
'track_type': "TType_Audio",
'track_timebase': "TTimebase_Samples",
'pagination_request': {
'limit': 1,
'offset': 0,
},
'insertion_point_position': "TIPoint_Unknown",
'insertion_point_track_name': ""
})
print(result)
assert result
assert 'created_track_ids' in result.keys()
assert len(result['created_track_ids']) == 1
return result['created_track_ids'][0]
def insert_audio(speaker: bpy.types.Object, scene: bpy.types.Scene,
track_id: str, client: ptsl.Client):
rendered_audio_file = mixdown_speaker(speaker, scene)
clip_id = create_clip(rendered_audio_file, client)
spot_clip(speaker, clip_id, track_id, client)
def apply_pan_automation(speaker: bpy.types.Object, scene: bpy.types.Scene,
track_id: str, room_size: float, client: ptsl.Client):
CId_SetTrackControlBreakpoints = 150
# fps = scene.render.fps
speaker_interval = speaker_active_time_range(speaker)
assert scene.camera, "Scene does not have a camera"
values: list[tuple[dict, dict, dict]] = []
for frame in range(speaker_interval.start_frame,
speaker_interval.end_frame + 1):
scene.frame_set(frame)
relative_vector = compute_relative_vector(camera=scene.camera,
target=speaker)
norm_vec = room_norm_vector(relative_vector,
room_size=room_size)
time_struct = {'location': str(frame), 'time_type': 'TLType_Frames'}
x_pos = {'time': time_struct, 'value': round(norm_vec.x, 4)}
y_pos = {'time': time_struct, 'value': round(norm_vec.y, 4)}
z_pos = {'time': time_struct, 'value': round(norm_vec.z, 4)}
values.append((x_pos, y_pos, z_pos))
for i, control in enumerate(['PCParameter_X', 'PCParameter_Y',
'PCParameter_Z']):
params ={
'track_id': track_id,
'control_id': {
'section': 'TSId_MainOut',
'control_type': 'TCType_Pan',
'pan': {
'pan_space': 'PSpace_3dCart3layer',
'parameter': control,
'channel': 'SChannel_Mono'
},
},
'breakpoints': [v[i] for v in values]
}
print("Sending parameter changes:")
pprint.pprint(params)
client.run_command(CId_SetTrackControlBreakpoints, params)
def send_to_pro_tools(speakers: list[bpy.types.Object],
room_size: float) -> bool:
try:
print("About to create client.")
client = ptsl.Client(company_name="Squad 51",
application_name="Send to Pro Tools Blender Extension")
scene = bpy.context.scene
for speaker in speakers:
new_track_id = create_track(speaker, client)
insert_audio(speaker, scene, new_track_id, client)
apply_pan_automation(speaker, scene, new_track_id, room_size,
client)
client.close()
return True
except ptsl.CommandError as e:
print("Command Error: {}".format(e))
return False
except AssertionError as e:
raise e

View File

@@ -1,72 +0,0 @@
import bpy
from bpy.types import Operator
from bpy.props import BoolProperty, StringProperty, EnumProperty, FloatProperty
from .intern.add_sound_to_meshes import add_speakers_to_meshes, TriggerMode
class AddSoundToMeshOperator(Operator):
"""Add a speaker to each selected object"""
bl_idname = "object.add_speakers_to_obj"
bl_label = "Add Sounds to Meshes"
TRIGGER_OPTIONS = (
(TriggerMode.START_FRAME,
"Start Frame",
"Sound will play on the first frame of the animation"),
(TriggerMode.MIN_DISTANCE,
"Minimum Distance",
"Sound will play when the object is closest to the camera"),
(TriggerMode.RANDOM,
"Random",
"Sound will play exactly once, at a random time"),
(TriggerMode.RANDOM_GAUSSIAN,
"Random (Gaussian)",
"Sound will play exactly once, at a guassian random time with " +
"stdev of 1 and mean in the middle of the animation")
)
@classmethod
def poll(cls, context):
sounds_avail = bpy.data.sounds
return len(context.selected_objects) > 0 and len(sounds_avail) > 0
use_sounds: StringProperty(
name="Sound Prefix",
description="Sounds having names starting with thie field will be assigned randomly to each speaker"
)
sync_audio_peak: BoolProperty(
name="Sync Audio Peak",
default=True,
description="Synchronize speaker audio to loudest peak instead of beginning of file"
)
trigger_mode: EnumProperty(
items=TRIGGER_OPTIONS,
name="Trigger",
description="Select when each sound will play",
default=TriggerMode.MIN_DISTANCE,
)
gaussian_stddev: FloatProperty(
name="Gaussian StDev",
description="Standard Deviation of Gaussian random time",
default=1.,
min=0.001,
max=6.,
)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
add_speakers_to_meshes(bpy.context.selected_objects, bpy.context,
sound=None,
sound_name_prefix=self.use_sounds,
trigger_mode=self.trigger_mode,
sync_peak=self.sync_audio_peak,
gaussian_stddev=self.gaussian_stddev)
return {'FINISHED'}

View File

@@ -1,52 +1,42 @@
from bpy_extras.io_utils import ExportHelper import bpy
from bpy.props import StringProperty, BoolProperty, FloatProperty, IntProperty
from bpy.types import Operator
from .intern.generate_adm import generate_adm from .intern import pro_tools
def filter_speakers(objs: list[bpy.types.Object]) -> list[bpy.types.Object]:
return [s for s in objs if s.type == 'SPEAKER']
class ProToolsExport(Operator, ExportHelper): class SendToProTools(bpy.types.Operator):
""" "Send Audio Objects to Pro Tools"
Export audio objects to a Pro Tools session
""" bl_idname = "object.pro_tools_live"
bl_idname = "export.pro_tools_live" # important since its how
bl_label = "Send Audio Objects to Pro Tools" bl_label = "Send Audio Objects to Pro Tools"
filepath: str
filename_ext = ".wav"
filter_glob = StringProperty( room_size: bpy.props.FloatProperty(
default="*.wav",
options={'HIDDEN'},
maxlen=255, # Max internal buffer length, longer would be clamped.
)
room_size = FloatProperty(
default=1.0,
name="Room Size", name="Room Size",
description="Distance from the lens to the front room boundary", description="Distance from the lens to the front room boundary",
default=1.0,
min=0.001, min=0.001,
step=1, step=1,
unit='LENGTH' unit='LENGTH'
) )
max_objects = IntProperty( @classmethod
name="Max Objects", def poll(cls, context):
description="Maximum number of object tracks to create", return len(filter_speakers(context.selected_objects)) > 0 and \
default=24, bpy.app.online_access and \
min=0, context.scene.camera is not None
max=118
)
create_bed = BoolProperty( def invoke(self, context, event):
name="Create 7.1 Bed", wm = context.window_manager
description="Create a bed for all sounds not included on object " return wm.invoke_props_dialog(self, confirm_text="Send to Pro Tools")
"tracks",
default=False,
options={'HIDDEN'}
)
def execute(self, context) -> set: def execute(self, context) -> set:
assert self.create_bed is False, "Create Bed is not supported" print("Execute called...")
if pro_tools.send_to_pro_tools(
return generate_adm(context, self.filepath, self.room_size, filter_speakers(context.selected_objects),
self.max_objects) self.room_size):
self.report({'INFO'}, "Speaker objects sent to Pro Tools")
return {'FINISHED'}
else:
self.report({'ERROR'}, "Could not connect to Pro Tools")
return {'FINISHED'}