1   package org.daisy.pipeline.tts.calabash.impl;
2   
3   import java.net.URI;
4   import java.time.Duration;
5   import java.util.concurrent.atomic.AtomicReference;
6   
7   import org.daisy.pipeline.audio.AudioClip;
8   import org.daisy.pipeline.tts.TTSLog;
9   
10  /**
11   * Mapping between a fragment in the XML document and a fragment in an audio file.
12   */
13  class SoundFileLink {
14  
15  	/** Reference to fragment in XML document */
16  	private final URI textRef;
17  	/** Time position where the audio fragment starts within <code>clipBase</code> */
18  	private final Duration clipBegin;
19  	/** Time position where the audio fragment ends within <code>clipBase</code> */
20  	private final Duration clipEnd;
21  
22  	/**
23  	 * Audio clip that is used as the base for <code>clipBegin</code> and <code>clipEnd</code>. The
24  	 * purpose of the reference is so that it can be easily shared between {@link TextToPcmThread}
25  	 * and {@link EncodingThread} (via {@link ContiguousPCM}), because {@link AudioClip} is
26  	 * immutable.
27  	 */
28  	public AtomicReference<AudioClip> clipBase;
29  
30  	public SoundFileLink(URI textRef, Duration clipBegin, Duration clipEnd) {
31  		if (clipBegin == null || clipEnd == null || textRef == null)
32  			throw new NullPointerException();
33  		if (clipBegin.compareTo(Duration.ZERO) < 0)
34  			throw new IllegalArgumentException("Clip begin can not be negative: " + clipBegin);
35  		if (clipEnd.compareTo(clipBegin) < 0)
36  			throw new IllegalArgumentException("Clip end can not come before clip begin: " + clipEnd + " < " + clipBegin);
37  		this.textRef = textRef;
38  		this.clipBegin = clipBegin;
39  		this.clipEnd = clipEnd;
40  	}
41  
42  	public URI getTextFragment() {
43  		return textRef;
44  	}
45  
46  	private AudioClip clip;
47  
48  	public AudioClip getAudioFragment(TTSLog log) {
49  		if (clip == null) {
50  			if (clipBase == null)
51  				throw new IllegalStateException(); // should not happen
52  			AudioClip base = clipBase.get();
53  			if (base == null)
54  				return null; // was not set in EncodingThread due to an error
55  			Duration begin = base.clipBegin.plus(clipBegin);
56  			Duration end = base.clipBegin.plus(clipEnd);
57  			if (begin.compareTo(base.clipEnd) > 0) {
58  				if (begin.minus(base.clipEnd).toMillis() > 1)
59  					// this can not be a rounding error due to sampling
60  					log.getOrCreateEntry(getTextFragment().toString()).addError(
61  						new TTSLog.Error(TTSLog.ErrorCode.AUDIO_MISSING, "clip not encoded"));
62  				begin = base.clipEnd;
63  				end = base.clipEnd;
64  			} else if (end.compareTo(base.clipEnd) > 0) {
65  				if (end.minus(base.clipEnd).toMillis() > 1)
66  					// this can not be a rounding error due to sampling
67  					log.getOrCreateEntry(getTextFragment().toString()).addError(
68  						new TTSLog.Error(TTSLog.ErrorCode.AUDIO_MISSING, "part of clip not encoded"));
69  				end = base.clipEnd;
70  			}
71  			clip = new AudioClip(base.src, begin, end);
72  		}
73  		return clip;
74  	}
75  }