]> git.basschouten.com Git - openhab-addons.git/blob
de193d7340fec7db33b53a21e72b9826917e2275
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.pulseaudio.internal;
14
15 import static org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants.*;
16
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.PrintStream;
20 import java.math.BigDecimal;
21 import java.net.Socket;
22 import java.net.SocketException;
23 import java.net.SocketTimeoutException;
24 import java.net.UnknownHostException;
25 import java.util.ArrayList;
26 import java.util.Comparator;
27 import java.util.List;
28 import java.util.Optional;
29 import java.util.Random;
30 import java.util.stream.Collectors;
31
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.openhab.binding.pulseaudio.internal.cli.Parser;
35 import org.openhab.binding.pulseaudio.internal.handler.DeviceIdentifier;
36 import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
37 import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig.State;
38 import org.openhab.binding.pulseaudio.internal.items.Module;
39 import org.openhab.binding.pulseaudio.internal.items.Sink;
40 import org.openhab.binding.pulseaudio.internal.items.SinkInput;
41 import org.openhab.binding.pulseaudio.internal.items.Source;
42 import org.openhab.binding.pulseaudio.internal.items.SourceOutput;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 /**
47  * The client connects to a pulseaudio server via TCP. It reads the current state of the
48  * pulseaudio server (available sinks, sources,...) and can send commands to the server.
49  * The syntax of the commands is the same as for the pactl command line tool provided by pulseaudio.
50  *
51  * On the pulseaudio server the module-cli-protocol-tcp has to be loaded.
52  *
53  * @author Tobias Bräutigam - Initial contribution
54  * @author Miguel Álvarez - changes for loading audio source module and nullability annotations
55  */
56 @NonNullByDefault
57 public class PulseaudioClient {
58
59     private final Logger logger = LoggerFactory.getLogger(PulseaudioClient.class);
60
61     private String host;
62     private int port;
63     private @Nullable Socket client;
64
65     private List<AbstractAudioDeviceConfig> items;
66     private List<Module> modules;
67
68     /**
69      * Corresponding to the global binding configuration
70      */
71     private PulseAudioBindingConfiguration configuration;
72
73     /**
74      * corresponding name to execute actions on sink items
75      */
76     private static final String ITEM_SINK = "sink";
77
78     /**
79      * corresponding name to execute actions on source items
80      */
81     private static final String ITEM_SOURCE = "source";
82
83     /**
84      * corresponding name to execute actions on sink-input items
85      */
86     private static final String ITEM_SINK_INPUT = "sink-input";
87
88     /**
89      * corresponding name to execute actions on source-output items
90      */
91     private static final String ITEM_SOURCE_OUTPUT = "source-output";
92
93     /**
94      * command to list the loaded modules
95      */
96     private static final String CMD_LIST_MODULES = "list-modules";
97
98     /**
99      * command to list the sinks
100      */
101     private static final String CMD_LIST_SINKS = "list-sinks";
102
103     /**
104      * command to list the sources
105      */
106     private static final String CMD_LIST_SOURCES = "list-sources";
107
108     /**
109      * command to list the sink-inputs
110      */
111     private static final String CMD_LIST_SINK_INPUTS = "list-sink-inputs";
112
113     /**
114      * command to list the source-outputs
115      */
116     private static final String CMD_LIST_SOURCE_OUTPUTS = "list-source-outputs";
117
118     /**
119      * command to load a module
120      */
121     private static final String CMD_LOAD_MODULE = "load-module";
122
123     /**
124      * command to unload a module
125      */
126     private static final String CMD_UNLOAD_MODULE = "unload-module";
127
128     /**
129      * name of the module-combine-sink
130      */
131     private static final String MODULE_COMBINE_SINK = "module-combine-sink";
132
133     public PulseaudioClient(String host, int port, PulseAudioBindingConfiguration configuration) {
134         this.host = host;
135         this.port = port;
136         this.configuration = configuration;
137
138         items = new ArrayList<>();
139         modules = new ArrayList<>();
140     }
141
142     public boolean isConnected() {
143         Socket clientSocket = client;
144         return clientSocket != null ? clientSocket.isConnected() : false;
145     }
146
147     /**
148      * updates the item states and their relationships
149      */
150     public synchronized void update() {
151         // one step copy
152         modules = new ArrayList<>(Parser.parseModules(listModules()));
153
154         List<AbstractAudioDeviceConfig> newItems = new ArrayList<>(); // prepare new list before assigning it
155         if (configuration.sink) {
156             logger.debug("reading sinks");
157             newItems.addAll(Parser.parseSinks(listSinks(), this));
158         }
159         if (configuration.source) {
160             logger.debug("reading sources");
161             newItems.addAll(Parser.parseSources(listSources(), this));
162         }
163         if (configuration.sinkInput) {
164             logger.debug("reading sink-inputs");
165             newItems.addAll(Parser.parseSinkInputs(listSinkInputs(), this));
166         }
167         if (configuration.sourceOutput) {
168             logger.debug("reading source-outputs");
169             newItems.addAll(Parser.parseSourceOutputs(listSourceOutputs(), this));
170         }
171         logger.debug("Pulseaudio server {}: {} modules and {} items updated", host, modules.size(), newItems.size());
172         items = newItems;
173     }
174
175     private String listModules() {
176         return this.sendRawRequest(CMD_LIST_MODULES);
177     }
178
179     private String listSinks() {
180         return this.sendRawRequest(CMD_LIST_SINKS);
181     }
182
183     private String listSources() {
184         return this.sendRawRequest(CMD_LIST_SOURCES);
185     }
186
187     private String listSinkInputs() {
188         return this.sendRawRequest(CMD_LIST_SINK_INPUTS);
189     }
190
191     private String listSourceOutputs() {
192         return this.sendRawRequest(CMD_LIST_SOURCE_OUTPUTS);
193     }
194
195     /**
196      * retrieves a module by its id
197      *
198      * @param id
199      * @return the corresponding {@link Module} to the given <code>id</code>
200      */
201     public @Nullable Module getModule(int id) {
202         for (Module module : modules) {
203             if (module.getId() == id) {
204                 return module;
205             }
206         }
207         return null;
208     }
209
210     /**
211      * send the command directly to the pulseaudio server
212      * for a list of available commands please take a look at
213      * http://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/CLI
214      *
215      * @param command
216      */
217     public void sendCommand(String command) {
218         sendRawCommand(command);
219     }
220
221     /**
222      * retrieves a {@link Sink} by its name
223      *
224      * @return the corresponding {@link Sink} to the given <code>name</code>
225      */
226     public @Nullable Sink getSink(String name) {
227         for (AbstractAudioDeviceConfig item : items) {
228             if (item.getPaName().equalsIgnoreCase(name) && item instanceof Sink sink) {
229                 return sink;
230             }
231         }
232         return null;
233     }
234
235     /**
236      * retrieves a {@link Sink} by its id
237      *
238      * @return the corresponding {@link Sink} to the given <code>id</code>
239      */
240     public @Nullable Sink getSink(int id) {
241         for (AbstractAudioDeviceConfig item : items) {
242             if (item.getId() == id && item instanceof Sink sink) {
243                 return sink;
244             }
245         }
246         return null;
247     }
248
249     /**
250      * retrieves a {@link Source} by its id
251      *
252      * @return the corresponding {@link Source} to the given <code>id</code>
253      */
254     public @Nullable Source getSource(int id) {
255         for (AbstractAudioDeviceConfig item : items) {
256             if (item.getId() == id && item instanceof Source source) {
257                 return source;
258             }
259         }
260         return null;
261     }
262
263     /**
264      * retrieves an {@link AbstractAudioDeviceConfig} by its identifier
265      * If several devices correspond to the deviceIdentifier, returns the first one (aphabetical order)
266      *
267      * @param deviceIdentifier The device identifier to match against
268      * @return the corresponding {@link AbstractAudioDeviceConfig} to the given <code>name</code>
269      */
270     public @Nullable AbstractAudioDeviceConfig getGenericAudioItem(DeviceIdentifier deviceIdentifier) {
271         List<AbstractAudioDeviceConfig> matchingDevices = items.stream()
272                 .filter(device -> device.matches(deviceIdentifier))
273                 .sorted(Comparator.comparing(AbstractAudioDeviceConfig::getPaName)).collect(Collectors.toList());
274         if (matchingDevices.size() == 1) {
275             return matchingDevices.get(0);
276         } else if (matchingDevices.size() > 1) {
277             logger.debug(
278                     "Cannot select exactly one audio device, so choosing the first. To choose without ambiguity between the {} devices matching the identifier {}, you can maybe use a more restrictive 'additionalFilter' parameter",
279                     matchingDevices.size(), deviceIdentifier.getNameOrDescription());
280             return matchingDevices.get(0);
281         }
282         return null;
283     }
284
285     /**
286      * Get all items previously parsed from the pulseaudio server.
287      *
288      * @return All items parsed from the pulseaudio server
289      */
290     public List<AbstractAudioDeviceConfig> getItems() {
291         return items;
292     }
293
294     /**
295      * changes the <code>mute</code> state of the corresponding {@link Sink}
296      *
297      * @param item the {@link Sink} to handle
298      * @param mute mutes the sink if true, unmutes if false
299      */
300     public void setMute(@Nullable AbstractAudioDeviceConfig item, boolean mute) {
301         if (item == null) {
302             return;
303         }
304         String itemCommandName = getItemCommandName(item);
305         if (itemCommandName == null) {
306             return;
307         }
308         String muteString = mute ? "1" : "0";
309         sendRawCommand("set-" + itemCommandName + "-mute " + item.getId() + " " + muteString);
310         // update internal data
311         item.setMuted(mute);
312     }
313
314     /**
315      * change the volume of an {@link AbstractAudioDeviceConfig}
316      *
317      * @param item the {@link AbstractAudioDeviceConfig} to handle
318      * @param vol the new volume value the {@link AbstractAudioDeviceConfig} should be changed to (possible values from
319      *            0 - 65536)
320      */
321     public void setVolume(AbstractAudioDeviceConfig item, int vol) {
322         String itemCommandName = getItemCommandName(item);
323         if (itemCommandName == null) {
324             return;
325         }
326         sendRawCommand("set-" + itemCommandName + "-volume " + item.getId() + " " + vol);
327         item.setVolume(Math.round(100f / 65536f * vol));
328     }
329
330     /**
331      * Locate or load (if needed) the simple protocol tcp module for the given sink
332      * and returns the port.
333      * The module loading (if needed) will be tried several times, on a new random port each time.
334      *
335      * @param item the sink we are searching for
336      * @param simpleTcpPortPref the port to use if we have to load the module
337      * @return the port on which the module is listening
338      * @throws InterruptedException
339      */
340     public Optional<Integer> loadModuleSimpleProtocolTcpIfNeeded(AbstractAudioDeviceConfig item,
341             Integer simpleTcpPortPref, @Nullable String format, @Nullable BigDecimal rate,
342             @Nullable BigDecimal channels) throws InterruptedException {
343         int currentTry = 0;
344         int simpleTcpPortToTry = simpleTcpPortPref;
345         String itemType = getItemCommandName(item);
346         do {
347             Optional<Integer> simplePort = findSimpleProtocolTcpModule(item, format, rate, channels);
348
349             if (simplePort.isPresent()) {
350                 return simplePort;
351             } else {
352                 String moduleOptions = itemType + "=" + item.getPaName() + " port=" + simpleTcpPortToTry;
353                 if (item instanceof Source && format != null && rate != null && channels != null) {
354                     moduleOptions = moduleOptions + String.format(" record=true format=%s rate=%d channels=%d", format,
355                             rate.longValue(), channels.intValue());
356                 }
357                 sendRawCommand("load-module module-simple-protocol-tcp " + moduleOptions);
358                 simpleTcpPortToTry = new Random().nextInt(64512) + 1024; // a random port above 1024
359             }
360             Thread.sleep(100);
361             update();
362             currentTry++;
363         } while (currentTry < 3);
364
365         logger.warn("""
366                 The pulseaudio binding tried 3 times to load the module-simple-protocol-tcp\
367                  on random port on the pulseaudio server and give up trying\
368                 """);
369         return Optional.empty();
370     }
371
372     /**
373      * Find a simple protocol module corresponding to the given sink in argument
374      * and returns the port it listens to
375      *
376      * @param item
377      * @return
378      */
379     private Optional<Integer> findSimpleProtocolTcpModule(AbstractAudioDeviceConfig item, @Nullable String format,
380             @Nullable BigDecimal rate, @Nullable BigDecimal channels) {
381         String itemType = getItemCommandName(item);
382         if (itemType == null) {
383             return Optional.empty();
384         }
385         List<Module> modulesCopy = new ArrayList<>(modules);
386         var isSource = item instanceof Source;
387         return modulesCopy.stream() // iteration on modules
388                 .filter(module -> MODULE_SIMPLE_PROTOCOL_TCP_NAME.equals(module.getPaName())) // filter on module name
389                 .filter(module -> {
390                     boolean nameMatch = extractArgumentFromLine(itemType, module.getArgument()) // extract sick|source
391                             .map(name -> name.equals(item.getPaName())).orElse(false);
392                     if (isSource && nameMatch) {
393                         boolean recordStream = extractArgumentFromLine("record", module.getArgument())
394                                 .map("true"::equals).orElse(false);
395                         if (!recordStream) {
396                             return false;
397                         }
398                         if (format != null) {
399                             boolean rateMatch = extractArgumentFromLine("format", module.getArgument())
400                                     .map(format::equals).orElse(false);
401                             if (!rateMatch) {
402                                 return false;
403                             }
404                         }
405                         if (rate != null) {
406                             boolean rateMatch = extractArgumentFromLine("rate", module.getArgument())
407                                     .map(value -> Long.parseLong(value) == rate.longValue()).orElse(false);
408                             if (!rateMatch) {
409                                 return false;
410                             }
411                         }
412                         if (channels != null) {
413                             boolean channelsMatch = extractArgumentFromLine("channels", module.getArgument())
414                                     .map(value -> Integer.parseInt(value) == channels.intValue()).orElse(false);
415                             if (!channelsMatch) {
416                                 return false;
417                             }
418                         }
419                     }
420                     return nameMatch;
421                 }) // filter on sink name
422                 .findAny() // get a corresponding module
423                 .map(module -> extractArgumentFromLine("port", module.getArgument())
424                         .orElse(Integer.toString(MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT))) // get port
425                 .map(portS -> Integer.parseInt(portS));
426     }
427
428     private Optional<String> extractArgumentFromLine(String argumentWanted, @Nullable String argumentLine) {
429         String argument = null;
430         if (argumentLine != null) {
431             int startPortIndex = argumentLine.indexOf(argumentWanted + "=");
432             if (startPortIndex != -1) {
433                 startPortIndex = startPortIndex + argumentWanted.length() + 1;
434                 int endPortIndex = argumentLine.indexOf(" ", startPortIndex);
435                 if (endPortIndex == -1) {
436                     endPortIndex = argumentLine.length();
437                 }
438                 argument = argumentLine.substring(startPortIndex, endPortIndex);
439             }
440         }
441         return Optional.ofNullable(argument);
442     }
443
444     /**
445      * returns the item names that can be used in commands
446      *
447      * @param item
448      * @return
449      */
450     private @Nullable String getItemCommandName(AbstractAudioDeviceConfig item) {
451         if (item instanceof Sink) {
452             return ITEM_SINK;
453         } else if (item instanceof Source) {
454             return ITEM_SOURCE;
455         } else if (item instanceof SinkInput) {
456             return ITEM_SINK_INPUT;
457         } else if (item instanceof SourceOutput) {
458             return ITEM_SOURCE_OUTPUT;
459         }
460         return null;
461     }
462
463     /**
464      * change the volume of an {@link AbstractAudioDeviceConfig}
465      *
466      * @param item the {@link AbstractAudioDeviceConfig} to handle
467      * @param vol the new volume percent value the {@link AbstractAudioDeviceConfig} should be changed to (possible
468      *            values from 0 - 100)
469      */
470     public void setVolumePercent(AbstractAudioDeviceConfig item, int vol) {
471         int volumeToSet = vol;
472         if (volumeToSet <= 100) {
473             volumeToSet = toAbsoluteVolume(volumeToSet);
474         }
475         setVolume(item, volumeToSet);
476     }
477
478     /**
479      * transform a percent volume to a value that can be send to the pulseaudio server (0-65536)
480      *
481      * @param percent
482      * @return
483      */
484     private int toAbsoluteVolume(int percent) {
485         return (int) Math.round(65536f / 100f * Double.valueOf(percent));
486     }
487
488     /**
489      * changes the combined sinks slaves to the given <code>sinks</code>
490      *
491      * @param combinedSink the combined sink which slaves should be changed
492      * @param sinks the list of new slaves
493      */
494     public void setCombinedSinkSlaves(@Nullable Sink combinedSink, List<Sink> sinks) {
495         if (combinedSink == null || !combinedSink.isCombinedSink()) {
496             return;
497         }
498         List<String> slaves = new ArrayList<>();
499         for (Sink sink : sinks) {
500             slaves.add(sink.getPaName());
501         }
502         // 1. delete old combined-sink
503         Module lastModule = combinedSink.getModule();
504         if (lastModule != null) {
505             sendRawCommand(CMD_UNLOAD_MODULE + " " + lastModule.getId());
506         }
507         // 2. add new combined-sink with same name and all slaves
508         sendRawCommand(CMD_LOAD_MODULE + " " + MODULE_COMBINE_SINK + " sink_name=" + combinedSink.getPaName()
509                 + " slaves=" + String.join(",", slaves));
510         // 3. update internal data structure because the combined sink has a new number + other slaves
511         update();
512     }
513
514     /**
515      * sets the sink a sink-input should be routed to
516      *
517      * @param sinkInput the sink-input to be rerouted
518      * @param sink the new sink the sink-input should be routed to
519      */
520     public void moveSinkInput(@Nullable SinkInput sinkInput, @Nullable Sink sink) {
521         if (sinkInput == null || sink == null) {
522             return;
523         }
524         sendRawCommand("move-sink-input " + sinkInput.getId() + " " + sink.getId());
525         sinkInput.setSink(sink);
526     }
527
528     /**
529      * sets the sink a source-output should be routed to
530      *
531      * @param sourceOutput the source-output to be rerouted
532      * @param source the new source the source-output should be routed to
533      */
534     public void moveSourceOutput(@Nullable SourceOutput sourceOutput, @Nullable Source source) {
535         if (sourceOutput == null || source == null) {
536             return;
537         }
538         sendRawCommand("move-sink-input " + sourceOutput.getId() + " " + source.getId());
539         sourceOutput.setSource(source);
540     }
541
542     /**
543      * suspend a source
544      *
545      * @param source the source which state should be changed
546      * @param suspend suspend it or not
547      */
548     public void suspendSource(@Nullable Source source, boolean suspend) {
549         if (source == null) {
550             return;
551         }
552         if (suspend) {
553             sendRawCommand("suspend-source " + source.getId() + " 1");
554             source.setState(State.SUSPENDED);
555         } else {
556             sendRawCommand("suspend-source " + source.getId() + " 0");
557             // unsuspending the source could result in different states (RUNNING,IDLE,...)
558             // update to get the new state
559             update();
560         }
561     }
562
563     /**
564      * suspend a sink
565      *
566      * @param sink the sink which state should be changed
567      * @param suspend suspend it or not
568      */
569     public void suspendSink(@Nullable Sink sink, boolean suspend) {
570         if (sink == null) {
571             return;
572         }
573         if (suspend) {
574             sendRawCommand("suspend-sink " + sink.getId() + " 1");
575             sink.setState(State.SUSPENDED);
576         } else {
577             sendRawCommand("suspend-sink " + sink.getId() + " 0");
578             // unsuspending the sink could result in different states (RUNNING,IDLE,...)
579             // update to get the new state
580             update();
581         }
582     }
583
584     /**
585      * changes the combined sinks slaves to the given <code>sinks</code>
586      *
587      * @param combinedSinkName the combined sink which slaves should be changed
588      * @param sinks the list of new slaves
589      */
590     public void setCombinedSinkSlaves(String combinedSinkName, List<Sink> sinks) {
591         if (getSink(combinedSinkName) != null) {
592             return;
593         }
594         List<String> slaves = new ArrayList<>();
595         for (Sink sink : sinks) {
596             slaves.add(sink.getPaName());
597         }
598         // add new combined-sink with same name and all slaves
599         sendRawCommand(CMD_LOAD_MODULE + " " + MODULE_COMBINE_SINK + " sink_name=" + combinedSinkName + " slaves="
600                 + String.join(",", slaves));
601         // update internal data structure because the combined sink is new
602         update();
603     }
604
605     private synchronized void sendRawCommand(String command) {
606         checkConnection();
607         Socket clientSocket = client;
608         if (clientSocket != null && clientSocket.isConnected()) {
609             try {
610                 PrintStream out = new PrintStream(clientSocket.getOutputStream(), true);
611                 logger.trace("sending command {} to pa-server {}", command, host);
612                 out.print(command + "\r\n");
613                 out.close();
614                 clientSocket.close();
615             } catch (IOException e) {
616                 logger.warn("{}", e.getMessage(), e);
617             }
618         }
619     }
620
621     private String sendRawRequest(String command) {
622         logger.trace("_sendRawRequest({})", command);
623         checkConnection();
624         String result = "";
625         Socket clientSocket = client;
626         if (clientSocket != null && clientSocket.isConnected()) {
627             try {
628                 PrintStream out = new PrintStream(clientSocket.getOutputStream(), true);
629                 out.print(command + "\r\n");
630
631                 InputStream instr = clientSocket.getInputStream();
632
633                 try {
634                     byte[] buff = new byte[1024];
635                     int retRead = 0;
636                     int lc = 0;
637                     do {
638                         retRead = instr.read(buff);
639                         lc++;
640                         if (retRead > 0) {
641                             String line = new String(buff, 0, retRead);
642                             if (line.endsWith(">>> ") && lc > 1) {
643                                 result += line.substring(0, line.length() - 4);
644                                 break;
645                             }
646                             result += line.trim();
647                         }
648                     } while (retRead > 0);
649                 } catch (SocketTimeoutException e) {
650                     // Timeout -> as newer PA versions (>=5.0) do not send the >>> we have no chance
651                     // to detect the end of the answer, except by this timeout
652                 } catch (SocketException e) {
653                     logger.warn("Socket exception while sending pulseaudio command: {}", e.getMessage());
654                 } catch (IOException e) {
655                     logger.warn("Exception while reading socket: {}", e.getMessage());
656                 }
657                 instr.close();
658                 out.close();
659                 clientSocket.close();
660                 return result;
661             } catch (IOException e) {
662                 logger.warn("{}", e.getMessage(), e);
663             }
664         }
665         return result;
666     }
667
668     private void checkConnection() {
669         try {
670             connect();
671         } catch (IOException e) {
672             logger.debug("{}", e.getMessage(), e);
673         }
674     }
675
676     /**
677      * Connects to the pulseaudio server (timeout 500ms)
678      */
679     public void connect() throws IOException {
680         Socket clientSocket = client;
681         if (clientSocket == null || clientSocket.isClosed() || !clientSocket.isConnected()) {
682             logger.trace("Try to connect...");
683             try {
684                 var clientFinal = new Socket(host, port);
685                 clientFinal.setSoTimeout(500);
686                 client = clientFinal;
687                 logger.trace("connected");
688             } catch (UnknownHostException e) {
689                 client = null;
690                 throw new IOException("Unknown host", e);
691             } catch (IllegalArgumentException e) {
692                 client = null;
693                 throw new IOException("Invalid port", e);
694             } catch (SecurityException | SocketException e) {
695                 client = null;
696                 throw new IOException(
697                         String.format("Cannot connect socket: %s", e.getMessage() != null ? e.getMessage() : ""), e);
698             } catch (IOException e) {
699                 client = null;
700                 throw e;
701             }
702         }
703     }
704
705     /**
706      * Disconnects from the pulseaudio server
707      */
708     public void disconnect() {
709         Socket clientSocket = client;
710         if (clientSocket != null) {
711             try {
712                 clientSocket.close();
713             } catch (IOException e) {
714                 logger.debug("{}", e.getMessage(), e);
715             }
716         }
717     }
718 }