├── .github └── workflows │ ├── test.yml │ └── wheels.yml ├── .gitignore ├── COPYING ├── MANIFEST.in ├── NEWS ├── PKG-INFO ├── README.md ├── doc ├── Makefile ├── conf.py ├── index.rst └── theme │ ├── nasophon │ ├── static │ │ └── nasophon.css │ └── theme.conf │ └── pydoctheme │ ├── static │ └── pydoctheme.css │ └── theme.conf ├── docs ├── Reference.md ├── generatedocs.py ├── index.md └── requirements.txt ├── examples ├── example_client.py ├── example_server.py ├── example_server_deco.py └── test_server_thread.py ├── mkdocs.yml ├── pyliblo3 ├── __init__.py ├── _liblo.pxd ├── _liblo.pyi └── _liblo.pyx ├── pyproject.toml ├── readthedocs.yml ├── scripts ├── dump_osc.1 ├── dump_osc.py ├── send_osc.1 └── send_osc.py ├── setup.py └── test ├── unit.py └── unit.py.patch /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push] 3 | jobs: 4 | alltest: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | matrix: 8 | os: [ 9 | "windows-latest", 10 | "ubuntu-latest", 11 | "macos-latest" 12 | ] 13 | python-version: [ "3.10", "3.12" ] # Test extremes only 14 | install-method: [ 15 | # "git", 16 | "pip" 17 | ] 18 | fail-fast: false 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - uses: actions/setup-python@v4 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | 27 | - name: check macos arch 28 | if: ${{ runner.os }} == macos 29 | run: | 30 | uname -m 31 | 32 | - name: install from pip 33 | if: ${{ matrix.install-method == 'pip' }} 34 | run: | 35 | pip install --verbose "pyliblo3>=0.16" 36 | 37 | - name: run unittests 38 | run: | 39 | cd test 40 | python unit.py 41 | -------------------------------------------------------------------------------- /.github/workflows/wheels.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | # on: create 6 | 7 | jobs: 8 | build_wheels: 9 | name: Build python wheels on ${{ matrix.os }} 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | os: [ 14 | macos-latest, 15 | windows-latest, 16 | ubuntu-latest 17 | ] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - uses: actions/setup-python@v3 23 | 24 | - name: Install cibuildwheel 25 | run: python -m pip install cibuildwheel 26 | 27 | - name: Windows - Enable Developer Command Prompt 28 | uses: ilammy/msvc-dev-cmd@v1.7.0 29 | 30 | - name: macos dependencies 31 | if: runner.os == 'macos' 32 | run: | 33 | python -m pip install delocate 34 | # brew install liblo 35 | git clone https://github.com/radarsat1/liblo 36 | cd liblo 37 | mkdir macosbuild 38 | cd macosbuild 39 | cmake -DCMAKE_OSX_DEPLOYMENT_TARGET=11 -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" ../cmake 40 | cmake --build . --config Release 41 | sudo cmake --install . 42 | file /usr/local/lib/liblo.dylib 43 | 44 | - name: Install dependencies windows 45 | if: runner.os == 'windows' 46 | run: | 47 | git clone https://github.com/radarsat1/liblo 48 | cd liblo 49 | New-Item -ItemType Directory -Force -Path "windowsbuild" 50 | cd windowsbuild 51 | cmake -A x64 -DCMAKE_GENERATOR_PLATFORM=x64 -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=OFF -DWITH_CPP_TESTS=OFF -DWITH_EXAMPLES=OFF -DWITH_TOOLS=OFF ../cmake 52 | cmake --build . --config Release 53 | Get-ChildItem -Recurse 54 | cmake --install . 55 | 56 | - name: Build wheels 57 | run: python -m cibuildwheel --output-dir wheelhouse 58 | # to supply options, put them in 'env', like: 59 | env: 60 | MACOSX_DEPLOYMENT_TARGET: 11 61 | CIBW_BUILD_VERBOSITY: 1 62 | CIBW_BUILD: 'cp310-* cp311-* cp312-* cp313-*' 63 | CIBW_ARCHS_MACOS: 'arm64' 64 | CIBW_ARCHS_WINDOWS: AMD64 65 | CIBW_SKIP: 'pp* *686* *-musllinux_*' 66 | # CIBW_BEFORE_ALL_LINUX: yum search liblo; yum install -y liblo-devel 67 | CIBW_BEFORE_ALL_LINUX: git clone https://github.com/radarsat1/liblo; cd liblo; mkdir linuxbuild; cd linuxbuild; cmake ../cmake; cmake --build .; cmake --install . 68 | # CIBW_BEFORE_ALL_MACOS: brew install liblo 69 | CIBW_REPAIR_WHEEL_COMMAND_MACOS: delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} --require-target-macos-version 11 70 | 71 | # Use delvewheel on windows 72 | CIBW_BEFORE_BUILD_WINDOWS: "pip install delvewheel" 73 | CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: 'delvewheel repair --add-path "C:/Program Files/liblo/lib;C:/Program Files/liblo/bin" -w {dest_dir} {wheel}' 74 | 75 | - name: Check wheels 76 | if: runner.os == 'macos' 77 | run: | 78 | delocate-listdeps --all wheelhouse/*.whl 79 | 80 | # CIBW_SOME_OPTION: value 81 | 82 | - uses: actions/upload-artifact@v4 83 | with: 84 | name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} 85 | path: ./wheelhouse/*.whl 86 | 87 | - name: Upload wheels 88 | env: 89 | TWINE_USERNAME: __token__ 90 | TWINE_PASSWORD: ${{ secrets.TWINETOKEN }} 91 | run: | 92 | echo $TWINE_USERNAME 93 | echo $TWINE_PASSWORD 94 | python -m pip install -U twine virtualenv 95 | twine upload --skip-existing wheelhouse/*.whl 96 | continue-on-error: true 97 | 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | site/ 2 | *.egg* 3 | build/ 4 | dist/ 5 | *.o 6 | *.lo 7 | *.la 8 | *.cache 9 | *.pyc 10 | *.bak 11 | *.so 12 | *.whl 13 | .deps 14 | .idea 15 | .mypy* 16 | pyliblo3/*.c 17 | wheelhouse/ 18 | *.kate* 19 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | 504 | 505 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include pyliblo3 *.py *.pyx *.pxd *.pyi 2 | recursive-include scripts * 3 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | 2024: pyliblo3 can build binary wheels for all platforms 2 | * Including native wheels for macos arm64 3 | * All dependencies (the liblo library itself) are included 4 | within the wheels 5 | 6 | 2020: pyliblo3 can build manylinux wheels 7 | 8 | 2019: pyliblo3 is forked from pylibo 9 | 10 | * Add the possibility to delete previous registered /methods 11 | * Modernize the setup.py to make it pip installable 12 | 13 | 2011-01-29: pyliblo 0.9.1 14 | 15 | * Changed send functions to raise an IOError if sending failed (probaby 16 | most useful with TCP connections). 17 | * Lots of code cleanup and deobfuscation. 18 | * Included unit tests in release tarball. 19 | 20 | 2010-10-22: pyliblo 0.9.0 21 | 22 | * Support Python 3.x. As a result, pyliblo can no longer be built with 23 | Pyrex, and requires Cython >= 0.12 instead. 24 | * Added free() method to Server and ServerThread classes. 25 | * Added fileno() method to Server and ServerThread classes (thanks to 26 | Edward George). 27 | * Prefer read-only properties over getter methods (which still exist, but 28 | are now deprecated). 29 | * Added proper docstrings (copied from the HTML docs). 30 | * The minimum version of liblo required by pyliblo is now 0.26. 31 | 32 | 2009-11-30: pyliblo 0.8.1 33 | 34 | * Release the Python GIL inside Server.recv(). 35 | * Fixed a possible segfault when the error handler was called from the 36 | * liblo server thread. 37 | 38 | 2009-09-13: pyliblo 0.8.0 39 | 40 | * Changed license from GPL 2 to LGPL 2.1 (as did liblo in version 0.26). 41 | * Added protocol parameter to the Server class. Among other things, this 42 | allows TCP connections. 43 | * The minumum version of liblo required by pyliblo is now 0.24. 44 | * pyliblo can now be built with either Pyrex or Cython. 45 | 46 | 2009-01-19: pyliblo 0.7.2 47 | 48 | * Fixed all compiler warnings properly in Pyrex, without patching the 49 | generated C code. 50 | * Return values of callback functions are no longer ignored, but handled 51 | as in liblo. 52 | * The send_osc script can now be run with an explicit type string, 53 | instead of trying to determine the argument types automatically. 54 | 55 | 2008-08-03: pyliblo 0.7.1 56 | 57 | * Added manpages for send_osc and dump_osc. 58 | 59 | 2008-03-03: pyliblo 0.7.0 60 | 61 | * Fixed memory leaks, caused by failure to free() the result of 62 | lo_server_get_url() and lo_address_get_url(). 63 | * Added parameter to Server.register_methods() to allow registering 64 | functions of an object other than the server itself. 65 | * Allow callback functions to have a variable number of arguments (*args). 66 | 67 | 2007-12-14: pyliblo 0.6.4 68 | 69 | * Avoid creating circular references when using methods as callback 70 | functions, which in some cases prevented the server object from being 71 | deleted properly. 72 | 73 | 2007-08-10: pyliblo 0.6.3 74 | 75 | * Patched the Pyrex-generated code to make it compile without warnings. 76 | * Always build from the existing C source by default. 77 | 78 | 2007-07-29: pyliblo 0.6.2 79 | 80 | * Minor code cleanup, hopefully not breaking anything. 81 | * Somewhat faster conversion of blob data from and to Python lists. 82 | 83 | 2007-07-07: pyliblo 0.6.1 84 | 85 | * Fixed a bug that caused the floats 0.0 and 1.0 to be sent as boolean. 86 | Thanks to Jesse Chappell for the patch. 87 | 88 | 2007-05-20: pyliblo 0.6 89 | 90 | * Added support for sending bundles, optionally with timestamps. 91 | * Added previously unsupported OSC data types (timetag, midi, symbol, 92 | true/false/nil/infinitum). 93 | * New @make_method decorator. 94 | * Various bugfixes. 95 | 96 | 2007-04-28: pyliblo 0.5.1 97 | 98 | * Fixed a stupid typo in Server.send(). 99 | 100 | 2007-04-26: pyliblo 0.5 101 | 102 | * Simplified the way arguments are passed to callback functions. For the 103 | server side, this release is therefore incompatible with previous 104 | versions! 105 | * Some more cleanup. 106 | 107 | 2007-04-02: pyliblo 0.3 108 | 109 | * Added class ServerThread for asynchronous dispatching of incoming 110 | messages. 111 | 112 | 2007-04-01: pyliblo 0.2 113 | 114 | * Minor improvements. 115 | 116 | 2007-02-20: pyliblo 0.1 117 | 118 | * Initial release. 119 | -------------------------------------------------------------------------------- /PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.0 2 | Name: pyliblo3 3 | Version: 0.16.1 4 | Summary: fork of pyliblo (http://das.nasophon.de/pyliblo/) 5 | Home-page: https://github.com/gesellkammer/pyliblo3 6 | Author: Dominic Sacre 7 | Author-email: dominic.sacre@gmx.de 8 | Maintainer: Eduardo Moguillansky 9 | Maintainer-email: 10 | License: LGPL 11 | Description: UNKNOWN 12 | Platform: UNKNOWN 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyliblo3 2 | 3 | This is a fork of the original bindings for liblo, making it pip installable. 4 | 5 | The provided wheels include the ``liblo`` library and don't have any further 6 | dependencies, making it completely pip installable 7 | 8 | 9 | ## Example 10 | 11 | ### Simple blocking server 12 | 13 | ```python 14 | 15 | import pyliblo3 as liblo 16 | server = liblo.Server(8080) 17 | 18 | def test_handler(path, args, types, src): 19 | print(args) 20 | 21 | server.add_method("/test", None, test_handler) 22 | 23 | while True: 24 | server.recv(100) 25 | ``` 26 | 27 | ### Threaded server 28 | 29 | ```python 30 | from pyliblo3 import * 31 | import time 32 | 33 | 34 | class MyServer(ServerThread): 35 | def __init__(self, port=1234): 36 | ServerThread.__init__(self, port) 37 | 38 | @make_method('/foo', 'ifs') 39 | def foo_callback(self, path, args): 40 | i, f, s = args 41 | print(f"Received message '{path}' with arguments: {i=}, {f=}, {s=}") 42 | 43 | @make_method(None, None) 44 | def fallback(self, path, args): 45 | print(f"received unknown message '{path}' with {args=}") 46 | 47 | 48 | server = MyServer() 49 | server.start() 50 | print(f"Server started in its own thread, send messages to {server.port}. Use CTRL-C to stop") 51 | 52 | while True: 53 | send(("127.0.0.0", server.port), "/foo", 10, 1.5, "bar") 54 | send(("127.0.0.0", server.port), "/unknown", (3, 4)) 55 | time.sleep(1) 56 | 57 | ``` 58 | 59 | ---------------------- 60 | 61 | 62 | ## Documentation 63 | 64 | 65 | https://pyliblo3.readthedocs.io 66 | 67 | 68 | ----------------------- 69 | 70 | 71 | ## Installation 72 | 73 | 74 | ```bash 75 | 76 | pip install pyliblo3 77 | 78 | ``` 79 | 80 | ## Installation from source 81 | 82 | When installing from source, ``liblo`` needs to be installed. 83 | 84 | #### Linux 85 | 86 | ```bash 87 | sudo apt install liblo-dev 88 | 89 | git clone https://github.com/gesellkammer/pyliblo3 90 | cd pyliblo3 91 | pip install . 92 | ``` 93 | 94 | #### MacOS 95 | 96 | First install liblo 97 | 98 | ```bash 99 | brew install liblo 100 | ``` 101 | 102 | Or, without using brew: 103 | 104 | ```bash 105 | git clone https://github.com/radarsat1/liblo 106 | cd liblo 107 | mkdir macosbuild && cd macosbuild 108 | cmake -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" ../cmake 109 | cmake --build . --config Release 110 | sudo cmake --install . 111 | ``` 112 | 113 | Then install pyliblo3 114 | 115 | ```bash 116 | git clone https://github.com/gesellkammer/pyliblo3 117 | cd pyliblo3 118 | pip install . 119 | ``` 120 | 121 | #### Windows 122 | 123 | ```bash 124 | git clone https://github.com/radarsat1/liblo 125 | cd liblo 126 | New-Item -ItemType Directory -Force -Path "windowsbuild" 127 | cd windowsbuild 128 | cmake -A x64 -DCMAKE_GENERATOR_PLATFORM=x64 -DWITH_TESTS=OFF -DWITH_CPP_TESTS=OFF -DWITH_EXAMPLES=OFF -DWITH_TOOLS=OFF ../cmake 129 | cmake --build . --config Release 130 | cmake --install . 131 | ``` 132 | 133 | ```bash 134 | git clone https://github.com/gesellkammer/pyliblo3 135 | cd pyliblo3 136 | pip install . 137 | ``` 138 | 139 | 140 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | SPHINXOPTS = 2 | SPHINXBUILD = sphinx-build 3 | BUILDDIR = build 4 | 5 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . 6 | 7 | .PHONY: clean html 8 | 9 | html: 10 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 11 | 12 | clean: 13 | -rm -rf $(BUILDDIR)/* 14 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | import sys, os 4 | 5 | sys.path.insert(0, os.path.abspath('..')) 6 | 7 | #extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.fulltoc'] 8 | extensions = ['sphinx.ext.autodoc'] 9 | 10 | templates_path = ['templates'] 11 | html_theme_path = ['theme'] 12 | exclude_patterns = ['build'] 13 | 14 | source_suffix = '.rst' 15 | master_doc = 'index' 16 | 17 | project = u'pyliblo' 18 | copyright = u'2007-2014, Dominic Sacré' 19 | version = '0.10.0' 20 | release = '' 21 | 22 | html_theme = 'nasophon' 23 | html_copy_source = False 24 | pygments_style = 'sphinx' 25 | 26 | add_module_names = False 27 | autodoc_member_order = 'bysource' 28 | autodoc_default_flags = ['members', 'undoc-members'] 29 | 30 | 31 | from sphinx.ext.autodoc import py_ext_sig_re 32 | from sphinx.util.docstrings import prepare_docstring 33 | from sphinx.domains.python import PyClassmember, PyObject, py_sig_re 34 | 35 | 36 | def process_docstring(app, what, name, obj, options, lines): 37 | """ 38 | Remove leading function signatures from docstring. 39 | """ 40 | while len(lines) and py_ext_sig_re.match(lines[0]) is not None: 41 | del lines[0] 42 | 43 | def process_signature(app, what, name, obj, 44 | options, signature, return_annotation): 45 | """ 46 | Replace function signature with those specified in the docstring. 47 | """ 48 | if hasattr(obj, '__doc__') and obj.__doc__ is not None: 49 | lines = prepare_docstring(obj.__doc__) 50 | siglines = [] 51 | 52 | for line in lines: 53 | if py_ext_sig_re.match(line) is not None: 54 | siglines.append(line) 55 | else: 56 | break 57 | 58 | if len(siglines): 59 | siglines[0] = siglines[0][siglines[0].index('('):] 60 | return ('\n'.join(siglines), None) 61 | 62 | return (signature, return_annotation) 63 | 64 | 65 | # monkey-patch PyClassmember.handle_signature() to replace __init__ 66 | # with the class name. 67 | handle_signature_orig = PyClassmember.handle_signature 68 | def handle_signature(self, sig, signode): 69 | if '__init__' in sig: 70 | m = py_sig_re.match(sig) 71 | name_prefix, name, arglist, retann = m.groups() 72 | sig = sig.replace('__init__', name_prefix[:-1]) 73 | return handle_signature_orig(self, sig, signode) 74 | PyClassmember.handle_signature = handle_signature 75 | 76 | 77 | # prevent exception fields from collapsing 78 | PyObject.doc_field_types[2].can_collapse = False 79 | 80 | 81 | def setup(app): 82 | app.connect('autodoc-process-docstring', process_docstring) 83 | app.connect('autodoc-process-signature', process_signature) 84 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. module:: liblo 2 | 3 | ############################## 4 | pyliblo 0.10 API Documentation 5 | ############################## 6 | 7 | Homepage: http://das.nasophon.de/pyliblo/ 8 | 9 | The latest version of this manual can be found at 10 | http://dsacre.github.io/pyliblo/doc/. 11 | 12 | For the most part, pyliblo is just a thin wrapper around 13 | `liblo `_, which does 14 | all the real work. 15 | For questions not answered here, also see the 16 | `liblo documentation `_ 17 | and the `OSC spec `_. 18 | 19 | 20 | Module-level Functions 21 | ====================== 22 | 23 | .. autofunction:: send 24 | 25 | .. autofunction:: time 26 | 27 | 28 | OSC Server Classes 29 | ================== 30 | 31 | .. autoclass:: Server 32 | :no-members: 33 | 34 | .. automethod:: __init__ 35 | .. automethod:: recv 36 | .. automethod:: send 37 | .. automethod:: add_method 38 | .. automethod:: del_method 39 | .. automethod:: register_methods 40 | .. automethod:: add_bundle_handlers 41 | .. autoattribute:: url 42 | .. autoattribute:: port 43 | .. autoattribute:: protocol 44 | .. automethod:: fileno 45 | .. automethod:: free 46 | 47 | ------- 48 | 49 | .. autoclass:: ServerThread 50 | :no-members: 51 | 52 | .. automethod:: __init__ 53 | .. automethod:: start 54 | .. automethod:: stop 55 | 56 | .. autoclass:: make_method 57 | 58 | .. automethod:: __init__ 59 | 60 | 61 | Utility Classes 62 | =============== 63 | 64 | .. autoclass:: Address 65 | :no-members: 66 | 67 | .. automethod:: __init__ 68 | .. autoattribute:: url 69 | .. autoattribute:: hostname 70 | .. autoattribute:: port 71 | .. autoattribute:: protocol 72 | 73 | ------- 74 | 75 | .. autoclass:: Message 76 | 77 | .. automethod:: __init__ 78 | 79 | .. autoclass:: Bundle 80 | 81 | .. automethod:: __init__ 82 | 83 | ------- 84 | 85 | .. autoexception:: ServerError 86 | 87 | .. autoexception:: AddressError 88 | 89 | 90 | Mapping between OSC and Python data types 91 | ========================================= 92 | 93 | When constructing a message, pyliblo automatically converts 94 | arguments to an appropriate OSC data type. 95 | To explicitly specify the OSC data type to be transmitted, pass a 96 | ``(typetag, data)`` tuple instead. Some types can't be unambiguously 97 | recognized, so they can only be sent that way. 98 | 99 | The mapping between OSC and Python data types is shown in the following table: 100 | 101 | ========= =============== ==================================================== 102 | typetag OSC data type Python data type 103 | ========= =============== ==================================================== 104 | ``'i'`` int32 :class:`int` 105 | ``'h'`` int64 :class:`long` (Python 2.x), :class:`int` (Python 3.x) 106 | ``'f'`` float :class:`float` 107 | ``'d'`` double :class:`float` 108 | ``'c'`` char :class:`str` (single character) 109 | ``'s'`` string :class:`str` 110 | ``'S'`` symbol :class:`str` 111 | ``'m'`` midi :class:`tuple` of four :class:`int`\ s 112 | ``'t'`` timetag :class:`float` 113 | ``'T'`` true 114 | ``'F'`` false 115 | ``'N'`` nil 116 | ``'I'`` infinitum 117 | ``'b'`` blob :class:`list` of :class:`int`\ s (Python 2.x), :class:`bytes` (Python 3.x) 118 | ========= =============== ==================================================== 119 | -------------------------------------------------------------------------------- /doc/theme/nasophon/static/nasophon.css: -------------------------------------------------------------------------------- 1 | @import url("default.css"); 2 | 3 | body { 4 | background-color: white; 5 | margin-left: 1em; 6 | margin-right: 1em; 7 | 8 | width: 68em; 9 | margin: 0 auto; 10 | font-size: 95%; 11 | } 12 | 13 | div.related { 14 | margin-bottom: 1.2em; 15 | padding: 0.5em 0; 16 | border-top: 1px solid #ccc; 17 | margin-top: 0.5em; 18 | } 19 | 20 | div.related:first-child { 21 | border-top: 0; 22 | border-bottom: 1px solid #ccc; 23 | } 24 | 25 | div.sphinxsidebar { 26 | background-color: #f4f4f4; 27 | border-radius: 5px; 28 | line-height: 110%; 29 | } 30 | 31 | div.sphinxsidebar ul, 32 | div.sphinxsidebar ul ul { 33 | list-style: none; 34 | } 35 | 36 | div.sphinxsidebar li { 37 | margin-bottom: 0.5em; 38 | } 39 | 40 | div.sphinxsidebar li > ul { 41 | margin-top: 0.5em; 42 | } 43 | 44 | div.sphinxsidebar h3, div.sphinxsidebar h4 { 45 | margin-top: 1.5em; 46 | } 47 | 48 | div.sphinxsidebarwrapper > h3:first-child { 49 | margin-top: 0.2em; 50 | } 51 | 52 | div.sphinxsidebarwrapper { 53 | padding: 10px 10px 0px 10px; 54 | } 55 | 56 | div.sphinxsidebarwrapper > ul { 57 | margin-left: 0px; 58 | margin-right: 0px; 59 | } 60 | 61 | div.sphinxsidebar li.current { 62 | background: #fff; 63 | width: auto; 64 | border-radius: 3px; 65 | padding-bottom: 1px; 66 | } 67 | 68 | div.sphinxsidebar a.current { 69 | font-weight: bold; 70 | color: #fff; 71 | background: #52576a; 72 | border: 3px solid #52576a; 73 | border-radius: 3px; 74 | width: auto; 75 | display: block; 76 | } 77 | 78 | 79 | div.sphinxsidebar input { 80 | font-family: 'Lucida Grande',Arial,sans-serif; 81 | border: 1px solid #999999; 82 | font-size: smaller; 83 | border-radius: 3px; 84 | } 85 | 86 | div.sphinxsidebar input[type=text] { 87 | max-width: 150px; 88 | } 89 | 90 | div.body { 91 | padding: 0 0.6em 0 2.2em; 92 | } 93 | 94 | 95 | div.body p { 96 | line-height: 140%; 97 | } 98 | 99 | div.body h1 { 100 | margin: 0 -0.4em 0 -0.6em; 101 | border: 0; 102 | color: #ffffff; 103 | background: #52576a; 104 | border-radius: 5px; 105 | font-size: 150%; 106 | } 107 | 108 | div.body h1 a, div.body h1 a:visited { 109 | color: #ffffff; 110 | } 111 | 112 | div.body h2 { 113 | margin: 1.5em -0.45em 0 -0.65em; 114 | border: 0; 115 | color: #111111; 116 | background: #eeeeee; 117 | border-radius: 5px; 118 | font-size: 135%; 119 | } 120 | 121 | div.body h3 { 122 | font-weight: bold; 123 | color: #444; 124 | border: 0; 125 | font-size: 105%; 126 | padding-left: 1.25em; 127 | margin-right: 0em; 128 | padding-bottom: 0em; 129 | margin-bottom: 0em; 130 | } 131 | 132 | div.body hr { 133 | border: 0; 134 | background-color: #ccc; 135 | height: 1px; 136 | } 137 | 138 | div.body pre { 139 | border-radius: 3px; 140 | border: 1px solid #ac9; 141 | } 142 | 143 | div.body div.admonition, div.body div.impl-detail { 144 | border-radius: 3px; 145 | } 146 | 147 | div.body div.impl-detail > p { 148 | margin: 0; 149 | } 150 | 151 | div.body div.seealso { 152 | border: 1px solid #dddd66; 153 | } 154 | 155 | div.body div.note { 156 | background-color: #ffc; 157 | border: 1px solid #dd6; 158 | } 159 | 160 | div.body div.note tt { 161 | background: transparent; 162 | } 163 | 164 | div.body a { 165 | color: #127; 166 | } 167 | 168 | div.body a:visited { 169 | color: #127; 170 | } 171 | 172 | 173 | tt, pre { 174 | font-family: monospace, sans-serif; 175 | font-size: 93%; 176 | } 177 | 178 | div.body tt { 179 | border-radius: 3px; 180 | } 181 | 182 | div.body tt.descname { 183 | font-size: 100%; 184 | } 185 | 186 | div.body tt.xref, div.body a tt { 187 | } 188 | 189 | .deprecated { 190 | border-radius: 3px; 191 | } 192 | 193 | table.docutils { 194 | border: 1px solid #ddd; 195 | min-width: 20%; 196 | border-radius: 3px; 197 | margin-top: 10px; 198 | margin-bottom: 10px; 199 | } 200 | 201 | table.docutils td, table.docutils th { 202 | border: 1px solid #ddd !important; 203 | border-radius: 3px; 204 | } 205 | 206 | table p, table li { 207 | text-align: left !important; 208 | } 209 | 210 | table.docutils th { 211 | background-color: #eee; 212 | padding: 0.3em 0.5em; 213 | } 214 | 215 | table.docutils td { 216 | background-color: white; 217 | padding: 0.3em 0.5em; 218 | } 219 | 220 | table.footnote, table.footnote td { 221 | border: 0 !important; 222 | } 223 | 224 | div.footer { 225 | line-height: 150%; 226 | margin-top: -2em; 227 | text-align: right; 228 | width: auto; 229 | margin-right: 10px; 230 | } 231 | 232 | 233 | /* fix space below last line in multi-line table cells */ 234 | td .line-block { 235 | margin-bottom: 0; 236 | } 237 | 238 | /* No line break before first line of parameter description */ 239 | td.field-body strong+p { 240 | display: inline; 241 | } 242 | 243 | /* No blank lines within parameter lists */ 244 | dd, dl { 245 | margin-bottom: 0px; 246 | } 247 | 248 | a.headerlink { 249 | float: right; 250 | } 251 | -------------------------------------------------------------------------------- /doc/theme/nasophon/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = default 3 | stylesheet = nasophon.css 4 | pygments_style = sphinx 5 | 6 | [options] 7 | bodyfont = 'Lucida Grande', Arial, sans-serif 8 | headfont = 'Lucida Grande', Arial, sans-serif 9 | footerbgcolor = white 10 | footertextcolor = #555 11 | relbarbgcolor = #fff 12 | relbartextcolor = #666 13 | relbarlinkcolor = #444 14 | sidebarbgcolor = white 15 | sidebartextcolor = #111 16 | sidebarlinkcolor = #111 17 | bgcolor = white 18 | textcolor = #222 19 | linkcolor = #127 20 | visitedlinkcolor = #0127 21 | headtextcolor = #111 22 | headbgcolor = white 23 | headlinkcolor = #aaa 24 | -------------------------------------------------------------------------------- /doc/theme/pydoctheme/static/pydoctheme.css: -------------------------------------------------------------------------------- 1 | @import url("default.css"); 2 | 3 | body { 4 | background-color: white; 5 | margin-left: 1em; 6 | margin-right: 1em; 7 | } 8 | 9 | div.related { 10 | margin-bottom: 1.2em; 11 | padding: 0.5em 0; 12 | border-top: 1px solid #ccc; 13 | margin-top: 0.5em; 14 | } 15 | 16 | div.related a:hover { 17 | color: #0095C4; 18 | } 19 | 20 | div.related:first-child { 21 | border-top: 0; 22 | border-bottom: 1px solid #ccc; 23 | } 24 | 25 | div.sphinxsidebar { 26 | background-color: #eeeeee; 27 | border-radius: 5px; 28 | line-height: 130%; 29 | font-size: smaller; 30 | } 31 | 32 | div.sphinxsidebar h3, div.sphinxsidebar h4 { 33 | margin-top: 1.5em; 34 | } 35 | 36 | div.sphinxsidebarwrapper > h3:first-child { 37 | margin-top: 0.2em; 38 | } 39 | 40 | div.sphinxsidebarwrapper > ul > li > ul > li { 41 | margin-bottom: 0.4em; 42 | } 43 | 44 | div.sphinxsidebar a:hover { 45 | color: #0095C4; 46 | } 47 | 48 | div.sphinxsidebar input { 49 | font-family: 'Lucida Grande',Arial,sans-serif; 50 | border: 1px solid #999999; 51 | font-size: smaller; 52 | border-radius: 3px; 53 | } 54 | 55 | div.sphinxsidebar input[type=text] { 56 | max-width: 150px; 57 | } 58 | 59 | div.body { 60 | padding: 0 0 0 1.2em; 61 | } 62 | 63 | div.body p { 64 | line-height: 140%; 65 | } 66 | 67 | div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { 68 | margin: 0; 69 | border: 0; 70 | padding: 0.3em 0; 71 | } 72 | 73 | div.body hr { 74 | border: 0; 75 | background-color: #ccc; 76 | height: 1px; 77 | } 78 | 79 | div.body pre { 80 | border-radius: 3px; 81 | border: 1px solid #ac9; 82 | } 83 | 84 | div.body div.admonition, div.body div.impl-detail { 85 | border-radius: 3px; 86 | } 87 | 88 | div.body div.impl-detail > p { 89 | margin: 0; 90 | } 91 | 92 | div.body div.seealso { 93 | border: 1px solid #dddd66; 94 | } 95 | 96 | div.body a { 97 | color: #00608f; 98 | } 99 | 100 | div.body a:visited { 101 | color: #30306f; 102 | } 103 | 104 | div.body a:hover { 105 | color: #00B0E4; 106 | } 107 | 108 | tt, pre { 109 | font-family: monospace, sans-serif; 110 | font-size: 96.5%; 111 | } 112 | 113 | div.body tt { 114 | border-radius: 3px; 115 | } 116 | 117 | div.body tt.descname { 118 | font-size: 120%; 119 | } 120 | 121 | div.body tt.xref, div.body a tt { 122 | font-weight: normal; 123 | } 124 | 125 | .deprecated { 126 | border-radius: 3px; 127 | } 128 | 129 | table.docutils { 130 | border: 1px solid #ddd; 131 | min-width: 20%; 132 | border-radius: 3px; 133 | margin-top: 10px; 134 | margin-bottom: 10px; 135 | } 136 | 137 | table.docutils td, table.docutils th { 138 | border: 1px solid #ddd !important; 139 | border-radius: 3px; 140 | } 141 | 142 | table p, table li { 143 | text-align: left !important; 144 | } 145 | 146 | table.docutils th { 147 | background-color: #eee; 148 | padding: 0.3em 0.5em; 149 | } 150 | 151 | table.docutils td { 152 | background-color: white; 153 | padding: 0.3em 0.5em; 154 | } 155 | 156 | table.footnote, table.footnote td { 157 | border: 0 !important; 158 | } 159 | 160 | div.footer { 161 | line-height: 150%; 162 | margin-top: -2em; 163 | text-align: right; 164 | width: auto; 165 | margin-right: 10px; 166 | } 167 | 168 | div.footer a:hover { 169 | color: #0095C4; 170 | } 171 | 172 | .refcount { 173 | color: #060; 174 | } 175 | 176 | .stableabi { 177 | color: #229; 178 | } 179 | -------------------------------------------------------------------------------- /doc/theme/pydoctheme/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = default 3 | stylesheet = pydoctheme.css 4 | pygments_style = sphinx 5 | 6 | [options] 7 | bodyfont = 'Lucida Grande', Arial, sans-serif 8 | headfont = 'Lucida Grande', Arial, sans-serif 9 | footerbgcolor = white 10 | footertextcolor = #555555 11 | relbarbgcolor = white 12 | relbartextcolor = #666666 13 | relbarlinkcolor = #444444 14 | sidebarbgcolor = white 15 | sidebartextcolor = #444444 16 | sidebarlinkcolor = #444444 17 | bgcolor = white 18 | textcolor = #222222 19 | linkcolor = #0090c0 20 | visitedlinkcolor = #00608f 21 | headtextcolor = #1a1a1a 22 | headbgcolor = white 23 | headlinkcolor = #aaaaaa 24 | -------------------------------------------------------------------------------- /docs/Reference.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | 4 | --------- 5 | 6 | 7 | | Class | Description | 8 | | :---- | :----------- | 9 | | [Address](#address) | Address(addr, addr2=None, proto=LO_UDP) | 10 | | [AddressError](#addresserror) | Raised when trying to create an invalid `Address` object. | 11 | | [Bundle](#bundle) | Bundle(*messages) | 12 | | [Callback](#callback) | Callback(func, user_data) | 13 | | [Message](#message) | Message(path, *args) | 14 | | [Server](#server) | Server(port=None, proto=LO_DEFAULT, reg_methods=True) | 15 | | [ServerError](#servererror) | Raised when creating a liblo OSC server fails. | 16 | | [ServerThread](#serverthread) | ServerThread(port=None, proto=LO_DEFAULT, reg_methods=True) | 17 | | [make_method](#make_method) | A decorator that serves as a more convenient alternative to [Server.add_method](#add_method). | 18 | | [struct](#struct) | | 19 | 20 | | Function | Description | 21 | | :------- | :----------- | 22 | | `send` | Send a message without requiring a server | 23 | | `time` | Return the current time as a floating point number (seconds since January 1, 1900). | 24 | 25 | 26 | 27 | --------- 28 | 29 | 30 | ## Address 31 | ### 32 | 33 | 34 | ```python 35 | 36 | Address(addr, addr2=None, proto=LO_UDP) 37 | 38 | ``` 39 | 40 | 41 | An Address represents a destination for a message 42 | 43 | 44 | Possible forms: 45 | 46 | * `Address(hostname: str, port: int, proto: [int | str] = LO_UDP`) 47 | * `Address(port: int)` # Assumes localhost 48 | * `Address(url: str)` # A URL of the form 'osc.udp://hostname:1234/' 49 | 50 | Create a new `Address` object from the given hostname/port 51 | or URL. 52 | 53 | Raises: 54 | AddressError: if the given parameters do not represent a valid address. 55 | 56 | 57 | 58 | **Args** 59 | 60 | * **hostname**: the target's hostname - the name or an IP of the form 61 | '127.0.0.0'. 62 | * **port**: the port number of the target 63 | * **proto**: one of the constants `LO_UDP`, `LO_TCP`, `LO_UNIX` or a string like 64 | 'UDP', 'TCP' or 'UNIX' 65 | * **url**: a URL in liblo notation, e.g. `'osc.udp://hostname:1234/'`. 66 | 67 | 68 | --------- 69 | 70 | 71 | **Summary** 72 | 73 | 74 | 75 | | Property | Description | 76 | | :-------- | :----------- | 77 | | hostname | The address's hostname. | 78 | | port | The address's port number. | 79 | | protocol | The address's protocol (one of the constants :const:`UDP`, 80 | :const:`TCP`, or :const:`UNIX`). | 81 | | url | The address's URL. | 82 | 83 | 84 | | Method | Description | 85 | | :------ | :----------- | 86 | | [get_hostname](#get_hostname) | The hostname of this Address | 87 | | [get_port](#get_port) | The port number of this Address | 88 | | [get_protocol](#get_protocol) | The protocol used as an int | 89 | | [get_url](#get_url) | This Address as a liblo URL | 90 | 91 | 92 | --------- 93 | 94 | 95 | **Attributes** 96 | 97 | * **hostname**: The address's hostname. 98 | * **port**: The address's port number. 99 | * **protocol**: The address's protocol (one of the constants :const:`UDP`, 100 | :const:`TCP`, or :const:`UNIX`). 101 | * **url**: The address's URL. 102 | 103 | 104 | --------- 105 | 106 | 107 | **Methods** 108 | 109 | ### get\_hostname 110 | 111 | 112 | ```python 113 | 114 | Address.get_hostname(self) 115 | 116 | ``` 117 | 118 | 119 | The hostname of this Address 120 | 121 | ---------- 122 | 123 | ### get\_port 124 | 125 | 126 | ```python 127 | 128 | Address.get_port(self) 129 | 130 | ``` 131 | 132 | 133 | The port number of this Address 134 | 135 | ---------- 136 | 137 | ### get\_protocol 138 | 139 | 140 | ```python 141 | 142 | Address.get_protocol(self) 143 | 144 | ``` 145 | 146 | 147 | The protocol used as an int 148 | 149 | 150 | #### Example 151 | 152 | ```python 153 | from pyliblo3 import * 154 | address = Address('127.0.0.0', 9876) 155 | assert address.get_protocol() == LO_UDP 156 | ``` 157 | 158 | ---------- 159 | 160 | ### get\_url 161 | 162 | 163 | ```python 164 | 165 | Address.get_url(self) 166 | 167 | ``` 168 | 169 | 170 | This Address as a liblo URL 171 | 172 | 173 | 174 | --------- 175 | 176 | 177 | ## Bundle 178 | ### 179 | 180 | 181 | ```python 182 | 183 | Bundle(*messages) 184 | 185 | ``` 186 | 187 | 188 | A bundle of one or more messages to be sent and dispatched together. 189 | 190 | 191 | Possible forms: 192 | 193 | * `Bundle(*messages)` 194 | * `Bundle(timetag: float, *messages)` 195 | 196 | Create a new `Bundle` object. You can optionally specify a 197 | time at which the messages should be dispatched (as an OSC timetag 198 | float), and any number of messages to be included in the bundle. 199 | 200 | 201 | 202 | **Args** 203 | 204 | * **timetag** (`float`): optional, speficies the time at which the message 205 | should be dispatched 206 | * **messages**: any number of `Message`s to include in this `Bundle` 207 | 208 | 209 | --------- 210 | 211 | 212 | **Summary** 213 | 214 | 215 | 216 | | Method | Description | 217 | | :------ | :----------- | 218 | | [add](#add) | Add one or more messages to this bundle | 219 | 220 | 221 | --------- 222 | 223 | 224 | --------- 225 | 226 | 227 | **Methods** 228 | 229 | ### add 230 | 231 | 232 | ```python 233 | 234 | Bundle.add(self, *args) 235 | 236 | ``` 237 | 238 | 239 | Add one or more messages to this bundle 240 | 241 | 242 | Possible forms: 243 | 244 | * `add(*messages: Message)` 245 | * `add(path: str, *args)`, where path is the osc path (for example, '/path1' or '/root/subpath') 246 | and `args` are passed directly to `Message` to create a Message to be added to this Bundle 247 | 248 | Add one or more messages to the bundle. 249 | 250 | 251 | 252 | **Args** 253 | 254 | * **args**: 255 | 256 | 257 | 258 | --------- 259 | 260 | 261 | ## Callback 262 | ### 263 | 264 | 265 | ```python 266 | 267 | Callback(func, user_data) 268 | 269 | ``` 270 | 271 | 272 | Used internally to wrap a python function as a callback 273 | 274 | 275 | 276 | **Args** 277 | 278 | * **func**: the function to call 279 | * **user_data**: any python object, will be passed to the callback as the last 280 | argument 281 | 282 | 283 | 284 | --------- 285 | 286 | 287 | ## Message 288 | ### 289 | 290 | 291 | ```python 292 | 293 | Message(path, *args) 294 | 295 | ``` 296 | 297 | 298 | An OSC message, consisting of a path and arbitrary arguments. 299 | 300 | 301 | 302 | **Args** 303 | 304 | * **path** (`str`): the path of the message 305 | * **args**: any arguments passed will be added to this messag 306 | 307 | 308 | --------- 309 | 310 | 311 | **Summary** 312 | 313 | 314 | 315 | | Method | Description | 316 | | :------ | :----------- | 317 | | [add](#add) | Append the given arguments to this message | 318 | 319 | 320 | --------- 321 | 322 | 323 | --------- 324 | 325 | 326 | **Methods** 327 | 328 | ### add 329 | 330 | 331 | ```python 332 | 333 | Message.add(self, *args) 334 | 335 | ``` 336 | 337 | 338 | Append the given arguments to this message 339 | 340 | 341 | Arguments can be single values or `(typetag, data)` tuples to specify 342 | the actual type. This might be needed for numbers, to specify if a float 343 | needs to be encoded as a 32-bit (typetag = 'f') or 64-bit float (typetag = 'd'). 344 | By default, float numbers are interpreted as 32-bit floats. 345 | 346 | 347 | 348 | **Args** 349 | 350 | * **args**: each argument can be a single value or a tuple `(typetag: str, data: 351 | Any)` 352 | 353 | 354 | 355 | --------- 356 | 357 | 358 | ## make\_method 359 | ### 360 | 361 | 362 | ```python 363 | 364 | def (path: str | None, types: str, user_data) -> None 365 | 366 | ``` 367 | 368 | 369 | A decorator that serves as a more convenient alternative to [Server.add_method](#add_method). 370 | 371 | 372 | 373 | **Args** 374 | 375 | * **path** (`str | None`): the message path to be handled by the registered 376 | method. `None` may be used as a wildcard to match any OSC message. 377 | * **types** (`str`): the argument types to be handled by the registered method. 378 | `None` may be used as a wildcard to match any OSC message. 379 | * **user_data**: An arbitrary object that will be passed on to the decorated 380 | method every time a matching message is received. 381 | 382 | 383 | --------- 384 | 385 | 386 | **Summary** 387 | 388 | 389 | 390 | | Method | Description | 391 | | :------ | :----------- | 392 | | [__init__](#__init__) | make_method.__init__(self, path, types, user_data=None) | 393 | 394 | 395 | --------- 396 | 397 | 398 | --------- 399 | 400 | 401 | **Methods** 402 | 403 | ### \_\_init\_\_ 404 | 405 | 406 | ```python 407 | 408 | def __init__(self, path, types, user_data=None) -> None 409 | 410 | ``` 411 | 412 | 413 | make_method.__init__(self, path, types, user_data=None) 414 | 415 | 416 | 417 | **Args** 418 | 419 | * **path**: 420 | * **types**: 421 | * **user_data**: (*default*: `None`) 422 | 423 | 424 | 425 | --------- 426 | 427 | 428 | ## struct 429 | 430 | --------- 431 | 432 | 433 | **Summary** 434 | 435 | 436 | 437 | | Method | Description | 438 | | :------ | :----------- | 439 | | [__init__](#__init__) | struct.__init__(self, **kwargs) | 440 | 441 | 442 | --------- 443 | 444 | 445 | --------- 446 | 447 | 448 | **Methods** 449 | 450 | ### \_\_init\_\_ 451 | 452 | 453 | ```python 454 | 455 | def __init__(self, kwargs) -> None 456 | 457 | ``` 458 | 459 | 460 | struct.__init__(self, **kwargs) 461 | 462 | 463 | 464 | **Args** 465 | 466 | * **kwargs**: 467 | 468 | 469 | 470 | --------- 471 | 472 | 473 | ## \_ServerBase 474 | ### 475 | 476 | 477 | ```python 478 | 479 | _ServerBase(reg_methods=True) 480 | 481 | ``` 482 | 483 | 484 | --------- 485 | 486 | 487 | **Summary** 488 | 489 | 490 | 491 | | Property | Description | 492 | | :-------- | :----------- | 493 | | port | The server's port number (int) | 494 | | protocol | The server's protocol (one of the constants `LO_UDP`, `LO_TCP` or `LO_UNIX`). | 495 | | url | The server's URL. | 496 | 497 | 498 | | Method | Description | 499 | | :------ | :----------- | 500 | | [add_bundle_handlers](#add_bundle_handlers) | Add bundle notification handlers. | 501 | | [add_method](#add_method) | Register a callback for OSC messages with matching path and argument types. | 502 | | [del_method](#del_method) | Delete a callback function | 503 | | [fileno](#fileno) | Return the file descriptor of the server socket | 504 | | [get_port](#get_port) | Returns the port number of this server | 505 | | [get_protocol](#get_protocol) | Returns the protocol of this server, as an int | 506 | | [get_url](#get_url) | Returns the url of the server | 507 | | [register_methods](#register_methods) | Called internally during init if reg_methods is True | 508 | | [send](#send) | Send a message or bundle from this server to the the given target. | 509 | 510 | 511 | --------- 512 | 513 | 514 | **Attributes** 515 | 516 | * **port**: The server's port number (int) 517 | * **protocol**: The server's protocol (one of the constants `LO_UDP`, `LO_TCP` 518 | or `LO_UNIX`). 519 | * **url**: The server's URL. 520 | 521 | 522 | --------- 523 | 524 | 525 | **Methods** 526 | 527 | ### add\_bundle\_handlers 528 | 529 | 530 | ```python 531 | 532 | _ServerBase.add_bundle_handlers(self, start_handler, end_handler, user_data=None) 533 | 534 | ``` 535 | 536 | 537 | Add bundle notification handlers. 538 | 539 | 540 | 541 | **Args** 542 | 543 | * **start_handler**: a callback which fires when at the start of a bundle. This 544 | is called with the bundle's timestamp and user_data. 545 | * **end_handler**: a callback which fires when at the end of a bundle. This is 546 | called with user_data. 547 | * **user_data**: data to pass to the handlers. (*default*: `None`) 548 | 549 | ---------- 550 | 551 | ### add\_method 552 | 553 | 554 | ```python 555 | 556 | _ServerBase.add_method(self, str path, str typespec, func, user_data=None) 557 | 558 | ``` 559 | 560 | 561 | Register a callback for OSC messages with matching path and argument types. 562 | 563 | 564 | 565 | **Args** 566 | 567 | * **path** (`str`): the message path to be handled by the registered method. 568 | `None` may be used as a wildcard to match any OSC message. 569 | * **typespec** (`str`): the argument types to be handled by the registered 570 | method. `None` may be used as a wildcard to match any OSC message. 571 | * **func**: the callback function. This may be a global function, a class 572 | method, or any other callable object, pyliblo will know what to do 573 | either way. 574 | * **user_data**: An arbitrary object that will be passed on to *func* every time 575 | a matching message is received. (*default*: `None`) 576 | 577 | ---------- 578 | 579 | ### del\_method 580 | 581 | 582 | ```python 583 | 584 | _ServerBase.del_method(self, path, typespec=None) 585 | 586 | ``` 587 | 588 | 589 | Delete a callback function 590 | 591 | 592 | Delete a callback function. For both *path* and *typespec*, `None` 593 | may be used as a wildcard. 594 | 595 | 596 | 597 | **Args** 598 | 599 | * **path** (`str | None`): the method to delete 600 | * **typespec** (`str | None`): the typespec to match, or None to delete any 601 | method matching the given path (*default*: `None`) 602 | 603 | ---------- 604 | 605 | ### fileno 606 | 607 | 608 | ```python 609 | 610 | _ServerBase.fileno(self) 611 | 612 | ``` 613 | 614 | 615 | Return the file descriptor of the server socket 616 | 617 | 618 | 619 | **Returns** 620 | 621 |     (`int`) the file descriptor, or -1 if not supported by the underlying server protocol 622 | 623 | ---------- 624 | 625 | ### get\_port 626 | 627 | 628 | ```python 629 | 630 | _ServerBase.get_port(self) 631 | 632 | ``` 633 | 634 | 635 | Returns the port number of this server 636 | 637 | 638 | 639 | **Returns** 640 | 641 |     (`int`) port number 642 | 643 | ---------- 644 | 645 | ### get\_protocol 646 | 647 | 648 | ```python 649 | 650 | _ServerBase.get_protocol(self) 651 | 652 | ``` 653 | 654 | 655 | Returns the protocol of this server, as an int 656 | 657 | 658 | This will be one of `LO_TCP`, `LO_TCP` or `LO_UNIX` 659 | 660 | 661 | 662 | **Returns** 663 | 664 |     (`int`) the protocol as an int 665 | 666 | ---------- 667 | 668 | ### get\_url 669 | 670 | 671 | ```python 672 | 673 | _ServerBase.get_url(self) 674 | 675 | ``` 676 | 677 | 678 | Returns the url of the server 679 | 680 | 681 | 682 | **Returns** 683 | 684 |     (`str`) url of the server 685 | 686 | ---------- 687 | 688 | ### register\_methods 689 | 690 | 691 | ```python 692 | 693 | _ServerBase.register_methods(self, obj=None) 694 | 695 | ``` 696 | 697 | 698 | Called internally during init if reg_methods is True 699 | 700 | 701 | This function is usually called automatically by the server's 702 | constructor, unless its *reg_methods* parameter was set to `False`. 703 | 704 | 705 | 706 | **Args** 707 | 708 | * **obj**: The object that implements the OSC callbacks to be registered. 709 | By default this is the server object itself. (*default*: `None`) 710 | 711 | ---------- 712 | 713 | ### send 714 | 715 | 716 | ```python 717 | 718 | _ServerBase.send(self, target, *args) 719 | 720 | ``` 721 | 722 | 723 | Send a message or bundle from this server to the the given target. 724 | 725 | 726 | * `send(target, *messages)` 727 | * `send(target, path, *args)` 728 | 729 | Send a message or bundle from this server to the the given target. 730 | Arguments may be one or more `Message` or `Bundle` 731 | objects, or a single message given by its path and optional arguments. 732 | 733 | Raises: 734 | AddressError: if the given target is invalid. 735 | IOError: if the message couldn't be sent. 736 | 737 | 738 | 739 | **Args** 740 | 741 | * **target** (`Address | tuple[str, int] | str`): the address to send the 742 | message to; an `Address` object, a port number, a `(hostname, port)` 743 | tuple, or a URL. 744 | * **args**: 745 | 746 | 747 | 748 | --------- 749 | 750 | 751 | ## Server 752 | - Base Class: [_ServerBase](#_serverbase) 753 | 754 | ### 755 | 756 | 757 | ```python 758 | 759 | Server(port=None, proto=LO_DEFAULT, reg_methods=True) 760 | 761 | ``` 762 | 763 | 764 | A server that can receive OSC messages, blocking 765 | 766 | 767 | Use [ServerThread](#ServerThread) for an OSC server that runs in its own thread 768 | and never blocks. 769 | 770 | Raises: 771 | ServerError: if an error occurs created the underlying liblo server 772 | 773 | 774 | 775 | **Args** 776 | 777 | * **port** (`int | None`): a decimal port number or a UNIX socket path. If 778 | omitted, an arbitrary free UDP port will be used. 779 | * **proto** (`int | str`): one of LO_UDP, LO_TCP, LO_UNIX or LO_DEFAULT, or one 780 | of the strings 'UDP', 'TCP', 'UNIX' 781 | * **reg_methods** (`bool`): if True, register any methods decorated with the 782 | [make_method](#make_method) decorator 783 | 784 | 785 | --------- 786 | 787 | 788 | **Summary** 789 | 790 | 791 | 792 | | Method | Description | 793 | | :------ | :----------- | 794 | | [free](#free) | Free the underlying server object and close its port. | 795 | | [recv](#recv) | Receive and dispatch one OSC message. | 796 | 797 | 798 | --------- 799 | 800 | 801 | --------- 802 | 803 | 804 | **Methods** 805 | 806 | ### free 807 | 808 | 809 | ```python 810 | 811 | Server.free(self) 812 | 813 | ``` 814 | 815 | 816 | Free the underlying server object and close its port. 817 | 818 | 819 | Note that this will also happen automatically when the server is deallocated. 820 | 821 | ---------- 822 | 823 | ### recv 824 | 825 | 826 | ```python 827 | 828 | Server.recv(self, timeout=None) 829 | 830 | ``` 831 | 832 | 833 | Receive and dispatch one OSC message. 834 | 835 | 836 | Blocking by default, unless *timeout* is specified. 837 | 838 | 839 | 840 | **Args** 841 | 842 | * **timeout** (`int, float`): Time in milliseconds after which the function 843 | returns if no messages have been received. May be 0, in which case 844 | the function always returns immediately, whether messages have been 845 | received or not. (*default*: `None`) 846 | 847 | **Returns** 848 | 849 |     `True` if a message was received, otherwise `False`. 850 | 851 | 852 | 853 | --------- 854 | 855 | 856 | ## ServerThread 857 | - Base Class: [_ServerBase](#_serverbase) 858 | 859 | ### 860 | 861 | 862 | ```python 863 | 864 | ServerThread(port=None, proto=LO_DEFAULT, reg_methods=True) 865 | 866 | ``` 867 | 868 | 869 | Server running in a thread 870 | 871 | 872 | Unlike `Server`, `ServerThread` uses its own thread which 873 | runs in the background to dispatch messages. `ServerThread` 874 | has the same methods as `Server`, with the 875 | exception of `.recv`. Instead, it defines two additional 876 | methods `.start` and `.stop`. 877 | 878 | Raises: 879 | ServerError: if creating the server fails, e.g. because the given port could not 880 | be opened. 881 | 882 | !!! note 883 | 884 | Because liblo creates its own thread to receive and dispatch 885 | messages, callback functions will not be run in the main Python 886 | thread! 887 | 888 | 889 | 890 | **Args** 891 | 892 | * **port** (`int | str`): a decimal port number or a UNIX socket path. If 893 | omitted, an arbitrary free UDP port will be used. 894 | * **proto** (`int | str`): one of the constants `LO_UDP`, `LO_TCP` or `LO_UNIX` 895 | or a corresponding string 'UDP', 'TCP', 'UNIX' 896 | * **reg_methods**: if True, register any method decorated with the 897 | [make_method](#make_method) decorator 898 | 899 | 900 | --------- 901 | 902 | 903 | **Summary** 904 | 905 | 906 | 907 | | Method | Description | 908 | | :------ | :----------- | 909 | | [free](#free) | Free the underlying server object and close its port. | 910 | | [start](#start) | Start the server thread. | 911 | | [stop](#stop) | Stop the server thread. | 912 | 913 | 914 | --------- 915 | 916 | 917 | --------- 918 | 919 | 920 | **Methods** 921 | 922 | ### free 923 | 924 | 925 | ```python 926 | 927 | ServerThread.free(self) 928 | 929 | ``` 930 | 931 | 932 | Free the underlying server object and close its port. 933 | 934 | 935 | !!! note 936 | 937 | This method is called automatically when the server is deallocated. 938 | 939 | ---------- 940 | 941 | ### start 942 | 943 | 944 | ```python 945 | 946 | ServerThread.start(self) 947 | 948 | ``` 949 | 950 | 951 | Start the server thread. 952 | 953 | 954 | liblo will now start to dispatch any messages it receives. 955 | 956 | ---------- 957 | 958 | ### stop 959 | 960 | 961 | ```python 962 | 963 | ServerThread.stop(self) 964 | 965 | ``` 966 | 967 | 968 | Stop the server thread. 969 | 970 | 971 | 972 | --------- 973 | 974 | 975 | ## send 976 | 977 | 978 | ```python 979 | 980 | send(target, *args) 981 | 982 | ``` 983 | 984 | 985 | Send a message without requiring a server 986 | 987 | 988 | The function has two forms: 989 | 990 | * `send(target, *messages)` 991 | * `send(target, path, *args)` 992 | 993 | Send messages to the the given target, without requiring a server. 994 | Arguments may be one or more `Message` or `Bundle` objects, 995 | or a single message given by its path and optional arguments. 996 | 997 | Raises: 998 | AddressError: if the given target is invalid 999 | IOError: if the message couldn't be sent. 1000 | 1001 | 1002 | 1003 | **Args** 1004 | 1005 | * **target** (`Address | tuple[str, int] | int | str`): the address to send the 1006 | message to; an `Address` object, a port number, a `(hostname, port)` 1007 | tuple, or a URL. 1008 | * **args** (`Any`): the information to send. These are used to construct a 1009 | message 1010 | 1011 | 1012 | --------- 1013 | 1014 | 1015 | ## time 1016 | 1017 | 1018 | ```python 1019 | 1020 | time() 1021 | 1022 | ``` 1023 | 1024 | 1025 | Return the current time as a floating point number (seconds since January 1, 1900). 1026 | 1027 | 1028 | 1029 | **Returns** 1030 | 1031 |     (`float`) The liblo timetag as a float, representing seconds since 1900 1032 | -------------------------------------------------------------------------------- /docs/generatedocs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import argparse 4 | import sys 5 | from pathlib import Path 6 | import logging 7 | import shutil 8 | 9 | try: 10 | import pyliblo3 as liblo 11 | from emlib import doctools 12 | except ImportError: 13 | import textwrap 14 | print("\n**WARNING**: Failed to update documentation. The python present in the current environment" 15 | " does not have the needed packages (pyliblo3, emlib)\n") 16 | sys.exit(1) 17 | 18 | 19 | def findRoot(): 20 | p = Path(__file__).parent 21 | if (p.parent/"setup.py").exists(): 22 | return p.parent 23 | if (p/"index.md").exists(): 24 | return p.parent 25 | if (p/"setup.py").exists(): 26 | return p 27 | raise RuntimeError(f"Could not locate the root folder, search started at {p}") 28 | 29 | 30 | def main(dest: Path): 31 | config = doctools.RenderConfig(splitName=True, includeInheritedMethods=False) 32 | markdown = doctools.generateDocsForModule(liblo._liblo, renderConfig=config, title='Reference', includeCustomExceptions=False) 33 | open(dest/"Reference.md", "w").write(markdown) 34 | 35 | 36 | if __name__ == "__main__": 37 | logging.basicConfig(level="DEBUG") 38 | doctools.logger.setLevel("DEBUG") 39 | root = findRoot() 40 | docsfolder = root / "docs" 41 | print("Documentation folder", docsfolder) 42 | assert docsfolder.exists() 43 | main(docsfolder) 44 | 45 | if not shutil.which('mkdocs'): 46 | print("\n**WARNING**: mkdocs not found, cannot generate HTML docs") 47 | else: 48 | os.chdir(root) 49 | os.system("mkdocs build") 50 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # pyliblo3 2 | 3 | Welcome to the **pyliblo3** documentation! 4 | 5 | **pyliblo3** is a cython wrapper for the C library [liblo](). 6 | 7 | 8 | ## Quick Introduction 9 | 10 | ```python 11 | 12 | from pyliblo3 import * 13 | import time 14 | 15 | 16 | class MyServer(ServerThread): 17 | def __init__(self, port=1234): 18 | ServerThread.__init__(self, port) 19 | 20 | @make_method('/foo', 'ifs') 21 | def foo_callback(self, path, args): 22 | i, f, s = args 23 | print(f"Received message '{path}' with arguments: {i=}, {f=}, {s=}") 24 | 25 | @make_method(None, None) 26 | def fallback(self, path, args): 27 | print(f"received unknown message '{path}' with {args=}") 28 | 29 | server = MyServer() 30 | server.start() 31 | 32 | print(f"Server started in its own thread, send messages to {server.port}. Use CTRL-C to stop") 33 | 34 | 35 | while True: 36 | send(("127.0.0.0", server.port), "/foo", 10, 1.5, "bar") 37 | send(("127.0.0.0", server.port), "/unknown", (3, 4)) 38 | time.sleep(1) 39 | 40 | ``` 41 | 42 | ---- 43 | 44 | ## Installation 45 | 46 | ```bash 47 | pip install pyliblo3 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | emlib>=0.16.0 2 | mkdocs-material 3 | -------------------------------------------------------------------------------- /examples/example_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | import liblo, sys 5 | 6 | # send all messages to port 1234 on the local machine 7 | try: 8 | target = liblo.Address(1234) 9 | except liblo.AddressError as err: 10 | print(err) 11 | sys.exit() 12 | 13 | # send message "/foo/message1" with int, float and string arguments 14 | liblo.send(target, "/foo/bar", 123, 456.789) 15 | 16 | # send double, int64 and char 17 | liblo.send(target, "/foo/message2", ('d', 3.1415), ('h', 2**42), ('c', 'x')) 18 | 19 | # we can also build a message object first... 20 | msg = liblo.Message("/foo/blah") 21 | # ... append arguments later... 22 | msg.add(123, "foo") 23 | # ... and then send it 24 | liblo.send(target, msg) 25 | 26 | # send a list of bytes as a blob 27 | blob = [4, 8, 15, 16, 23, 42] 28 | liblo.send(target, "/foo/blob", blob) 29 | 30 | # wrap a message in a bundle, to be dispatched after 2 seconds 31 | bundle = liblo.Bundle(liblo.time() + 2.0, liblo.Message("/blubb", 123)) 32 | liblo.send(target, bundle) 33 | -------------------------------------------------------------------------------- /examples/example_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | import liblo, sys 5 | 6 | # create server, listening on port 1234 7 | try: 8 | server = liblo.Server(1234) 9 | except liblo.ServerError as err: 10 | print(err) 11 | sys.exit() 12 | 13 | def foo_bar_callback(path, args): 14 | i, f = args 15 | print("/foo/bar: received message '%s' with arguments '%d' and '%f'" % (path, i, f)) 16 | 17 | def foo_baz_callback(path, args, types, src, data): 18 | print("/foo/baz: received message '%s'" % path) 19 | print("blob contains %d bytes, user data was '%s'" % (len(args[0]), data)) 20 | 21 | def fallback(path, args, types, src): 22 | print("got unknown message '%s' from '%s'" % (path, src.url)) 23 | for a, t in zip(args, types): 24 | print("argument of type '%s': %s" % (t, a)) 25 | 26 | # register method taking an int and a float 27 | server.add_method("/foo/bar", 'if', foo_bar_callback) 28 | 29 | # register method taking a blob, and passing user data to the callback 30 | server.add_method("/foo/baz", 'b', foo_baz_callback, "blah") 31 | 32 | # register a fallback for unhandled messages 33 | server.add_method(None, None, fallback) 34 | 35 | # loop and dispatch messages every 100ms 36 | while True: 37 | server.recv(100) 38 | 39 | -------------------------------------------------------------------------------- /examples/example_server_deco.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from pyliblo3 import * 4 | import time 5 | 6 | 7 | class MyServer(ServerThread): 8 | def __init__(self, port=1234): 9 | ServerThread.__init__(self, port) 10 | 11 | @make_method('/foo', 'ifs') 12 | def foo_callback(self, path, args): 13 | i, f, s = args 14 | print(f"Received message '{path}' with arguments: {i=}, {f=}, {s=}") 15 | 16 | @make_method(None, None) 17 | def fallback(self, path, args): 18 | print(f"received unknown message '{path}' with {args=}") 19 | 20 | server = MyServer() 21 | server.start() 22 | 23 | print(f"Server started in its own thread, send messages to {server.port}. Use CTRL-C to stop") 24 | 25 | 26 | while True: 27 | send(("127.0.0.0", server.port), "/foo", 10, 1.5, "bar") 28 | send(("127.0.0.0", server.port), "/unknown", (3, 4)) 29 | time.sleep(1) 30 | 31 | -------------------------------------------------------------------------------- /examples/test_server_thread.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | import liblo 5 | import time 6 | 7 | st = liblo.ServerThread() 8 | print("Created Server Thread on Port", st.port) 9 | 10 | def foo_cb(path, args, types): 11 | print("foo_cb():") 12 | for a, t in zip(args, types): 13 | print("received argument %s of type %s" % (a, t)) 14 | 15 | def bar_cb(path, args, types, src): 16 | print("bar_cb():") 17 | print("message from", src.url) 18 | print("typespec:", types) 19 | for a, t in zip(args, types): 20 | print("received argument %s of type %s" % (a, t)) 21 | 22 | class Blah: 23 | def __init__(self, x): 24 | self.x = x 25 | def baz_cb(self, path, args, types, src, user_data): 26 | print("baz_cb():") 27 | print(args[0]) 28 | print("self.x is", self.x, ", user data was", user_data) 29 | 30 | st.add_method('/foo', 'ifs', foo_cb) 31 | st.add_method('/bar', 'hdc', bar_cb) 32 | 33 | b = Blah(123) 34 | st.add_method('/baz', 'b', b.baz_cb, 456) 35 | 36 | st.start() 37 | 38 | liblo.send(st.port, "/foo", 123, 456.789, "buh!") 39 | l = 1234567890123456 40 | liblo.send(st.port, "/bar", l, 666, ('c', "x")) 41 | 42 | time.sleep(1) 43 | st.stop() 44 | st.start() 45 | 46 | liblo.send(st.port, "/baz", [1,2,3,4,5,6,7,8,9,10]) 47 | 48 | time.sleep(1) 49 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: pyliblo3 2 | theme: 3 | name: material 4 | features: 5 | - navigation.tabs 6 | - navigation.sections 7 | 8 | extra_css: 9 | - stylesheets/extra.css 10 | 11 | markdown_extensions: 12 | - toc: 13 | toc_depth: 3 14 | - pymdownx.highlight 15 | - pymdownx.superfences 16 | - admonition 17 | 18 | use_directory_urls: false 19 | 20 | repo_url: https://github.com/gesellkammer/pyliblo3 21 | 22 | nav: 23 | - index.md 24 | - Reference.md 25 | 26 | -------------------------------------------------------------------------------- /pyliblo3/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import platform 3 | import os 4 | 5 | if platform.system() == "Windows": 6 | libspath = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'pyliblo3.libs')) 7 | if os.path.exists(libspath) and os.path.isdir(libspath): 8 | os.add_dll_directory(libspath) 9 | else: 10 | possible_paths = ['C:/Program Files/liblo/lib', 'C:/Program Files/liblo/bin'] 11 | for path in ['C:/Program Files/liblo/lib', 'C:/Program Files/liblo/bin']: 12 | if os.path.exists(path) and os.path.isdir(path): 13 | os.add_dll_directory(path) 14 | del platform 15 | del os 16 | 17 | 18 | from ._liblo import * 19 | -------------------------------------------------------------------------------- /pyliblo3/_liblo.pxd: -------------------------------------------------------------------------------- 1 | # 2 | # pyliblo - Python bindings for the liblo OSC library 3 | # 4 | # Copyright (C) 2007-2011 Dominic Sacré 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU Lesser General Public License as 8 | # published by the Free Software Foundation; either version 2.1 of the 9 | # License, or (at your option) any later version. 10 | # 11 | 12 | from libc.stdint cimport int32_t, uint32_t, int64_t, uint8_t 13 | from libc.stdio cimport const_char 14 | 15 | 16 | cdef extern from 'lo/lo.h': 17 | # type definitions 18 | ctypedef void *lo_server 19 | ctypedef void *lo_server_thread 20 | ctypedef void *lo_method 21 | ctypedef void *lo_address 22 | ctypedef void *lo_message 23 | ctypedef void *lo_blob 24 | ctypedef void *lo_bundle 25 | 26 | ctypedef struct lo_timetag: 27 | uint32_t sec 28 | uint32_t frac 29 | 30 | ctypedef union lo_arg: 31 | int32_t i 32 | int64_t h 33 | float f 34 | double d 35 | unsigned char c 36 | char s 37 | uint8_t m[4] 38 | lo_timetag t 39 | 40 | cdef enum: 41 | LO_DEFAULT 42 | LO_UDP 43 | LO_UNIX 44 | LO_TCP 45 | 46 | ctypedef void(*lo_err_handler)(int num, const_char *msg, const_char *where) 47 | ctypedef int(*lo_method_handler)(const_char *path, const_char *types, lo_arg **argv, int argc, lo_message msg, void *user_data) 48 | ctypedef int(*lo_bundle_start_handler)(lo_timetag time, void *user_data) 49 | ctypedef int(*lo_bundle_end_handler)(void *user_data) 50 | 51 | # send 52 | int lo_send_message_from(lo_address targ, lo_server serv, char *path, lo_message msg) 53 | int lo_send_bundle_from(lo_address targ, lo_server serv, lo_bundle b) 54 | 55 | # server 56 | lo_server lo_server_new_with_proto(char *port, int proto, lo_err_handler err_h) 57 | void lo_server_free(lo_server s) 58 | char *lo_server_get_url(lo_server s) 59 | int lo_server_get_port(lo_server s) 60 | int lo_server_get_protocol(lo_server s) 61 | lo_method lo_server_add_method(lo_server s, char *path, char *typespec, lo_method_handler h, void *user_data) 62 | void lo_server_del_method(lo_server s, char *path, char *typespec) 63 | int lo_server_add_bundle_handlers(lo_server s, lo_bundle_start_handler sh, lo_bundle_end_handler eh, void *user_data) 64 | int lo_server_recv(lo_server s) nogil 65 | int lo_server_recv_noblock(lo_server s, int timeout) nogil 66 | int lo_server_get_socket_fd(lo_server s) 67 | 68 | # server thread 69 | lo_server_thread lo_server_thread_new_with_proto(char *port, int proto, lo_err_handler err_h) 70 | void lo_server_thread_free(lo_server_thread st) 71 | lo_server lo_server_thread_get_server(lo_server_thread st) 72 | void lo_server_thread_start(lo_server_thread st) 73 | void lo_server_thread_stop(lo_server_thread st) 74 | 75 | # address 76 | lo_address lo_address_new(char *host, char *port) 77 | lo_address lo_address_new_with_proto(int proto, char *host, char *port) 78 | lo_address lo_address_new_from_url(char *url) 79 | void lo_address_free(lo_address) 80 | char *lo_address_get_url(lo_address a) 81 | char *lo_address_get_hostname(lo_address a) 82 | char *lo_address_get_port(lo_address a) 83 | int lo_address_get_protocol(lo_address a) 84 | const_char* lo_address_errstr(lo_address a) 85 | 86 | # message 87 | lo_message lo_message_new() 88 | void lo_message_free(lo_message) 89 | void lo_message_add_int32(lo_message m, int32_t a) 90 | void lo_message_add_int64(lo_message m, int64_t a) 91 | void lo_message_add_float(lo_message m, float a) 92 | void lo_message_add_double(lo_message m, double a) 93 | void lo_message_add_char(lo_message m, char a) 94 | void lo_message_add_string(lo_message m, char *a) 95 | void lo_message_add_symbol(lo_message m, char *a) 96 | void lo_message_add_true(lo_message m) 97 | void lo_message_add_false(lo_message m) 98 | void lo_message_add_nil(lo_message m) 99 | void lo_message_add_infinitum(lo_message m) 100 | void lo_message_add_midi(lo_message m, uint8_t a[4]) 101 | void lo_message_add_timetag(lo_message m, lo_timetag a) 102 | void lo_message_add_blob(lo_message m, lo_blob a) 103 | lo_address lo_message_get_source(lo_message m) 104 | 105 | # blob 106 | lo_blob lo_blob_new(int32_t size, void *data) 107 | void lo_blob_free(lo_blob b) 108 | void *lo_blob_dataptr(lo_blob b) 109 | uint32_t lo_blob_datasize(lo_blob b) 110 | 111 | # bundle 112 | lo_bundle lo_bundle_new(lo_timetag tt) 113 | void lo_bundle_free(lo_bundle b) 114 | void lo_bundle_add_message(lo_bundle b, char *path, lo_message m) 115 | 116 | # timetag 117 | void lo_timetag_now(lo_timetag *t) 118 | -------------------------------------------------------------------------------- /pyliblo3/_liblo.pyi: -------------------------------------------------------------------------------- 1 | 2 | UDP = 1 3 | UNIX = 2 4 | TCP = 4 5 | 6 | class Callback: 7 | def __init__(self, func: Callable, user_data: Any): ... 8 | 9 | 10 | def time() -> float: ... 11 | 12 | 13 | def send(target: Address | tuple[str, str] | int, str, *args) -> None: ... 14 | 15 | 16 | class ServerError(Exception): 17 | def __init__(self, num: int, msg: str, where: str): ... 18 | 19 | 20 | 21 | 22 | # decorator to register callbacks 23 | 24 | class make_method: 25 | def __init__(self, path: str, types: str, user_data: Any = None): ... 26 | 27 | def __call__(self, f: Callable) -> Callable: ... 28 | 29 | # common base class for both Server and ServerThread 30 | 31 | class _ServerBase: 32 | 33 | def __init__(self, reg_methods: bool = True): ... 34 | 35 | def register_methods(self, obj: _ServerBase = None) -> None: ... 36 | 37 | def get_url(self) -> str: ... 38 | 39 | def get_port(self) -> int: ... 40 | 41 | def get_protocol(self) -> int: ... 42 | 43 | def fileno(self) -> int: ... 44 | 45 | def add_method(self, path: str, typespec: str, func: Callable, user_data: Any = None) -> None: ... 46 | 47 | def del_method(self, path: str, typespec: str | None = None) -> None: ... 48 | 49 | def add_bundle_handlers(self, start_handler: Callable, end_handler: Callable, user_data=None) -> None: ... 50 | 51 | def send(self, target: Address | tuple[str, int] | str, *args) -> None: ... 52 | 53 | @property 54 | def url(self) -> str: ... 55 | 56 | @property 57 | def port(self) -> int: ... 58 | 59 | @property 60 | def protocol(self) -> int: ... 61 | 62 | 63 | class Server(_ServerBase): 64 | def __init__(self, port: int = None, proto: int | str = LO_DEFAULT, reg_methods: bool = True): ... 65 | def __dealloc__(self): ... 66 | def free(self) -> None: ... 67 | def recv(self, timeout: int | float | None = None) -> bool: ... 68 | 69 | class ServerThread(_ServerBase): 70 | def __init__(self, port: int = None, proto: int | str = LO_DEFAULT, reg_methods: bool = True): ... 71 | def __dealloc__(self): ... 72 | def free(self) -> None: ... 73 | def start(self) -> None: ... 74 | def stop(self) -> None: ... 75 | 76 | class AddressError(Exception): 77 | def __init__(self, msg: str): ... 78 | 79 | class Address: 80 | """ 81 | An Address represents a destination for a message 82 | 83 | Possible forms: 84 | 85 | * `Address(hostname: str, port: int, proto: [int | str] = LO_UDP`) 86 | * `Address(port: int)` # Assumes localhost 87 | * `Address(url: str)` # A URL of the form 'osc.udp://hostname:1234/' 88 | 89 | Create a new `Address` object from the given hostname/port 90 | or URL. 91 | 92 | Args: 93 | hostname: the target's hostname - the name or an IP of the form '127.0.0.0'. 94 | port: the port number of the target 95 | proto: one of the constants `LO_UDP`, `LO_TCP`, `LO_UNIX` or a string like 'UDP', 'TCP' or 'UNIX' 96 | url: a URL in liblo notation, e.g. `'osc.udp://hostname:1234/'`. 97 | 98 | Raises: 99 | AddressError: if the given parameters do not represent a valid address. 100 | 101 | """ 102 | 103 | cdef lo_address _address 104 | 105 | def __init__(self, addr: str | int, addr2: int = None, proto: int | str = None): ... 106 | def __dealloc__(self) -> None: ... 107 | def get_url(self) -> str: ... 108 | 109 | def get_hostname(self) -> str: ... 110 | def get_port(self) -> int | str: ... 111 | 112 | def get_protocol(self) -> int: ... 113 | 114 | @property 115 | def url(self) -> str: ... 116 | 117 | @property 118 | def hostname(self) -> str: ... 119 | 120 | @property 121 | def port(self) -> int: ... 122 | 123 | @property 124 | def protocol(self) -> int: ... 125 | 126 | 127 | class Message: 128 | def __init__(self, path: str, *args): ... 129 | def add(self, *args: tuple[str, Any] | int | float | str | bool) -> None: ... 130 | 131 | 132 | class Bundle: 133 | def __init__(self, *messages): ... 134 | def add(self, *args) -> None: ... 135 | -------------------------------------------------------------------------------- /pyliblo3/_liblo.pyx: -------------------------------------------------------------------------------- 1 | # cython: embedsignature=True 2 | # 3 | # pyliblo3 - Python bindings for the lublo OSC library, based on pyliblo 4 | # 5 | # Original copyright: 6 | # 7 | # pyliblo - Python bindings for the liblo OSC library 8 | # 9 | # Copyright (C) 2007-2015 Dominic Sacré 10 | # 11 | # This program is free software; you can redistribute it and/or modify 12 | # it under the terms of the GNU Lesser General Public License as 13 | # published by the Free Software Foundation; either version 2.1 of the 14 | # License, or (at your option) any later version. 15 | # 16 | 17 | __version__ = '0.16.1' 18 | 19 | 20 | from cpython cimport PY_VERSION_HEX 21 | cdef extern from 'Python.h': 22 | void PyEval_InitThreads() 23 | 24 | from libc.stdlib cimport malloc, free 25 | from libc.math cimport modf 26 | from libc.stdint cimport int32_t, int64_t 27 | 28 | from _liblo cimport * 29 | 30 | import inspect as _inspect 31 | import weakref as _weakref 32 | 33 | 34 | def _protostr_to_int(str proto): 35 | if proto == 'UDP': 36 | return LO_UDP 37 | elif proto == 'TCP': 38 | return LO_TCP 39 | elif proto == 'UNIX': 40 | return LO_UNIX 41 | else: 42 | raise ValueError(f"Proto {proto} not understood, expected one of 'UDP', 'TCP' or 'UNIX'") 43 | 44 | 45 | class _weakref_method: 46 | """ 47 | Weak reference to a function, including support for bound methods. 48 | """ 49 | __slots__ = ('_func', 'obj') 50 | 51 | def __init__(self, f): 52 | if _inspect.ismethod(f): 53 | self._func = f.__func__ 54 | self.obj = _weakref.ref(f.__self__) 55 | else: 56 | self._func = f 57 | self.obj = None 58 | 59 | @property 60 | def func(self): 61 | if self.obj: 62 | return self._func.__get__(self.obj(), self.obj().__class__) 63 | else: 64 | return self._func 65 | 66 | def __call__(self, *args, **kwargs): 67 | return self.func(*args, **kwargs) 68 | 69 | 70 | class struct: 71 | def __init__(self, **kwargs): 72 | for k, v in kwargs.items(): 73 | setattr(self, k, v) 74 | 75 | 76 | cdef class Callback: 77 | """ 78 | Used internally to wrap a python function as a callback 79 | 80 | Args: 81 | func: the function to call 82 | user_data: any python object, will be passed to the callback as the last argument 83 | """ 84 | cdef object func 85 | cdef object user_data 86 | cdef int numargs 87 | cdef int has_varargs 88 | 89 | def __init__(self, func, user_data): 90 | self.func = _weakref_method(func) 91 | self.user_data = user_data, 92 | spec = _inspect.getfullargspec(func) 93 | numargs = len(spec.args) 94 | if _inspect.ismethod(func): 95 | numargs -= 1 96 | self.numargs = numargs 97 | self.has_varargs = 1 if spec.varargs is not None else 0 98 | 99 | 100 | cdef inline str _decode(s): 101 | # convert to standard string type, depending on python version 102 | if PY_VERSION_HEX >= 0x03000000 and isinstance(s, bytes): 103 | return s.decode() 104 | else: 105 | return s 106 | 107 | cdef bytes _encode(s): 108 | # convert unicode to bytestring 109 | if isinstance(s, unicode): 110 | return s.encode() 111 | else: 112 | return s 113 | 114 | 115 | # forward declarations 116 | cdef class _ServerBase 117 | cdef class Address 118 | cdef class Message 119 | cdef class Bundle 120 | 121 | 122 | # liblo protocol constants 123 | UDP = LO_UDP 124 | TCP = LO_TCP 125 | UNIX = LO_UNIX 126 | 127 | 128 | ################################################################################ 129 | # timetag 130 | ################################################################################ 131 | 132 | cdef lo_timetag _double_to_timetag(double f): 133 | cdef lo_timetag tt 134 | cdef double intr, frac 135 | frac = modf(f, &intr) 136 | tt.sec = intr 137 | tt.frac = (frac * 4294967296.0) 138 | return tt 139 | 140 | cdef double _timetag_to_double(lo_timetag tt): 141 | return tt.sec + ((tt.frac) / 4294967296.0) 142 | 143 | def time(): 144 | """ 145 | Return the current time as a floating point number (seconds since January 1, 1900). 146 | 147 | Returns: 148 | (float) The liblo timetag as a float, representing seconds since 1900 149 | """ 150 | cdef lo_timetag tt 151 | lo_timetag_now(&tt) 152 | return _timetag_to_double(tt) 153 | 154 | 155 | ################################################################################ 156 | # send 157 | ################################################################################ 158 | 159 | cdef _send(target, _ServerBase src, args): 160 | cdef lo_server from_server 161 | cdef Address target_address 162 | cdef int r 163 | 164 | # convert target to Address object, if necessary 165 | if isinstance(target, Address): 166 | target_address = target 167 | elif isinstance(target, tuple): 168 | # unpack tuple 169 | target_address = Address(*target) 170 | else: 171 | target_address = Address(target) 172 | 173 | # 'from' parameter is NULL if no server was specified 174 | from_server = src._server if src else NULL 175 | 176 | if isinstance(args[0], (Message, Bundle)): 177 | # args is already a list of Messages/Bundles 178 | packets = args 179 | else: 180 | # make a single Message from all arguments 181 | packets = [Message(*args)] 182 | 183 | # send all packets 184 | for p in packets: 185 | if isinstance(p, Message): 186 | message = p 187 | r = lo_send_message_from(target_address._address, 188 | from_server, 189 | message._path, 190 | message._message) 191 | else: 192 | bundle = p 193 | r = lo_send_bundle_from(target_address._address, 194 | from_server, 195 | bundle._bundle) 196 | 197 | if r == -1: 198 | raise IOError("sending failed: %s" % 199 | lo_address_errstr(target_address._address)) 200 | 201 | 202 | def send(target, *args): 203 | """ 204 | Send a message without requiring a server 205 | 206 | The function has two forms: 207 | 208 | * `send(target, *messages)` 209 | * `send(target, path, *args)` 210 | 211 | Send messages to the the given target, without requiring a server. 212 | Arguments may be one or more `Message` or `Bundle` objects, 213 | or a single message given by its path and optional arguments. 214 | 215 | Args: 216 | target (Address | tuple[str, int] | int | str): the address to send the message to; 217 | an `Address` object, a port number, a `(hostname, port)` tuple, or a URL. 218 | path (str): the path of the message to be sent 219 | args (Any): the information to send. These are used to construct a message 220 | messages (Message | Bundle): one or more objects of type `Message` or `Bundle`. 221 | 222 | Raises: 223 | AddressError: if the given target is invalid 224 | IOError: if the message couldn't be sent. 225 | """ 226 | _send(target, None, args) 227 | 228 | 229 | ################################################################################ 230 | # Server 231 | ################################################################################ 232 | 233 | class ServerError(Exception): 234 | """ 235 | Raised when creating a liblo OSC server fails. 236 | """ 237 | def __init__(self, num, msg, where): 238 | self.num = num 239 | self.msg = msg 240 | self.where = where 241 | 242 | def __str__(self): 243 | s = "server error %d" % self.num 244 | if self.where: s += " in %s" % self.where 245 | s += ": %s" % self.msg 246 | return s 247 | 248 | 249 | cdef int _msg_callback(const_char *path, const_char *types, lo_arg **argv, 250 | int argc, lo_message msg, void *cb_data) noexcept with gil: 251 | cdef int i 252 | cdef char t 253 | cdef unsigned char *ptr 254 | cdef uint32_t size, j 255 | 256 | args = [] 257 | 258 | for i from 0 <= i < argc: 259 | t = types[i] 260 | if t == 'i': v = argv[i].i 261 | elif t == 'h': v = argv[i].h 262 | elif t == 'f': v = argv[i].f 263 | elif t == 'd': v = argv[i].d 264 | elif t == 'c': v = chr(argv[i].c) 265 | elif t == 's': v = _decode(&argv[i].s) 266 | elif t == 'S': v = _decode(&argv[i].s) 267 | elif t == 'T': v = True 268 | elif t == 'F': v = False 269 | elif t == 'N': v = None 270 | elif t == 'I': v = float('inf') 271 | elif t == 'm': v = (argv[i].m[0], argv[i].m[1], argv[i].m[2], argv[i].m[3]) 272 | elif t == 't': v = _timetag_to_double(argv[i].t) 273 | elif t == 'b': 274 | v = bytes(lo_blob_dataptr(argv[i])) 275 | else: 276 | v = None # unhandled data type 277 | 278 | args.append(v) 279 | 280 | cdef char *url = lo_address_get_url(lo_message_get_source(msg)) 281 | src = Address(url) 282 | free(url) 283 | 284 | cb = cb_data 285 | 286 | func_args = (_decode(path), 287 | args, 288 | _decode(types), 289 | src, 290 | cb.user_data) 291 | 292 | # call function 293 | # spec = _inspect.getfullargspec(func) 294 | if cb.has_varargs == 1: 295 | r = cb.func(*func_args) 296 | else: 297 | r = cb.func(*func_args[0:cb.numargs]) 298 | 299 | return r if r is not None else 0 300 | 301 | 302 | cdef int _bundle_start_callback(lo_timetag t, void *cb_data) noexcept with gil: 303 | cb = cb_data 304 | r = cb.start_func(_timetag_to_double(t), cb.user_data) 305 | return r if r is not None else 0 306 | 307 | 308 | cdef int _bundle_end_callback(void *cb_data) noexcept with gil: 309 | cb = cb_data 310 | r = cb.end_func(cb.user_data) 311 | return r if r is not None else 0 312 | 313 | 314 | cdef void _err_handler(int num, const_char *msg, const_char *where) noexcept with gil: 315 | # can't raise exception in cdef callback function, so use a global variable 316 | # instead 317 | global __exception 318 | __exception = ServerError(num, msg, None) 319 | if where: __exception.where = where 320 | 321 | 322 | # decorator to register callbacks 323 | 324 | class make_method: 325 | """ 326 | A decorator that serves as a more convenient alternative to [Server.add_method](#add_method). 327 | 328 | Args: 329 | path (str | None): the message path to be handled by the registered method. 330 | `None` may be used as a wildcard to match any OSC message. 331 | types (str): the argument types to be handled by the registered method. 332 | `None` may be used as a wildcard to match any OSC message. 333 | user_data: An arbitrary object that will be passed on to the decorated 334 | method every time a matching message is received. 335 | 336 | """ 337 | # counter to keep track of the order in which the callback functions where 338 | # defined 339 | _counter = 0 340 | 341 | def __init__(self, path, types, user_data=None): 342 | self.spec = struct(counter=make_method._counter, 343 | path=path, 344 | types=types, 345 | user_data=user_data) 346 | make_method._counter += 1 347 | 348 | def __call__(self, f): 349 | # we can't access the Server object here, because at the time the 350 | # decorator is run it doesn't even exist yet, so we store the 351 | # path/typespec in the function object instead... 352 | if not hasattr(f, '_method_spec'): 353 | f._method_spec = [] 354 | f._method_spec.append(self.spec) 355 | return f 356 | 357 | 358 | # common base class for both Server and ServerThread 359 | 360 | cdef class _ServerBase: 361 | cdef lo_server _server 362 | cdef list _keep_refs 363 | 364 | def __init__(self, reg_methods=True): 365 | self._keep_refs = [] 366 | 367 | if reg_methods: 368 | self.register_methods() 369 | 370 | cdef _check(self): 371 | if self._server == NULL: 372 | raise RuntimeError("Server method called after free()") 373 | 374 | def register_methods(self, obj=None): 375 | """ 376 | Called internally during init if reg_methods is True 377 | 378 | Args: 379 | obj: The object that implements the OSC callbacks to be registered. 380 | By default this is the server object itself. 381 | 382 | This function is usually called automatically by the server's 383 | constructor, unless its *reg_methods* parameter was set to `False`. 384 | """ 385 | if obj == None: 386 | obj = self 387 | # find and register methods that were defined using decorators 388 | methods = [] 389 | for m in _inspect.getmembers(obj): 390 | if hasattr(m[1], '_method_spec'): 391 | for spec in m[1]._method_spec: 392 | methods.append(struct(spec=spec, name=m[1])) 393 | # sort by counter 394 | methods.sort(key=lambda x: x.spec.counter) 395 | for e in methods: 396 | self.add_method(e.spec.path, e.spec.types, e.name, e.spec.user_data) 397 | 398 | def get_url(self): 399 | """ 400 | Returns the url of the server 401 | 402 | Returns: 403 | (str) url of the server 404 | """ 405 | self._check() 406 | cdef char *tmp = lo_server_get_url(self._server) 407 | cdef object r = tmp 408 | free(tmp) 409 | return _decode(r) 410 | 411 | def get_port(self): 412 | """ 413 | Returns the port number of this server 414 | 415 | Returns: 416 | (int) port number 417 | """ 418 | self._check() 419 | return lo_server_get_port(self._server) 420 | 421 | def get_protocol(self): 422 | """ 423 | Returns the protocol of this server, as an int 424 | 425 | This will be one of `LO_TCP`, `LO_TCP` or `LO_UNIX` 426 | 427 | Returns: 428 | (int) the protocol as an int 429 | """ 430 | self._check() 431 | return lo_server_get_protocol(self._server) 432 | 433 | def fileno(self): 434 | """ 435 | Return the file descriptor of the server socket 436 | 437 | Returns: 438 | (int) the file descriptor, or -1 if not supported by the underlying server protocol 439 | """ 440 | self._check() 441 | return lo_server_get_socket_fd(self._server) 442 | 443 | def add_method(self, str path, str typespec, func, user_data=None): 444 | """ 445 | Register a callback for OSC messages with matching path and argument types. 446 | 447 | Args: 448 | path (str): the message path to be handled by the registered method. 449 | `None` may be used as a wildcard to match any OSC message. 450 | typespec (str): the argument types to be handled by the registered method. 451 | `None` may be used as a wildcard to match any OSC message. 452 | func: the callback function. This may be a global function, a class 453 | method, or any other callable object, pyliblo will know what 454 | to do either way. 455 | user_data: An arbitrary object that will be passed on to *func* every time 456 | a matching message is received. 457 | """ 458 | cdef char *p 459 | cdef char *t 460 | 461 | if isinstance(path, (bytes, unicode)): 462 | s = _encode(path) 463 | p = s 464 | elif path is None: 465 | p = NULL 466 | else: 467 | raise TypeError("path must be a string or None") 468 | 469 | if isinstance(typespec, (bytes, unicode)): 470 | s2 = _encode(typespec) 471 | t = s2 472 | elif typespec is None: 473 | t = NULL 474 | else: 475 | raise TypeError("typespec must be a string or None") 476 | 477 | self._check() 478 | 479 | # use a weak reference if func is a method, to avoid circular 480 | # references in cases where func is a method of an object that also 481 | # has a reference to the server (e.g. when deriving from the Server 482 | # class) 483 | # cb = struct(func=_weakref_method(func), user_data=user_data) 484 | cb = Callback(func, user_data) 485 | # keep a reference to the callback data around 486 | self._keep_refs.append(cb) 487 | 488 | lo_server_add_method(self._server, p, t, _msg_callback, cb) 489 | 490 | def del_method(self, path, typespec=None): 491 | """ 492 | Delete a callback function 493 | 494 | Delete a callback function. For both *path* and *typespec*, `None` 495 | may be used as a wildcard. 496 | 497 | Args: 498 | path (str | None): the method to delete 499 | typespec (str | None): the typespec to match, or None to delete any 500 | method matching the given path 501 | """ 502 | cdef char *p 503 | cdef char *t 504 | 505 | if isinstance(path, (bytes, unicode)): 506 | s = _encode(path) 507 | p = s 508 | elif path == None: 509 | p = NULL 510 | else: 511 | raise TypeError("path must be a string or None") 512 | 513 | if isinstance(typespec, (bytes, unicode)): 514 | s2 = _encode(typespec) 515 | t = s2 516 | elif typespec == None: 517 | t = NULL 518 | else: 519 | raise TypeError("typespec must be a string or None") 520 | 521 | self._check() 522 | lo_server_del_method(self._server, p, t) 523 | 524 | def add_bundle_handlers(self, start_handler, end_handler, user_data=None): 525 | """ 526 | Add bundle notification handlers. 527 | 528 | Args: 529 | start_handler: a callback which fires when at the start of a bundle. This is 530 | called with the bundle's timestamp and user_data. 531 | end_handler: a callback which fires when at the end of a bundle. This is called 532 | with user_data. 533 | user_data: data to pass to the handlers. 534 | 535 | """ 536 | cb_data = struct(start_func=_weakref_method(start_handler), 537 | end_func=_weakref_method(end_handler), 538 | user_data=user_data) 539 | self._keep_refs.append(cb_data) 540 | 541 | lo_server_add_bundle_handlers(self._server, _bundle_start_callback, 542 | _bundle_end_callback, cb_data) 543 | 544 | def send(self, target, *args): 545 | """ 546 | Send a message or bundle from this server to the the given target. 547 | 548 | * `send(target, *messages)` 549 | * `send(target, path, *args)` 550 | 551 | Send a message or bundle from this server to the the given target. 552 | Arguments may be one or more `Message` or `Bundle` 553 | objects, or a single message given by its path and optional arguments. 554 | 555 | Args: 556 | target (Address | tuple[str, int] | str): the address to send the message to; 557 | an `Address` object, a port number, a `(hostname, port)` tuple, or a URL. 558 | messages (Message | Bundle): one or more objects of type `Message` or `Bundle`. 559 | path (str): the path of the message to be sent. 560 | 561 | Raises: 562 | AddressError: if the given target is invalid. 563 | IOError: if the message couldn't be sent. 564 | """ 565 | self._check() 566 | _send(target, self, args) 567 | 568 | property url: 569 | """ 570 | The server's URL. 571 | """ 572 | def __get__(self): 573 | return self.get_url() 574 | 575 | property port: 576 | """ 577 | The server's port number (int) 578 | """ 579 | def __get__(self): 580 | return self.get_port() 581 | 582 | property protocol: 583 | """ 584 | The server's protocol (one of the constants `LO_UDP`, `LO_TCP` or `LO_UNIX`). 585 | """ 586 | def __get__(self): 587 | return self.get_protocol() 588 | 589 | 590 | cdef class Server(_ServerBase): 591 | """ 592 | A server that can receive OSC messages, blocking 593 | 594 | Use [ServerThread](#ServerThread) for an OSC server that runs in its own thread 595 | and never blocks. 596 | 597 | Args: 598 | port (int | None): a decimal port number or a UNIX socket path. If omitted, an 599 | arbitrary free UDP port will be used. 600 | proto (int | str): one of LO_UDP, LO_TCP, LO_UNIX or LO_DEFAULT, or one of the 601 | strings 'UDP', 'TCP', 'UNIX' 602 | reg_methods (bool): if True, register any methods decorated with the [make_method](#make_method) 603 | decorator 604 | 605 | Raises: 606 | ServerError: if an error occurs created the underlying liblo server 607 | 608 | """ 609 | def __init__(self, port=None, proto=LO_DEFAULT, reg_methods=True): 610 | cdef char *cs 611 | 612 | if port != None: 613 | p = _encode(str(port)); 614 | cs = p 615 | else: 616 | cs = NULL 617 | 618 | if isinstance(proto, str): 619 | proto = _protostr_to_int(proto) 620 | 621 | global __exception 622 | __exception = None 623 | self._server = lo_server_new_with_proto(cs, proto, _err_handler) 624 | if __exception: 625 | raise __exception 626 | 627 | _ServerBase.__init__(self, reg_methods=reg_methods) 628 | 629 | def __dealloc__(self): 630 | self.free() 631 | 632 | def free(self): 633 | """ 634 | Free the underlying server object and close its port. 635 | 636 | Note that this will also happen automatically when the server is deallocated. 637 | """ 638 | if self._server: 639 | lo_server_free(self._server) 640 | self._server = NULL 641 | 642 | def recv(self, timeout=None): 643 | """ 644 | Receive and dispatch one OSC message. 645 | 646 | Blocking by default, unless *timeout* is specified. 647 | 648 | Args: 649 | timeout (int, float): Time in milliseconds after which the function returns if no 650 | messages have been received. May be 0, in which case the function always returns 651 | immediately, whether messages have been received or not. 652 | 653 | Returns: 654 | (bool) `True` if a message was received, otherwise `False`. 655 | """ 656 | cdef int t, r 657 | self._check() 658 | if timeout != None: 659 | t = timeout 660 | with nogil: 661 | r = lo_server_recv_noblock(self._server, t) 662 | return r and True or False 663 | else: 664 | with nogil: 665 | lo_server_recv(self._server) 666 | return True 667 | 668 | 669 | cdef class ServerThread(_ServerBase): 670 | """ 671 | Server running in a thread 672 | 673 | Unlike `Server`, `ServerThread` uses its own thread which 674 | runs in the background to dispatch messages. `ServerThread` 675 | has the same methods as `Server`, with the 676 | exception of `.recv`. Instead, it defines two additional 677 | methods `.start` and `.stop`. 678 | 679 | Args: 680 | port (int | str): a decimal port number or a UNIX socket path. If omitted, an 681 | arbitrary free UDP port will be used. 682 | proto (int | str): one of the constants `LO_UDP`, `LO_TCP` or `LO_UNIX` or 683 | a corresponding string 'UDP', 'TCP', 'UNIX' 684 | reg_methods: if True, register any method decorated with the [make_method](#make_method) 685 | decorator 686 | 687 | Raises: 688 | ServerError: if creating the server fails, e.g. because the given port could not 689 | be opened. 690 | 691 | !!! note 692 | 693 | Because liblo creates its own thread to receive and dispatch 694 | messages, callback functions will not be run in the main Python 695 | thread! 696 | 697 | """ 698 | cdef lo_server_thread _server_thread 699 | 700 | def __init__(self, port=None, proto=LO_DEFAULT, reg_methods=True): 701 | cdef char *cs 702 | 703 | if port != None: 704 | p = _encode(str(port)); 705 | cs = p 706 | else: 707 | cs = NULL 708 | 709 | # make sure python can handle threading 710 | PyEval_InitThreads() 711 | 712 | global __exception 713 | __exception = None 714 | self._server_thread = lo_server_thread_new_with_proto(cs, proto, _err_handler) 715 | if __exception: 716 | raise __exception 717 | self._server = lo_server_thread_get_server(self._server_thread) 718 | 719 | _ServerBase.__init__(self, reg_methods=reg_methods) 720 | 721 | def __dealloc__(self): 722 | self.free() 723 | 724 | def free(self): 725 | """ 726 | Free the underlying server object and close its port. 727 | 728 | !!! note 729 | 730 | This method is called automatically when the server is deallocated. 731 | """ 732 | if self._server_thread: 733 | lo_server_thread_free(self._server_thread) 734 | self._server_thread = NULL 735 | self._server = NULL 736 | 737 | def start(self): 738 | """ 739 | Start the server thread. 740 | 741 | liblo will now start to dispatch any messages it receives. 742 | """ 743 | self._check() 744 | lo_server_thread_start(self._server_thread) 745 | 746 | def stop(self): 747 | """ 748 | Stop the server thread. 749 | """ 750 | self._check() 751 | lo_server_thread_stop(self._server_thread) 752 | 753 | 754 | ################################################################################ 755 | # Address 756 | ################################################################################ 757 | 758 | class AddressError(Exception): 759 | """ 760 | Raised when trying to create an invalid `Address` object. 761 | """ 762 | def __init__(self, msg): 763 | self.msg = msg 764 | def __str__(self): 765 | return "address error: %s" % self.msg 766 | 767 | 768 | cdef class Address: 769 | """ 770 | An Address represents a destination for a message 771 | 772 | Possible forms: 773 | 774 | * `Address(hostname: str, port: int, proto: [int | str] = LO_UDP`) 775 | * `Address(port: int)` # Assumes localhost 776 | * `Address(url: str)` # A URL of the form 'osc.udp://hostname:1234/' 777 | 778 | Create a new `Address` object from the given hostname/port 779 | or URL. 780 | 781 | Args: 782 | hostname: the target's hostname - the name or an IP of the form '127.0.0.0'. 783 | port: the port number of the target 784 | proto: one of the constants `LO_UDP`, `LO_TCP`, `LO_UNIX` or a string like 'UDP', 'TCP' or 'UNIX' 785 | url: a URL in liblo notation, e.g. `'osc.udp://hostname:1234/'`. 786 | 787 | Raises: 788 | AddressError: if the given parameters do not represent a valid address. 789 | 790 | """ 791 | 792 | cdef lo_address _address 793 | 794 | def __init__(self, addr, addr2=None, proto=LO_UDP): 795 | if isinstance(proto, str): 796 | proto = _protostr_to_int(proto) 797 | 798 | if addr2: 799 | # Address(host, port[, proto]) 800 | s = _encode(addr) 801 | s2 = _encode(str(addr2)) 802 | self._address = lo_address_new_with_proto(proto, s, s2) 803 | if not self._address: 804 | raise AddressError("invalid protocol") 805 | elif isinstance(addr, int) or (isinstance(addr, str) and addr.isdigit()): 806 | # Address(port) 807 | s = str(addr).encode() 808 | self._address = lo_address_new(NULL, s) 809 | else: 810 | # Address(url) 811 | s = _encode(addr) 812 | self._address = lo_address_new_from_url(s) 813 | # lo_address_errno() is of no use if self._addr == NULL 814 | if not self._address: 815 | raise AddressError("invalid URL '%s'" % str(addr)) 816 | 817 | def __dealloc__(self): 818 | lo_address_free(self._address) 819 | 820 | def get_url(self): 821 | """This Address as a liblo URL""" 822 | cdef char *tmp = lo_address_get_url(self._address) 823 | cdef object r = tmp 824 | free(tmp) 825 | return _decode(r) 826 | 827 | def get_hostname(self): 828 | """The hostname of this Address""" 829 | return _decode(lo_address_get_hostname(self._address)) 830 | 831 | def get_port(self): 832 | """The port number of this Address""" 833 | cdef bytes s = lo_address_get_port(self._address) 834 | if s.isdigit(): 835 | return int(s) 836 | else: 837 | return _decode(s) 838 | 839 | def get_protocol(self): 840 | """ 841 | The protocol used as an int 842 | 843 | Example 844 | ------- 845 | 846 | ```python 847 | from pyliblo3 import * 848 | address = Address('127.0.0.0', 9876) 849 | assert address.get_protocol() == LO_UDP 850 | ``` 851 | 852 | """ 853 | return lo_address_get_protocol(self._address) 854 | 855 | property url: 856 | """ 857 | The address's URL. 858 | """ 859 | def __get__(self): 860 | return self.get_url() 861 | 862 | property hostname: 863 | """ 864 | The address's hostname. 865 | """ 866 | def __get__(self): 867 | return self.get_hostname() 868 | 869 | property port: 870 | """ 871 | The address's port number. 872 | """ 873 | def __get__(self): 874 | return self.get_port() 875 | 876 | property protocol: 877 | """ 878 | The address's protocol (one of the constants :const:`UDP`, 879 | :const:`TCP`, or :const:`UNIX`). 880 | """ 881 | def __get__(self): 882 | return self.get_protocol() 883 | 884 | 885 | ################################################################################ 886 | # Message 887 | ################################################################################ 888 | 889 | cdef class _Blob: 890 | cdef lo_blob _blob 891 | 892 | def __init__(self, arr): 893 | # arr can by any sequence type 894 | cdef unsigned char *p 895 | cdef uint32_t size, i 896 | size = len(arr) 897 | if size < 1: 898 | raise ValueError("blob is empty") 899 | # copy each element of arr to a C array 900 | p = malloc(size) 901 | try: 902 | if isinstance(arr[0], (str, unicode)): 903 | # use ord() if arr is a string (but not bytes) 904 | for i from 0 <= i < size: 905 | p[i] = ord(arr[i]) 906 | else: 907 | for i from 0 <= i < size: 908 | p[i] = arr[i] 909 | # build blob 910 | self._blob = lo_blob_new(size, p) 911 | finally: 912 | free(p) 913 | 914 | def __dealloc__(self): 915 | lo_blob_free(self._blob) 916 | 917 | 918 | cdef class Message: 919 | """ 920 | An OSC message, consisting of a path and arbitrary arguments. 921 | 922 | Args: 923 | path (str): the path of the message 924 | args: any arguments passed will be added to this messag 925 | """ 926 | cdef bytes _path 927 | cdef lo_message _message 928 | cdef list _keep_refs 929 | 930 | def __init__(self, path, *args): 931 | self._keep_refs = [] 932 | # encode path to bytestring if necessary 933 | self._path = _encode(path) 934 | self._message = lo_message_new() 935 | 936 | self.add(*args) 937 | 938 | def __dealloc__(self): 939 | lo_message_free(self._message) 940 | 941 | def add(self, *args): 942 | """ 943 | Append the given arguments to this message 944 | 945 | Arguments can be single values or `(typetag, data)` tuples to specify 946 | the actual type. This might be needed for numbers, to specify if a float 947 | needs to be encoded as a 32-bit (typetag = 'f') or 64-bit float (typetag = 'd'). 948 | By default, float numbers are interpreted as 32-bit floats. 949 | 950 | Args: 951 | args: each argument can be a single value or a tuple `(typetag: str, data: Any)` 952 | """ 953 | for arg in args: 954 | if (isinstance(arg, tuple) and len(arg) <= 2 and 955 | isinstance(arg[0], (bytes, unicode)) and len(arg[0]) == 1): 956 | # type explicitly specified 957 | if len(arg) == 2: 958 | self._add(arg[0], arg[1]) 959 | else: 960 | self._add(arg[0], None) 961 | else: 962 | # detect type automatically 963 | self._add_auto(arg) 964 | 965 | cdef _add(self, type, value): 966 | cdef uint8_t midi[4] 967 | 968 | # accept both bytes and unicode as type specifier 969 | cdef char t = ord(_decode(type)[0]) 970 | 971 | if t == 'i': 972 | lo_message_add_int32(self._message, int(value)) 973 | elif t == 'h': 974 | lo_message_add_int64(self._message, long(value)) 975 | elif t == 'f': 976 | lo_message_add_float(self._message, float(value)) 977 | elif t == 'd': 978 | lo_message_add_double(self._message, float(value)) 979 | elif t == 'c': 980 | lo_message_add_char(self._message, ord(value)) 981 | elif t == 's': 982 | s = _encode(value) 983 | lo_message_add_string(self._message, s) 984 | elif t == 'S': 985 | s = _encode(value) 986 | lo_message_add_symbol(self._message, s) 987 | elif t == 'T': 988 | lo_message_add_true(self._message) 989 | elif t == 'F': 990 | lo_message_add_false(self._message) 991 | elif t == 'N': 992 | lo_message_add_nil(self._message) 993 | elif t == 'I': 994 | lo_message_add_infinitum(self._message) 995 | elif t == 'm': 996 | for n from 0 <= n < 4: 997 | midi[n] = value[n] 998 | lo_message_add_midi(self._message, midi) 999 | elif t == 't': 1000 | lo_message_add_timetag(self._message, _double_to_timetag(value)) 1001 | elif t == 'b': 1002 | b = _Blob(value) 1003 | # make sure the blob is not deleted as long as this message exists 1004 | self._keep_refs.append(b) 1005 | lo_message_add_blob(self._message, (<_Blob>b)._blob) 1006 | else: 1007 | raise TypeError("unknown OSC data type '%c'" % t) 1008 | 1009 | cdef _add_auto(self, value): 1010 | # bool is a subclass of int, so check those first 1011 | if value is True: 1012 | lo_message_add_true(self._message) 1013 | elif value is False: 1014 | lo_message_add_false(self._message) 1015 | elif isinstance(value, (int, long)): 1016 | try: 1017 | lo_message_add_int32(self._message, value) 1018 | except OverflowError: 1019 | lo_message_add_int64(self._message, value) 1020 | elif isinstance(value, float): 1021 | lo_message_add_float(self._message, float(value)) 1022 | elif isinstance(value, (bytes, unicode)): 1023 | s = _encode(value) 1024 | lo_message_add_string(self._message, s) 1025 | elif value == None: 1026 | lo_message_add_nil(self._message) 1027 | elif value == float('inf'): 1028 | lo_message_add_infinitum(self._message) 1029 | else: 1030 | # last chance: could be a blob 1031 | try: 1032 | iter(value) 1033 | except TypeError: 1034 | raise TypeError("unsupported message argument type") 1035 | self._add('b', value) 1036 | 1037 | 1038 | ################################################################################ 1039 | # Bundle 1040 | ################################################################################ 1041 | 1042 | cdef class Bundle: 1043 | """ 1044 | A bundle of one or more messages to be sent and dispatched together. 1045 | 1046 | Possible forms: 1047 | 1048 | * `Bundle(*messages)` 1049 | * `Bundle(timetag: float, *messages)` 1050 | 1051 | Create a new `Bundle` object. You can optionally specify a 1052 | time at which the messages should be dispatched (as an OSC timetag 1053 | float), and any number of messages to be included in the bundle. 1054 | 1055 | Args: 1056 | timetag (float): optional, speficies the time at which the message 1057 | should be dispatched 1058 | messages: any number of `Message`s to include in this `Bundle` 1059 | """ 1060 | cdef lo_bundle _bundle 1061 | cdef list _keep_refs 1062 | 1063 | def __init__(self, *messages): 1064 | cdef lo_timetag tt 1065 | tt.sec, tt.frac = 0, 0 1066 | self._keep_refs = [] 1067 | 1068 | if len(messages) and not isinstance(messages[0], Message): 1069 | t = messages[0] 1070 | if isinstance(t, (float, int, long)): 1071 | tt = _double_to_timetag(t) 1072 | elif isinstance(t, tuple) and len(t) == 2: 1073 | tt.sec, tt.frac = t 1074 | else: 1075 | raise TypeError("invalid timetag") 1076 | # first argument was timetag, so continue with second 1077 | messages = messages[1:] 1078 | 1079 | self._bundle = lo_bundle_new(tt) 1080 | if len(messages): 1081 | self.add(*messages) 1082 | 1083 | def __dealloc__(self): 1084 | lo_bundle_free(self._bundle) 1085 | 1086 | def add(self, *args): 1087 | """ 1088 | Add one or more messages to this bundle 1089 | 1090 | Possible forms: 1091 | 1092 | * `add(*messages: Message)` 1093 | * `add(path: str, *args)`, where path is the osc path (for example, '/path1' or '/root/subpath') 1094 | and `args` are passed directly to `Message` to create a Message to be added to this Bundle 1095 | 1096 | Add one or more messages to the bundle. 1097 | 1098 | Args: 1099 | args: either a Message or a set or arguments passed directly to `Message` to create 1100 | a `Message` which is added to this `Bundle` 1101 | """ 1102 | if isinstance(args[0], Message): 1103 | # args is already a list of Messages 1104 | messages = args 1105 | else: 1106 | # make a single Message from all arguments 1107 | messages = [Message(*args)] 1108 | 1109 | # add all messages 1110 | for m in messages: 1111 | self._keep_refs.append(m) 1112 | message = m 1113 | lo_bundle_add_message(self._bundle, message._path, message._message) 1114 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | 3 | requires = [ 4 | "setuptools>=42", 5 | "wheel", 6 | "cython>=3.0" 7 | ] 8 | 9 | build-backend = "setuptools.build_meta" 10 | 11 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Optionally build your docs in additional formats such as PDF 9 | # formats: 10 | # - pdf 11 | 12 | build: 13 | os: ubuntu-22.04 14 | tools: 15 | python: "3.11" 16 | 17 | python: 18 | install: 19 | - requirements: docs/requirements.txt 20 | # setup_py_install: true 21 | 22 | mkdocs: 23 | configuration: mkdocs.yml 24 | -------------------------------------------------------------------------------- /scripts/dump_osc.1: -------------------------------------------------------------------------------- 1 | .TH dump_osc 1 2 | 3 | .SH NAME 4 | dump_osc \- Prints incoming OSC messages 5 | 6 | .SH SYNOPSIS 7 | .B dump_osc 8 | \fIport\fP 9 | 10 | .SH DESCRIPTION 11 | .B dump_osc 12 | prints all OSC messages received on \fIport\fP (UDP port number, or any other 13 | address string supported by liblo). 14 | 15 | .SH AUTHOR 16 | Dominic Sacre 17 | 18 | .SH SEE ALSO 19 | send_osc(1) 20 | -------------------------------------------------------------------------------- /scripts/dump_osc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # pyliblo - Python bindings for the liblo OSC library 5 | # 6 | # Copyright (C) 2007-2011 Dominic Sacré 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | import sys 15 | import pyliblo3 as liblo 16 | 17 | 18 | class DumpOSC: 19 | 20 | def blob_to_hex(self, b): 21 | return " ".join([ (hex(v/16).upper()[-1] + hex(v%16).upper()[-1]) for v in b ]) 22 | 23 | def callback(self, path, args, types, src): 24 | write = sys.stdout.write 25 | ## print source 26 | #write("from " + src.get_url() + ": ") 27 | # print path 28 | write(path + " ,") 29 | # print typespec 30 | write(types) 31 | # loop through arguments and print them 32 | for a, t in zip(args, types): 33 | write(" ") 34 | if t == None: 35 | #unknown type 36 | write("[unknown type]") 37 | elif t == 'b': 38 | # it's a blob 39 | write("[" + self.blob_to_hex(a) + "]") 40 | else: 41 | # anything else 42 | write(str(a)) 43 | write('\n') 44 | 45 | def __init__(self, port = None): 46 | # create server object 47 | try: 48 | self.server = liblo.Server(port) 49 | except liblo.ServerError as err: 50 | sys.exit(str(err)) 51 | 52 | print("listening on URL: " + self.server.get_url()) 53 | 54 | # register callback function for all messages 55 | self.server.add_method(None, None, self.callback) 56 | 57 | def run(self): 58 | # just loop and dispatch messages every 10ms 59 | while True: 60 | self.server.recv(10) 61 | 62 | 63 | if __name__ == '__main__': 64 | # display help 65 | if len(sys.argv) == 1 or sys.argv[1] in ("-h", "--help"): 66 | sys.exit("Usage: " + sys.argv[0] + " port") 67 | 68 | # require one argument (port number) 69 | if len(sys.argv) < 2: 70 | sys.exit("please specify a port or URL") 71 | 72 | app = DumpOSC(sys.argv[1]) 73 | try: 74 | app.run() 75 | except KeyboardInterrupt: 76 | del app 77 | -------------------------------------------------------------------------------- /scripts/send_osc.1: -------------------------------------------------------------------------------- 1 | .TH send_osc 1 2 | 3 | .SH NAME 4 | send_osc \- Sends a single OSC message 5 | 6 | .SH SYNOPSIS 7 | .B send_osc 8 | \fIport\fP \fIpath\fP [,\fItypes\fP] [\fIargs\fP...] 9 | 10 | .SH DESCRIPTION 11 | .B send_osc 12 | sends an OSC message to the specified \fIport\fP (UDP port number, or any 13 | other address string supported by liblo). 14 | The message is delivered to \fIpath\fP. 15 | If the first argument following the path starts with a comma, it's interpreted 16 | as a type string, specifying the OSC data types to be used for sending the 17 | message arguments. 18 | Otherwise, send_osc automatically tries to use appropriate data types. 19 | Valid integer and float values are sent as such, anything else is sent as a 20 | string. 21 | 22 | .SH AUTHOR 23 | Dominic Sacre 24 | 25 | .SH SEE ALSO 26 | dump_osc(1) 27 | -------------------------------------------------------------------------------- /scripts/send_osc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # pyliblo - Python bindings for the liblo OSC library 5 | # 6 | # Copyright (C) 2007-2011 Dominic Sacré 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | import sys 15 | import pyliblo3 as liblo 16 | 17 | 18 | def make_message_auto(path, *args): 19 | msg = liblo.Message(path) 20 | 21 | for a in args: 22 | try: v = int(a) 23 | except ValueError: 24 | try: v = float(a) 25 | except ValueError: 26 | v = a 27 | msg.add(v) 28 | 29 | return msg 30 | 31 | 32 | def make_message_manual(path, types, *args): 33 | if len(types) != len(args): 34 | sys.exit("length of type string doesn't match number of arguments") 35 | 36 | msg = liblo.Message(path) 37 | try: 38 | for a, t in zip(args, types): 39 | msg.add((t, a)) 40 | except Exception as e: 41 | sys.exit(str(e)) 42 | 43 | return msg 44 | 45 | 46 | if __name__ == '__main__': 47 | # display help 48 | if len(sys.argv) == 1 or sys.argv[1] in ("-h", "--help"): 49 | sys.exit("Usage: " + sys.argv[0] + " port path [,types] [args...]") 50 | 51 | # require at least two arguments (target port/url and message path) 52 | if len(sys.argv) < 2: 53 | sys.exit("please specify a port or URL") 54 | if len(sys.argv) < 3: 55 | sys.exit("please specify a message path") 56 | 57 | if len(sys.argv) > 3 and sys.argv[3].startswith(','): 58 | msg = make_message_manual(sys.argv[2], sys.argv[3][1:], *sys.argv[4:]) 59 | else: 60 | msg = make_message_auto(*sys.argv[2:]) 61 | 62 | try: 63 | liblo.send(sys.argv[1], msg) 64 | except IOError as e: 65 | sys.exit(str(e)) 66 | else: 67 | sys.exit(0) 68 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from setuptools import setup, Extension 4 | 5 | import platform 6 | import glob 7 | import os 8 | import shutil 9 | import subprocess 10 | 11 | VERSION = '0.16.3' 12 | 13 | platformname = platform.system().lower() 14 | 15 | include_dirs = ["pyliblo3"] 16 | library_dirs = [] 17 | libraries = [] 18 | compile_args = [] 19 | 20 | 21 | def append_if_exists(lst: list[str], path: str) -> None: 22 | if os.path.exists(path): 23 | if path not in lst: 24 | lst.append(path) 25 | print(f"~~~~~ Added path: {path}") 26 | else: 27 | print(f"***** Path does not exists, skipping: '{path}'") 28 | 29 | 30 | if platformname == 'darwin': 31 | libraries.append('lo') 32 | brewpath = shutil.which("brew") 33 | if brewpath: 34 | brewprefix = subprocess.getoutput("brew --prefix") 35 | append_if_exists(include_dirs, brewprefix + "/include") 36 | append_if_exists(library_dirs, brewprefix + "/lib") 37 | include_dirs.append("/usr/local/include/") 38 | append_if_exists(include_dirs, "/opt/local/include/") 39 | 40 | library_dirs.append("/usr/local/lib") 41 | append_if_exists(library_dirs, "/opt/local/lib") 42 | 43 | compile_args += [ 44 | '-fno-strict-aliasing', 45 | '-Werror-implicit-function-declaration', 46 | '-Wfatal-errors' 47 | ] 48 | elif platformname == 'linux': 49 | libraries.append('lo') 50 | include_dirs.extend(['/usr/include', '/usr/local/include']) 51 | library_dirs.append("/usr/local/lib") 52 | compile_args += [ 53 | '-fno-strict-aliasing', 54 | '-Werror-implicit-function-declaration', 55 | '-Wfatal-errors' 56 | ] 57 | elif platformname == "windows": 58 | libraries.append('liblo') 59 | # Default install directory for liblo built from source 60 | # See also the wheel build script where we add the .../lib and .../bin 61 | # directories so that the wheel repair script can find the liblo dll 62 | # to add it to the wheel. When building from source, the user needs 63 | # to install liblo and add its lib and bin directories to the path 64 | append_if_exists(include_dirs, "C:/Program Files/liblo/include") 65 | append_if_exists(library_dirs, "C:/Program Files/liblo/lib") 66 | else: 67 | pass 68 | 69 | # read the contents of your README file 70 | thisdir = os.path.abspath(os.path.dirname(__file__)) 71 | with open(os.path.join(thisdir, 'README.md')) as f: 72 | long_description = f.read() 73 | 74 | setup( 75 | name='pyliblo3', 76 | python_requires='>=3.9', 77 | version=VERSION, 78 | scripts=glob.glob("scripts/*.py"), 79 | ext_modules=[ 80 | Extension( 81 | 'pyliblo3._liblo', 82 | #sources = ['src/liblo.pyx', 'src/liblo.pxd'], 83 | sources = ['pyliblo3/_liblo.pyx'], 84 | extra_compile_args=compile_args, 85 | libraries=libraries, 86 | library_dirs=library_dirs, 87 | include_dirs=include_dirs) 88 | ], 89 | packages=['pyliblo3'], 90 | author='Dominic Sacre', 91 | author_email='dominic.sacre@gmx.de', 92 | maintainer='Eduardo Moguillansky', 93 | maintainer_email='eduardo.moguillansky@gmail.com', 94 | url='https://github.com/gesellkammer/pyliblo3', 95 | description='Python bindings for the liblo OSC library', 96 | long_description=long_description, 97 | long_description_content_type='text/markdown', 98 | license='LGPL', 99 | ) 100 | -------------------------------------------------------------------------------- /test/unit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # pyliblo - Python bindings for the liblo OSC library 5 | # 6 | # Copyright (C) 2007-2011 Dominic Sacré 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | 14 | import unittest 15 | import re 16 | import time 17 | import sys 18 | import pyliblo3 as liblo 19 | import platform 20 | 21 | portnum = 9876 22 | 23 | def approx(a, b, e = 0.0002): 24 | return abs(a - b) < e 25 | 26 | def matchHost(host, regex): 27 | r = re.compile(regex) 28 | return r.match(host) != None 29 | 30 | 31 | class Arguments: 32 | def __init__(self, path, args, types, src, data): 33 | self.path = path 34 | self.args = args 35 | self.types = types 36 | self.src = src 37 | self.data = data 38 | 39 | 40 | class ServerTestCaseBase(unittest.TestCase): 41 | def setUp(self): 42 | self.cb = None 43 | 44 | def callback(self, path, args, types, src, data): 45 | self.cb = Arguments(path, args, types, src, data) 46 | 47 | def callback_dict(self, path, args, types, src, data): 48 | if self.cb == None: 49 | self.cb = { } 50 | self.cb[path] = Arguments(path, args, types, src, data) 51 | 52 | 53 | class ServerTestCase(ServerTestCaseBase): 54 | def setUp(self): 55 | ServerTestCaseBase.setUp(self) 56 | self.server = liblo.Server(str(portnum)) 57 | 58 | def tearDown(self): 59 | del self.server 60 | 61 | def testPort(self): 62 | assert self.server.get_port() == portnum 63 | 64 | def testURL(self): 65 | assert matchHost(self.server.get_url(), fr'osc\.udp://.*:{portnum}/') 66 | 67 | def testSendInt(self): 68 | self.server.add_method('/foo', 'i', self.callback, "data") 69 | self.server.send(str(portnum), '/foo', 123) 70 | assert self.server.recv() == True 71 | assert self.cb is not None and isinstance(self.cb, Arguments), f".cb is not set, {self.cb=}" 72 | assert self.cb.path == '/foo' 73 | assert self.cb.args[0] == 123 74 | assert self.cb.types == 'i' 75 | assert self.cb.data == ("data",) 76 | assert matchHost(self.cb.src.get_url(), fr'osc\.udp://.*:{portnum}/') 77 | 78 | def testSendBlob(self): 79 | self.server.add_method('/blob', 'b', self.callback) 80 | self.server.send(str(portnum), '/blob', [4, 8, 15, 16, 23, 42]) 81 | assert self.server.recv() == True 82 | if sys.hexversion < 0x03000000: 83 | assert list(self.cb.args[0]) == [4, 8, 15, 16, 23, 42] 84 | else: 85 | assert self.cb.args[0] == b'\x04\x08\x0f\x10\x17\x2a' 86 | 87 | def testSendVarious(self): 88 | self.server.add_method('/blah', 'ihfdscb', self.callback) 89 | if sys.hexversion < 0x03000000: 90 | self.server.send(portnum, '/blah', 123, 2**42, 123.456, 666.666, "hello", ('c', 'x'), (12, 34, 56)) 91 | else: 92 | self.server.send(portnum, '/blah', 123, ('h', 2**42), 123.456, 666.666, "hello", ('c', 'x'), (12, 34, 56)) 93 | assert self.server.recv() == True 94 | assert self.cb.types == 'ihfdscb' 95 | assert len(self.cb.args) == len(self.cb.types) 96 | assert self.cb.args[0] == 123 97 | assert self.cb.args[1] == 2**42 98 | assert approx(self.cb.args[2], 123.456) 99 | assert approx(self.cb.args[3], 666.666) 100 | assert self.cb.args[4] == "hello" 101 | assert self.cb.args[5] == 'x' 102 | if sys.hexversion < 0x03000000: 103 | assert list(self.cb.args[6]) == [12, 34, 56] 104 | else: 105 | assert self.cb.args[6] == b'\x0c\x22\x38' 106 | 107 | def testSendOthers(self): 108 | self.server.add_method('/blubb', 'tmSTFNI', self.callback) 109 | self.server.send(portnum, '/blubb', ('t', 666666.666), ('m', (1, 2, 3, 4)), ('S', 'foo'), True, ('F',), None, ('I',)) 110 | assert self.server.recv() == True 111 | assert self.cb.types == 'tmSTFNI' 112 | assert approx(self.cb.args[0], 666666.666) 113 | assert self.cb.args[1] == (1, 2, 3, 4) 114 | assert self.cb.args[2] == 'foo' 115 | assert self.cb.args[3] == True 116 | assert self.cb.args[4] == False 117 | assert self.cb.args[5] == None 118 | assert self.cb.args[6] == float('inf') 119 | 120 | def testSendMessage(self): 121 | self.server.add_method('/blah', 'is', self.callback) 122 | m = liblo.Message('/blah', 42, 'foo') 123 | self.server.send(portnum, m) 124 | assert self.server.recv() == True 125 | assert self.cb.types == 'is' 126 | assert self.cb.args[0] == 42 127 | assert self.cb.args[1] == 'foo' 128 | 129 | def testSendBundle(self): 130 | self.server.add_method('/foo', 'i', self.callback_dict) 131 | self.server.add_method('/bar', 's', self.callback_dict) 132 | self.server.send(portnum, liblo.Bundle( 133 | liblo.Message('/foo', 123), 134 | liblo.Message('/bar', "blubb") 135 | )) 136 | assert self.server.recv(100) == True 137 | assert self.cb['/foo'].args[0] == 123 138 | assert self.cb['/bar'].args[0] == "blubb" 139 | 140 | def testSendTimestamped(self): 141 | self.server.add_method('/blubb', 'i', self.callback) 142 | d = 1.23 143 | t1 = time.time() 144 | b = liblo.Bundle(liblo.time() + d) 145 | b.add('/blubb', 42) 146 | self.server.send(portnum, b) 147 | while not self.cb: 148 | self.server.recv(1) 149 | t2 = time.time() 150 | assert approx(t2 - t1, d, 0.01) 151 | 152 | def testSendInvalid(self): 153 | try: 154 | self.server.send(portnum, '/blubb', ('x', 'y')) 155 | except TypeError as e: 156 | pass 157 | else: 158 | assert False 159 | 160 | def testRecvTimeout(self): 161 | t1 = time.time() 162 | assert self.server.recv(500) == False 163 | t2 = time.time() 164 | assert t2 - t1 < 0.666 165 | 166 | def testRecvImmediate(self): 167 | t1 = time.time() 168 | assert self.server.recv(0) == False 169 | t2 = time.time() 170 | assert t2 - t1 < 0.01 171 | 172 | 173 | class ServerCreationTestCase(unittest.TestCase): 174 | def testNoPermission(self): 175 | try: 176 | s = liblo.Server('22') 177 | except liblo.ServerError as e: 178 | pass 179 | except OSError as e: 180 | # On macos this should raise OSError 181 | # And it should in fact fail 182 | if platform.system().lower() == 'darwin': 183 | pass 184 | else: 185 | assert False 186 | else: 187 | print(f"Should have failed with exception OSError") 188 | 189 | def testRandomPort(self): 190 | s = liblo.Server() 191 | assert 1024 <= s.get_port() <= 65535 192 | 193 | def testPort(self): 194 | s = liblo.Server(1234) 195 | t = liblo.Server('5678') 196 | assert s.port == 1234 197 | assert t.port == 5678 198 | assert matchHost(s.url, r'osc\.udp://.*:1234/') 199 | 200 | def testPortProto(self): 201 | s = liblo.Server(1234, liblo.TCP) 202 | assert matchHost(s.url, r'osc\.tcp://.*:1234/') 203 | 204 | 205 | class ServerThreadTestCase(ServerTestCaseBase): 206 | def setUp(self): 207 | ServerTestCaseBase.setUp(self) 208 | self.server = liblo.ServerThread('1234') 209 | 210 | def tearDown(self): 211 | del self.server 212 | 213 | def testSendAndReceive(self): 214 | self.server.add_method('/foo', 'i', self.callback) 215 | self.server.send('1234', '/foo', 42) 216 | self.server.start() 217 | time.sleep(0.2) 218 | self.server.stop() 219 | assert self.cb.args[0] == 42 220 | 221 | 222 | class DecoratorTestCase(unittest.TestCase): 223 | class TestServer(liblo.Server): 224 | def __init__(self): 225 | liblo.Server.__init__(self, portnum) 226 | 227 | @liblo.make_method('/foo', 'ibm') 228 | def foo_cb(self, path, args, types, src, data): 229 | self.cb = Arguments(path, args, types, src, data) 230 | 231 | def setUp(self): 232 | self.server = self.TestServer() 233 | 234 | def tearDown(self): 235 | del self.server 236 | 237 | def testSendReceive(self): 238 | liblo.send(portnum, '/foo', 42, ('b', [4, 8, 15, 16, 23, 42]), ('m', (6, 6, 6, 0))) 239 | assert self.server.recv() == True 240 | assert self.server.cb.path == '/foo' 241 | assert len(self.server.cb.args) == 3 242 | 243 | 244 | class AddressTestCase(unittest.TestCase): 245 | def testPort(self): 246 | a = liblo.Address(1234) 247 | b = liblo.Address('5678') 248 | assert a.port == 1234 249 | assert b.port == 5678 250 | assert a.url == 'osc.udp://localhost:1234/' 251 | 252 | def testUrl(self): 253 | a = liblo.Address('osc.udp://foo:1234/') 254 | assert a.url == 'osc.udp://foo:1234/' 255 | assert a.hostname == 'foo' 256 | assert a.port == 1234 257 | assert a.protocol == liblo.UDP 258 | 259 | def testHostPort(self): 260 | a = liblo.Address('foo', 1234) 261 | assert a.url == 'osc.udp://foo:1234/' 262 | 263 | def testHostPortProto(self): 264 | a = liblo.Address('foo', 1234, liblo.TCP) 265 | assert a.url == 'osc.tcp://foo:1234/' 266 | 267 | 268 | if __name__ == "__main__": 269 | unittest.main() 270 | -------------------------------------------------------------------------------- /test/unit.py.patch: -------------------------------------------------------------------------------- 1 | 79c79 2 | < assert list(self.cb.args[0]) == [4, 8, 15, 16, 23, 42] 3 | --- 4 | > assert self.cb.args[0] == [4, 8, 15, 16, 23, 42] 5 | 99c99 6 | < assert list(self.cb.args[6]) == [12, 34, 56] 7 | --- 8 | > assert self.cb.args[6] == [12, 34, 56] 9 | --------------------------------------------------------------------------------