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 }