001package org.intellimate.izou.system.sound; 002 003import org.intellimate.izou.addon.AddOnModel; 004import org.intellimate.izou.events.EventLifeCycle; 005import org.intellimate.izou.events.EventListenerModel; 006import org.intellimate.izou.events.EventMinimalImpl; 007import org.intellimate.izou.events.EventModel; 008import org.intellimate.izou.identification.Identification; 009import org.intellimate.izou.identification.IdentificationManager; 010import org.intellimate.izou.main.Main; 011import org.intellimate.izou.resource.ResourceMinimalImpl; 012import org.intellimate.izou.resource.ResourceModel; 013import org.intellimate.izou.util.AddonThreadPoolUser; 014import org.intellimate.izou.util.IzouModule; 015import ro.fortsoft.pf4j.AspectOrAffected; 016 017import java.lang.ref.Reference; 018import java.lang.ref.WeakReference; 019import java.lang.reflect.InvocationTargetException; 020import java.lang.reflect.Method; 021import java.net.URL; 022import java.time.LocalDateTime; 023import java.time.temporal.ChronoUnit; 024import java.util.*; 025import java.util.concurrent.ConcurrentHashMap; 026import java.util.concurrent.Future; 027import java.util.concurrent.TimeUnit; 028import java.util.concurrent.atomic.AtomicBoolean; 029import java.util.concurrent.locks.Condition; 030import java.util.concurrent.locks.Lock; 031import java.util.concurrent.locks.ReentrantLock; 032import java.util.function.Function; 033import java.util.function.Predicate; 034import java.util.stream.Collectors; 035 036/** 037 * the SoundManager manages all IzouSoundLine, tracks them and is responsible for enforcing that only one permanent- 038 * AddOn can play at one time. 039 * @author LeanderK 040 * @version 1.0 041 */ 042//TODO: native sound code enforcing (mute, stop(?) etc. 043//TODO: we must enforce sequential access (only one addon can defacto play sound). We can hide this using the IzouSoundLines (closing and opening the underlying lines). 044public class SoundManager extends IzouModule implements AddonThreadPoolUser, EventListenerModel { 045 //non-permanent and general fields 046 private ConcurrentHashMap<AddOnModel, List<WeakReference<IzouSoundLineBaseClass>>> nonPermanent = new ConcurrentHashMap<>(); 047 //not null if this AddOn is currently muting the others Lines 048 private MutingManager mutingManager = null; 049 private final Object mutingLock = new Object(); 050 051 //permanent fields, there is a Read/Write lock! 052 private List<WeakReference<IzouSoundLineBaseClass>> permanentLines = null; 053 private AddOnModel permanentAddOn = null; 054 //gets filled when the event got fired 055 private Identification knownIdentification = null; 056 //Addon has 10 sec to obtain an IzouSoundLine 057 private LocalDateTime permissionWithoutUsageLimit = null; 058 //if true we can do nothing to check whether he closed. 059 private boolean isUsingNonJava = false; 060 private Future permissionWithoutUsageCloseThread = null; 061 private final Object permanentUserReadWriteLock = new Object(); 062 private AtomicBoolean isUsing = new AtomicBoolean(false); 063 064 public SoundManager(Main main) { 065 super(main); 066 main.getEventDistributor().registerEventListener(Arrays.asList(SoundIDs.StartEvent.descriptor, SoundIDs.StartRequest.descriptor, 067 SoundIDs.EndedEvent.descriptor), this); 068 069 URL mixerURL = this.getClass().getClassLoader().getResource("org/intellimate/izou/system/sound/replaced/MixerAspect.class"); 070 AspectOrAffected mixer = new AspectOrAffected(mixerURL, 071 "org.intellimate.izou.system.sound.replaced.MixerAspect", 072 aClass -> { 073 try { 074 Method init = aClass.getMethod("init", Main.class); 075 init.invoke(null, main); 076 return aClass; 077 } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { 078 error("error while trying to initialize the MixerAspect"); 079 return aClass; 080 } 081 }, 082 true); 083 URL audioSystemURL = this.getClass().getClassLoader().getResource("javax/sound/sampled/AudioSystem.class"); 084 AspectOrAffected audioSystem = new AspectOrAffected(audioSystemURL, 085 "javax.sound.sampled.AudioSystem", 086 Function.identity(), 087 false); 088 getMain().getAddOnManager().addAspectOrAffected(audioSystem); 089 getMain().getAddOnManager().addAspectOrAffected(mixer); 090 } 091 092 /** 093 * removes obsolete references 094 */ 095 private void tidy() { 096 nonPermanent.entrySet().stream() 097 .map(entry -> { 098 List<WeakReference<IzouSoundLineBaseClass>> collect = entry.getValue().stream() 099 .filter(izouSoundLineWeakReference -> izouSoundLineWeakReference.get() != null) 100 .collect(Collectors.toList()); 101 if (!collect.isEmpty()) { 102 nonPermanent.put(entry.getKey(), collect); 103 return null; 104 } else { 105 return entry; 106 } 107 }) 108 .filter(Objects::nonNull) 109 .forEach(entry -> nonPermanent.remove(entry.getKey())); 110 } 111 112 /** 113 * adds an IzouSoundLine, will now be tracked by the SoundManager 114 * @param addOnModel the addOnModel where the IzouSoundLine belongs to 115 * @param izouSoundLine the IzouSoundLine to add 116 */ 117 public void addIzouSoundLine(AddOnModel addOnModel, IzouSoundLineBaseClass izouSoundLine) { 118 debug("adding soundLine " + izouSoundLine + " from " + addOnModel); 119 if (permanentAddOn != null && permanentAddOn.equals(addOnModel)) { 120 addPermanent(izouSoundLine); 121 } else { 122 addNonPermanent(addOnModel, izouSoundLine); 123 } 124 izouSoundLine.registerCloseCallback(voit -> closeCallback(addOnModel, izouSoundLine)); 125 izouSoundLine.registerMuteCallback(voit -> muteCallback(addOnModel, izouSoundLine)); 126 } 127 128 protected void muteCallback(AddOnModel addOnModel, IzouSoundLineBaseClass izouSoundLine) { 129 synchronized (mutingLock) { 130 if (mutingManager != null && mutingManager.getMuting().equals(addOnModel)) { 131 mutingManager.add(izouSoundLine); 132 } else if (mutingManager == null || mutingManager.isTimeOut()) { 133 mutingManager = new MutingManager(this, addOnModel, izouSoundLine); 134 } 135 } 136 } 137 138 /** 139 * the close-callback or the AddonModel, removes now redundant references 140 * @param addOnModel the addOnModel where the IzouSoundLine belongs to 141 * @param izouSoundLine the izouSoundLine 142 */ 143 private void closeCallback(AddOnModel addOnModel, IzouSoundLine izouSoundLine) { 144 debug("removing soundline " + izouSoundLine + " from " + addOnModel); 145 Predicate<WeakReference<IzouSoundLineBaseClass>> removeFromList = 146 weakReference -> weakReference.get() != null && weakReference.get().equals(izouSoundLine); 147 synchronized (permanentUserReadWriteLock) { 148 if (permanentAddOn != null && permanentAddOn.equals(addOnModel) && permanentLines != null) { 149 permanentLines.removeIf(removeFromList); 150 if (permanentLines.isEmpty()) { 151 permanentLines = null; 152 permissionWithoutUsage(); 153 } 154 } 155 } 156 List<WeakReference<IzouSoundLineBaseClass>> weakReferences = nonPermanent.get(addOnModel); 157 if (weakReferences != null) { 158 weakReferences.removeIf(removeFromList); 159 synchronized (mutingLock) { 160 if (mutingManager != null && mutingManager.getMuting().equals(addOnModel)) { 161 mutingManager = mutingManager.remove(izouSoundLine); 162 } 163 } 164 } 165 submit(this::tidy); 166 } 167 168 /** 169 * creates a LocaleDateTime-Object 10 seconds in the Future and a Thread which will remove it, if it passes the threshold. 170 * the Thread 171 */ 172 private void permissionWithoutUsage() { 173 if (isUsingNonJava) 174 return; 175 synchronized (permanentUserReadWriteLock) { 176 permissionWithoutUsageLimit = LocalDateTime.now().plus(10, ChronoUnit.SECONDS); 177 permissionWithoutUsageCloseThread = getMain().getThreadPoolManager().getAddOnsThreadPool() 178 .submit((Runnable)() -> { 179 try { 180 Thread.sleep(10000); 181 fireLineAccessEndedNotification(); 182 endPermanent(permanentAddOn); 183 } catch (InterruptedException ignored) { 184 //ignored.printStackTrace(); 185 } 186 }); 187 } 188 } 189 190 /** 191 * removes the LocaleDateTime and Thread (if exisiting) 192 */ 193 private void endWaitingForUsage() { 194 synchronized (permanentUserReadWriteLock) { 195 if (permissionWithoutUsageLimit != null) 196 permissionWithoutUsageLimit = null; 197 if (permissionWithoutUsageCloseThread != null) { 198 permissionWithoutUsageCloseThread.cancel(true); 199 permissionWithoutUsageLimit = null; 200 } 201 } 202 } 203 204 /** 205 * adds the IzouSoundLine as permanent 206 * @param izouSoundLine the izouSoundLine to add 207 */ 208 private void addPermanent(IzouSoundLineBaseClass izouSoundLine) { 209 debug("adding " + izouSoundLine + " to permanent"); 210 if (!izouSoundLine.isPermanent()) 211 izouSoundLine.setToPermanent(); 212 synchronized (permanentUserReadWriteLock) { 213 endWaitingForUsage(); 214 if (permanentLines == null) { 215 permanentLines = Collections.synchronizedList(new ArrayList<>()); 216 } 217 permanentLines.add(new WeakReference<>(izouSoundLine)); 218 } 219 //TODO: STOP the addon via the stop event 220 } 221 222 /** 223 * adds the IzouSoundLine as NonPermanent 224 * @param addOnModel the AddonModel to 225 * @param izouSoundLine the IzouSoundLine to add 226 */ 227 private void addNonPermanent(AddOnModel addOnModel, IzouSoundLineBaseClass izouSoundLine) { 228 debug("adding " + izouSoundLine + " from " + addOnModel + " to non-permanent"); 229 if (izouSoundLine.isPermanent()) 230 izouSoundLine.setToNonPermanent(); 231 List<WeakReference<IzouSoundLineBaseClass>> weakReferences = nonPermanent.get(addOnModel); 232 if (weakReferences == null) 233 weakReferences = Collections.synchronizedList(new ArrayList<>()); 234 nonPermanent.put(addOnModel, weakReferences); 235 weakReferences.add(new WeakReference<>(izouSoundLine)); 236 } 237 238 /** 239 * tries to register the AddonModel as permanent 240 * @param addOnModel the AddonModel to register 241 * @param source the Source which requested the usage 242 * @param nonJava true if it is not using java to play sounds 243 */ 244 public void requestPermanent(AddOnModel addOnModel, Identification source, boolean nonJava) { 245 debug("requesting permanent for addon: " + addOnModel); 246 boolean notUsing = isUsing.compareAndSet(false, true); 247 if (!notUsing) { 248 debug("already used by " + permanentAddOn); 249 synchronized (permanentUserReadWriteLock) { 250 if (permanentAddOn != null && permanentAddOn.equals(addOnModel)) { 251 if (knownIdentification == null) 252 knownIdentification = source; 253 return; 254 } else { 255 endPermanent(permanentAddOn); 256 addAsPermanent(addOnModel, source, nonJava); 257 } 258 } 259 } else { 260 addAsPermanent(addOnModel, source, nonJava); 261 } 262 } 263 264 protected void addAsPermanent(AddOnModel addOnModel, Identification source, boolean nonJava) { 265 synchronized (permanentUserReadWriteLock) { 266 permanentAddOn = addOnModel; 267 knownIdentification = source; 268 isUsingNonJava = nonJava; 269 permissionWithoutUsageLimit = null; 270 if (permissionWithoutUsageCloseThread != null) 271 permissionWithoutUsageCloseThread.cancel(true); 272 permissionWithoutUsageCloseThread = null; 273 274 List<WeakReference<IzouSoundLineBaseClass>> weakReferences = nonPermanent.remove(addOnModel); 275 if (weakReferences == null) { 276 if (isUsingNonJava) { 277 permanentLines = new ArrayList<>(); 278 } else { 279 permissionWithoutUsage(); 280 } 281 } else { 282 nonPermanent.remove(addOnModel); 283 permanentLines = weakReferences; 284 permanentLines.forEach(weakReferenceLine -> { 285 IzouSoundLineBaseClass izouSoundLineBaseClass = weakReferenceLine.get(); 286 if (izouSoundLineBaseClass != null) { 287 izouSoundLineBaseClass.setToPermanent(); 288 izouSoundLineBaseClass.setResponsibleID(source); 289 } 290 }); 291 } 292 synchronized (mutingLock) { 293 if (mutingManager != null && mutingManager.getMuting().equals(addOnModel)) { 294 mutingManager.cancel(); 295 mutingManager = null; 296 } 297 } 298 } 299 } 300 301 /** 302 * unregisters the AddonModel as permanent 303 * @param addOnModel the addonModel to check 304 */ 305 public void endPermanent(AddOnModel addOnModel) { 306 if (!isUsing.get() || (permanentAddOn != null && !permanentAddOn.equals(addOnModel))) 307 return; 308 synchronized (permanentUserReadWriteLock) { 309 permanentAddOn = null; 310 Identification tempID = this.knownIdentification; 311 this.knownIdentification = null; 312 if (permanentLines != null) { 313 permanentLines.forEach(weakReferenceLine -> { 314 if (weakReferenceLine.get() != null) 315 weakReferenceLine.get().setToNonPermanent(); 316 }); 317 nonPermanent.put(addOnModel, permanentLines); 318 permanentLines = null; 319 } 320 stopAddon(tempID); 321 endWaitingForUsage(); 322 isUsing.set(false); 323 } 324 } 325 326 private void stopAddon(Identification identification) { 327 Lock lock = new ReentrantLock(); 328 Condition callback = lock.newCondition(); 329 if (identification != null) { 330 IdentificationManager.getInstance() 331 .getIdentification(this) 332 .map(id -> new EventMinimalImpl(SoundIDs.StopEvent.type, id, SoundIDs.StopEvent.descriptors, eventLifeCycle -> { 333 if (eventLifeCycle.equals(EventLifeCycle.ENDED)) { 334 lock.lock(); 335 try { 336 callback.signal(); 337 } finally { 338 lock.unlock(); 339 } 340 } 341 })) 342 .map(eventMinimal -> eventMinimal.addResource( 343 new ResourceMinimalImpl<>(SoundIDs.StopEvent.resourceSelector, eventMinimal.getSource(), identification, null))) 344 .ifPresent(event -> getMain().getEventDistributor().fireEventConcurrently(event)); 345 } 346 lock.lock(); 347 try { 348 callback.await(1, TimeUnit.SECONDS); 349 } catch (InterruptedException ignored) { 350 351 } finally { 352 lock.unlock(); 353 } 354 } 355 356 private void fireLineAccessEndedNotification() { 357 if (knownIdentification != null) { 358 EventModel event = new EventMinimalImpl(SoundIDs.EndedEvent.type, knownIdentification, SoundIDs.EndedEvent.descriptors); 359 getMain().getEventDistributor().fireEventConcurrently(event); 360 } 361 } 362 363 /** 364 * mutes the other Addons 365 * @param addOnModel the addonModel responsible 366 */ 367 void muteOthers(AddOnModel addOnModel) { 368 Set<AddOnModel> toMute = nonPermanent.entrySet().stream() 369 .filter(entry -> !entry.getKey().equals(addOnModel)) 370 .flatMap(entry -> entry.getValue().stream()) 371 .map(Reference::get) 372 .filter(Objects::nonNull) 373 .peek(izouSoundLineBaseClass -> izouSoundLineBaseClass.setMutedFromSystem(true)) 374 .map(IzouSoundLine::getAddOnModel) 375 .collect(Collectors.toSet()); 376 if (permanentAddOn != null && !permanentAddOn.equals(addOnModel) && permanentLines != null) { 377 toMute.add(addOnModel); 378 permanentLines.stream() 379 .map(Reference::get) 380 .filter(Objects::nonNull) 381 .forEach(izouSoundLineBaseClass -> izouSoundLineBaseClass.setMutedFromSystem(true)); 382 } 383 toMute.forEach(this::mute); 384 List<WeakReference<IzouSoundLineBaseClass>> weakReferences = nonPermanent.get(addOnModel); 385 if (weakReferences != null) { 386 weakReferences.stream() 387 .map(Reference::get) 388 .filter(Objects::nonNull) 389 .forEach(izouSoundLine -> izouSoundLine.setMutedFromSystem(false)); 390 } 391 } 392 393 /** 394 * mutes the list of soundLines and fires the Mute-Event 395 * @param model the addonModel to mute 396 */ 397 private void mute(AddOnModel model) { 398 IdentificationManager.getInstance() 399 .getIdentification(this) 400 .map(id -> new EventMinimalImpl(SoundIDs.MuteEvent.type, id, SoundIDs.MuteEvent.descriptors)) 401 .map(eventMinimal -> eventMinimal.addResource( 402 new ResourceMinimalImpl<>(SoundIDs.MuteEvent.resourceSelector, eventMinimal.getSource(), model, null))) 403 .ifPresent(event -> getMain().getEventDistributor().fireEventConcurrently(event)); 404 } 405 406 /** 407 * unmutes all 408 */ 409 void unmute() { 410 nonPermanent.entrySet().stream() 411 .flatMap(entry -> entry.getValue().stream()) 412 .map(Reference::get) 413 .filter(Objects::nonNull) 414 .forEach(izouSoundLineBaseClass -> izouSoundLineBaseClass.setMutedFromSystem(false)); 415 416 if (permanentLines != null) 417 permanentLines.stream() 418 .map(Reference::get) 419 .filter(Objects::nonNull) 420 .forEach(izouSoundLineBaseClass -> izouSoundLineBaseClass.setMutedFromSystem(false)); 421 422 IdentificationManager.getInstance() 423 .getIdentification(this) 424 .map(id -> new EventMinimalImpl(SoundIDs.UnMuteEvent.type, id, SoundIDs.UnMuteEvent.descriptors)) 425 .ifPresent(event -> getMain().getEventDistributor().fireEventConcurrently(event)); 426 } 427 428 private void checkAndUpdateIdentification(Identification identification) { 429 AddOnModel addonModel = getMain().getInternalIdentificationManager().getAddonModel(identification); 430 if (permanentAddOn.equals(addonModel)) { 431 synchronized (permanentUserReadWriteLock) { 432 if (permanentAddOn.equals(addonModel)) { 433 knownIdentification = identification; 434 } 435 } 436 } 437 } 438 439 /** 440 * Invoked when an activator-event occurs. 441 * 442 * @param event an instance of Event 443 */ 444 @Override 445 public void eventFired(EventModel event) { 446 if (event.containsDescriptor(SoundIDs.StartRequest.descriptor)) { 447 Identification identification = event.getListResourceContainer().provideResource("izou.common.resource.selector").stream() 448 .map(ResourceModel::getResource) 449 .filter(resource -> resource instanceof Identification) 450 .map(resource -> (Identification) resource) 451 .findFirst() 452 .orElseGet(event::getSource); 453 454 AddOnModel addonModel = getMain().getInternalIdentificationManager().getAddonModel(identification); 455 456 if (addonModel != null) { 457 requestPermanent(addonModel, 458 event.getSource(), event.containsDescriptor(SoundIDs.StartEvent.isUsingNonJava)); 459 } 460 461 } else if (event.containsDescriptor(SoundIDs.StartEvent.descriptor)) { 462 checkAndUpdateIdentification(event.getSource()); 463 } else { 464 Identification identification = event.getListResourceContainer().provideResource("izou.common.resource.selector").stream() 465 .map(ResourceModel::getResource) 466 .filter(resource -> resource instanceof Identification) 467 .map(resource -> (Identification) resource) 468 .findFirst() 469 .orElseGet(event::getSource); 470 AddOnModel addonModel = getMain().getInternalIdentificationManager().getAddonModel(identification); 471 if (addonModel != null) { 472 endPermanent(addonModel); 473 } 474 } 475 } 476}