Crossfire JXClient, Trunk
ClipManager.java
Go to the documentation of this file.
1 /*
2  * This file is part of JXClient, the Fullscreen Java Crossfire Client.
3  *
4  * JXClient is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * JXClient is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with JXClient; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17  *
18  * Copyright (C) 2005-2008 Yann Chachkoff
19  * Copyright (C) 2006-2017,2019-2023 Andreas Kirschbaum
20  * Copyright (C) 2010-2012,2014-2018,2020-2023 Nicolas Weeger
21  */
22 
23 package com.realtime.crossfire.jxclient.sound;
24 
26 import java.util.concurrent.Callable;
27 import java.util.concurrent.CompletionService;
28 import java.util.concurrent.ExecutionException;
29 import java.util.concurrent.ExecutorCompletionService;
30 import java.util.concurrent.ExecutorService;
31 import java.util.concurrent.Executors;
32 import java.util.concurrent.Future;
33 import java.util.concurrent.Semaphore;
34 import javax.sound.sampled.DataLine;
35 import org.jetbrains.annotations.NotNull;
36 import org.jetbrains.annotations.Nullable;
37 
43 public class ClipManager {
44 
49  @Nullable
50  private final DebugWriter debugSound;
51 
55  @NotNull
57 
61  @NotNull
62  private final ExecutorService executorService = Executors.newCachedThreadPool();
63 
67  @NotNull
68  private final CompletionService<@Nullable Task> executorCompletionService = new ExecutorCompletionService<>(executorService);
69 
73  @NotNull
74  private final ClipLoader clipLoader;
75 
79  @NotNull
80  private final Semaphore concurrentClips = new Semaphore(8);
81 
85  private int nextClipId;
86 
94  public ClipManager(@NotNull final AudioFileLoader audioFileLoader, @Nullable final DebugWriter debugSound, @NotNull final SoundTaskExecutor soundTaskExecutor) {
95  this.debugSound = debugSound;
96  this.soundTaskExecutor = soundTaskExecutor;
97  clipLoader = new ClipLoader(audioFileLoader, debugSound);
98  executorService.execute(this::playClips);
99  }
100 
105  public void play(@NotNull final CharSequence action) {
106  if (concurrentClips.tryAcquire()) {
107  final int id = nextClipId++;
108  if (debugSound != null) {
109  debugSound.debugProtocolWrite(id+".0 starting sound clip '"+action+"'");
110  }
111  executorCompletionService.submit(() -> new ClipLoadTask(id, action));
112  } else if (debugSound != null) {
113  debugSound.debugProtocolWrite("not playing sound clip '"+action+"' because too many sound clips are already active");
114  }
115  }
116 
120  private void playClips() {
121  if (debugSound != null) {
122  debugSound.debugProtocolWrite("sound clip thread started");
123  }
124  try {
125  while (!Thread.currentThread().isInterrupted()) {
126  final Future<@Nullable Task> future;
127  try {
128  future = executorCompletionService.take();
129  } catch (final InterruptedException ignored) {
130  Thread.currentThread().interrupt();
131  break;
132  }
133 
134  final Task task;
135  try {
136  task = future.get();
137  } catch (final InterruptedException ex) {
138  Thread.currentThread().interrupt();
139  if (debugSound != null) {
140  debugSound.debugProtocolWrite("interrupted while allocating clip", ex);
141  }
142  return;
143  } catch (final ExecutionException ex) {
144  if (debugSound != null) {
145  debugSound.debugProtocolWrite("failed to allocate clip", ex);
146  }
147  return;
148  }
149 
150  if (task == null) {
151  continue;
152  }
153  if (debugSound != null) {
154  debugSound.debugProtocolWrite(task+" scheduling next step");
155  }
156 
157  executorCompletionService.submit(task.getCallable());
158  }
159  } finally {
160  if (debugSound != null) {
161  debugSound.debugProtocolWrite("sound clip thread existed");
162  }
163  }
164  }
165 
171  public void shutdown() throws InterruptedException {
172  soundTaskExecutor.executeAndWait(executorService::shutdownNow);
173  }
174 
178  private interface Task {
179 
184  @NotNull
185  Callable<@Nullable Task> getCallable();
186 
187  }
188 
192  private static abstract class AbstractTask {
193 
197  private final int id;
198 
202  private final int subId;
203 
208  private AbstractTask(final int id) {
209  this.id = id;
210  subId = 1;
211  }
212 
217  private AbstractTask(@NotNull final AbstractTask parent) {
218  id = parent.id;
219  subId = parent.subId+1;
220  }
221 
222  @NotNull
223  @Override
224  public String toString() {
225  return id+"."+subId+"/"+getClass().getSimpleName();
226  }
227 
228  }
229 
233  private class ClipLoadTask extends AbstractTask implements Task {
234 
238  @NotNull
239  private final CharSequence action;
240 
246  private ClipLoadTask(final int id, @NotNull final CharSequence action) {
247  super(id);
248  this.action = action;
249  }
250 
251  @NotNull
252  @Override
253  public Callable<@Nullable Task> getCallable() {
254  return () -> {
255  if (debugSound != null) {
256  debugSound.debugProtocolWrite(this+" loading sound clip for action="+action);
257  }
258  final long t0 = System.currentTimeMillis();
259  final DataLine clip = clipLoader.allocateClip(action);
260  if (clip == null) {
261  concurrentClips.release();
262  if (debugSound != null) {
263  debugSound.debugProtocolWrite(this+" done (could not load sound clip for action="+action+")");
264  }
265  return null;
266  }
267  if (debugSound != null) {
268  debugSound.debugProtocolWrite(this+" loading sound clip took "+(System.currentTimeMillis()-t0)+"ms");
269  }
270  return new ClipStartTask(this, clip);
271  };
272  }
273 
274  }
275 
279  private class ClipStartTask extends AbstractTask implements Task {
280 
284  @NotNull
285  private final DataLine clip;
286 
292  private ClipStartTask(@NotNull final AbstractTask parent, @NotNull final DataLine clip) {
293  super(parent);
294  this.clip = clip;
295  }
296 
297  @NotNull
298  @Override
299  public Callable<@NotNull Task> getCallable() {
300  return () -> {
301  if (debugSound != null) {
302  debugSound.debugProtocolWrite(this+" starting to play sound clip");
303  }
304  clip.start();
305  return new ClipPlayTask(this, clip);
306  };
307  }
308 
309  }
310 
314  private class ClipPlayTask extends AbstractTask implements Task {
315 
319  @NotNull
320  private final DataLine clip;
321 
327  private ClipPlayTask(@NotNull final AbstractTask parent, @NotNull final DataLine clip) {
328  super(parent);
329  this.clip = clip;
330  }
331 
332  @NotNull
333  @Override
334  public Callable<@NotNull Task> getCallable() {
335  return () -> {
336  if (debugSound != null) {
337  debugSound.debugProtocolWrite(this+" waiting until sound clip has finished playing");
338  }
339  clip.drain();
340  return new ClipStopTask(this, clip);
341  };
342  }
343 
344  }
345 
349  private class ClipStopTask extends AbstractTask implements Task {
350 
354  @NotNull
355  private final DataLine clip;
356 
362  private ClipStopTask(@NotNull final AbstractTask parent, @NotNull final DataLine clip) {
363  super(parent);
364  this.clip = clip;
365  }
366 
367  @NotNull
368  @Override
369  public Callable<@NotNull Task> getCallable() {
370  return () -> {
371  if (debugSound != null) {
372  debugSound.debugProtocolWrite(this+" stopping to play sound clip");
373  }
374  clip.stop();
375  return new ClipFreeTask(this, clip);
376  };
377  }
378 
379  }
380 
384  private class ClipFreeTask extends AbstractTask implements Task {
385 
389  @NotNull
390  private final DataLine clip;
391 
397  private ClipFreeTask(@NotNull final AbstractTask parent, @NotNull final DataLine clip) {
398  super(parent);
399  this.clip = clip;
400  }
401 
402  @NotNull
403  @Override
404  public Callable<@Nullable Task> getCallable() {
405  return () -> {
406  if (debugSound != null) {
407  debugSound.debugProtocolWrite(this+" deallocating sound clip");
408  }
409  final long t0 = System.currentTimeMillis();
411  if (debugSound != null) {
412  debugSound.debugProtocolWrite(this+" deallocating sound clip took "+(System.currentTimeMillis()-t0)+"ms");
413  }
414  concurrentClips.release();
415  if (debugSound != null) {
416  debugSound.debugProtocolWrite(this+" done");
417  }
418  return null;
419  };
420  }
421 
422  }
423 
424 }
com.realtime.crossfire.jxclient.sound.ClipManager.nextClipId
int nextClipId
Definition: ClipManager.java:85
com.realtime.crossfire.jxclient
com.realtime.crossfire.jxclient.sound.ClipManager.playClips
void playClips()
Definition: ClipManager.java:120
com.realtime.crossfire.jxclient.sound.ClipManager.ClipPlayTask.ClipPlayTask
ClipPlayTask(@NotNull final AbstractTask parent, @NotNull final DataLine clip)
Definition: ClipManager.java:327
com.realtime.crossfire.jxclient.sound.ClipManager.shutdown
void shutdown()
Definition: ClipManager.java:171
com.realtime.crossfire.jxclient.sound.ClipManager.executorService
final ExecutorService executorService
Definition: ClipManager.java:62
com.realtime.crossfire.jxclient.sound.ClipManager.ClipStartTask.ClipStartTask
ClipStartTask(@NotNull final AbstractTask parent, @NotNull final DataLine clip)
Definition: ClipManager.java:292
com.realtime.crossfire.jxclient.sound.ClipLoader
Definition: ClipLoader.java:43
com.realtime.crossfire.jxclient.sound.ClipManager.ClipPlayTask.getCallable
Callable< @NotNull Task > getCallable()
Definition: ClipManager.java:334
com.realtime.crossfire.jxclient.sound.ClipManager.ClipStartTask.clip
final DataLine clip
Definition: ClipManager.java:285
com.realtime.crossfire.jxclient.sound.ClipManager.ClipLoadTask.getCallable
Callable< @Nullable Task > getCallable()
Definition: ClipManager.java:253
com.realtime.crossfire.jxclient.sound.ClipManager.ClipFreeTask.clip
final DataLine clip
Definition: ClipManager.java:390
com.realtime.crossfire.jxclient.sound.ClipManager.ClipLoadTask
Definition: ClipManager.java:233
com.realtime.crossfire.jxclient.sound.ClipManager.ClipStopTask.clip
final DataLine clip
Definition: ClipManager.java:355
com.realtime.crossfire.jxclient.sound.ClipManager.clipLoader
final ClipLoader clipLoader
Definition: ClipManager.java:74
com.realtime.crossfire.jxclient.sound.ClipManager.ClipStopTask
Definition: ClipManager.java:349
com.realtime.crossfire.jxclient.sound.ClipManager.ClipStartTask.getCallable
Callable< @NotNull Task > getCallable()
Definition: ClipManager.java:299
com.realtime.crossfire.jxclient.sound.ClipManager.Task.getCallable
Callable< @Nullable Task > getCallable()
com.realtime.crossfire.jxclient.sound.ClipManager.AbstractTask.subId
final int subId
Definition: ClipManager.java:202
com.realtime.crossfire.jxclient.sound.ClipManager.ClipLoadTask.action
final CharSequence action
Definition: ClipManager.java:239
com.realtime.crossfire.jxclient.sound.ClipManager.ClipStopTask.ClipStopTask
ClipStopTask(@NotNull final AbstractTask parent, @NotNull final DataLine clip)
Definition: ClipManager.java:362
com.realtime.crossfire.jxclient.sound.ClipManager.ClipStartTask
Definition: ClipManager.java:279
com.realtime.crossfire.jxclient.sound.ClipManager.AbstractTask.id
final int id
Definition: ClipManager.java:197
com.realtime.crossfire.jxclient.sound.AudioFileLoader
Definition: AudioFileLoader.java:41
com.realtime.crossfire.jxclient.sound.ClipManager.ClipFreeTask.getCallable
Callable< @Nullable Task > getCallable()
Definition: ClipManager.java:404
com.realtime.crossfire.jxclient.sound.ClipLoader.freeClip
void freeClip(@NotNull final Line clip)
Definition: ClipLoader.java:73
com.realtime.crossfire.jxclient.sound.ClipManager.concurrentClips
final Semaphore concurrentClips
Definition: ClipManager.java:80
com.realtime.crossfire.jxclient.sound.ClipManager.ClipPlayTask
Definition: ClipManager.java:314
com.realtime.crossfire.jxclient.sound.ClipManager.ClipFreeTask.ClipFreeTask
ClipFreeTask(@NotNull final AbstractTask parent, @NotNull final DataLine clip)
Definition: ClipManager.java:397
com.realtime.crossfire.jxclient.sound.ClipManager.AbstractTask
Definition: ClipManager.java:192
com.realtime.crossfire.jxclient.util
Definition: Codec.java:23
com.realtime.crossfire.jxclient.sound.ClipManager.AbstractTask.toString
String toString()
Definition: ClipManager.java:224
com.realtime.crossfire.jxclient.sound.ClipManager.debugSound
final DebugWriter debugSound
Definition: ClipManager.java:50
com.realtime.crossfire
com.realtime.crossfire.jxclient.sound.ClipLoader.allocateClip
DataLine allocateClip(@NotNull final CharSequence action)
Definition: ClipLoader.java:86
com.realtime
com.realtime.crossfire.jxclient.sound.ClipManager.ClipStopTask.getCallable
Callable< @NotNull Task > getCallable()
Definition: ClipManager.java:369
com.realtime.crossfire.jxclient.sound.SoundTaskExecutor
Definition: SoundTaskExecutor.java:34
com.realtime.crossfire.jxclient.sound.ClipManager.soundTaskExecutor
final SoundTaskExecutor soundTaskExecutor
Definition: ClipManager.java:56
com
com.realtime.crossfire.jxclient.sound.ClipManager.AbstractTask.AbstractTask
AbstractTask(@NotNull final AbstractTask parent)
Definition: ClipManager.java:217
com.realtime.crossfire.jxclient.sound.ClipManager.Task
Definition: ClipManager.java:178
com.realtime.crossfire.jxclient.sound.ClipManager.play
void play(@NotNull final CharSequence action)
Definition: ClipManager.java:105
com.realtime.crossfire.jxclient.sound.ClipManager.ClipLoadTask.ClipLoadTask
ClipLoadTask(final int id, @NotNull final CharSequence action)
Definition: ClipManager.java:246
com.realtime.crossfire.jxclient.util.DebugWriter
Definition: DebugWriter.java:36
com.realtime.crossfire.jxclient.sound.ClipManager.ClipManager
ClipManager(@NotNull final AudioFileLoader audioFileLoader, @Nullable final DebugWriter debugSound, @NotNull final SoundTaskExecutor soundTaskExecutor)
Definition: ClipManager.java:94
com.realtime.crossfire.jxclient.sound.ClipManager
Definition: ClipManager.java:43
com.realtime.crossfire.jxclient.sound.ClipManager.executorCompletionService
final CompletionService< @Nullable Task > executorCompletionService
Definition: ClipManager.java:68
com.realtime.crossfire.jxclient.sound.SoundTaskExecutor.executeAndWait
void executeAndWait(@NotNull final Runnable task)
Definition: SoundTaskExecutor.java:95
com.realtime.crossfire.jxclient.sound.ClipManager.ClipFreeTask
Definition: ClipManager.java:384
com.realtime.crossfire.jxclient.sound.ClipManager.AbstractTask.AbstractTask
AbstractTask(final int id)
Definition: ClipManager.java:208
com.realtime.crossfire.jxclient.util.DebugWriter.debugProtocolWrite
void debugProtocolWrite(@NotNull final CharSequence str)
Definition: DebugWriter.java:68
com.realtime.crossfire.jxclient.sound.ClipManager.ClipPlayTask.clip
final DataLine clip
Definition: ClipManager.java:320