From 94589eb2ebf0a828d25903a3816e737d646e4df9 Mon Sep 17 00:00:00 2001 From: Alexander Kraus Date: Wed, 11 Oct 2023 08:37:00 +0200 Subject: [PATCH] feat(examples): add example playing sointu tracks from Python (#108) * Added Python code example. * Added pyinstaller build. * Clarified debugging steps in README.md. * Added linux implementation. * Cosmetics. * Updated README with correct steps. --- examples/code/Python/.gitignore | 7 + examples/code/Python/README.md | 18 ++ examples/code/Python/build.py | 149 ++++++++++++ examples/code/Python/poetry.lock | 132 +++++++++++ examples/code/Python/pyproject.toml | 27 +++ examples/code/Python/sointu.c | 212 ++++++++++++++++++ .../code/Python/sointu_python/__init__.py | 0 .../code/Python/sointu_python/__main__.py | 16 ++ .../Python/sointu_python/sointu_python.spec | 58 +++++ 9 files changed, 619 insertions(+) create mode 100644 examples/code/Python/.gitignore create mode 100644 examples/code/Python/README.md create mode 100644 examples/code/Python/build.py create mode 100644 examples/code/Python/poetry.lock create mode 100644 examples/code/Python/pyproject.toml create mode 100644 examples/code/Python/sointu.c create mode 100644 examples/code/Python/sointu_python/__init__.py create mode 100644 examples/code/Python/sointu_python/__main__.py create mode 100644 examples/code/Python/sointu_python/sointu_python.spec diff --git a/examples/code/Python/.gitignore b/examples/code/Python/.gitignore new file mode 100644 index 0000000..c690e6f --- /dev/null +++ b/examples/code/Python/.gitignore @@ -0,0 +1,7 @@ +.venv +build +dist +__pycache__ +setup.py +*.egg-info +*.so diff --git a/examples/code/Python/README.md b/examples/code/Python/README.md new file mode 100644 index 0000000..24e62c6 --- /dev/null +++ b/examples/code/Python/README.md @@ -0,0 +1,18 @@ +# Embed Sointu in Python +This is an example for embedding Sointu into Python code. + +# Configure the track +Edit the `track` variable in `build.py` according to your needs. + +# Build +* Install Python 3.11 and poetry. +* Download nasm and golang; place both of them in your system `PATH`. +* Enable cgo by downloading a gcc and placing it into your system `PATH`. +* Get the dependencies with `poetry install`. +* Run the player using `poetry run python -m sointu_python`. +* Pack everything into an executable using `poetry run pyinstaller sointu_python/sointu_python.spec`. The executable will be built in the `dist` subfolder. + +# Rebuild after changes +* Rebuild the example track bindings with `poetry build`. +* Update the bindings module with `poetry install`. +* Proceed iteration. diff --git a/examples/code/Python/build.py b/examples/code/Python/build.py new file mode 100644 index 0000000..477fc91 --- /dev/null +++ b/examples/code/Python/build.py @@ -0,0 +1,149 @@ +from distutils.command.build_ext import build_ext +from distutils.errors import ( + CCompilerError, + DistutilsExecError, + DistutilsPlatformError, +) +from distutils.core import Extension +from os.path import ( + dirname, + join, + abspath, + exists, + basename, + splitext, +) +from os import mkdir +from subprocess import run +from platform import system +from sys import exit + +track = "../../patches/physics_girl_st.yml" + +class BuildFailed(Exception): + pass + +class ExtBuilder(build_ext): + + def run(self): + try: + build_ext.run(self) + except (DistutilsPlatformError, FileNotFoundError): + raise BuildFailed('File not found. Could not compile C extension.') + + def build_extension(self, ext): + try: + build_ext.build_extension(self, ext) + except (CCompilerError, DistutilsExecError, DistutilsPlatformError, ValueError): + raise BuildFailed('Could not compile C extension.') + +def build(setup_kwargs): + # Make sure the build directory exists and setup the + # relative paths correctly. + cwd = abspath(".") + print("Running from:", cwd) + + current_source_dir = abspath(dirname(__file__)) + project_source_dir = abspath(join(current_source_dir, "..", "..", "..")) + current_binary_dir = join(current_source_dir, 'build') + if not exists(current_binary_dir): + mkdir(current_binary_dir) + host_is_windows = system() == "Windows" + executable_suffix = ".exe" if host_is_windows else "" + object_suffix = ".obj" if host_is_windows else ".o" + + # Build the sointu compiler first. + compiler_executable = join(current_binary_dir, "sointu-compile{}".format(executable_suffix)) + result = run( + args=[ + "go", "build", + "-o", compiler_executable, + "cmd/sointu-compile/main.go", + ], + cwd=project_source_dir, + shell=True if host_is_windows else False, + ) + if result.returncode != 0: + print("sointu-compile build process exited with:", result.returncode) + print(result.stdout) + exit(1) + + track_file_name = abspath(join(current_source_dir, track)) + (track_name_base, _) = splitext(basename(track_file_name)) + print("Compiling track:", track_file_name) + + # Compile the track. + sointu_compiler_arch = "amd64" + track_asm_file = join(current_binary_dir, '{}.asm'.format(track_name_base)) + result = run( + args=[ + compiler_executable, + "-o", track_asm_file, + "-arch={}".format(sointu_compiler_arch), + track_file_name, + ], + ) + if result.returncode != 0: + print("sointu-compile process exited with:", result.returncode) + print(result.stdout) + exit(1) + + # Assemble the track. + nasm_abi = "Win64" if host_is_windows else "Elf64" + track_object_file = join(current_binary_dir, '{}{}'.format(track_name_base, object_suffix)) + print("Assembling track asm source:", track_asm_file) + result = run( + args=[ + 'nasm', + '-o', track_object_file, + '-f', nasm_abi, + track_asm_file, + ], + ) + if result.returncode != 0: + print("nasm process exited with:", result.returncode) + print(result.stdout) + exit(1) + + # Export the plugin. + print("Linking object file into Python extension module:", track_object_file) + setup_kwargs.update({ + "ext_modules": [ + Extension( + "sointu", + include_dirs=[ + current_binary_dir, + current_source_dir, + ], + sources=[ + "sointu.c", + ], + extra_compile_args=[ + "-DTRACK_HEADER=\"{}.h\"".format(track_name_base), + ] + ([ + "-DWIN32", + ] if host_is_windows else [ + "-DUNIX", + "-fPIC", + ]), + extra_objects=[ + track_object_file, + ], + extra_link_args=[ + "dsound.lib", + "ws2_32.lib", + "ucrt.lib", + "user32.lib", + ] if host_is_windows else [ + "-z", "noexecstack", + "--no-pie", + "-lasound", + "-lpthread", + "-lpython3.11", + ], + ), + ], + "cmdclass": { + "build_ext": ExtBuilder, + }, + }) diff --git a/examples/code/Python/poetry.lock b/examples/code/Python/poetry.lock new file mode 100644 index 0000000..12c0b03 --- /dev/null +++ b/examples/code/Python/poetry.lock @@ -0,0 +1,132 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "altgraph" +version = "0.17.4" +description = "Python graph (network) package" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"}, + {file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"}, +] + +[[package]] +name = "macholib" +version = "1.16.3" +description = "Mach-O header analysis and editing" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"}, + {file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"}, +] + +[package.dependencies] +altgraph = ">=0.17" + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pefile" +version = "2023.2.7" +description = "Python PE parsing module" +category = "dev" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"}, + {file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"}, +] + +[[package]] +name = "pyinstaller" +version = "6.0.0" +description = "PyInstaller bundles a Python application and all its dependencies into a single package." +category = "dev" +optional = false +python-versions = "<3.13,>=3.8" +files = [ + {file = "pyinstaller-6.0.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:d84b06fb9002109bfc542e76860b81459a8585af0bbdabcfc5dcf272ef230de7"}, + {file = "pyinstaller-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:aa922d1d73881d0820a341d2c406a571cc94630bdcdc275427c844a12e6e376e"}, + {file = "pyinstaller-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:52e5b3a2371d7231de17515c7c78d8d4a39d70c8c095e71d55b3b83434a193a8"}, + {file = "pyinstaller-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4a75bde5cda259bb31f2294960d75b9d5c148001b2b0bd20a91f9c2116675a6c"}, + {file = "pyinstaller-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:5314f6f08d2bcbc031778618ba97d9098d106119c2e616b3b081171fe42f5415"}, + {file = "pyinstaller-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:0ad7cc3776ca17d0bededcc352cba2b1c89eb4817bfabaf05972b9da8c424935"}, + {file = "pyinstaller-6.0.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:cccdad6cfe7a5db7d7eb8df2e5678f8375268739d5933214e180da300aa54e37"}, + {file = "pyinstaller-6.0.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:fb6af82989dac7c58bd25ed9ba3323bc443f8c1f03804f69c9f5e363bf4a021c"}, + {file = "pyinstaller-6.0.0-py3-none-win32.whl", hash = "sha256:68769f5e6722474bb1038e35560444659db8b951388bfe0c669bb52a640cd0eb"}, + {file = "pyinstaller-6.0.0-py3-none-win_amd64.whl", hash = "sha256:438a9e0d72a57d5bba4f112d256e39ea4033c76c65414c0693d8311faa14b090"}, + {file = "pyinstaller-6.0.0-py3-none-win_arm64.whl", hash = "sha256:16a473065291dd7879bf596fa20e65bd9d1e8aafc2cef1bffa3e42e707e2e68e"}, + {file = "pyinstaller-6.0.0.tar.gz", hash = "sha256:d702cff041f30e7a53500b630e07b081e5328d4655023319253d73935e75ade2"}, +] + +[package.dependencies] +altgraph = "*" +macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} +packaging = ">=20.0" +pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} +pyinstaller-hooks-contrib = ">=2021.4" +pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} +setuptools = ">=42.0.0" + +[package.extras] +hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] + +[[package]] +name = "pyinstaller-hooks-contrib" +version = "2023.9" +description = "Community maintained hooks for PyInstaller" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyinstaller-hooks-contrib-2023.9.tar.gz", hash = "sha256:76084b5988e3957a9df169d2a935d65500136967e710ddebf57263f1a909cd80"}, + {file = "pyinstaller_hooks_contrib-2023.9-py2.py3-none-any.whl", hash = "sha256:f34f4c6807210025c8073ebe665f422a3aa2ac5f4c7ebf4c2a26cc77bebf63b5"}, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.2" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, + {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, +] + +[[package]] +name = "setuptools" +version = "68.2.2" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, + {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.11,<3.13" +content-hash = "797bde9c30c55b3ddb24b1d3eceedd093d8a89eb934e6fe8fe7191dc9247224d" diff --git a/examples/code/Python/pyproject.toml b/examples/code/Python/pyproject.toml new file mode 100644 index 0000000..4b5b838 --- /dev/null +++ b/examples/code/Python/pyproject.toml @@ -0,0 +1,27 @@ +[tool.poetry] +name = "sointu-python" +version = "0.1.0" +description = "Play back Sointu tracks in Python." +authors = ["Alexander Kraus "] +license = "MIT" +readme = "README.md" +packages = [ + { include = "sointu_python" }, +] +include = [ + { path = "sointu*.so", format="wheel" } +] + +[tool.poetry.build] +script = "build.py" +generate-setup-file = true + +[tool.poetry.dependencies] +python = ">=3.11,<3.13" + +[tool.poetry.group.dev.dependencies] +pyinstaller = "^6.0.0" + +[build-system] +requires = ["poetry-core>=1.0.0a3", "poetry>=0.12", "setuptools", "wheel"] +build-backend = "poetry.core.masonry.api" diff --git a/examples/code/Python/sointu.c b/examples/code/Python/sointu.c new file mode 100644 index 0000000..f3b5ed8 --- /dev/null +++ b/examples/code/Python/sointu.c @@ -0,0 +1,212 @@ +#define PY_SSIZE_T_CLEAN +#include "Python.h" + +#include TRACK_HEADER + +#include +#include +#include +#include + +SUsample sound_buffer[SU_LENGTH_IN_SAMPLES * SU_CHANNEL_COUNT]; +#ifdef WIN32 +#define WIN32_LEAN_AND_MEAN +#define WIN32_EXTRA_LEAN +#include +#include "mmsystem.h" +#include "mmreg.h" +#define CINTERFACE +#include + +static WAVEFORMATEX wave_format = { +#ifdef SU_SAMPLE_FLOAT + WAVE_FORMAT_IEEE_FLOAT, +#else + WAVE_FORMAT_PCM, +#endif + SU_CHANNEL_COUNT, + SU_SAMPLE_RATE, + SU_SAMPLE_RATE * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT, + SU_SAMPLE_SIZE * SU_CHANNEL_COUNT, + SU_SAMPLE_SIZE*8, + 0 +}; +DSBUFFERDESC buffer_description = { + sizeof(DSBUFFERDESC), + DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_GLOBALFOCUS | DSBCAPS_TRUEPLAYPOSITION, + SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT, + 0, + &wave_format, + 0 +}; + +static HWND hWnd; +static LPDIRECTSOUND direct_sound; +static LPDIRECTSOUNDBUFFER direct_sound_buffer; +static LPVOID p1; +static DWORD l1; +static DWORD last_play_cursor = 0; +#endif /* WIN32 */ + +#ifdef UNIX +#include +#include +#include + +static snd_pcm_t *pcm_handle; +static pthread_t render_thread; +static uint32_t render_thread_handle; +static pthread_t playback_thread; +static uint32_t playback_thread_handle; +snd_htimestamp_t start_ts; + +static int _snd_pcm_writei(void *params) { + (void) params; + snd_pcm_writei(pcm_handle, sound_buffer, SU_LENGTH_IN_SAMPLES); + return 0; +} +#endif /* UNIX */ + +static PyObject *sointuError; + +static PyObject *sointu_play_song(PyObject *self, PyObject *args) { +#ifdef WIN32 + +#ifdef SU_LOAD_GMDLS + su_load_gmdls(); +#endif // SU_LOAD_GMDLS + + hWnd = GetForegroundWindow(); + if(hWnd == NULL) { + hWnd = GetDesktopWindow(); + } + + DirectSoundCreate(0, &direct_sound, 0); + IDirectSound_SetCooperativeLevel(direct_sound, hWnd, DSSCL_PRIORITY); + IDirectSound_CreateSoundBuffer(direct_sound, &buffer_description, &direct_sound_buffer, NULL); + IDirectSoundBuffer_Lock(direct_sound_buffer, 0, SU_LENGTH_IN_SAMPLES * SU_CHANNEL_COUNT * SU_SAMPLE_SIZE, &p1, &l1, NULL, NULL, 0); + CreateThread(0, 0, (LPTHREAD_START_ROUTINE)su_render_song, p1, 0, 0); + IDirectSoundBuffer_Play(direct_sound_buffer, 0, 0, 0); + +#endif /* WIN32 */ + +#ifdef UNIX + render_thread_handle = pthread_create(&render_thread, 0, (void * (*)(void *))su_render_song, sound_buffer); + + // We can't start playing too early or the missing samples will be audible. + sleep(2.); + + // Play the track. + snd_pcm_open(&pcm_handle, "default", SND_PCM_STREAM_PLAYBACK, 0); + snd_pcm_set_params( + pcm_handle, +#ifdef SU_SAMPLE_FLOAT + SND_PCM_FORMAT_FLOAT, +#else // SU_SAMPLE_FLOAT + SND_PCM_FORMAT_S16_LE, +#endif // SU_SAMPLE_FLOAT + SND_PCM_ACCESS_RW_INTERLEAVED, + SU_CHANNEL_COUNT, + SU_SAMPLE_RATE, + 0, + SU_LENGTH_IN_SAMPLES + ); + + // Enable playback time querying. + snd_pcm_sw_params_t *swparams; + snd_pcm_sw_params_alloca(&swparams); + snd_pcm_sw_params_current(pcm_handle, swparams); + snd_pcm_sw_params_set_tstamp_mode(pcm_handle, swparams, SND_PCM_TSTAMP_ENABLE); + snd_pcm_sw_params_set_tstamp_type(pcm_handle, swparams, SND_PCM_TSTAMP_TYPE_GETTIMEOFDAY); + snd_pcm_sw_params(pcm_handle, swparams); + + playback_thread_handle = pthread_create(&playback_thread, 0, (void *(*)(void *))_snd_pcm_writei, 0); + + // Get the start time stamp. + snd_pcm_uframes_t avail; + snd_pcm_htimestamp(pcm_handle, &avail, &start_ts); +#endif /* UNIX */ + + return PyLong_FromLong(0); +} + +static PyObject *sointu_playback_position(PyObject *self, PyObject *args) { +#ifdef WIN32 + DWORD play_cursor = 0; + IDirectSoundBuffer_GetCurrentPosition(direct_sound_buffer, (DWORD*)&play_cursor, NULL); + return Py_BuildValue("i", play_cursor / SU_CHANNEL_COUNT / sizeof(SUsample)); +#endif /* WIN32 */ + +#ifdef UNIX + snd_htimestamp_t ts; + snd_pcm_uframes_t avail; + snd_pcm_htimestamp(pcm_handle, &avail, &ts); + + return Py_BuildValue("i", (int)((ts.tv_sec - start_ts.tv_sec + 1.e-9 * (ts.tv_nsec - start_ts.tv_nsec)) * SU_SAMPLE_RATE)); +#endif /* UNIX */ +} + +static PyObject *sointu_playback_finished(PyObject *self, PyObject *args) { + bool result = false; + +#ifdef WIN32 + DWORD play_cursor = 0; + IDirectSoundBuffer_GetCurrentPosition(direct_sound_buffer, (DWORD*)&play_cursor, NULL); + result = play_cursor < last_play_cursor; + last_play_cursor = play_cursor; +#endif /* WIN32 */ + +#ifdef UNIX + snd_htimestamp_t ts; + snd_pcm_uframes_t avail; + snd_pcm_htimestamp(pcm_handle, &avail, &ts); + + result = ts.tv_sec - start_ts.tv_sec < 0; +#endif /* UNIX */ + + return PyBool_FromLong(result); +} + +static PyObject *sointu_sample_rate(PyObject *self, PyObject *args) { + return Py_BuildValue("i", SU_SAMPLE_RATE); +} + +static PyObject *sointu_track_length(PyObject *self, PyObject *args) { + return Py_BuildValue("i", SU_LENGTH_IN_SAMPLES); +} + +static PyMethodDef sointuMethods[] = { + {"play_song", sointu_play_song, METH_VARARGS, "Play sointu track."}, + {"playback_position", sointu_playback_position, METH_VARARGS, "Get playback position of sointu track currently playing."}, + {"playback_finished", sointu_playback_finished, METH_VARARGS, "Check if currently playing sointu track has finished playing."}, + {"sample_rate", sointu_sample_rate, METH_VARARGS, "Return the sample rate of the track compiled into this module."}, + {"track_length", sointu_track_length, METH_VARARGS, "Return the track length in samples."}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +static struct PyModuleDef sointumodule = { + PyModuleDef_HEAD_INIT, + "sointu", + NULL, + -1, + sointuMethods +}; + +PyMODINIT_FUNC PyInit_sointu(void) { + PyObject *module = PyModule_Create(&sointumodule); + if(module == NULL) { + return NULL; + } + + sointuError = PyErr_NewException("sointu.sointuError", NULL, NULL); + Py_XINCREF(sointuError); + + if(PyModule_AddObject(module, "error", sointuError) < 0) { + Py_XDECREF(sointuError); + Py_CLEAR(sointuError); + Py_DECREF(module); + return NULL; + } + + return module; +} diff --git a/examples/code/Python/sointu_python/__init__.py b/examples/code/Python/sointu_python/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/code/Python/sointu_python/__main__.py b/examples/code/Python/sointu_python/__main__.py new file mode 100644 index 0000000..435902e --- /dev/null +++ b/examples/code/Python/sointu_python/__main__.py @@ -0,0 +1,16 @@ +from sointu import ( + play_song, + playback_position, + playback_finished, + sample_rate, + track_length, +) +from sys import exit + +if __name__ == '__main__': + play_song() + + while not playback_finished(): + print("Playback time:", playback_position() / sample_rate()) + + exit(0) diff --git a/examples/code/Python/sointu_python/sointu_python.spec b/examples/code/Python/sointu_python/sointu_python.spec new file mode 100644 index 0000000..1d8c075 --- /dev/null +++ b/examples/code/Python/sointu_python/sointu_python.spec @@ -0,0 +1,58 @@ +# -*- mode: python ; coding: utf-8 -*- +from os.path import abspath, join +from zipfile import ZipFile +from platform import system + +moduleName = 'sointu_python' +rootPath = abspath('.') +buildPath = join(rootPath, 'build') +distPath = join(rootPath, 'dist') +sourcePath = join(rootPath, moduleName) + +block_cipher = None + +a = Analysis( + [ + join(sourcePath, '__main__.py'), + ], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='{}'.format(moduleName), + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=None, +) + +exeFileName = '{}{}'.format(moduleName, '.exe' if system() == 'Windows' else '') +zipFileName = '{}-{}.zip'.format(moduleName, 'windows' if system() == 'Windows' else 'linux') +ZipFile(join(distPath, zipFileName), mode='w').write(join(distPath, exeFileName), arcname=exeFileName)