Skip to content

Instantly share code, notes, and snippets.

@vinimonteiro
Created November 24, 2020 12:23
Show Gist options
  • Save vinimonteiro/8ffad10a32e89ac55077fcb88af4e311 to your computer and use it in GitHub Desktop.
Save vinimonteiro/8ffad10a32e89ac55077fcb88af4e311 to your computer and use it in GitHub Desktop.
Data sonification service controller
package com.logmonitor.controller;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.util.Base64;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.jfugue.midi.MidiFileManager;
import org.jfugue.pattern.Pattern;
import org.jfugue.player.Player;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import com.logmonitor.utils.Const;
@RestController
@RequestMapping("/")
public class SonificationController {
private static final String EXCEPTION_SOUND = "C5maj7w ";
private static final String TIMEOUT_SOUND = "V9 [Hand_Clap]q+[Crash_Cymbal_1]q";
private static final Logger logger = LoggerFactory.getLogger(SonificationController.class);
private static ExecutorService executorService = Executors.newSingleThreadExecutor();
Player player = new Player();
Player playerPiano = new Player();
private Object lock = new Object();
/*
* It reads from file infinitely. If line is empty / file has finished, it waits 500ms and tries again as it is inside while true block.
* The file contains response times and whether an exception or timeout occurred.
* Such rule is just for demonstration purposes, a real log file would contain other things of course.
* But basically one would parse it to have in the end: response times and other events of interest, like exceptions and timeouts.
* For each line in the file, it gets correspondent sound based on pre-defined rules.
* This can also vary, each one can define it's own rules.
* I'm far from a musician, I was not very creative for sure. I only set for high response times to play a high note, and low response times to play low note.
* And for exception and timeout I defined to play some note chosen mostly arbitrarily, just wanted a sound to call for attention.
*/
@RequestMapping(value = "/application/{appId}/sonify", method = RequestMethod.GET)
public SseEmitter sonifyData(@PathVariable("appId") String appId) {
final SseEmitter sseEmitter = new SseEmitter();
executorService.submit(() -> {
ByteArrayOutputStream baos = null;
try (BufferedReader br = new BufferedReader(new FileReader("/logs/"+appId+".log"))) {
while(true){
String line = br.readLine();
if(line==null){
synchronized (lock){
lock.wait(500);
}
}else{
TimeUnit.MILLISECONDS.sleep(500);
baos = new ByteArrayOutputStream();
if(Const.TIMEOUT.equals(line)){
MidiFileManager.save(player.getSequence(new Pattern(TIMEOUT_SOUND)), baos);
TimeUnit.MILLISECONDS.sleep(2000);
sseEmitter.send("base64="+encoder(baos.toByteArray()));
}else{
MidiFileManager.save(playerPiano.getSequence(new Pattern(getNote(line))), baos);
sseEmitter.send("base64="+encoder(baos.toByteArray()));
}
sseEmitter.send(line);
}
}
} catch (IOException | InterruptedException e) {
logger.error(e.getMessage());
}finally {
sseEmitter.complete();
}
});
return sseEmitter;
}
private static String encoder(byte[] byteArray) {
return Base64.getEncoder().encodeToString(byteArray);
}
private String getNote(String line) throws IOException, InterruptedException{
if(Const.EXCEPTION.equals(line)){
return EXCEPTION_SOUND;
}else{
return (int)normalizeToRange(Double.valueOf(line))+"q ";
}
}
/*
* Normalizes value to a range. This range is the min and max the note can be in a piano.
*/
private static double normalizeToRange(double value) {
return ((value - Const.DATA_MIN) / (Const.DATA_MAX - Const.DATA_MIN))
* (Const.NORMALIZED_RANGE_MAX - Const.NORMALIZED_RANGE_MIN) + Const.NORMALIZED_RANGE_MIN;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment