001package org.intellimate.izou.system.sound;
002
003import org.intellimate.izou.util.IzouModule;
004import org.intellimate.izou.addon.AddOnModel;
005import org.intellimate.izou.identification.Identification;
006import org.intellimate.izou.main.Main;
007
008import javax.sound.sampled.*;
009import java.util.concurrent.Future;
010import java.util.function.Consumer;
011
012/**
013 * the base class for every IzouSoundLine, provides basic implementation of the Methods defined int IzouSoundLine and
014 * delegates to Line, AutoCloseable etc.
015 * @author LeanderK
016 * @version 1.0
017 */
018public class IzouSoundLineBaseClass extends IzouModule implements Line, AutoCloseable, IzouSoundLine {
019    protected final Line line;
020    private Future<?> closingThread;
021    private boolean isPermanent;
022    protected final SoundManager soundManager;
023    private final AddOnModel addOnModel;
024    protected final boolean isMutable;
025    protected boolean isMutedFromSystem = false;
026    private boolean isMutedFromUser = false;
027    private Consumer<Void> closeCallback = null;
028    private boolean muteIfNonPermanent = true;
029    private Consumer<Void> muteCallback = null;
030    private Identification responsibleID;
031
032    public IzouSoundLineBaseClass(Line line, Main main, boolean isPermanent, AddOnModel addOnModel) {
033        super(main, false);
034        this.line = line;
035        this.isPermanent = isPermanent;
036        this.addOnModel = addOnModel;
037        if (!isPermanent) {
038            closingThread = getClosingThread(line, main, addOnModel);
039        } else {
040            closingThread = null;
041        }
042        soundManager = null;
043        boolean mutable;
044        try {
045            line.getControl(BooleanControl.Type.MUTE);
046            mutable = true;
047        } catch (IllegalArgumentException e) {
048            mutable = false;
049        }
050        isMutable = mutable;
051    }
052
053    private Future<?> getClosingThread(Line line, Main main, AddOnModel addOnModel) {
054        return main.getThreadPoolManager().getIzouThreadPool().submit(() -> {
055            try {
056                Thread.sleep(600000);
057            } catch (InterruptedException e) {
058                error("interrupted while sleeping, canceling");
059                return;
060            }
061            if (line.isOpen()) {
062                debug("closing line " + line + "for Addon " + addOnModel);
063                line.close();
064            }
065        });
066    }
067
068    /**
069     * Obtains the <code>Line.Info</code> object describing this
070     * line.
071     * @return description of the line
072     */
073    @Override
074    public Info getLineInfo() {
075        return line.getLineInfo();
076    }
077
078    /**
079     * Opens the line, indicating that it should acquire any required
080     * system resources and become operational.
081     * If this operation
082     * succeeds, the line is marked as open, and an <code>OPEN</code> event is dispatched
083     * to the line's listeners.
084     * <p>
085     * Note that some lines, once closed, cannot be reopened.  Attempts
086     * to reopen such a line will always result in an <code>LineUnavailableException</code>.
087     * <p>
088     * Some types of lines have configurable properties that may affect
089     * resource allocation.   For example, a <code>DataLine</code> must
090     * be opened with a particular format and buffer size.  Such lines
091     * should provide a mechanism for configuring these properties, such
092     * as an additional <code>open</code> method or methods which allow
093     * an application to specify the desired settings.
094     * <p>
095     * This method takes no arguments, and opens the line with the current
096     * settings.  For <code>SourceDataLine</code> and
097     * <code>TargetDataLine</code> objects, this means that the line is
098     * opened with default settings.  For a <code>Clip</code>, however,
099     * the buffer size is determined when data is loaded.  Since this method does not
100     * allow the application to specify any data to load, an IllegalArgumentException
101     * is thrown. Therefore, you should instead use one of the <code>open</code> methods
102     * provided in the <code>Clip</code> interface to load data into the <code>Clip</code>.
103     * <p>
104     * For <code>DataLine</code>'s, if the <code>DataLine.Info</code>
105     * object which was used to retrieve the line, specifies at least
106     * one fully qualified audio format, the last one will be used
107     * as the default format.
108     *
109     * @throws IllegalArgumentException if this method is called on a Clip instance.
110     * @throws LineUnavailableException if the line cannot be
111     * opened due to resource restrictions.
112     * @throws SecurityException if the line cannot be
113     * opened due to security restrictions.
114     *
115     * @see #close
116     * @see #isOpen
117     */
118    @Override
119    public void open() throws LineUnavailableException {
120        opening();
121        line.open();
122    }
123
124    protected void opening() {
125        System.out.println("opening for " + addOnModel);
126        if (!line.isOpen() && !isPermanent && muteCallback != null)
127            muteCallback.accept(null);
128    }
129
130    /**
131     * Closes the line, indicating that any system resources
132     * in use by the line can be released.  If this operation
133     * succeeds, the line is marked closed and a <code>CLOSE</code> event is dispatched
134     * to the line's listeners.
135     * @throws SecurityException if the line cannot be
136     * closed due to security restrictions.
137     *
138     * @see #open
139     * @see #isOpen
140     */
141    @Override
142    public void close() {
143        System.out.println("closing for " + addOnModel);
144        closeCallback.accept(null);
145        line.close();
146    }
147
148    /**
149     * Indicates whether the line is open, meaning that it has reserved
150     * system resources and is operational, although it might not currently be
151     * playing or capturing sound.
152     * @return <code>true</code> if the line is open, otherwise <code>false</code>
153     *
154     * @see #open()
155     * @see #close()
156     */
157    @Override
158    public boolean isOpen() {
159        return line.isOpen();
160    }
161
162    /**
163     * Obtains the set of controls associated with this line. Some controls may only be available when the line is open.
164     * If there are no controls, this method returns an array of length 0.
165     * The mute-control operation may be overridden by the System.
166     * @return the array of controls
167     * @see #isMutedFromSystem()
168     */
169    @Override
170    public Control[] getControls() {
171        Control[] controls = line.getControls();
172        for (int i = 0; i < controls.length; i++) {
173            Control control = controls[i];
174            if (control.getType().toString().equals(BooleanControl.Type.MUTE.toString())) {
175                controls[i] = new FakeMuteControl();
176            }
177        }
178        return controls;
179    }
180
181    /**
182     * Indicates whether the line supports a control of the specified type. Some controls may only be available when the line is open.
183     * @param control the type of the control for which support is queried
184     * @return true if at least one control of the specified type is supported, otherwise false.
185     */
186    @Override
187    public boolean isControlSupported(Control.Type control) {
188        return line.isControlSupported(control);
189    }
190
191    /**
192     * Obtains a control of the specified type, if there is any.
193     * Some controls may only be available when the line is open.
194     * The mute-control operation may be overridden by the System.
195     * @param control the type of the requested control
196     * @return a control of the specified type
197     * @throws IllegalArgumentException - if a control of the specified type is not supported
198     * @see #isMutedFromSystem()
199     */
200    @Override
201    public Control getControl(Control.Type control) throws IllegalArgumentException {
202        if (control.toString().equals(BooleanControl.Type.MUTE.toString())) {
203            return new FakeMuteControl();
204        } else {
205            return line.getControl(control);
206        }
207    }
208
209    /**
210     * follows no predictable behaviour, can be seen as not implemented.
211     * @param listener the listener
212     */
213    @Override
214    public void addLineListener(LineListener listener) {
215        line.addLineListener(listener);
216    }
217
218    /**
219     * follows no predictable behaviour, can be seen as not implemented.
220     * @param listener the listener
221     */
222    @Override
223    public void removeLineListener(LineListener listener) {
224        line.removeLineListener(listener);
225    }
226
227    /**
228     * returns whether the line is permanently-available.
229     * If a line is not permanently available, it will close after max. 10 minutes
230     * @return true if permanent.
231     */
232    @Override
233    public boolean isPermanent() {
234        return isPermanent;
235    }
236
237    void setToPermanent() {
238        if (isPermanent)
239            return;
240        closingThread.cancel(true);
241        closingThread = null;
242        isPermanent = true;
243    }
244
245    void setToNonPermanent() {
246        if (!isPermanent)
247            return;
248        closingThread = getClosingThread(line, main, addOnModel);
249        isPermanent = false;
250    }
251
252    /**
253     * gets the associated AddonModel
254     * @return the AddonModel
255     */
256    @Override
257    @SuppressWarnings("unused")
258    public AddOnModel getAddOnModel() {
259        return addOnModel;
260    }
261
262    /**
263     * gets the ID responsible
264     *
265     * @return the the ID
266     */
267    @Override
268    public Identification getResponsibleID() {
269        return responsibleID;
270    }
271
272    void setResponsibleID(Identification responsibleID) {
273        this.responsibleID = responsibleID;
274    }
275
276    /**
277     * returns whether the Line is muted
278     * @return true if muted.
279     */
280    @Override
281    @SuppressWarnings("unused")
282    public boolean isMutedFromSystem() {
283        return isMutedFromSystem;
284    }
285
286    /**
287     * sets whether other Addons audio-inputs should be muted while this line is open (only works for non-permanent lines).
288     * The standard is true.
289     * @param muteIfNonPermanent true if muted, false if not
290     */
291    @Override
292    @SuppressWarnings("unused")
293    public void setMuteIfNonPermanent(boolean muteIfNonPermanent) {
294        this.muteIfNonPermanent = muteIfNonPermanent;
295    }
296
297    /**
298     * retruns whether other Addons audio-inputs should be muted while this line is open (only works for non-permanent lines).
299     * @return true if muted, false if not
300     */
301    @Override
302    public boolean isMuteIfNonPermanent() {
303        return muteIfNonPermanent;
304    }
305
306    void setMutedFromSystem(boolean isMuted) {
307        if (isMutable) {
308            BooleanControl bc = (BooleanControl) line.getControl(BooleanControl.Type.MUTE);
309            if (bc != null) {
310                if (isMuted) {
311                    bc.setValue(true); // true to mute the line, false to unmute
312                } else {
313                    bc.setValue(isMutedFromUser); // true to mute the line, false to unmute
314                }
315            }
316        }
317        this.isMutedFromSystem = isMuted;
318    }
319
320    void registerCloseCallback(Consumer<Void> consumer) {
321        this.closeCallback = consumer;
322    }
323
324    void registerMuteCallback(Consumer<Void> consumer) {
325        this.muteCallback = consumer;
326    }
327
328    private class FakeMuteControl extends BooleanControl {
329        private final BooleanControl control;
330
331        /**
332         * Constructs a new boolean control object with the given parameters.
333         * The labels for the <code>true</code> and <code>false</code> states
334         * default to "true" and "false."
335         */
336        protected FakeMuteControl() {
337            super(BooleanControl.Type.MUTE, isMutedFromUser);
338            this.control = (BooleanControl) line.getControl(BooleanControl.Type.MUTE);
339        }
340
341        /**
342         * Sets the current value for the control.  The default
343         * implementation simply sets the value as indicated.
344         * Some controls require that their line be open before they can be affected
345         * by setting a value.
346         *
347         * @param value desired new value.
348         */
349        @Override
350        public void setValue(boolean value) {
351            isMutedFromUser = value;
352            if (!isMutedFromSystem) {
353                control.setValue(isMutedFromUser);
354            }
355        }
356
357        /**
358         * Obtains this control's current value.
359         *
360         * @return current value.
361         */
362        @Override
363        public boolean getValue() {
364            return isMutedFromUser;
365        }
366
367        /**
368         * Obtains the label for the specified state.
369         *
370         * @param state the state whose label will be returned
371         * @return the label for the specified state, such as "true" or "on"
372         * for <code>true</code>, or "false" or "off" for <code>false</code>.
373         */
374        @Override
375        public String getStateLabel(boolean state) {
376            if (state) {
377                return "true";
378            } else {
379                return "false";
380            }
381        }
382    }
383}