├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── examples ├── buffer-read.py ├── buffer-set.py ├── buffer-write.py ├── bus-example.py ├── group-example.py └── hello-world.py ├── setup.cfg ├── setup.py ├── supercollider ├── __init__.py ├── allocators.py ├── buffer.py ├── bus.py ├── exceptions.py ├── globals.py ├── group.py ├── server.py └── synth.py └── tests ├── __init__.py ├── shared.py ├── test_buffer.py ├── test_bus.py ├── test_group.py ├── test_server.py └── test_synth.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.pyc 3 | dist/ 4 | sdist/ 5 | *.egg-info/ 6 | .DS_Store 7 | *.swp 8 | *.pickle 9 | .idea 10 | .eggs 11 | test.scd 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v0.0.5](https://github.com/ideoforms/python-supercollider/releases/tag/v0.0.5) (2020-06-26) 4 | 5 | - Implement `Buffer.get`, to query samples from a Buffer 6 | 7 | ## [v0.0.4](https://github.com/ideoforms/python-supercollider/releases/tag/v0.0.4) (2019-10-15) 8 | 9 | - Fixed Linux dependencies (thanks to Dan Stowell) 10 | 11 | ## [v0.0.3](https://github.com/ideoforms/python-supercollider/releases/tag/v0.0.3) (2019-10-09) 12 | 13 | - Add `AudioBus`/`ControlBus` 14 | - Add further examples: `buffer-read`, `buffer-set`, `buffer-write`, `bus-example` 15 | 16 | ## [v0.0.2](https://github.com/ideoforms/python-supercollider/releases/tag/v0.0.2) (2019-10-07) 17 | 18 | - Add `Buffer` 19 | - Add `Group` 20 | - Add support for synchronous operations 21 | 22 | ## [v0.0.1](https://github.com/ideoforms/python-supercollider/releases/tag/v0.0.1) (2019-10-05) 23 | 24 | Minimum viable release. 25 | - Add `Server` 26 | - Add `Synth`, with support for get/set/free 27 | 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Tests 4 | 5 | To run unit tests: 6 | 7 | * start the SuperCollider server 8 | * store the following synthdef: `SynthDef(\sine, { |out = 0, freq = 440.0| Out.ar(out, SinOsc.ar(freq)); }).store;` 9 | * run `python3 setup.py test` 10 | 11 | ## Distribution 12 | 13 | To push to PyPi: 14 | 15 | * increment version in `setup.py` 16 | * tag and create GitHub release 17 | * `python3 setup.py sdist` 18 | * `twine upload dist/supercollider-x.y.z.tar.gz` 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) Daniel John Jones 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python client for SuperCollider 2 | 3 | A lightweight Python module to control the [SuperCollider](https://supercollider.github.io) audio synthesis engine. 4 | 5 | ## Installation 6 | 7 | To install the client library: 8 | 9 | ```python 10 | pip3 install supercollider 11 | ``` 12 | 13 | ## Usage 14 | 15 | Before using the library, start the SuperCollider server, either through the SuperCollider GUI or with `scsynth -u 57110`. 16 | 17 | Within the SC client, create the below SynthDef: 18 | 19 | ``` 20 | SynthDef(\sine, { |out = 0, freq = 440.0, gain = 0.0| 21 | Out.ar(out, SinOsc.ar(freq) * gain.dbamp); 22 | }).store; 23 | ``` 24 | 25 | From Python, you can now create and trigger Synths: 26 | 27 | ```python 28 | from supercollider import Server, Synth 29 | 30 | server = Server() 31 | 32 | synth = Synth(server, "sine", { "freq" : 440.0, "gain" : -12.0 }) 33 | synth.set("freq", 880.0) 34 | ``` 35 | 36 | For further examples, see [examples](https://github.com/ideoforms/python-supercollider/tree/master/examples). 37 | 38 | ## License 39 | 40 | This library is made available under the terms of the MIT license. 41 | 42 | ## Contributors 43 | 44 | - Thanks to [Itaborala](https://github.com/Itaborala) for porting from liblo to python-osc 45 | 46 | ## See also 47 | 48 | * If you want a more comprehensive framework that lets you construct and compile SynthDefs from Python, take a look at [Supriya](https://github.com/josiah-wolf-oberholtzer/supriya). 49 | * For an excellent Python + SuperCollider live coding environment, check out [FoxDot](https://foxdot.org) 50 | -------------------------------------------------------------------------------- /examples/buffer-read.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Example: Read and play an audio file. 5 | 6 | Before running this script, the SC server must be started, and the following 7 | SynthDef stored: 8 | 9 | SynthDef(\playbuf, { |out = 0, bufnum = 0, gain = 0.0, rate=1.0| 10 | var data = PlayBuf.ar(1, bufnum, rate: rate, loop: 1) * gain.dbamp; 11 | Out.ar(out, Pan2.ar(data)); 12 | }).store; 13 | 14 | You will also need to download some sample audio: 15 | curl https://nssdc.gsfc.nasa.gov/planetary/sound/apollo_13_problem.wav -o apollo.wav 16 | """ 17 | 18 | AUDIO_FILE = "apollo.wav" 19 | 20 | from supercollider import Server, Synth, Buffer 21 | import time 22 | 23 | #------------------------------------------------------------------------------- 24 | # Create connection to default server on localhost:57110 25 | #------------------------------------------------------------------------------- 26 | server = Server() 27 | 28 | #------------------------------------------------------------------------------- 29 | # Read sample data into a Buffer. 30 | #------------------------------------------------------------------------------- 31 | buf = Buffer.read(server, AUDIO_FILE) 32 | buf_info = buf.get_info() 33 | print("Read buffer, sample rate %d, duration %.1fs" % (buf_info["sample_rate"], buf_info["num_frames"] / buf_info["sample_rate"])) 34 | 35 | #------------------------------------------------------------------------------- 36 | # Calculate the required playback rate (akin to SC BufRateScale.kr) 37 | # and begin playback. 38 | #------------------------------------------------------------------------------- 39 | server_status = server.get_status() 40 | server_sample_rate = server_status["sample_rate_nominal"] 41 | buffer_sample_rate = buf_info["sample_rate"] 42 | print("Server sample rate: %d" % server_sample_rate) 43 | print("Buffer sample rate: %d" % buffer_sample_rate) 44 | rate_scale = buffer_sample_rate / server_sample_rate 45 | synth = Synth(server, 'playbuf', {"buffer": buf, "rate": rate_scale}) 46 | 47 | try: 48 | while True: 49 | time.sleep(0.1) 50 | except KeyboardInterrupt: 51 | synth.free() 52 | buf.free() 53 | -------------------------------------------------------------------------------- /examples/buffer-set.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Example: Buffer creation and playback. 5 | 6 | Before running this script, the SC server must be started, and the following 7 | SynthDef stored: 8 | 9 | SynthDef(\playbuf, { |out = 0, bufnum = 0, gain = 0.0| 10 | var data = PlayBuf.ar(1, bufnum, loop: 1) * gain.dbamp; 11 | Out.ar(out, Pan2.ar(data)); 12 | }).store; 13 | """ 14 | 15 | from supercollider import Server, Synth, Buffer 16 | 17 | import time 18 | import math 19 | import random 20 | 21 | #------------------------------------------------------------------------------- 22 | # Create connection to default server on localhost:57110 23 | #------------------------------------------------------------------------------- 24 | server = Server() 25 | 26 | #------------------------------------------------------------------------------- 27 | # Create a Buffer, loop playback, and periodically rewrite its contents 28 | # with uniformly random samples. 29 | #------------------------------------------------------------------------------- 30 | buf = Buffer.alloc(server, 1024) 31 | print("Created buffer: %s" % buf.get_info()) 32 | 33 | synth = Synth(server, 'playbuf', {"buffer": buf, "gain": -18.0}) 34 | try: 35 | while True: 36 | buf.set([random.uniform(-1, 1) for n in range(buf.num_frames)]) 37 | time.sleep(0.1) 38 | except KeyboardInterrupt: 39 | synth.free() 40 | buf.free() 41 | -------------------------------------------------------------------------------- /examples/buffer-write.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Example: Write the contents of a Buffer to disk. 5 | """ 6 | 7 | from supercollider import Server, Synth, Buffer, HEADER_FORMAT_WAV 8 | import math 9 | 10 | OUTPUT_FILE = "/tmp/440.wav" 11 | 12 | #------------------------------------------------------------------------------- 13 | # Create connection to default server on localhost:57110 14 | #------------------------------------------------------------------------------- 15 | server = Server() 16 | 17 | #------------------------------------------------------------------------------- 18 | # Create a Buffer, generate a 440Hz sine, and write to a .wav. 19 | #------------------------------------------------------------------------------- 20 | length = 1024 21 | server_status = server.get_status() 22 | sample_rate = server_status["sample_rate_nominal"] 23 | 24 | buf = Buffer.alloc(server, length) 25 | buf.set([math.sin(n * math.pi * 2.0 * 440.0 / sample_rate) for n in range(int(length))]) 26 | buf.write(OUTPUT_FILE, HEADER_FORMAT_WAV) 27 | print("Written buffer to %s" % OUTPUT_FILE) 28 | -------------------------------------------------------------------------------- /examples/bus-example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Example: Audio routing with buses. 5 | 6 | Before running this script, the SC server must be started, and the following 7 | SynthDefs stored: 8 | 9 | SynthDef(\dust, { |out = 0, gain = 0.0| 10 | var data = Dust.ar(10) * gain.dbamp; 11 | Out.ar(out, Pan2.ar(data)); 12 | }).store; 13 | 14 | SynthDef(\reverb, { |in = 0, out = 2| 15 | var data = In.ar(in, 2); 16 | data = FreeVerb.ar(data, 0.7, 0.8, 0.5); 17 | Out.ar(out, data); 18 | }).store; 19 | """ 20 | 21 | from supercollider import Server, Synth, Buffer, AudioBus, ADD_TO_TAIL 22 | 23 | import time 24 | 25 | #------------------------------------------------------------------------------- 26 | # Create connection to default server on localhost:57110 27 | #------------------------------------------------------------------------------- 28 | server = Server() 29 | 30 | #------------------------------------------------------------------------------- 31 | # Create an audio bus to route audio between two synths. 32 | #------------------------------------------------------------------------------- 33 | bus = AudioBus(server, 2) 34 | synth = Synth(server, 'dust', {"out": bus}) 35 | reverb = Synth(server, 'reverb', {"in": bus, "out": 0}, target=server, action=ADD_TO_TAIL) 36 | 37 | try: 38 | while True: 39 | time.sleep(0.1) 40 | except KeyboardInterrupt: 41 | bus.free() 42 | synth.free() 43 | reverb.free() 44 | -------------------------------------------------------------------------------- /examples/group-example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Slightly contrived approach to additive synthesis, 5 | demonstrating how a Group can be used to contain 6 | multiple Synths. 7 | 8 | Requires the \sine synth defined in hello-world.py. 9 | """ 10 | 11 | fundamental_min = 220 12 | fundamental_max = 1760 13 | partial_count = 5 14 | partial_attenuate = -6.0 15 | tone_duration = 0.1 16 | 17 | from supercollider import Server, Group, Synth 18 | 19 | import time 20 | import random 21 | 22 | def create_group(freq): 23 | group = Group(server) 24 | for n in range(1, partial_count + 1): 25 | Synth(server, "sine", { 26 | "freq": freq * n * random.uniform(0.99, 1.01), 27 | "gain": partial_attenuate * n 28 | }, target=group) 29 | return group 30 | 31 | server = Server() 32 | 33 | try: 34 | while True: 35 | freq = random.uniform(fundamental_min, fundamental_max) 36 | group = create_group(freq) 37 | time.sleep(tone_duration) 38 | group.free() 39 | except KeyboardInterrupt: 40 | group.free() 41 | -------------------------------------------------------------------------------- /examples/hello-world.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Example: Create a sine wave synth. 5 | 6 | Before running this script, the SC server must be started, and the following 7 | SynthDef stored: 8 | 9 | SynthDef(\sine, { |out = 0, freq = 440.0, gain = 0.0| 10 | Out.ar(out, SinOsc.ar(freq) * gain.dbamp); 11 | }).store; 12 | """ 13 | 14 | from supercollider import Server, Synth 15 | import time 16 | 17 | #------------------------------------------------------------------------------- 18 | # Create connection to default server on localhost:57110 19 | #------------------------------------------------------------------------------- 20 | server = Server() 21 | print("Created server") 22 | print("Server version:") 23 | version = server.get_version() 24 | for key, value in version.items(): 25 | print(f" - {key}: {value}") 26 | 27 | #------------------------------------------------------------------------------- 28 | # Create a Synth, set its parameter, query the parameter, and free it. 29 | #------------------------------------------------------------------------------- 30 | try: 31 | synth = Synth(server, "sine", {"freq": 440.0, "gain": -18.0}) 32 | print("Created synth") 33 | print("Frequency: %.1f" % synth.get("freq")) 34 | while True: 35 | time.sleep(0.1) 36 | except KeyboardInterrupt: 37 | synth.free() 38 | print("Freed synth") 39 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [tool:pytest] 5 | addopts = --verbose 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name='supercollider', 7 | version='0.0.6', 8 | python_requires='>=3.9', 9 | description='Interact with the SuperCollider audio synthesis engine', 10 | long_description=open("README.md", "r").read(), 11 | long_description_content_type="text/markdown", 12 | author='Daniel Jones', 13 | author_email='dan-code@erase.net', 14 | url='https://github.com/ideoforms/supercollider', 15 | packages=['supercollider'], 16 | install_requires=['python-osc'], 17 | keywords=('sound', 'music', 'supercollider', 'synthesis'), 18 | classifiers=[ 19 | 'Topic :: Multimedia :: Sound/Audio', 20 | 'Topic :: Multimedia :: Sound/Audio :: Sound Synthesis', 21 | 'Topic :: Artistic Software', 22 | 'Topic :: Communications', 23 | 'Development Status :: 4 - Beta', 24 | 'Intended Audience :: Developers' 25 | ], 26 | setup_requires=['pytest-runner'], 27 | tests_require=['pytest'] 28 | ) 29 | -------------------------------------------------------------------------------- /supercollider/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Control the SuperCollider audio synthesis server. 3 | 4 | Example: 5 | 6 | from supercollider import Server, Synth 7 | server = Server() 8 | synth = Synth(server, 'sine', { 'freq' : 440.0 }) 9 | synth.free() 10 | 11 | """ 12 | 13 | __author__ = "Daniel Jones " 14 | __all__ = ["Server", "Synth", "Group", "Buffer"] 15 | __all__ += ["SuperColliderConnectionError"] 16 | __all__ += ["ADD_AFTER", "ADD_BEFORE", "ADD_REPLACE", "ADD_TO_HEAD", "ADD_TO_TAIL"] 17 | __all__ += ["HEADER_FORMAT_WAV", "HEADER_FORMAT_AIFF", "HEADER_FORMAT_IRCAM", "HEADER_FORMAT_NEXT", "HEADER_FORMAT_RAW"] 18 | __all__ += ["SAMPLE_FORMAT_FLOAT", "SAMPLE_FORMAT_ALAW", "SAMPLE_FORMAT_DOUBLE", "SAMPLE_FORMAT_INT8", "SAMPLE_FORMAT_INT16", "SAMPLE_FORMAT_INT24", "SAMPLE_FORMAT_INT32", "SAMPLE_FORMAT_MULAW"] 19 | 20 | from .server import Server 21 | from .synth import Synth 22 | from .group import Group 23 | from .buffer import Buffer 24 | from .bus import ControlBus, AudioBus 25 | from .exceptions import SuperColliderConnectionError, SuperColliderAllocationError 26 | 27 | from .globals import ADD_AFTER, ADD_BEFORE, ADD_REPLACE, ADD_TO_HEAD, ADD_TO_TAIL 28 | from .globals import HEADER_FORMAT_WAV, HEADER_FORMAT_AIFF, HEADER_FORMAT_IRCAM, HEADER_FORMAT_NEXT, HEADER_FORMAT_RAW 29 | from .globals import SAMPLE_FORMAT_FLOAT, SAMPLE_FORMAT_ALAW, SAMPLE_FORMAT_DOUBLE, SAMPLE_FORMAT_INT8, SAMPLE_FORMAT_INT16, SAMPLE_FORMAT_INT24, SAMPLE_FORMAT_INT32, SAMPLE_FORMAT_MULAW 30 | -------------------------------------------------------------------------------- /supercollider/allocators.py: -------------------------------------------------------------------------------- 1 | from . import globals 2 | from .exceptions import SuperColliderAllocationError 3 | 4 | class Allocator: 5 | def __init__(self, resource_name="generic"): 6 | self.index = globals.ALLOCATOR_BUS_START_INDEX 7 | self.resource_name = resource_name 8 | self.capacity = globals.ALLOCATOR_BUS_CAPACITY 9 | 10 | def allocate(self, channels): 11 | if self.index <= globals.ALLOCATOR_BUS_START_INDEX + self.capacity - channels: 12 | rv = self.index 13 | self.index += channels 14 | return rv 15 | else: 16 | raise SuperColliderAllocationError("No more %s resources available" % self.resource_name) 17 | 18 | def free(self, index): 19 | # TODO: Implement me 20 | pass 21 | -------------------------------------------------------------------------------- /supercollider/buffer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from . import globals 3 | from .globals import SAMPLE_FORMAT_FLOAT 4 | from .globals import HEADER_FORMAT_WAV 5 | from typing import TYPE_CHECKING, Callable 6 | import os 7 | 8 | if TYPE_CHECKING: 9 | from .server import Server 10 | 11 | class Buffer: 12 | def __init__(self, 13 | server: Server, 14 | id: int = 0): 15 | """ 16 | Creates a Buffer object, but does not allocate any memory for it. This constructor should only be used if 17 | you want to create an object to interface with an already-created buffer. 18 | 19 | If the `id` passed is None, the created Buffer will have an automatically-allocated id. 20 | 21 | Args: 22 | server (Server): The SC server on which the Group is created. 23 | id (int): The buffer's unique ID, or None to auto-allocate. 24 | """ 25 | self.server = server 26 | self.num_frames = None 27 | self.num_channels = None 28 | 29 | if id is None: 30 | self.id = globals.LAST_BUFFER_ID 31 | globals.LAST_BUFFER_ID += 1 32 | else: 33 | self.id = id 34 | 35 | @classmethod 36 | def alloc(cls, server: Server, num_frames: int, num_channels: int = 1, blocking: bool = True): 37 | """ 38 | Create and allocate a new Buffer. 39 | 40 | Args: 41 | server (Server): The SC server on which the Buffer is allocated. 42 | num_frames (int): The number of frames to allocate. 43 | num_channels (int): The number of channels in the buffer. 44 | blocking (bool): Wait for the alloc task to complete before returning. 45 | 46 | Returns: 47 | A new Buffer object. 48 | """ 49 | buf = Buffer(server, id=None) 50 | buf.num_frames = num_frames 51 | buf.num_channels = num_channels 52 | buf.server._send_msg("/b_alloc", buf.id, num_frames, num_channels) 53 | 54 | if blocking: 55 | buf.server._await_response("/done", ["/b_alloc", buf.id]) 56 | 57 | return buf 58 | 59 | @classmethod 60 | def read(cls, 61 | server: Server, 62 | path: str, 63 | start_frame: int = 0, 64 | num_frames: int = 0, 65 | blocking: bool = True) -> Buffer: 66 | """ 67 | Create a new Buffer and read its contents from disk. 68 | 69 | Args: 70 | server (Server): The SC server on which the Buffer is created. 71 | path (str): The pathname to the audio file to read. 72 | start_frame (int): The frame index to start reading from. 73 | num_frames (int): The number of frames to read. 74 | blocking (bool): Wait for the read task to complete before returning. 75 | 76 | Returns: 77 | A new Buffer object. 78 | 79 | Raises: 80 | FileNotFoundError: If the path does not exist. 81 | """ 82 | if not os.path.exists(path): 83 | raise FileNotFoundError("File not found: %s" % path) 84 | 85 | if not path.startswith("/"): 86 | path = os.path.abspath(path) 87 | buf = Buffer(server, id=None) 88 | buf.server._send_msg("/b_allocRead", buf.id, path, start_frame, num_frames) 89 | 90 | def _handler(address, *args): 91 | return args 92 | 93 | if blocking: 94 | buf.server._await_response("/done", ["/b_allocRead", buf.id], _handler) 95 | 96 | return buf 97 | 98 | def write(self, 99 | path: str, 100 | header_format: int = HEADER_FORMAT_WAV, 101 | sample_format: int = SAMPLE_FORMAT_FLOAT, 102 | num_frames: int = -1, 103 | start_frame: int = 0, 104 | leave_open: bool = False, 105 | blocking: bool = True): 106 | """ 107 | Write the Buffer's contents to an audio file. 108 | 109 | Args: 110 | path (str): Pathname to the audio file to write. 111 | header_format (str): Format of the file. See `supercollider.globals` for supported formats. 112 | sample_format (str): Format of the audio samples. See `supercollider.globals` for supported formats. 113 | num_frames (int): The number of frames to write. Defaults to the full buffer. 114 | start_frame (int): Index of the first frame to write. 115 | leave_open (bool): Whether to leave the file open after write. 116 | blocking (bool): Wait for the write task to complete before returning. 117 | """ 118 | self.server._send_msg("/b_write", self.id, path, header_format, sample_format, 119 | num_frames, start_frame, int(leave_open)) 120 | 121 | if blocking: 122 | self.server._await_response("/done", ["/b_write", self.id]) 123 | 124 | def get(self, start_index: int = 0, count: int = 1024) -> list[float]: 125 | """ 126 | Query the Buffer's contents. 127 | 128 | Note that, as per the SuperCollider Command Reference, this is not designed to query 129 | a lot of samples. 130 | 131 | https://doc.sccode.org/Reference/Server-Command-Reference.html 132 | 133 | Args: 134 | start_index (int): Index of first frame in the Buffer to read from. 135 | count (int): Number of samples to retrieve. 136 | """ 137 | 138 | def _handler(address, *args): 139 | return args[3:] 140 | 141 | self.server._send_msg("/b_getn", self.id, start_index, count) 142 | return self.server._await_response("/b_setn", [self.id], _handler) 143 | 144 | def set(self, samples: list[float], start_index: int = 0): 145 | """ 146 | Set the Buffer's contents to the values given in the supplied float array. 147 | 148 | Args: 149 | samples (List[float]): Array of floats to write to the Buffer. 150 | start_index (int): Index of first frame in the Buffer to write to. 151 | """ 152 | self.server._send_msg("/b_setn", self.id, start_index, len(samples), *samples) 153 | 154 | def fill(self, count: int, value: float, start_index: int = 0): 155 | """ 156 | Fill the Buffer's contents with a specified sample. 157 | 158 | Args: 159 | count (int): The number of frames to write. 160 | value (float): The sample to write. 161 | start_index (int): Index of first frame in the Buffer to write to. 162 | """ 163 | self.server._send_msg("/b_fill", self.id, start_index, count, value) 164 | 165 | def free(self): 166 | """ 167 | Free the buffer. 168 | """ 169 | self.server._send_msg("/b_free", self.id) 170 | 171 | def get_info(self, callback: Callable = None, blocking: bool = True): 172 | """ 173 | Returns info about the Buffer. 174 | 175 | Example: 176 | >>> buffer.info 177 | {'num_frames': 1024, 'num_channels': 1, 'sample_rate': 44100.0} 178 | """ 179 | 180 | def _handler(address, *args): 181 | rv = { 182 | "num_frames": args[1], 183 | "num_channels": args[2], 184 | "sample_rate": args[3] 185 | } 186 | 187 | return rv 188 | 189 | self.server._send_msg("/b_query", self.id) 190 | if blocking: 191 | return self.server._await_response("/b_info", [self.id], _handler) 192 | else: 193 | self.server.dispatcher.map("/b_info", lambda *args: callback(_handler(*args))) 194 | -------------------------------------------------------------------------------- /supercollider/bus.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from . import globals 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from .server import Server 7 | 8 | class Bus: 9 | def __init__(self, server: Server, channels: int): 10 | self.server = server 11 | self.channels = channels 12 | self.id = None 13 | 14 | def free(self): 15 | pass 16 | 17 | class ControlBus(Bus): 18 | def __init__(self, server, channels): 19 | super(type(self), self).__init__(server, channels) 20 | self.id = globals.CONTROL_BUS_ALLOCATOR.allocate(channels) 21 | 22 | def free(self): 23 | globals.CONTROL_BUS_ALLOCATOR.free(self.id) 24 | 25 | class AudioBus(Bus): 26 | def __init__(self, server, channels): 27 | super(type(self), self).__init__(server, channels) 28 | self.id = globals.AUDIO_BUS_ALLOCATOR.allocate(channels) 29 | 30 | def free(self): 31 | globals.AUDIO_BUS_ALLOCATOR.free(self.id) 32 | -------------------------------------------------------------------------------- /supercollider/exceptions.py: -------------------------------------------------------------------------------- 1 | class SuperColliderConnectionError (Exception): 2 | pass 3 | 4 | class SuperColliderAllocationError (Exception): 5 | pass 6 | -------------------------------------------------------------------------------- /supercollider/globals.py: -------------------------------------------------------------------------------- 1 | from .allocators import Allocator 2 | 3 | ALLOCATOR_BUS_START_INDEX = 32 4 | ALLOCATOR_BUS_CAPACITY = 1024 5 | 6 | CONTROL_BUS_ALLOCATOR = Allocator("control") 7 | AUDIO_BUS_ALLOCATOR = Allocator("audio") 8 | 9 | LAST_NODE_ID = 1000 10 | LAST_BUFFER_ID = 0 11 | RESPONSE_TIMEOUT = 0.25 12 | 13 | ADD_TO_HEAD = 0 14 | ADD_TO_TAIL = 1 15 | ADD_AFTER = 2 16 | ADD_BEFORE = 3 17 | ADD_REPLACE = 4 18 | 19 | HEADER_FORMAT_WAV = "wav" 20 | HEADER_FORMAT_AIFF = "aiff" 21 | HEADER_FORMAT_RAW = "raw" 22 | HEADER_FORMAT_IRCAM = "ircam" 23 | HEADER_FORMAT_NEXT = "next" 24 | 25 | SAMPLE_FORMAT_INT8 = "int8" 26 | SAMPLE_FORMAT_INT16 = "int16" 27 | SAMPLE_FORMAT_INT24 = "int24" 28 | SAMPLE_FORMAT_INT32 = "int32" 29 | SAMPLE_FORMAT_FLOAT = "float" 30 | SAMPLE_FORMAT_DOUBLE = "double" 31 | SAMPLE_FORMAT_MULAW = "mulaw" 32 | SAMPLE_FORMAT_ALAW = "alaw" 33 | -------------------------------------------------------------------------------- /supercollider/group.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from . import globals 4 | from typing import TYPE_CHECKING 5 | 6 | if TYPE_CHECKING: 7 | from .server import Server 8 | 9 | class Group: 10 | def __init__(self, 11 | server: Server, 12 | action: int = 0, 13 | target: int = 0): 14 | """ 15 | Create a new Group. 16 | 17 | Args: 18 | server (Server): The SC server on which the Group is created. 19 | target (int): The Group to create the Group in, default 0. 20 | action (int): The add action. 21 | """ 22 | self.server = server 23 | self.id = globals.LAST_NODE_ID 24 | globals.LAST_NODE_ID += 1 25 | 26 | self.server._send_msg("/g_new", self.id, action, target) 27 | 28 | def free(self) -> None: 29 | """ 30 | Free the group and all Synths within it. 31 | """ 32 | # /g_deepFree does not free the group itself; must also call /n_free. 33 | self.server._send_msg("/g_deepFree", self.id) 34 | self.server._send_msg("/n_free", self.id) 35 | -------------------------------------------------------------------------------- /supercollider/server.py: -------------------------------------------------------------------------------- 1 | from pythonosc.osc_server import ThreadingOSCUDPServer 2 | from pythonosc.udp_client import SimpleUDPClient 3 | from pythonosc.dispatcher import Dispatcher 4 | from threading import Thread, Event 5 | from .exceptions import SuperColliderConnectionError 6 | from typing import Optional, Callable 7 | from . import globals 8 | import logging 9 | import socket 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | class Server: 14 | def __init__(self, hostname: str = "127.0.0.1", port: int = 57110): 15 | """ 16 | Create a new Server object, which is a local representation of a remote 17 | SuperCollider server. 18 | 19 | Supercollider communication is (unfortunatelly for UDP) made in the same 20 | port used by the client. Hence, the OSC server and the UDP client should 21 | share the same port. Setting this up is possible, but slightly harder. 22 | Check this github issue to see how this is possible with python-osc: 23 | https://github.com/attwad/python-osc/issues/41 24 | 25 | Args: 26 | hostname (str): Hostname or IP address of the server 27 | port (int): Port of the server 28 | """ 29 | 30 | # UDP Client for sending messages 31 | self.client_address = (hostname, port) 32 | self.sc_client = SimpleUDPClient(hostname, port) 33 | self.sc_client._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 34 | self.sc_client._sock.bind(('', 0)) 35 | 36 | # OSC Server for receiving messages. 37 | self.dispatcher = Dispatcher() 38 | self.osc_server_address = ("127.0.0.1", self.sc_client._sock.getsockname()[1]) 39 | ThreadingOSCUDPServer.allow_reuse_address = True 40 | self.osc_server = ThreadingOSCUDPServer(self.osc_server_address, self.dispatcher) 41 | 42 | self.osc_server_thread = Thread(target=self._osc_server_listen, daemon=True) 43 | self.osc_server_thread.start() 44 | 45 | # For callback timeouts 46 | self.event = Event() 47 | 48 | # SC node ID for Add actions 49 | self.id = 0 50 | 51 | self.sync() 52 | 53 | #-------------------------------------------------------------------------------- 54 | # Client messages 55 | #-------------------------------------------------------------------------------- 56 | 57 | def _send_msg(self, address: str, *args) -> None: 58 | self.sc_client.send_message(address, [*args]) 59 | 60 | def sync(self, ping_id: int = 1): 61 | def _handler(address, *args): 62 | return args 63 | 64 | self._send_msg("/sync", ping_id) 65 | return self._await_response("/synced", None, _handler) 66 | 67 | def query_tree(self, group=None): 68 | def _handler(address, *args): 69 | return args 70 | 71 | self._send_msg("/g_queryTree", group.id if group else 0, 0) 72 | return self._await_response("/g_queryTree.reply", callback=_handler) 73 | 74 | def get_status(self): 75 | """ 76 | Query the current Server status, including the number of active units, CPU 77 | load, etc. 78 | 79 | Example: 80 | >>> server.status 81 | { 82 | 'num_ugens': 5, 83 | 'num_synths': 1, 84 | 'num_groups': 2, 85 | 'num_synthdefs': 107, 86 | 'cpu_average': 0.08170516043901443, 87 | 'cpu_peak': 0.34912213683128357, 88 | 'sample_rate_nominal': 44100.0, 89 | 'sample_rate_actual': 44100.07866992249 90 | } 91 | """ 92 | 93 | def _handler(address, *args): 94 | status_dict = { 95 | "num_ugens": args[1], 96 | "num_synths": args[2], 97 | "num_groups": args[3], 98 | "num_synthdefs": args[4], 99 | "cpu_average": args[5], 100 | "cpu_peak": args[6], 101 | "sample_rate_nominal": args[7], 102 | "sample_rate_actual": args[8]} 103 | 104 | return status_dict 105 | 106 | self._send_msg("/status") 107 | 108 | return self._await_response("/status.reply", None, _handler) 109 | 110 | def get_version(self) -> dict: 111 | """ 112 | Returns the current Server version. 113 | 114 | Example: 115 | >>> server.version 116 | { 117 | 'program_name': "scsynth", 118 | 'version_major': 3, 119 | 'version_minor': 10, 120 | 'version_patch': ".3", 121 | 'git_branch': "HEAD", 122 | 'commit_hash': "67a1eb18" 123 | } 124 | """ 125 | 126 | def _handler(*args): 127 | version_dict = { 128 | "program_name": args[1], 129 | "version_major": args[2], 130 | "version_minor": args[3], 131 | "version_patch": args[4], 132 | "git_branch": args[5], 133 | "commit_hash": args[6]} 134 | 135 | return version_dict 136 | 137 | self._send_msg("/version") 138 | 139 | return self._await_response("/version.reply", None, _handler) 140 | 141 | #-------------------------------------------------------------------------------- 142 | # Callback timeout logic 143 | #-------------------------------------------------------------------------------- 144 | 145 | def _await_response(self, 146 | address: str, 147 | match_args=(), 148 | callback: Optional[Callable] = None) -> list: 149 | rv = None 150 | 151 | # Sets the thread event when message is received 152 | # It also overwrites the callback (dispatcher) function to capture the return value. 153 | # This is necessary because the dispatcher handler can't return anything. 154 | # Make sure to unmap the callback before it is overwritten to avoid duplicate callbacks. 155 | def _callback_with_timeout(*args): 156 | self.event.set() 157 | if callback: 158 | nonlocal rv 159 | rv = callback(address, *args[1:]) 160 | 161 | self.dispatcher.map(address, _callback_with_timeout) 162 | 163 | responded_before_timeout = self.event.wait(globals.RESPONSE_TIMEOUT) 164 | if responded_before_timeout: 165 | self.event.clear() 166 | else: 167 | raise SuperColliderConnectionError("Connection to SuperCollider server timed out. Is scsynth running?") 168 | 169 | self.dispatcher.unmap(address, _callback_with_timeout) 170 | 171 | return rv 172 | 173 | #-------------------------------------------------------------------------------- 174 | # OSC server thread 175 | #-------------------------------------------------------------------------------- 176 | 177 | def _osc_server_listen(self): 178 | logger.debug(f'Listening for OSC messages on @ {self.osc_server_address}') 179 | self.osc_server.serve_forever() 180 | logger.warning(f'OSC server @ {self.osc_server_address} stopped') 181 | 182 | def clear_all_handlers(self): 183 | """ 184 | Useful for cleaning up after using handlers with blocking=False. 185 | """ 186 | for address, handlers in self.dispatcher._map.items(): 187 | for handler in handlers.copy(): 188 | self.dispatcher.unmap(address, handler) 189 | -------------------------------------------------------------------------------- /supercollider/synth.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union, Callable 2 | from . import globals 3 | from .buffer import Buffer 4 | from .bus import Bus 5 | from .server import Server 6 | 7 | class Synth: 8 | def __init__(self, 9 | server: Server, 10 | name: str, 11 | args: dict = None, 12 | action: int = globals.ADD_TO_HEAD, 13 | target: Optional[int] = None): 14 | """ 15 | Create a new SuperCollider Synth object. 16 | 17 | Args: 18 | server (Server): The SC server on which the Synth is created. 19 | name (str): The name of the SynthDef. 20 | args (dict): A dict of parameters and values. 21 | target (int): The Group to create the Synth in, default 0. 22 | action (int): The add action. See supercollider.globals for available actions. 23 | """ 24 | self.server = server 25 | self.name = name 26 | self.args = args 27 | self.id = globals.LAST_NODE_ID 28 | globals.LAST_NODE_ID += 1 29 | 30 | args_list = [] 31 | if args: 32 | for item, value in args.items(): 33 | # TODO: Move this to a more general place (so `set` can also work with Bus/Buffer objects) 34 | if isinstance(value, Buffer): 35 | args_list += [item, value.id] 36 | elif isinstance(value, Bus): 37 | args_list += [item, value.id] 38 | else: 39 | args_list += [item, value] 40 | 41 | target_id = target.id if target else 0 42 | self.server._send_msg("/s_new", self.name, self.id, action, target_id, *args_list) 43 | 44 | def set(self, 45 | parameter: str, 46 | value: Union[int, float, str]) -> None: 47 | """ 48 | Set a named parameter of the Synth. 49 | 50 | Args: 51 | parameter (str): The parameter name. 52 | value: The value. Can be of type int, float, str. 53 | """ 54 | self.server._send_msg("/n_set", self.id, parameter, value) 55 | 56 | def get(self, 57 | parameter: str, 58 | callback: Optional[Callable] = None, 59 | blocking: bool = True) -> Union[int, float, str]: 60 | """ 61 | Get the current value of a named parameter of the Synth. 62 | 63 | Args: 64 | parameter (str): The name of the parameter to query. 65 | callback (function): Called when the return value is received from the SC server. 66 | blocking (bool): Set to False to query the value asynchronously and return None. 67 | 68 | Example: 69 | >>> synth.get("freq") 70 | 440.0 71 | """ 72 | 73 | def _handler(_, *args): 74 | return args[2] 75 | 76 | self.server._send_msg("/s_get", self.id, parameter) 77 | 78 | if blocking: 79 | rv = self.server._await_response("/n_set", [self.id, parameter], _handler) 80 | return rv 81 | else: 82 | self.server.dispatcher.map("/n_set", lambda *args: callback(_handler(*args))) 83 | 84 | def free(self): 85 | """ 86 | Free the Synth. 87 | """ 88 | self.server._send_msg("/n_free", self.id) 89 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------ 2 | # This file is included so that pytest can find the package 3 | # root and import the `supercollider` module from a local relative path. 4 | # 5 | # https://docs.pytest.org/en/latest/goodpractices.html#test-package-name 6 | #------------------------------------------------------------------------ 7 | -------------------------------------------------------------------------------- /tests/shared.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import supercollider 3 | 4 | SC_DUMMY_PORT = 57999 5 | SC_REAL_PORT = 57110 6 | 7 | @pytest.fixture(scope="module") 8 | def server(): 9 | server = supercollider.Server(port=SC_REAL_PORT) 10 | return server 11 | -------------------------------------------------------------------------------- /tests/test_buffer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import wave 3 | import struct 4 | import pytest 5 | import supercollider 6 | from threading import Event 7 | 8 | from tests.shared import server 9 | 10 | def test_buffer_write(server): 11 | length = 100 12 | 13 | output_path = "/tmp/output.wav" 14 | data = [0.01 * n for n in range(length)] 15 | buf = supercollider.Buffer.alloc(server, len(data)) 16 | buf.set(data) 17 | 18 | buf.write(output_path, supercollider.HEADER_FORMAT_WAV, supercollider.SAMPLE_FORMAT_INT16) 19 | assert os.path.exists(output_path) 20 | 21 | with wave.open(output_path, "r") as fd: 22 | input_binary = fd.readframes(len(data)) 23 | input_frames = list(struct.unpack("H" * len(data), input_binary)) 24 | data_uint16 = [int(sample * 32768) for sample in data] 25 | assert input_frames == data_uint16 26 | 27 | os.unlink(output_path) 28 | 29 | def test_buffer_read(server): 30 | length = 100 31 | output_path = "/tmp/output.wav" 32 | data = [0.01 * n for n in range(length)] 33 | 34 | with wave.open(output_path, "w") as fd: 35 | fd.setnchannels(1) 36 | fd.setsampwidth(2) 37 | fd.setframerate(44100) 38 | output_frames = [int(sample * 32768) for sample in data] 39 | output_binary = struct.pack("H" * len(output_frames), *output_frames) 40 | fd.writeframes(output_binary) 41 | buf = supercollider.Buffer.read(server, output_path) 42 | samples = buf.get(0, length) 43 | assert data == pytest.approx(samples, abs=0.0001) 44 | 45 | # Query buffer info - blocking approach 46 | rv = buf.get_info(blocking=True) 47 | assert rv == {'num_frames': 100, 'num_channels': 1, 'sample_rate': 44100} 48 | 49 | # Query buffer info - non-blocking approach 50 | event = Event() 51 | rv = None 52 | def callback(args): 53 | nonlocal rv 54 | rv = args 55 | event.set() 56 | buf.get_info(callback=callback, blocking=False) 57 | event.wait(1.0) 58 | assert rv == {'num_frames': 100, 'num_channels': 1, 'sample_rate': 44100} 59 | -------------------------------------------------------------------------------- /tests/test_bus.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import supercollider 3 | from supercollider.globals import ALLOCATOR_BUS_START_INDEX, ALLOCATOR_BUS_CAPACITY 4 | 5 | from tests.shared import server 6 | 7 | def test_bus(server): 8 | for cls in [supercollider.AudioBus, supercollider.ControlBus]: 9 | bus1 = cls(server, 2) 10 | assert bus1.id == ALLOCATOR_BUS_START_INDEX 11 | bus2 = cls(server, 2) 12 | assert bus2.id == ALLOCATOR_BUS_START_INDEX + 2 13 | remaining_channel_count = ALLOCATOR_BUS_CAPACITY - 4 14 | bus3 = cls(server, remaining_channel_count) 15 | assert bus3.id == ALLOCATOR_BUS_START_INDEX + 4 16 | with pytest.raises(supercollider.SuperColliderAllocationError) as excinfo: 17 | bus4 = cls(server, 2) 18 | bus1.free() 19 | bus2.free() 20 | bus3.free() 21 | -------------------------------------------------------------------------------- /tests/test_group.py: -------------------------------------------------------------------------------- 1 | import supercollider 2 | 3 | from tests.shared import server 4 | 5 | def test_group(server): 6 | group1 = supercollider.Group(server) 7 | assert group1.id > 0 8 | group2 = supercollider.Group(server) 9 | assert group2.id == group1.id + 1 10 | group1.free() 11 | group2.free() 12 | -------------------------------------------------------------------------------- /tests/test_server.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import supercollider 3 | 4 | from tests.shared import SC_DUMMY_PORT, SC_REAL_PORT 5 | 6 | @pytest.fixture(scope="module") 7 | def server(): 8 | server = supercollider.Server(port=SC_REAL_PORT) 9 | return server 10 | 11 | def test_server_connection_fail(): 12 | with pytest.raises(supercollider.SuperColliderConnectionError) as excinfo: 13 | server = supercollider.Server(port=SC_DUMMY_PORT) 14 | 15 | def test_server_sync(server): 16 | rv = server.sync() 17 | assert rv 18 | 19 | def test_server_get_status(server): 20 | rv = server.get_status() 21 | assert rv 22 | assert rv["num_ugens"] >= 0 23 | assert rv["num_synths"] >= 0 24 | assert rv["num_groups"] >= 0 25 | assert rv["num_synthdefs"] >= 0 26 | assert rv["cpu_average"] > 0 27 | assert rv["cpu_peak"] > 0 28 | assert rv["sample_rate_nominal"] > 0 29 | assert rv["sample_rate_actual"] > 0 30 | 31 | def test_server_get_version(server): 32 | rv = server.get_version() 33 | print(rv) 34 | assert rv 35 | assert isinstance(rv["program_name"], str) 36 | assert rv["version_major"] == 3 37 | assert rv["version_minor"] >= 0 38 | assert isinstance(rv["version_patch"], str) 39 | assert isinstance(rv["git_branch"], str) 40 | assert isinstance(rv["commit_hash"], str) 41 | -------------------------------------------------------------------------------- /tests/test_synth.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import supercollider 3 | from threading import Event 4 | 5 | from tests.shared import server 6 | 7 | def test_synth_create(server): 8 | synth = supercollider.Synth(server, "sine", {"freq": 440.0, "gain": -24}) 9 | assert synth.id > 0 10 | synth.free() 11 | 12 | def test_synth_get_set(server): 13 | synth = supercollider.Synth(server, "sine", {"freq": 440.0, "gain": -24}) 14 | assert synth.get("freq") == 440.0 15 | synth.set("freq", 880.0) 16 | assert synth.get("freq") == 880.0 17 | synth.free() 18 | 19 | def test_synth_get_nonblocking(server): 20 | synth = supercollider.Synth(server, "sine", {"freq": 440.0, "gain": -24}) 21 | 22 | rv = None 23 | event = Event() 24 | def callback(args): 25 | nonlocal rv 26 | rv = args 27 | event.set() 28 | 29 | synth.get("freq", callback=callback, blocking=False) 30 | event.wait(1.0) 31 | assert rv == 440.0 32 | synth.free() 33 | 34 | def test_synth_actions(server): 35 | group = supercollider.Group(server) 36 | 37 | synth0 = supercollider.Synth(server, "sine", {"gain": -96}, target=group) 38 | tree = server.query_tree(group) 39 | assert tree[2] == 1 40 | assert tree[3] == synth0.id 41 | 42 | synth1 = supercollider.Synth(server, "sine", {"gain": -96}, target=group, action=supercollider.ADD_TO_HEAD) 43 | tree = server.query_tree(group) 44 | assert tree[2] == 2 45 | assert tree[3] == synth1.id 46 | 47 | synth2 = supercollider.Synth(server, "sine", {"gain": -96}, target=group, action=supercollider.ADD_TO_TAIL) 48 | tree = server.query_tree(group) 49 | print(tree) 50 | assert tree[2] == 3 51 | assert tree[3] == synth1.id 52 | assert tree[9] == synth2.id 53 | 54 | synth3 = supercollider.Synth(server, "sine", {"gain": -96}, target=synth1, action=supercollider.ADD_AFTER) 55 | tree = server.query_tree(group) 56 | print(tree) 57 | assert tree[2] == 4 58 | assert tree[3] == synth3.id 59 | 60 | synth4 = supercollider.Synth(server, "sine", {"gain": -96}, target=synth1, action=supercollider.ADD_BEFORE) 61 | tree = server.query_tree(group) 62 | print(tree) 63 | assert tree[2] == 5 64 | assert tree[9] == synth4.id 65 | 66 | group.free() 67 | --------------------------------------------------------------------------------