1   package org.daisy.pipeline.tts.calabash.impl;
2   
3   import java.io.File;
4   import java.util.Map;
5   import java.util.concurrent.BlockingQueue;
6   
7   import javax.sound.sampled.AudioFileFormat;
8   import javax.sound.sampled.AudioInputStream;
9   
10  import org.daisy.common.messaging.MessageAppender;
11  import org.daisy.common.messaging.MessageBuilder;
12  import org.daisy.pipeline.audio.AudioClip;
13  import org.daisy.pipeline.audio.AudioEncoder;
14  import org.daisy.pipeline.audio.AudioServices;
15  import org.daisy.pipeline.tts.AudioFootprintMonitor;
16  import org.daisy.pipeline.tts.TTSLog;
17  import org.daisy.pipeline.tts.TTSTimeout;
18  import org.daisy.pipeline.tts.TTSLog.ErrorCode;
19  
20  /**
21   * Consumes a shared queue of PCM packets. PCM packets are then provided to
22   * audio encoders. The thread stops when it receives an 'EndOfQueue' marker.
23   */
24  public class EncodingThread {
25  
26  	class EncodingException extends RuntimeException {
27  		public EncodingException(String message, Throwable cause) {
28  			super(message, cause);
29  		}
30  		public EncodingException(Throwable t) {
31  			super(t);
32  		}
33  		public EncodingException(String message) {
34  			super(message);
35  		}
36  	}
37  	
38  	private Thread mThread;
39  	private Throwable criticalError;
40  
41  	void start(final AudioFileFormat.Type fileType, final AudioServices encoderRegistry,
42  	        final BlockingQueue<ContiguousPCM> inputPCM, final AudioFootprintMonitor audioFootprintMonitor,
43  	        Map<String, String> TTSproperties, final TTSLog ttslog, MessageAppender messageAppender) {
44  
45  		//max seconds of encoded audio per seconds of encoding
46  		//it would be more accurate with a byte rate instead, but less intuitive
47  		float encodingSpeed = 2.0f;
48  		String speedProp = "org.daisy.pipeline.tts.encoding.speed";
49  		String speedParam = TTSproperties.get(speedProp);
50  		if (speedParam != null) {
51  			try {
52  				encodingSpeed = Float.valueOf(speedParam);
53  			} catch (NumberFormatException e) {
54  				ttslog.addGeneralError(
55  					ErrorCode.WARNING,
56  					"wrong format for property " + speedProp + ". A float is expected, not " + speedParam,
57  					e);
58  			}
59  		}
60  
61  		//Eventually, we should select the encoder using the audio format as criterion, but for now
62  		//we always employ the same encoder for every chunk of PCM
63  		AudioEncoder encoder = encoderRegistry.newEncoder(fileType, TTSproperties).orElse(null);
64  		if (encoder == null) {
65  			ttslog.addGeneralError(ErrorCode.CRITICAL_ERROR, "No audio encoder found");
66  		}
67  
68  		final AudioEncoder fencoder = encoder;
69  		final float fEncodingSpeed = encodingSpeed;
70  		final TTSTimeout timeout = new TTSTimeout();
71  
72  		mThread = new Thread() {
73  			@Override
74  			public void run() {
75  				// wrap the messages from this thread in a (empty) block so that there is always an
76  				// active block for this thread, so that SLF4J log messages always have a destination
77  				MessageAppender messageThread = messageAppender != null
78  					? messageAppender.append(new MessageBuilder())
79  					: null;
80  				try {
81  					while (!interrupted()) {
82  						ContiguousPCM job;
83  						try {
84  							job = inputPCM.take();
85  						} catch (InterruptedException e) {
86  							ttslog.addGeneralError(
87  								ErrorCode.CRITICAL_ERROR, "encoding thread has been interrupted");
88  							break; //warning: encoding bytes are not freed
89  						}
90  						if (job.isEndOfQueue()) {
91  							//nothing to release
92  							break;
93  						}
94  						int jobSize = job.sizeInBytes();
95  						// FIXME: why do we end up in endless loop when encoder is null??
96  						if (fencoder != null) {
97  							AudioInputStream audio = job.getAudio();
98  							float secs = jobSize / audio.getFormat().getFrameRate();
99  							int maxTime = (int) (1.0 + secs / fEncodingSpeed);
100 							timeout.enableForCurrentThread(maxTime);
101 							try {
102 								File encodedFile = new File(
103 									job.getDestinationDirectory(),
104 									job.getDestinationFilePrefix() + "." + fileType.getExtension());
105 								AudioClip clip = fencoder.encode(
106 									audio,
107 									fileType,
108 									encodedFile);
109 								audio.close();
110 								job.getDestinationFile().set(clip);
111 							} catch (InterruptedException e) {
112 								ttslog.addGeneralError(
113 									ErrorCode.CRITICAL_ERROR,
114 									"timeout while encoding audio to " + job.getDestinationFilePrefix(),
115 									e);
116 								audioFootprintMonitor.releaseEncodersMemory(jobSize);
117 								throw new EncodingException(e);
118 							} catch (Throwable t) {
119 								ttslog.addGeneralError(
120 									ErrorCode.CRITICAL_ERROR,
121 									"error while encoding audio to " + job.getDestinationFilePrefix(),
122 									t);
123 								audioFootprintMonitor.releaseEncodersMemory(jobSize);
124 								throw new EncodingException(t);
125 							} finally {
126 								timeout.disable();
127 							}
128 						}
129 						audioFootprintMonitor.releaseEncodersMemory(jobSize);
130 					}
131 				} finally {
132 					timeout.close();
133 					if (messageThread != null)
134 						messageThread.close();
135 				}
136 			}
137 		};
138 		mThread.setUncaughtExceptionHandler(
139 			(thread, throwable) -> { criticalError = throwable; }
140 		);
141 		mThread.start();
142 	}
143 
144 	void waitToFinish() throws EncodingException {
145 		try {
146 			mThread.join();
147 		} catch (InterruptedException e) {
148 			throw new RuntimeException(); // should not happen
149 		}
150 		if (criticalError != null) {
151 			if (criticalError instanceof EncodingException)
152 				throw (EncodingException)criticalError;
153 			else
154 				throw new RuntimeException("coding error", criticalError); // should not happen
155 		}
156 	}
157 }