1import os
2import importlib
3from .. import ports
4
5DEFAULT_BACKEND = 'mido.backends.rtmidi'
6
7
8class Backend(object):
9    """
10    Wrapper for backend module.
11
12    A backend module implements classes for input and output ports for
13    a specific MIDI library. The Backend object wraps around the
14    object and provides convenient 'open_*()' and 'get_*_names()'
15    functions.
16    """
17    def __init__(self, name=None, api=None, load=False, use_environ=True):
18        self.name = name or os.environ.get('MIDO_BACKEND', DEFAULT_BACKEND)
19        self.api = api
20        self.use_environ = use_environ
21        self._module = None
22
23        # Split out api (if present).
24        if api:
25            self.api = api
26        elif self.name and '/' in self.name:
27            self.name, self.api = self.name.split('/', 1)
28        else:
29            self.api = None
30
31        if load:
32            self.load()
33
34    @property
35    def module(self):
36        """A reference module implementing the backend.
37
38        This will always be a valid reference to a module. Accessing
39        this property will load the module. Use .loaded to check if
40        the module is loaded.
41        """
42        self.load()
43        return self._module
44
45    @property
46    def loaded(self):
47        """Return True if the module is loaded."""
48        return self._module is not None
49
50    def load(self):
51        """Load the module.
52
53        Does nothing if the module is already loaded.
54
55        This function will be called if you access the 'module'
56        property."""
57        if not self.loaded:
58            self._module = importlib.import_module(self.name)
59
60    def _env(self, name):
61        if self.use_environ:
62            return os.environ.get(name)
63        else:
64            return None
65
66    def _add_api(self, kwargs):
67        if self.api and 'api' not in kwargs:
68            kwargs['api'] = self.api
69        return kwargs
70
71    def open_input(self, name=None, virtual=False, callback=None, **kwargs):
72        """Open an input port.
73
74        If the environment variable MIDO_DEFAULT_INPUT is set,
75        if will override the default port.
76
77        virtual=False
78          Passing True opens a new port that other applications can
79          connect to. Raises IOError if not supported by the backend.
80
81        callback=None
82          A callback function to be called when a new message arrives.
83          The function should take one argument (the message).
84          Raises IOError if not supported by the backend.
85        """
86        kwargs.update(dict(virtual=virtual, callback=callback))
87
88        if name is None:
89            name = self._env('MIDO_DEFAULT_INPUT')
90
91        return self.module.Input(name, **self._add_api(kwargs))
92
93    def open_output(self, name=None, virtual=False, autoreset=False, **kwargs):
94        """Open an output port.
95
96        If the environment variable MIDO_DEFAULT_OUTPUT is set,
97        if will override the default port.
98
99        virtual=False
100          Passing True opens a new port that other applications can
101          connect to. Raises IOError if not supported by the backend.
102
103        autoreset=False
104          Automatically send all_notes_off and reset_all_controllers
105          on all channels. This is the same as calling `port.reset()`.
106        """
107        kwargs.update(dict(virtual=virtual, autoreset=autoreset))
108
109        if name is None:
110            name = self._env('MIDO_DEFAULT_OUTPUT')
111
112        return self.module.Output(name, **self._add_api(kwargs))
113
114    def open_ioport(self, name=None, virtual=False,
115                    callback=None, autoreset=False, **kwargs):
116        """Open a port for input and output.
117
118        If the environment variable MIDO_DEFAULT_IOPORT is set,
119        if will override the default port.
120
121        virtual=False
122          Passing True opens a new port that other applications can
123          connect to. Raises IOError if not supported by the backend.
124
125        callback=None
126          A callback function to be called when a new message arrives.
127          The function should take one argument (the message).
128          Raises IOError if not supported by the backend.
129
130        autoreset=False
131          Automatically send all_notes_off and reset_all_controllers
132          on all channels. This is the same as calling `port.reset()`.
133        """
134        kwargs.update(dict(virtual=virtual, callback=callback,
135                           autoreset=autoreset))
136
137        if name is None:
138            name = self._env('MIDO_DEFAULT_IOPORT') or None
139
140        if hasattr(self.module, 'IOPort'):
141            # Backend has a native IOPort. Use it.
142            return self.module.IOPort(name, **self._add_api(kwargs))
143        else:
144            # Backend has no native IOPort. Use the IOPort wrapper
145            # in midi.ports.
146            #
147            # We need an input and an output name.
148
149            # MIDO_DEFAULT_IOPORT overrides the other two variables.
150            if name:
151                input_name = output_name = name
152            else:
153                input_name = self._env('MIDO_DEFAULT_INPUT')
154                output_name = self._env('MIDO_DEFAULT_OUTPUT')
155
156            kwargs = self._add_api(kwargs)
157
158            return ports.IOPort(self.module.Input(input_name, **kwargs),
159                                self.module.Output(output_name, **kwargs))
160
161    def _get_devices(self, **kwargs):
162        if hasattr(self.module, 'get_devices'):
163            return self.module.get_devices(**self._add_api(kwargs))
164        else:
165            return []
166
167    def get_input_names(self, **kwargs):
168        """Return a list of all input port names."""
169        devices = self._get_devices(**self._add_api(kwargs))
170        names = [device['name'] for device in devices if device['is_input']]
171        return names
172
173    def get_output_names(self, **kwargs):
174        """Return a list of all output port names."""
175        devices = self._get_devices(**self._add_api(kwargs))
176        names = [device['name'] for device in devices if device['is_output']]
177        return names
178
179    def get_ioport_names(self, **kwargs):
180        """Return a list of all I/O port names."""
181        devices = self._get_devices(**self._add_api(kwargs))
182        inputs = [device['name'] for device in devices if device['is_input']]
183        outputs = set(
184            [device['name'] for device in devices if device['is_output']])
185        return [name for name in inputs if name in outputs]
186
187    def __repr__(self):
188        if self.loaded:
189            status = 'loaded'
190        else:
191            status = 'not loaded'
192
193        if self.api:
194            name = '{}/{}'.format(self.name, self.api)
195        else:
196            name = self.name
197
198        return '<backend {} ({})>'.format(name, status)
199