Java tutorial
/* * Computoser is a music-composition algorithm and a website to present the results * Copyright (C) 2012-2014 Bozhidar Bozhanov * * Computoser is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * Computoser is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Computoser. If not, see <http://www.gnu.org/licenses/>. */ package com.music; import it.sauronsoftware.jave.AudioAttributes; import it.sauronsoftware.jave.Encoder; import it.sauronsoftware.jave.EncodingAttributes; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Field; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.Semaphore; import javax.annotation.PostConstruct; import javax.sound.midi.InvalidMidiDataException; import javax.sound.midi.MidiSystem; import javax.sound.midi.MidiUnavailableException; import javax.sound.midi.Soundbank; import javax.sound.midi.Synthesizer; import javax.swing.JFrame; import jm.midi.MidiSynth; import jm.music.data.Note; import jm.music.data.Part; import jm.music.data.Phrase; import jm.music.data.Score; import jm.music.tools.PhraseAnalysis; import jm.util.Play; import jm.util.Read; import jm.util.Write; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.util.ReflectionUtils; import com.google.common.collect.Maps; import com.music.model.ExtendedPhrase; import com.music.model.PartType; import com.music.model.prefs.Ternary; import com.music.model.prefs.UserPreferences; import com.music.tools.SongChart.GraphicsPanel; import com.music.util.MutingPrintStream; import com.music.util.music.SMFTools; import com.music.util.music.ToneResolver; import com.music.web.util.StartupListener; @Service public class Generator { private static final Logger logger = LoggerFactory.getLogger(Generator.class); @Value("${music.config.location}") private String configLocation; @Value("${max.concurrent.generations}") private int maxConcurrentGenerations; private Semaphore semaphore; private static List<Soundbank> soundbanks = new ArrayList<>(); private List<ScoreManipulator> manipulators = new ArrayList<>(); private Encoder encoder = new Encoder(); @PostConstruct public void init() throws MidiUnavailableException, IOException, InvalidMidiDataException { //TODO http://marsyas.info/ when input signal processing is needed semaphore = new Semaphore(maxConcurrentGenerations); manipulators.add(new MetreConfigurer()); manipulators.add(new PartConfigurer()); manipulators.add(new ScaleConfigurer()); manipulators.add(new MainPartGenerator()); manipulators.add(new AccompanimentPartGenerator()); manipulators.add(new Arpeggiator()); manipulators.add(new PercussionGenerator()); manipulators.add(new SimpleBeatGenerator()); manipulators.add(new BassPartGenerator()); manipulators.add(new DroneGenerator()); manipulators.add(new EffectsGenerator()); manipulators.add(new PadsGenerator()); manipulators.add(new TimpaniPartGenerator()); manipulators.add(new TitleGenerator()); try { Collection<File> files = FileUtils.listFiles(new File(configLocation + "/soundbanks/"), new String[] { "sf2" }, false); for (File file : files) { InputStream is = new BufferedInputStream(new FileInputStream(file)); soundbanks.add(MidiSystem.getSoundbank(is)); } } catch (IOException ex) { logger.warn("Problem loading soundbank: " + ex.getMessage()); // ignore } //initJMusicSynthesizer(); } public ScoreContext generatePiece() { Score score = new Score(); final ScoreContext ctx = new ScoreContext(); ctx.setScore(score); for (ScoreManipulator manipulator : manipulators) { manipulator.handleScore(score, ctx); } verifyResult(ctx); return ctx; } private void verifyResult(ScoreContext ctx) { Part[] parts = ctx.getScore().getPartArray(); Map<String, Double> lengths = Maps.newHashMap(); double currentLength = 0; for (Part part : parts) { if (part.getTitle().equals(PartType.DRONE.getTitle())) { continue; } int partMeasures = 0; Phrase[] phrases = part.getPhraseArray(); boolean hasChords = part.getTitle().equals(PartType.ACCOMPANIMENT.getTitle()); double noteLength = 0; double restLength = 0; for (Phrase phrase : phrases) { double previousStartTime = -1; double currentMeasureSize = 0; Note[] notes = phrase.getNoteArray(); int measures = 0; int outOfScaleNotes = 0; for (Note note : notes) { if (phrase instanceof ExtendedPhrase && !ToneResolver .isInScale(((ExtendedPhrase) phrase).getScale().getDefinition(), note.getPitch())) { outOfScaleNotes++; } if (note.isRest()) { restLength += note.getRhythmValue(); } else { noteLength += note.getRhythmValue(); } if (note.getSampleStartTime() != previousStartTime || note.getSampleStartTime() == 0) { currentLength += note.getRhythmValue(); previousStartTime = note.getSampleStartTime(); } currentMeasureSize += note.getRhythmValue(); if (currentMeasureSize > ctx.getNormalizedMeasureSize() && !ignorePartMeasureVerification(part)) { logger.warn( part.getTitle() + " of " + ctx.getScore().getTitle() + " has unbalanced measures"); } if (currentMeasureSize >= ctx.getNormalizedMeasureSize()) { currentMeasureSize = 0; measures++; } } if (phrase instanceof ExtendedPhrase) { int actualMeasures = ((ExtendedPhrase) phrase).getMeasures(); if (actualMeasures != measures) { logger.warn("Discrepancy in calculated measures for phrase " + phrase.getTitle() + ". Actual are " + measures + " but stored " + actualMeasures); } } if (outOfScaleNotes > 1) { logger.warn("Out of scale notes for part " + part.getTitle() + ": " + outOfScaleNotes); } partMeasures += measures; } // not exact, but a good approximation if (hasChords) { currentLength = noteLength / 3 + restLength; } lengths.put(part.getTitle(), currentLength); currentLength = 0; if (partMeasures != ctx.getMeasures() && partMeasures > 0) { logger.warn("Discrepancy in calculated measures for Part " + part.getTitle() + ". Actual " + partMeasures + " but stored " + ctx.getMeasures()); } } logger.debug("Part lengths: " + lengths); } private boolean ignorePartMeasureVerification(Part part) { String title = part.getTitle(); return title.equals(PartType.ACCOMPANIMENT.getTitle()) || title.equals(PartType.PAD1.getTitle()) || title.equals(PartType.PAD2.getTitle()); } public byte[] toMp3(byte[] midi) throws Exception { //allowing a maximum number users to generate tracks at the same time so that the system remains stable (midi->wav->mp3 is heavy) semaphore.acquire(); try { File wav = File.createTempFile("gen", ".wav"); long start = System.currentTimeMillis(); try (OutputStream fos = new BufferedOutputStream(new FileOutputStream(wav))) { Midi2WavRenderer.midi2wav(new ByteArrayInputStream(midi), fos); IOUtils.write(midi, fos); } logger.info("midi2wav conversion took: " + (System.currentTimeMillis() - start) + " millis"); start = System.currentTimeMillis(); EncodingAttributes attrs = new EncodingAttributes(); attrs.setFormat("mp3"); AudioAttributes audio = new AudioAttributes(); // audio.setBitRate(36000); // audio.setSamplingRate(20000); attrs.setAudioAttributes(audio); attrs.setThreads(1); File mp3 = File.createTempFile("gen", ".mp3"); encoder.encode(wav, mp3, attrs); logger.info("wav2mp3 conversion took: " + (System.currentTimeMillis() - start) + " millis"); wav.delete(); //cleanup the big wav file byte[] mp3Bytes = FileUtils.readFileToByteArray(mp3); mp3.delete(); return mp3Bytes; } finally { semaphore.release(); } } public static void main1(String[] args) throws Exception { // testing soundbanks Generator generator = new Generator(); generator.configLocation = "c:/config/music"; generator.maxConcurrentGenerations = 5; generator.init(); byte[] midi = FileUtils.readFileToByteArray(new File("c:/tmp/classical.midi")); byte[] mp3 = generator.toMp3(midi); FileUtils.writeByteArrayToFile(new File("c:/tmp/aa" + System.currentTimeMillis() + ".mp3"), mp3); } public static void mainMusicXml(String[] args) throws Exception { InputStream is = new FileInputStream("C:\\Users\\bozho\\Downloads\\7743.midi"); ByteArrayOutputStream result = new ByteArrayOutputStream(); Score score = new Score(); SMFTools localSMF = new SMFTools(); localSMF.read(is); SMFTools.SMFToScore(score, localSMF); score.setTitle("foo"); score.setNumerator(12); score.setDenominator(16); MusicXmlRenderer.render(score, result); System.out.println(new String(result.toByteArray())); is.close(); } public static void main(String[] args) throws Exception { Score score1 = new Score(); Read.midi(score1, "c:/tmp/gen.midi"); for (Part part : score1.getPartArray()) { System.out.println(part.getInstrument()); } new StartupListener().contextInitialized(null); Generator generator = new Generator(); generator.configLocation = "c:/config/music"; generator.maxConcurrentGenerations = 5; generator.init(); UserPreferences prefs = new UserPreferences(); prefs.setElectronic(Ternary.YES); //prefs.setSimpleMotif(Ternary.YES); //prefs.setMood(Mood.MAJOR); //prefs.setDrums(Ternary.YES); //prefs.setClassical(true); prefs.setAccompaniment(Ternary.YES); prefs.setElectronic(Ternary.YES); final ScoreContext ctx = generator.generatePiece(); Score score = ctx.getScore(); for (Part part : score.getPartArray()) { System.out.println(part.getTitle() + ": " + part.getInstrument()); } System.out.println("Metre: " + ctx.getMetre()[0] + "/" + ctx.getMetre()[1]); System.out.println(ctx); Write.midi(score, "c:/tmp/gen.midi"); new Thread(new Runnable() { @Override public void run() { JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setBounds(0, 0, 500, 500); frame.setVisible(true); Part part = ctx.getParts().get(PartType.MAIN); Note[] notes = part.getPhrase(0).getNoteArray(); List<Integer> pitches = new ArrayList<Integer>(); for (Note note : notes) { if (!note.isRest()) { pitches.add(note.getPitch()); } } GraphicsPanel gp = new GraphicsPanel(pitches); frame.setContentPane(gp); } }).start(); DecimalFormat df = new DecimalFormat("#.##"); for (Part part : score.getPartArray()) { StringBuilder sb = new StringBuilder(); printStatistics(ctx, part); System.out.println("------------ " + part.getTitle() + "-----------------"); Phrase[] phrases = part.getPhraseArray(); for (Phrase phr : phrases) { if (phr instanceof ExtendedPhrase) { sb.append("Contour=" + ((ExtendedPhrase) phr).getContour() + " "); } double measureSize = 0; int measures = 0; double totalLength = 0; List<String> pitches = new ArrayList<String>(); List<String> lengths = new ArrayList<String>(); System.out.println("((Phrase notes: " + phr.getNoteArray().length + ")"); for (Note note : phr.getNoteArray()) { if (!note.isRest()) { int degree = 0; if (phr instanceof ExtendedPhrase) { degree = Arrays.binarySearch(((ExtendedPhrase) phr).getScale().getDefinition(), note.getPitch() % 12); } pitches.add(String.valueOf(note.getPitch() + " (" + degree + ") ")); } else { pitches.add(" R "); } lengths.add(df.format(note.getRhythmValue())); measureSize += note.getRhythmValue(); totalLength += note.getRhythmValue(); ; if (measureSize >= ctx.getNormalizedMeasureSize()) { pitches.add(" || "); lengths.add(" || " + (measureSize > ctx.getNormalizedMeasureSize() ? "!" : "")); measureSize = 0; measures++; } } sb.append(pitches.toString() + "\r\n"); sb.append(lengths.toString() + "\r\n"); if (part.getTitle().equals(PartType.MAIN.getTitle())) { sb.append("\r\n"); } System.out.println("Phrase measures: " + measures); System.out.println("Phrase length: " + totalLength); } System.out.println(sb.toString()); } MutingPrintStream.ignore.set(true); Write.midi(score, "c:/tmp/gen.midi"); MutingPrintStream.ignore.set(null); Play.midi(score); generator.toMp3(FileUtils.readFileToByteArray(new File("c:/tmp/gen.midi"))); } @SuppressWarnings({ "unchecked", "unused" }) private static void printStatistics(ScoreContext ctx, Part part) { if (false) { Hashtable<String, Object> table = PhraseAnalysis.getAllStatistics(part.getPhrase(0), 1, 0, ctx.getScale().getDefinition()); for (Entry<String, Object> entry : table.entrySet()) { System.out.println(entry.getKey() + "=" + entry.getValue()); } } } @SuppressWarnings("unused") private static void initJMusicSynthesizer() { try { Field fld = ReflectionUtils.findField(Play.class, "ms"); ReflectionUtils.makeAccessible(fld); MidiSynth synth = (MidiSynth) fld.get(null); // playing for the first time initializes the synthesizer try { synth.play(null); } catch (Exception ex) { } ; Field synthField = ReflectionUtils.findField(MidiSynth.class, "m_synth"); ReflectionUtils.makeAccessible(synthField); Synthesizer synthsizer = (Synthesizer) synthField.get(synth); loadSoundbankInstruments(synthsizer); } catch (Exception ex) { throw new IllegalStateException(ex); } } public static void mainConvert(String[] args) throws Exception { Generator gen = new Generator(); gen.maxConcurrentGenerations = 5; gen.configLocation = "c:/config/music"; gen.init(); byte[] bytes = FileUtils.readFileToByteArray(new File("c:/tmp/183.midi")); byte[] mp3 = gen.toMp3(bytes); FileUtils.writeByteArrayToFile(new File("c:/tmp/183.mp3"), mp3); } public static void loadSoundbankInstruments(Synthesizer synthesizer) { for (Soundbank soundbank : soundbanks) { synthesizer.loadAllInstruments(soundbank); } } }