├── .gitignore ├── LICENSE.md ├── README.md └── examples └── basics ├── 01_load_comp_basic.py ├── 02_load_comp.py ├── 03_set_pars.py ├── 04_chops.py ├── 05_dats.py ├── 06_tops.py ├── 07_on_loaded.py ├── 08_on_loaded_async.py ├── 09_concurrent_comps.py ├── ExampleComps.toe ├── ExampleReceive.toe ├── README.md ├── TopChopDatIO.tox ├── example_sync_dat.txt └── modules ├── __pycache__ ├── image_filter.cpython-311.pyc └── utils.cpython-311.pyc ├── image_filter.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | *Backup 2 | *.*.toe -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # PolyForm Noncommercial License 1.0.0 2 | 3 | 4 | 5 | ## Acceptance 6 | 7 | In order to get any license under these terms, you must agree 8 | to them as both strict obligations and conditions to all 9 | your licenses. 10 | 11 | ## Copyright License 12 | 13 | The licensor grants you a copyright license for the 14 | software to do everything you might do with the software 15 | that would otherwise infringe the licensor's copyright 16 | in it for any permitted purpose. However, you may 17 | only distribute the software according to [Distribution 18 | License](#distribution-license) and make changes or new works 19 | based on the software according to [Changes and New Works 20 | License](#changes-and-new-works-license). 21 | 22 | ## Distribution License 23 | 24 | The licensor grants you an additional copyright license 25 | to distribute copies of the software. Your license 26 | to distribute covers distributing the software with 27 | changes and new works permitted by [Changes and New Works 28 | License](#changes-and-new-works-license). 29 | 30 | ## Notices 31 | 32 | You must ensure that anyone who gets a copy of any part of 33 | the software from you also gets a copy of these terms or the 34 | URL for them above, as well as copies of any plain-text lines 35 | beginning with `Required Notice:` that the licensor provided 36 | with the software. For example: 37 | 38 | > Required Notice: Intent Productions, Inc. (http://intentdev.io) 39 | 40 | ## Changes and New Works License 41 | 42 | The licensor grants you an additional copyright license to 43 | make changes and new works based on the software for any 44 | permitted purpose. 45 | 46 | ## Patent License 47 | 48 | The licensor grants you a patent license for the software that 49 | covers patent claims the licensor can license, or becomes able 50 | to license, that you would infringe by using the software. 51 | 52 | ## Noncommercial Purposes 53 | 54 | Any noncommercial purpose is a permitted purpose. 55 | 56 | ## Personal Uses 57 | 58 | Personal use for research, experiment, and testing for 59 | the benefit of public knowledge, personal study, private 60 | entertainment, hobby projects, amateur pursuits, or religious 61 | observance, without any anticipated commercial application, 62 | is use for a permitted purpose. 63 | 64 | ## Noncommercial Organizations 65 | 66 | Use by any charitable organization, educational institution, 67 | public research organization, public safety or health 68 | organization, environmental protection organization, 69 | or government institution is use for a permitted purpose 70 | regardless of the source of funding or obligations resulting 71 | from the funding. 72 | 73 | ## Fair Use 74 | 75 | You may have "fair use" rights for the software under the 76 | law. These terms do not limit them. 77 | 78 | ## No Other Rights 79 | 80 | These terms do not allow you to sublicense or transfer any of 81 | your licenses to anyone else, or prevent the licensor from 82 | granting licenses to anyone else. These terms do not imply 83 | any other licenses. 84 | 85 | ## Patent Defense 86 | 87 | If you make any written claim that the software infringes or 88 | contributes to infringement of any patent, your patent license 89 | for the software granted under these terms ends immediately. If 90 | your company makes such a claim, your patent license ends 91 | immediately for work on behalf of your company. 92 | 93 | ## Violations 94 | 95 | The first time you are notified in writing that you have 96 | violated any of these terms, or done anything with the software 97 | not covered by your licenses, your licenses can nonetheless 98 | continue if you come into full compliance with these terms, 99 | and take practical steps to correct past violations, within 100 | 32 days of receiving notice. Otherwise, all your licenses 101 | end immediately. 102 | 103 | ## No Liability 104 | 105 | ***As far as the law allows, the software comes as is, without 106 | any warranty or condition, and the licensor will not be liable 107 | to you for any damages arising out of these terms or the use 108 | or nature of the software, under any kind of legal claim.*** 109 | 110 | ## Definitions 111 | 112 | The **licensor** is the individual or entity offering these 113 | terms, and the **software** is the software the licensor makes 114 | available under these terms. 115 | 116 | **You** refers to the individual or entity agreeing to these 117 | terms. 118 | 119 | **Your company** is any legal entity, sole proprietorship, 120 | or other kind of organization that you work for, plus all 121 | organizations that have control over, are under the control of, 122 | or are under common control with that organization. **Control** 123 | means ownership of substantially all the assets of an entity, 124 | or the power to direct its management and policies by vote, 125 | contract, or otherwise. Control can be direct or indirect. 126 | 127 | **Your licenses** are all the licenses granted to you for the 128 | software under these terms. 129 | 130 | **Use** means anything you do with the software requiring one 131 | of your licenses. 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI version](https://badge.fury.io/py/touchpy.svg)](https://badge.fury.io/py/touchpy) 2 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/touchpy)](https://pypi.org/project/touchpy/) 3 | 4 | 5 | 6 | # TouchPy 7 | TouchPy is a high-performance toolset to work with TouchDesigner components in Python. 8 | 9 | By leveraging Vulkan, CUDA, and TouchEngine, TouchPy opens new pathways for integration, particularly with libraries such as PyTorch and Nvidia Warp. TouchPy supports GPU-to-GPU (zero-copy) data transfers, streamlining data exchange between standalone Python applications and Touchdesigner. 10 | 11 | Please refer to the project [Documentation](https://intentdev.github.io/touchpy/) for installation instructions, API and language reference. 12 | 13 | More usage examples and tutorials are coming soon. 14 | 15 | #### Video introduction / tutorials: 16 | - The first public version of TouchPy was released during the TouchDesigner Event Berlin 2024 - see the [video of the TouchPy release presentation](https://www.youtube.com/live/hxCsPlc6W-o?t=10315s). 17 | 18 | - The TouchPy workshop on Friday May 24th at Spatial Media Lab in Berlin was also recorded, here's the [video of the TouchPy workshop at SML](https://www.youtube.com/watch?v=XDZkcEkWTOE). 19 | 20 | ## Installation 21 | 22 | TouchPy supports Python 3.9 onwards and runs on Windows. 23 | A Vulkan capable GPU and driver is required. To work with TOPs a Nvidia card is required. 24 | 25 | As TouchPy uses TouchEngine, it requires TouchDesigner or TouchPlayer (release 2023 or later) to be installed with a paid license (Educational, Commercial, or Pro). 26 | 27 | The easiest way to install TouchPy is from [PyPI](https://pypi.org/project/touchpy/): 28 | 29 | `$ pip install touchpy` 30 | 31 | -------------------------------------------------------------------------------- /examples/basics/01_load_comp_basic.py: -------------------------------------------------------------------------------- 1 | import touchpy as tp 2 | 3 | # create a class that inherits from tp.Comp 4 | # inheriting from tp.Comp is not required but it is the recommended way to interface 5 | # with a component 6 | 7 | class MyComp (tp.Comp): 8 | def __init__(self): 9 | 10 | # call the parent class constructor to initialize the component 11 | super().__init__() 12 | 13 | # create a frame counter 14 | self.frame = 0 15 | 16 | # set the on_frame_callback to the on_frame method 17 | self.set_on_frame_callback(self.on_frame, {}) 18 | 19 | # define the on_frame method that will be called on every frame 20 | # the info argument is user data that can be passed to the callback 21 | # in this case it is an empty dictionary 22 | def on_frame(self, info): 23 | 24 | # stop running the component after 1200 frames (20 seconds at 60 fps) 25 | if self.frame == 1200: 26 | self.stop() 27 | return 28 | 29 | # start_next_frame() is called to advance the component to the next frame 30 | self.start_next_frame() 31 | self.frame += 1 32 | 33 | # create an instance of the MyCompBasic class 34 | comp = MyComp() 35 | 36 | # load the tox file into the component 37 | comp.load('TopChopDatIO.tox') 38 | 39 | # start the component, this will block until self.stop() is called when not running 40 | # the component in async mode 41 | comp.start() 42 | 43 | # unload the component to cleanly free up resources 44 | comp.unload() 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /examples/basics/02_load_comp.py: -------------------------------------------------------------------------------- 1 | import touchpy as tp 2 | import modules.utils as utils # utils.py is in the same directory as this script 3 | 4 | # set the logging level to INFO which will print out verbose information 5 | tp.init_logging(level=tp.LogLevel.INFO) 6 | 7 | # create a class that inherits from tp.Comp 8 | class MyComp (tp.Comp): 9 | 10 | # these are the default arguments for tp.Comp 11 | # flags: are the flags that control the behavior of the component (can only be set at initialization) 12 | # fps: is the frames per second that the component will run at (can only be set at initialization) 13 | # device: is the device (GPU) that the component will run on (0 is defualt, can only be set at initialization) 14 | # td_path: is the path to the TouchDesigner executable ("" will use the latest installed version) 15 | def __init__(self, flags=tp.CompFlags.INTERNAL_TIME_AUTO, fps=60, device=0, td_path=""): 16 | 17 | # call the parent class constructor to initialize the component 18 | super().__init__(flags=flags, fps=fps, device=device, td_path=td_path) 19 | 20 | # create a frame counter 21 | self.frame = 0 22 | 23 | # set the on_layout_change_callback to the on_layout_change method 24 | self.set_on_layout_change_callback(self.on_layout_change, {}) 25 | 26 | # set the on_frame_callback to the on_frame method 27 | self.set_on_frame_callback(self.on_frame, {}) 28 | 29 | # this gets called at least once after the component is loaded, at this point 30 | # all the pars, in and out ops are available. If the a par, in or out op is added or removed 31 | # this method will be called again 32 | def on_layout_change(self, info): 33 | print('layout changed:') 34 | print('in tops:\n', *[f"\t{name}\n" for name in self.in_tops.names]) 35 | print('out tops:\n', *[f"\t{name}\n" for name in self.out_tops.names]) 36 | print('in chops:\n', *[f"\t{name}\n" for name in self.in_chops.names]) 37 | print('out chops:\n', *[f"\t{name}\n" for name in self.out_chops.names]) 38 | print('in dats:\n', *[f"\t{name}\n" for name in self.in_dats.names]) 39 | print('out dats:\n', *[f"\t{name}\n" for name in self.out_dats.names]) 40 | print('pars:\n', *[f"\t{name}\n" for name in self.par.names]) 41 | 42 | # define the on_frame method that will be called on every frame 43 | # the info argument is user data that can be passed to the callback 44 | # in this case it is an empty dictionary 45 | def on_frame(self, info): 46 | 47 | # stop running the if the 'q' key is pressed (terminal must be in focus) 48 | # always stop the component before calling start_next_frame() 49 | if utils.check_key('q'): 50 | self.stop() 51 | return 52 | 53 | # start_next_frame() is called to advance the component to the next frame 54 | self.start_next_frame() 55 | self.frame += 1 56 | 57 | # INTERNAL_TIME_AUTO will automatically run the component from this thread 58 | # component will run in a different process but this thread will still block 59 | # when comp.start() is called, after which callbacks are used to do work 60 | flags = tp.CompFlags.INTERNAL_TIME_AUTO 61 | 62 | # create an instance of the MyCompBasic class with specific flags, device and td_path 63 | # set td_path to the path of a TouchDesigner installation folder on your system 64 | # if the installation is not found, the latest installed version will be used 65 | td_path = 'C:/Program Files/Derivative/TouchDesigner.2023.11764.22' 66 | 67 | fps = 60 68 | device = 0 69 | 70 | comp = MyComp(flags, fps, device, td_path) 71 | 72 | # load the tox file into the component 73 | comp.load('TopChopDatIO.tox') 74 | 75 | # start the component, this will block until self.stop() is called when not running 76 | # the component in async mode 77 | comp.start() 78 | 79 | # unload the component to cleanly free up resources 80 | comp.unload() 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /examples/basics/03_set_pars.py: -------------------------------------------------------------------------------- 1 | import touchpy as tp 2 | import modules.utils as utils 3 | import math 4 | 5 | class MyComp (tp.Comp): 6 | def __init__(self): 7 | super().__init__() 8 | self.set_on_layout_change_callback(self.on_layout_change, {}) 9 | self.set_on_frame_callback(self.on_frame, {}) 10 | 11 | self.frame = 0 12 | 13 | def on_layout_change(self, info): 14 | print('layout changed:') 15 | print('pars:\n', *[f"\t{name}\n" for name in comp.par.names]) 16 | 17 | # pulse a par 18 | if 'Openwindow' in self.par.names: 19 | # only works on supported TD builds 20 | # official build with support should be available soon 21 | self.par['Openwindow'].pulse() 22 | 23 | def on_frame(self, info): 24 | if utils.check_key('q'): 25 | self.stop() 26 | return 27 | 28 | # it's typically a good idea to call start_next_frame() at the beginning of the 29 | # on_frame method before doing any processing and setting of data but it's not required 30 | self.start_next_frame() 31 | 32 | # get pars 33 | scale = self.par['Scale'] 34 | translate = self.par['Translate'] 35 | 36 | # get a par value 37 | if self.frame % 60 == 0: 38 | # printing is slow... 39 | # print('Scale:', scale.val, ', Translate:', translate.val, translate.get()) 40 | pass 41 | 42 | # pars can be set with the .val member or the .set() method 43 | scale.val = math.sin(self.frame * 0.01) * 0.25 + 0.75 44 | self.par['Rotate'].set(-self.frame * 0.2) 45 | 46 | # pars with multiple components can be accessed with the .x, .y, .z, .w members 47 | theta = self.frame * 0.005 48 | radius = 0.2 49 | x = radius * math.cos(theta) 50 | y = radius * math.sin(theta) 51 | 52 | # get/set by component member, although it's better (faster) to set all the 53 | # # components at once with .val or .set() 54 | translate.x = x 55 | translate.y = y 56 | 57 | # get/set by val member 58 | my_float2 = tp.Float2(x, y) 59 | translate.val = my_float2 60 | 61 | # set with the .set() method 62 | translate.set(x, y) 63 | translate.set((x, y)) 64 | translate.set([x, y]) 65 | translate.set(my_float2) 66 | 67 | # color pars can be accessed with the .r, .g, .b, .a members 68 | color = self.par['Rgba'] 69 | 70 | # best to set all the values at once 71 | my_color = tp.Color(1, math.sin(.5 + self.frame * .1) * 0.5 + 0.5, math.sin(self.frame * .1) * 0.5 + 0.5, 1) 72 | color.val = my_color 73 | 74 | # valid par types are: 75 | # bool, 76 | # float, 77 | # int, 78 | # str, 79 | # tp.Float2, 80 | # tp.Float3, 81 | # tp.Float4, 82 | # tp.Color, 83 | # tp.Int2, 84 | # tp.Int3, 85 | # tp.Int4 86 | 87 | # currently the only exposed functionality is getting and setting members of multi-component pars 88 | # in the future more functionality will be exposed, especially for the tp.Color type 89 | 90 | self.frame += 1 91 | 92 | comp = MyComp() 93 | comp.load('TopChopDatIO.tox') 94 | comp.start() 95 | comp.unload() 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /examples/basics/04_chops.py: -------------------------------------------------------------------------------- 1 | import touchpy as tp 2 | import modules.utils as utils 3 | import numpy as np 4 | 5 | class MyComp (tp.Comp): 6 | def __init__(self): 7 | super().__init__() 8 | self.set_on_layout_change_callback(self.on_layout_change, {}) 9 | self.set_on_frame_callback(self.on_frame, {}) 10 | 11 | self.frame = 0 12 | 13 | # create a new ChopChannels object 1 sample long with 4 channels 14 | self.my_chans = tp.ChopChannels(1, channel_names=['a', 'c', 'd']) 15 | self.my_chans.insert_channel(1, 'b', [1.0]) 16 | self.my_chans.append_channel('e') 17 | 18 | def on_layout_change(self, info): 19 | print('layout changed:') 20 | print('out chops:\n', *[f"\t{name}\n" for name in self.out_chops.names]) 21 | print('in chops:\n', *[f"\t{name}\n" for name in self.in_chops.names]) 22 | 23 | if 'Openwindow' in self.par.names: 24 | # only works on supported TD builds 25 | # official build with support should be available soon 26 | # self.par['Openwindow'].pulse() 27 | pass 28 | 29 | def on_frame(self, info): 30 | if utils.check_key('q'): 31 | self.stop() 32 | return 33 | 34 | # all "out" data retrieved from a component (from out TOPs, CHOPs, DATs) must be done 35 | # before calling start_next_frame() (pars are an exception) 36 | 37 | # get an out CHOP by index or by name 38 | out_chop1 = self.out_chops[0] 39 | out_chop1 = self.out_chops['chopOut1'] 40 | 41 | if (self.frame == 60): 42 | print('out_chop1 channel names:', out_chop1.chan_names) 43 | 44 | # get a ChopChannels object from the chop 45 | out_chop1_chans = out_chop1.chans() 46 | 47 | print('out_chop1_chans:') 48 | print('\tnum channels:', out_chop1_chans.num_chans) 49 | print('\tnum samples:', out_chop1_chans.num_samples) 50 | print('\tsample rate', out_chop1_chans.rate) 51 | print('\tis time dependent:', out_chop1_chans.is_time_dependent) 52 | print('\tstart time:', out_chop1_chans.start_time) 53 | print('\tend time:', out_chop1_chans.end_time) 54 | 55 | # get the first sample of the first channel by index 56 | print('\tout_chop1_chans[0][0] = ', out_chop1_chans[0][0]) 57 | 58 | # get the first sample of the third channel by name 59 | print('\tout_chop1_chans["test_chan3"][0] = ', out_chop1_chans['test_chan3'][0]) 60 | 61 | # add 10 to the first sample of the third channel 62 | out_chop1_chans['test_chan3'][0] += 10 63 | print('\tout_chop1_chans["test_chan3"][0] = ', out_chop1_chans['test_chan3'][0]) 64 | 65 | 66 | # get all the values of the chop as a numpy array 67 | np_array = out_chop1.as_numpy() 68 | np_array += 100 69 | 70 | if (self.frame == 60): 71 | print('\nnp_array.shape:', np_array.shape) 72 | # print the value of the first sample of each channel 73 | for i in range(np_array.shape[0]): 74 | print(f'out_chop1 {out_chop1.chan_names[i]} sample 0 = ', np_array[i][0]) 75 | 76 | # note the third channel in the numpy array has 10 added to it, 77 | # both the ChopChannels object and the numpy array is a view of the chop data 78 | 79 | # set the first sample of each channel of self.my_chans 80 | self.my_chans['a'][0] = np.sin(self.frame * 0.1) 81 | self.my_chans['b'][0] = np.cos(self.frame * 0.1) 82 | 83 | if 'c' in self.my_chans.names: # will be removed after 600 frames 84 | self.my_chans['c'][0] = np.tan(self.frame * 0.1) 85 | 86 | self.my_chans['d'][0] = np.arctan(self.frame * 0.1) 87 | self.my_chans['e'][0] = np.sin(self.frame * 0.1) + 10 88 | 89 | if self.frame == 600: 90 | # remove the 'c' channel 91 | self.my_chans.remove_channel('c') 92 | 93 | # set the first in chop with the data from self.my_chans 94 | self.in_chops[0].from_channels(self.my_chans) 95 | 96 | # set second input chop with numpy array np_array 97 | self.in_chops[1].from_numpy(np_array) 98 | 99 | # get audio data from 2nd output chop as ChopChannels and as numpy array 100 | audio_chans = self.out_chops[1].chans() 101 | audio_array = audio_chans.as_numpy() 102 | audio_array *= .5 # reduce volume by half 103 | audio_names = self.out_chops[1].chan_names 104 | 105 | # get time info from the Comp 106 | time_info = self.time() 107 | 108 | # calculate start and end times in samples 109 | sample_rate = audio_chans.rate 110 | samples_per_frame = sample_rate / time_info.rate 111 | start_time = int(time_info.frame * samples_per_frame) 112 | end_time = int(start_time + samples_per_frame) 113 | 114 | new_audio_chans = tp.ChopChannels( 115 | audio_array, 116 | rate=sample_rate, 117 | is_time_dependent=True, 118 | start_time=start_time, 119 | end_time=end_time, 120 | channel_names=audio_names 121 | ) 122 | 123 | self.in_chops['chopIn3'].from_channels(new_audio_chans) 124 | 125 | self.start_next_frame() 126 | 127 | self.frame += 1 128 | 129 | 130 | 131 | comp = MyComp() 132 | comp.load('TopChopDatIO.tox') 133 | comp.start() 134 | comp.unload() 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /examples/basics/05_dats.py: -------------------------------------------------------------------------------- 1 | import touchpy as tp 2 | import modules.utils as utils 3 | import numpy as np 4 | 5 | class MyComp (tp.Comp): 6 | def __init__(self): 7 | super().__init__() 8 | self.set_on_layout_change_callback(self.on_layout_change, {}) 9 | self.set_on_frame_callback(self.on_frame, {}) 10 | 11 | self.frame = 0 12 | 13 | # create a new DatTable object with 3 rows and 3 columns 14 | self.my_table = tp.DatTable(2, 3) 15 | self.my_table.set_row(0, ['a', 'b', 'c']) 16 | self.my_table.set_row(1, ['1', '2', '3']) 17 | self.my_table.append_row(['d', 'e', 'f']) 18 | self.my_table.append_col(['4', '5', '6']) 19 | 20 | print( 21 | 'my_table:\n', 22 | *[f"\t{self.my_table.row(i)}\n" for i in range(self.my_table.num_rows)] 23 | ) 24 | 25 | 26 | 27 | def on_layout_change(self, info): 28 | print('layout changed:') 29 | print('out dats:\n', *[f"\t{name}\n" for name in self.out_dats.names]) 30 | print('in dats:\n', *[f"\t{name}\n" for name in self.in_dats.names]) 31 | 32 | if 'Openwindow' in self.par.names: 33 | # only works on supported TD builds 34 | # official build with support should be available soon 35 | # self.par['Openwindow'].pulse() 36 | pass 37 | 38 | def on_frame(self, info): 39 | if utils.check_key('q'): 40 | self.stop() 41 | return 42 | 43 | # all "out" data retrieved from a component (from out TOPs, CHOPs, DATs) must be done 44 | # before calling start_next_frame() (pars are an exception) 45 | 46 | out_dat1 = self.out_dats[0] 47 | out_dat1_table = out_dat1.as_table() 48 | out_dat1_str = out_dat1.as_string() 49 | 50 | out_dat2 = self.out_dats['datOut2'] 51 | out_dat2_table = out_dat2.as_table() 52 | out_dat2_str = out_dat2.as_string() 53 | 54 | if (self.frame == 60): 55 | print('out_dat1 num rows:', out_dat1_table.num_rows) 56 | print('out_dat1 num cols:', out_dat1_table.num_cols, '\n') 57 | 58 | print('out_dat1 as string:\n', out_dat1_str, '\n') 59 | 60 | print('out_dat1 rows:\n', *[f"\t{row}\n" for row in out_dat1_table.as_list()]) 61 | 62 | print('out_dat1 1, 1:', out_dat1_table[2, 2], '\n') 63 | print('out_dat1 3, "col3":', out_dat1_table[2, 'col2'], '\n') 64 | print('out_dat1 "row3", 1:', out_dat1_table.cell('row2', 1), '\n') 65 | 66 | 67 | print('out_dat2 rows:\n', *[f"\t{row}\n" for row in out_dat2_table.as_list()]) 68 | print('out_dat2 as string:', out_dat2_str, '\n') 69 | 70 | # set first in DAT to a string from a string 71 | self.in_dats[0].from_string(f"Hello World! frame: {self.frame}") 72 | 73 | # set second in DAT to a table from a DatTable 74 | self.in_dats['datIn2'].from_table(self.my_table) 75 | 76 | # set third in DAT to a table from a list of lists 77 | # optional set cast flag to convert values to string 78 | table_data = [ 79 | ['values', 'str', 'int', 'float'], 80 | ['row2', 'some string', 1, 1.111111], 81 | ['row3', 'another string', 2, 2.222222], 82 | ['row4', 'and another', 3, 3.3333333] 83 | ] 84 | self.in_dats['datIn3'].from_list(table_data, cast=True) 85 | 86 | 87 | 88 | 89 | self.start_next_frame() 90 | 91 | self.frame += 1 92 | 93 | 94 | 95 | comp = MyComp() 96 | comp.load('TopChopDatIO.tox') 97 | comp.start() 98 | comp.unload() 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /examples/basics/06_tops.py: -------------------------------------------------------------------------------- 1 | import touchpy as tp 2 | from pprint import pprint 3 | 4 | # follow instructions from https://pytorch.org/ to install torch for your system 5 | import torch 6 | 7 | import modules.utils as utils 8 | from modules.image_filter import ImageFilter 9 | 10 | class MyComp (tp.Comp): 11 | def __init__(self): 12 | super().__init__() 13 | self.set_on_layout_change_callback(self.on_layout_change, {}) 14 | self.set_on_frame_callback(self.on_frame, {}) 15 | 16 | # CUDA compatible device is required for this example 17 | self.device = torch.device('cuda:0') 18 | 19 | self.frame = 0 20 | 21 | # create an ImageFilter object and move it to the GPU 22 | self.imag_filter = ImageFilter( 23 | in_channels=3, 24 | out_channels=3, 25 | kernel_size=12, 26 | stride=2, 27 | groups=3 28 | ).to(self.device) 29 | 30 | # create 1x1080x1920 tensor filled with ones for alpha channel 31 | # this tensor will be used to create a 4 channel tensor 32 | self.alpha_tensor = torch.ones(1, 1080, 1920, device=self.device, dtype=torch.float16) 33 | 34 | def on_layout_change(self, info): 35 | print('layout changed:') 36 | print('out tops:\n', *[f"\t{name}\n" for name in self.out_tops.names]) 37 | print('in tops:\n', *[f"\t{name}\n" for name in self.in_tops.names]) 38 | 39 | # there is a SelectTOP in inside of the component that will select 40 | # any TOP we specify in the 'Monitortop' parameter 41 | if 'Monitortop' in self.par.names: 42 | self.par['Monitortop'].val = 'topIn1' 43 | 44 | if 'Openwindow' in self.par.names: 45 | # only works on supported TD builds 46 | # official build with window support should be available soon 47 | self.par['Openwindow'].pulse() 48 | pass 49 | 50 | # "RGBA" 8 bit fixed textures in TOPs in TD are actualy stored as BGRA on the GPU. 51 | # Float types (16 and 32 bit) are stored as RGBA on the GPU. 52 | # All RG types are stored as RG on the GPU. 53 | # 54 | # The memory layout in all TOPs in TD is interleaved: 55 | # (B, G, R, A, B, G, R, A, ...) for BGRA 56 | # (R, G, B, A, R, G, B, A, ...) for RGBA 57 | # 58 | # 59 | # The fastest way to get data into a CUDA array (tensor etc.) is to use the 60 | # the same channel order and same memory layout as the TOPs in TD, 61 | # but most libraries default operations expect a Planar layout: 62 | # (B, B, B, ..., G, G, G, ..., R, R, R, ..., A, A, A, ...) or 63 | # (R, R, R, ..., G, G, G, ..., B, B, B, ..., A, A, A, ...) or 64 | # (R, R, R, ..., G, G, G, ..., B, B, B, ...) etc. 65 | # 66 | # Operations such as tensor.permute((2, 0, 1)).contiguous() can be done in 67 | # various libraries to permute the data to the modify the layout but these 68 | # have a cost (copying data). Instead we can set flags on the In/Out TOPs in 69 | # TouchPy to control the layout and channel order of the data when it is copied 70 | # to/from the texture which must happen in either case. 71 | # TouchPy uses the CudaFlags enum to set these flags. 72 | # 73 | # The RGBA, BGRA, RGB, BGR flags are used to set the channel order of the data and 74 | # to specify the number of channels in the data. 75 | # CHW and HWC flags are used to set the memory layout and the shape of the data, 76 | # which follows the torch convention: 77 | # CHW: (C, H, W) channel, height, width - planar layout 78 | # HWC: (H, W, C) height, width, channel - interleaved layout 79 | 80 | # The first output TOP in the TopChopDatIO.tox component is an 8 bit fixed RGBA 81 | # texture in TD which is actually stored as BGRA on the GPU. 82 | # Flags set on OutTops should only be set in the on_layout_change callback 83 | # or rarely in the on_frame callback (not every frame). 84 | # Channel order by default is CHW but we can set it to HWC if we want the fastest 85 | # possible copy to a CUDA array (tensor etc.) and this will now result in BGRA tensor 86 | # of the shape (H, W, C) with an interleaved memory layout 87 | self.out_tops[0].set_cuda_flags(tp.CudaFlags.HWC) 88 | 89 | # The second output TOP in the TopChopDatIO.tox component is a 32 bit float RGBA 90 | # texture. The channel order is RGBA and the memory layout is interleaved. 91 | # We can set the flags to RGB CHW to get a tensor of shape (C, H, W) with a planar 92 | # memory layout. This will result in a tensor with the shape (3, H, W) with the 93 | # memory layout (R, G, B, R, G, B, ...) 94 | # We specify the tp.CudaFlags.CHW flag for clarity but it is not necessary 95 | # since CHW is default shape. 96 | self.out_tops[1].set_cuda_flags(tp.CudaFlags.RGB | tp.CudaFlags.CHW) 97 | 98 | self.out_tops[2].set_cuda_flags(tp.CudaFlags.RGB) 99 | 100 | 101 | if 'Openwindow' in self.par.names: 102 | # only works on supported TD builds 103 | # official build with support should be available soon 104 | # self.par['Openwindow'].pulse() 105 | pass 106 | 107 | def on_frame(self, info): 108 | if utils.check_key('q'): 109 | self.stop() 110 | return 111 | 112 | if (self.frame == 30): 113 | 114 | # Print the all the gpu data info for each OutTOP 115 | for name in self.out_tops.names: 116 | print(f"OutTOP: {name}") 117 | 118 | # note tp.get_dlpack_capsule_info is a utility function that will print the 119 | # data info for any DLPack compatible object (torch tensor, numpy array etc.) 120 | pprint(tp.get_dlpack_capsule_info(self.out_tops[name].as_dlpack()), indent=4) 121 | print() 122 | 123 | # all "out" data retrieved from a component (from out TOPs, CHOPs, DATs) must be done 124 | # before calling start_next_frame() (pars are an exception) 125 | 126 | # get the data from the first OutTOP as a BGRA HWC tensor (see on_layout_change) 127 | out_top1 = self.out_tops[0] 128 | out_top1_tensor = out_top1.as_tensor() 129 | 130 | # this result in a RGB CHW tensor (see on_layout_change) 131 | out_top2 = self.out_tops[1] 132 | 133 | # get the CudaMemory object for the tensor, this gives access to the raw data 134 | # for interop with libraries that don't support the DLPack standard such as 135 | # CUDA_ARRAY_INTERFACE 136 | out_top2_cuda_mem = out_top2.cuda_memory() 137 | if (self.frame == 30): 138 | print(f"OutTOP3 CudaMemory:") 139 | print(f"\tsize: {out_top2_cuda_mem.size}") 140 | print(f"\tptr: {out_top2_cuda_mem.ptr}") 141 | print(f"\tshape: {out_top2_cuda_mem.desc.shape}") 142 | print(f"\tstrides: {out_top2_cuda_mem.desc.strides}") 143 | print(f"\tdata_type: {out_top2_cuda_mem.desc.data_type}") 144 | print(f"\tcomponent_size: {out_top2_cuda_mem.desc.component_size}") 145 | 146 | # get the DLPack object for the memory, this can be used with any library that 147 | # supports the DLPack standard 148 | out_top2_dlpack = out_top2.as_dlpack() 149 | 150 | # get the tensor from the OutTOP3 as a CHW tensor for image filter below 151 | out_top2_tensor = out_top2.as_tensor() 152 | 153 | # this is an RGB CHW tensor which we'll concat the alpha channel to 154 | out_top3_tensor = self.out_tops[2].as_tensor() 155 | 156 | 157 | 158 | self.start_next_frame() 159 | 160 | # convert the tensor to float so we can modify value in the tensor 161 | # note torch will not automatically normalize the values so the resulting tensor 162 | # will have values in the range 0.0-255.0 163 | modified_tensor = out_top1_tensor.float() 164 | modified_tensor[:, :, :-1] *= .5 # multiply the BGR channels by 0.5 165 | modified_tensor[:, :, :-1] += 100 # add 100 to the BGR channels 166 | modified_tensor = torch.clamp(modified_tensor, 0, 255) # clamp the values to 0-255 167 | modified_tensor = modified_tensor.to(torch.uint8) # convert the tensor back to uint8 168 | 169 | # Set the data back to the OutTOP 170 | # We could set the flags parameter to inform the InTop of the layout and channel order 171 | # but it is not necessary since the InTop will implicitly infer the layout and channel order. 172 | # The InTop is not able to implicitly infer the channel order... (RGB, BGR etc.) 173 | self.in_tops[0].from_tensor(modified_tensor) 174 | 175 | # Apply the image filter to the OutTOP3 tensor and return a new tensor 176 | # The filter expects a BCHW tensor so we need to add a batch dimension to the tensor 177 | # and remove it after the filter is applied 178 | filtered_tensor = self.imag_filter(out_top2_tensor.unsqueeze(0)).squeeze(0) 179 | 180 | # Set the data back to the OutTOP3 181 | self.in_tops[1].from_tensor(filtered_tensor) 182 | 183 | # concat the alpha channel to the tensor 184 | rgba_tensor = torch.cat((out_top3_tensor, self.alpha_tensor), dim=0) 185 | 186 | # Set the data back to the OutTOP3 187 | self.in_tops[2].from_tensor(rgba_tensor) 188 | 189 | self.frame += 1 190 | 191 | 192 | 193 | comp = MyComp() 194 | comp.load('TopChopDatIO.tox') 195 | comp.start() 196 | comp.unload() 197 | 198 | 199 | 200 | 201 | 202 | 203 | -------------------------------------------------------------------------------- /examples/basics/07_on_loaded.py: -------------------------------------------------------------------------------- 1 | import touchpy as tp 2 | import modules.utils as utils 3 | import concurrent.futures 4 | 5 | class MyComp (tp.Comp): 6 | def __init__(self): 7 | super().__init__() 8 | 9 | self.frame = 0 10 | self.set_on_layout_change_callback(self.on_layout_change, {}) 11 | self.set_on_frame_callback(self.on_frame, {}) 12 | 13 | def on_layout_change(self, info): 14 | print('layout changed:') 15 | 16 | if 'Openwindow' in self.par.names: 17 | self.par['Openwindow'].pulse() 18 | 19 | def on_frame(self, info): 20 | if utils.check_key('q'): 21 | self.stop() 22 | return 23 | 24 | self.start_next_frame() 25 | self.frame += 1 26 | 27 | 28 | # create a future to signal when the component is loaded 29 | future_load = concurrent.futures.Future() 30 | 31 | # define the on_loaded callback that will be called when the component is loaded 32 | def on_loaded(info): 33 | future_load.set_result(True) 34 | 35 | # create a new instance of MyComp 36 | comp = MyComp() 37 | 38 | # set the on_loaded callback 39 | comp.set_on_loaded_callback(on_loaded, {}) 40 | 41 | # if no on_loaded callback is set, comp.load() will block until the component is loaded 42 | # otherwise, comp.load() will return immediately 43 | comp.load('TopChopDatIO.tox') 44 | 45 | # do stuff here before waiting for the component to be loaded 46 | print('waiting for component to be loaded...') 47 | 48 | # wait for the component to be loaded (blocks until the future is set) 49 | future_load.result() 50 | 51 | print('component loaded') 52 | 53 | comp.start() 54 | comp.unload() 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /examples/basics/08_on_loaded_async.py: -------------------------------------------------------------------------------- 1 | import touchpy as tp 2 | import modules.utils as utils 3 | import concurrent.futures 4 | import time 5 | 6 | class MyComp (tp.Comp): 7 | def __init__(self, flags): 8 | super().__init__(flags=flags) 9 | 10 | self.frame = 0 11 | self.set_on_layout_change_callback(self.on_layout_change, {}) 12 | self.set_on_frame_callback(self.on_frame, {}) 13 | 14 | def on_layout_change(self, info): 15 | print('layout changed:') 16 | 17 | if 'Openwindow' in self.par.names: 18 | self.par['Openwindow'].pulse() 19 | 20 | def on_frame(self, info): 21 | self.start_next_frame() 22 | 23 | if self.frame % 60 == 0: 24 | print(f'frame: {self.frame}') 25 | 26 | self.frame += 1 27 | 28 | # create a future to signal when the component is loaded 29 | future_load = concurrent.futures.Future() 30 | 31 | # define the on_loaded callback that will be called when the component is loaded 32 | def on_loaded(info): 33 | future_load.set_result(True) 34 | 35 | # create a future to signal when the component is stopped 36 | future_stop = concurrent.futures.Future() 37 | 38 | # define the on_stop callback that will be called when the component is stopped 39 | def on_stop(info): 40 | future_stop.set_result(True) 41 | 42 | # create a new instance of MyComp and set the ASYNC flag 43 | # INTERNAL_TIME_ASYNC will load the update function in a separate thread 44 | # which will not block and run asynchronously in the background 45 | comp = MyComp(tp.CompFlags.INTERNAL_TIME_ASYNC) 46 | 47 | # set the on_loaded and on_stop callbacks 48 | comp.set_on_loaded_callback(on_loaded, {}) 49 | comp.set_on_stop_callback(on_stop, {}) 50 | 51 | comp.load('TopChopDatIO.tox') 52 | 53 | # wait for the component to be loaded 54 | print('waiting for component to be loaded...') 55 | future_load.result() 56 | 57 | comp.start() 58 | 59 | # wait for the user to press 'q' to continue which will result in 60 | # comp.stop() being called 61 | print('waiting for user to press "q" to continue...') 62 | while not utils.check_key('q'): 63 | time.sleep(0.1) 64 | 65 | comp.stop() 66 | 67 | # wait for the component to be stopped to unload it and clean up 68 | print('waiting for component to be stopped...') 69 | future_stop.result() 70 | 71 | comp.unload() 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /examples/basics/09_concurrent_comps.py: -------------------------------------------------------------------------------- 1 | import touchpy as tp 2 | import modules.utils as utils 3 | import asyncio 4 | 5 | class MyComp (tp.Comp): 6 | def __init__(self, name, flags, device=0): 7 | super().__init__(flags=flags, device=device) 8 | 9 | self.name = name 10 | self.frame = 0 11 | self.set_on_layout_change_callback(self.on_layout_change, {}) 12 | self.set_on_frame_callback(self.on_frame, {}) 13 | 14 | def on_layout_change(self, info): 15 | print('layout changed:') 16 | 17 | if 'Openwindow' in self.par.names: 18 | self.par['Openwindow'].pulse() 19 | 20 | def on_frame(self, info): 21 | self.start_next_frame() 22 | 23 | if self.frame % 180 == 0: 24 | print(f'{self.name} - frame: {self.frame}') 25 | 26 | self.frame += 1 27 | 28 | # global dictionary to store the futures for each comp instance 29 | # used to signal when the comp is loaded, stopped, or unloaded 30 | comp_futures = {} 31 | 32 | # global loop variable to call futures from the main thread 33 | loop = None 34 | 35 | # define the on_loaded callback that will be called when the component is loaded 36 | def on_loaded(info): 37 | global comp_futures, loop 38 | comp_id = info['comp_id'] 39 | if comp_id in comp_futures: 40 | loop.call_soon_threadsafe(comp_futures[comp_id].set_result, True) 41 | 42 | # define the on_stop callback that will be called when the component is stopped 43 | def on_stop(info): 44 | global comp_futures, loop 45 | comp_id = info['comp_id'] 46 | if comp_id in comp_futures: 47 | loop.call_soon_threadsafe(comp_futures[comp_id].set_result, True) 48 | 49 | # define the on_unloaded callback that will be called when the component is unloaded 50 | def on_unloaded(info): 51 | global comp_futures, loop 52 | comp_id = info['comp_id'] 53 | if comp_id in comp_futures: 54 | loop.call_soon_threadsafe(comp_futures[comp_id].set_result, True) 55 | 56 | # async function to load multiple comps concurrently 57 | async def load_comps(comps): 58 | global comp_futures, loop 59 | loop = asyncio.get_running_loop() 60 | 61 | for comp in comps: 62 | comp_id = id(comp) 63 | comp_futures[id(comp)] = loop.create_future() 64 | comp.set_on_loaded_callback(on_loaded, {'comp_id': comp_id}) 65 | comp.set_on_stop_callback(on_stop, {'comp_id': comp_id}) 66 | comp.set_on_unloaded_callback(on_unloaded, {'comp_id': comp_id}) 67 | comp.load('TopChopDatIO.tox') 68 | 69 | await asyncio.gather(*comp_futures.values()) 70 | 71 | # async function to start multiple comps concurrently 72 | async def start_comps(comps): 73 | for comp in comps: 74 | comp.start() 75 | 76 | # async function to wait for the user to press 'q' to continue 77 | async def wait_for_q_key(comps): 78 | while True: 79 | if utils.check_key('q'): 80 | break 81 | await asyncio.sleep(0.1) 82 | 83 | # async function to stop multiple comps concurrently 84 | async def stop_comps(comps): 85 | global comp_futures, loop 86 | loop = asyncio.get_running_loop() 87 | 88 | for comp in comps: 89 | comp_futures[id(comp)] = loop.create_future() 90 | comp.stop() 91 | 92 | await asyncio.gather(*comp_futures.values()) 93 | 94 | # async function to unload multiple comps concurrently 95 | async def unload_comps(comps): 96 | global comp_futures, loop 97 | loop = asyncio.get_running_loop() 98 | 99 | for comp in comps: 100 | comp_futures[id(comp)] = loop.create_future() 101 | comp.unload() 102 | 103 | await asyncio.gather(*comp_futures.values()) 104 | 105 | # main function to load, start, stop, and unload multiple comps concurrently 106 | async def main(): 107 | 108 | base_name = 'my_comp_gpu0_' 109 | comps = [MyComp(f"my_comp_gpu0_{i}", flags=tp.CompFlags.INTERNAL_TIME_ASYNC) for i in range(3)] 110 | 111 | # uncomment to load 3 comps on the second GPU as well (if available) 112 | # comps += [MyComp(f"my_comp_gpu1_{i}", flags=tp.CompFlags.INTERNAL_TIME_ASYNC, device=1) for _ in range(3)] 113 | 114 | print('loading comps...') 115 | await load_comps(comps) 116 | 117 | print('starting comps...') 118 | await start_comps(comps) 119 | 120 | print('waiting for user to press "q" to continue...') 121 | await wait_for_q_key(comps) 122 | 123 | print('stopping comps...') 124 | await stop_comps(comps) 125 | 126 | print('unloading comps...') 127 | await unload_comps(comps) 128 | 129 | asyncio.run(main()) 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /examples/basics/ExampleComps.toe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntentDev/touchpy/e3987b48bdefe761b67d5fe0503052927fa85de5/examples/basics/ExampleComps.toe -------------------------------------------------------------------------------- /examples/basics/ExampleReceive.toe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntentDev/touchpy/e3987b48bdefe761b67d5fe0503052927fa85de5/examples/basics/ExampleReceive.toe -------------------------------------------------------------------------------- /examples/basics/README.md: -------------------------------------------------------------------------------- 1 | # TouchPy Basic Examples 2 | 3 | This folder contains basic examples of how to use TouchPy, with relatively thorough explanations of the concepts and tools. Each example focuses on a particular aspect of interfacing with the library. 4 | 5 | ## Dependencies 6 | Currently only two examples have dependencies other than TouchPy, 02_chops.py ([NumPy](https://numpy.org/doc/stable/index.html)) and 06_tops.py ([PyTorch](https://pytorch.org)). 7 | 8 | pip install numpy 9 | 10 | Follow instructions from https://pytorch.org/ to install PyTorch for your system. Typically something like: 11 | 12 | pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 13 | 14 | ## Usage 15 | - Clone the TouchPy repository: 16 | 17 | git clone https://github.com/IntentDev/touchpy.git 18 | cd touchpy/examples/basics 19 | 20 | - Open ExampleComps.toe to review the component that will be loaded in each example (TopChopDatIO.tox). 21 | - Close ExampleComps.toe 22 | - Open ExampleReceive.toe to monitor data being set on Pars and In operators of the component. (If running a TD build that supports opening a window in a TouchEngine instance this is not required). 23 | - Open touchpy/examples/basics folder in a text editor to review code and comments 24 | - To start the first example run: 25 | 26 | python 01_load_comp_basic.py 27 | 28 | 29 | ## Jurigged 30 | Installing [Jurigged](https://github.com/breuleux/jurigged) is also useful for experimenation providing the ability to update code while it's running. 31 | 32 | pip install jurigged 33 | 34 | Then run in terminal: 35 | 36 | jurigged 03_set_pars.py 37 | 38 | Or any other example script. Now you can edit values, add and remove lines while the script is running and it will update. -------------------------------------------------------------------------------- /examples/basics/TopChopDatIO.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntentDev/touchpy/e3987b48bdefe761b67d5fe0503052927fa85de5/examples/basics/TopChopDatIO.tox -------------------------------------------------------------------------------- /examples/basics/example_sync_dat.txt: -------------------------------------------------------------------------------- 1 | You can edit this file and the data in Python will be updated -------------------------------------------------------------------------------- /examples/basics/modules/__pycache__/image_filter.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntentDev/touchpy/e3987b48bdefe761b67d5fe0503052927fa85de5/examples/basics/modules/__pycache__/image_filter.cpython-311.pyc -------------------------------------------------------------------------------- /examples/basics/modules/__pycache__/utils.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntentDev/touchpy/e3987b48bdefe761b67d5fe0503052927fa85de5/examples/basics/modules/__pycache__/utils.cpython-311.pyc -------------------------------------------------------------------------------- /examples/basics/modules/image_filter.py: -------------------------------------------------------------------------------- 1 | 2 | from torch import nn 3 | 4 | class ImageFilter(nn.Module): 5 | """ 6 | Filters (blurs) components of a tensor. 7 | 8 | Function to test io with TopLink tensors. 9 | """ 10 | def __init__(self, in_channels=4, out_channels=4, kernel_size=3, stride=1, groups=4): 11 | super().__init__() 12 | 13 | self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride=stride, 14 | padding=kernel_size // 2, groups=groups, bias=False) 15 | 16 | nn.init.constant_(self.conv.weight, 1.0 / (kernel_size ** 2) ) 17 | # nn.init.normal_(self.conv.weight, 0.0, 1) 18 | # nn.init.xavier_uniform_(self.conv.weight, gain=2.0) 19 | 20 | def forward(self, x): 21 | # Assuming x is of shape [batch_size, channels, height, width] 22 | return self.conv(x) 23 | 24 | def normalize(self, tensor): 25 | tensor_min = tensor.min() 26 | tensor_max = tensor.max() 27 | normalized_tensor = (tensor - tensor_min) / (tensor_max - tensor_min) 28 | return normalized_tensor 29 | -------------------------------------------------------------------------------- /examples/basics/modules/utils.py: -------------------------------------------------------------------------------- 1 | import msvcrt 2 | 3 | def check_key(key): 4 | if msvcrt.kbhit(): 5 | char = msvcrt.getch() 6 | if char in [b'\000', b'\xe0', b'\x00']: 7 | return False 8 | 9 | return char.decode("utf-8") == key 10 | --------------------------------------------------------------------------------