1   package org.daisy.pipeline.audio.saxon.impl;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.io.UncheckedIOException;
6   import java.math.BigDecimal;
7   import java.net.URI;
8   import java.time.Duration;
9   import java.util.HashMap;
10  import java.util.Map;
11  import java.util.Optional;
12  
13  import javax.sound.sampled.AudioFileFormat;
14  import javax.sound.sampled.AudioInputStream;
15  import javax.sound.sampled.UnsupportedAudioFileException;
16  
17  import org.daisy.common.transform.TransformerException;
18  import org.daisy.common.xpath.saxon.ExtensionFunctionProvider;
19  import org.daisy.common.xpath.saxon.ReflexiveExtensionFunctionProvider;
20  import org.daisy.pipeline.audio.AudioClip;
21  import org.daisy.pipeline.audio.AudioDecoder;
22  import org.daisy.pipeline.audio.AudioEncoder;
23  import org.daisy.pipeline.audio.AudioFileTypes;
24  import org.daisy.pipeline.audio.AudioServices;
25  import org.daisy.pipeline.audio.AudioUtils;
26  
27  import org.osgi.service.component.annotations.Component;
28  import org.osgi.service.component.annotations.Reference;
29  import org.osgi.service.component.annotations.ReferenceCardinality;
30  import org.osgi.service.component.annotations.ReferencePolicy;
31  
32  @Component(
33  	name = "TranscodeAudioFile",
34  	service = { ExtensionFunctionProvider.class }
35  )
36  public class TranscodeAudioFileDefinition extends ReflexiveExtensionFunctionProvider {
37  
38  	public TranscodeAudioFileDefinition() {
39  		super(TranscodeAudioFile.class);
40  	}
41  
42  	private AudioServices audioServices;
43  
44  	@Reference(
45  		name = "AudioServices",
46  		unbind = "-",
47  		service = AudioServices.class,
48  		cardinality = ReferenceCardinality.MANDATORY,
49  		policy = ReferencePolicy.STATIC
50  	)
51  	protected void setAudioServices(AudioServices audioServices) {
52  		this.audioServices = audioServices;
53  	}
54  
55  	public class TranscodeAudioFile {
56  
57  		public Map<String,Object> run(URI href, Optional<String> oldMediaType, String newMediaType, URI outputDirURI) {
58  			File inputFile; {
59  				try {
60  					inputFile = new File(href);
61  				} catch (IllegalArgumentException e) {
62  					throw new IllegalArgumentException("href parameter is not a file URI: " + href, e);
63  				}
64  			}
65  			if (!inputFile.isFile())
66  				throw new IllegalArgumentException(new IOException("File does not exist: " + inputFile));
67  			Optional<AudioFileFormat.Type> oldFileType; {
68  				if (oldMediaType.isPresent()) {
69  					String mediaType = oldMediaType.get();
70  					AudioFileFormat.Type fileType = AudioFileTypes.fromMediaType(mediaType);
71  					if (fileType == null)
72  						throw new TransformerException(
73  							AudioErrors.ERR_AUDIO_001,
74  							new IllegalArgumentException("media-type is not a recognized mime type: " + mediaType));
75  					oldFileType = Optional.of(fileType);
76  				} else
77  					oldFileType = Optional.empty();
78  			}
79  			AudioFileFormat.Type newFileType = AudioFileTypes.fromMediaType(newMediaType);
80  			if (newFileType == null)
81  				throw new IllegalArgumentException("new-file-type parameter is not a recognized mime type: " + newMediaType);
82  			File outputDir; {
83  				try {
84  					outputDir = new File(outputDirURI);
85  				} catch (IllegalArgumentException e) {
86  					throw new IllegalArgumentException("output-dir parameter is not a file URI: " + outputDirURI, e);
87  				}
88  			}
89  			if (outputDir.isFile())
90  				throw new IllegalArgumentException(new IOException("File already exist and is not a directory: " + outputDir));
91  			File outputFile; {
92  				String extension = newFileType.getExtension();
93  				String fileNameWithoutExtension = inputFile.getName().replaceAll("\\.[^\\.]+$", "");
94  				outputFile = new File(outputDir, fileNameWithoutExtension + "." + extension);
95  				if (outputFile.exists()) {
96  					int n = 2;
97  					do {
98  						outputFile = new File(outputDir, fileNameWithoutExtension + "_" + (n++) + "." + extension);
99  					} while (outputFile.exists());
100 				}
101 			}
102 			Optional<AudioDecoder> decoder; {
103 				if (oldFileType.isPresent()) {
104 					decoder = audioServices.newDecoder(oldFileType.get(), new HashMap<String,String>());
105 					if (!decoder.isPresent())
106 						throw new RuntimeException("No decoder found for file type " + oldFileType);
107 				} else
108 					decoder = audioServices.newDecoder(new HashMap<String,String>());
109 			}
110 			Optional<AudioEncoder> encoder = audioServices.newEncoder(newFileType, new HashMap<String,String>());
111 			if (!encoder.isPresent())
112 				throw new RuntimeException("No encoder found for file type " + newFileType);
113 			outputDir.mkdirs();
114 			AudioInputStream pcm; {
115 				try {
116 					pcm = decoder.get().decode(inputFile);
117 				} catch (UnsupportedAudioFileException e) {
118 					throw new UncheckedIOException(
119 						new IOException("Audio file could not be read (unsupported file type): " + inputFile, e));
120 				} catch (Throwable e) {
121 					throw new UncheckedIOException(new IOException("Audio file could not be read: " + inputFile, e));
122 				}
123 			}
124 			try {
125 				AudioClip clip = encoder.get().encode(pcm, newFileType, outputFile);
126 				Map<String,Object> rv = new HashMap<>();
127 				rv.put("href", outputFile.toURI());
128 				Duration originalDuration = AudioUtils.getDuration(pcm);
129 				if (!(clip.clipBegin.equals(Duration.ZERO) && clip.clipEnd.equals(originalDuration))) {
130 					rv.put("clipBegin", new BigDecimal(clip.clipBegin.toMillis()).movePointLeft(3));
131 					rv.put("clipEnd", new BigDecimal(clip.clipEnd.toMillis()).movePointLeft(3));
132 					rv.put("original-clipBegin", BigDecimal.ZERO);
133 					rv.put("original-clipEnd", new BigDecimal(originalDuration.toMillis()).movePointLeft(3));
134 				}
135 				return rv;
136 			} catch (Throwable e) {
137 				throw new UncheckedIOException(
138 					new IOException("Audio could not be written to file (type " + newFileType + "): " + outputFile, e));
139 			}
140 		}
141 	}
142 }