Last active
February 9, 2019 23:24
-
-
Save s5bug/a274981ca9194fab1837e8d65b95b6c1 to your computer and use it in GitHub Desktop.
Script that generates a set of speaker-test commands from a MIDI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.io.File | |
import com.sun.media.sound.StandardMidiFileReader | |
import javax.sound.midi.spi.MidiFileReader | |
import javax.sound.midi.{MidiSystem, ShortMessage, Track} | |
import scala.io.Source | |
object Main { | |
final val noteFunc = | |
"""note() { | |
| sleep `echo "$1" | bc -l`s | |
| ( speaker-test --frequency $2 --test sine )& pid=$! | |
| sleep `echo "$3" | bc -l`s | |
| kill -9 $pid | |
|} | |
""".stripMargin | |
final val noteOn = 0x90 | |
final val noteOff = 0x80 | |
def main(args: Array[String]): Unit = { | |
val sequence = new StandardMidiFileReader().getSequence(new File(args(0)) | |
val msl = sequence.getMicrosecondLength | |
val tl = sequence.getTickLength | |
val spt = (msl / 1e6) / tl | |
val ns = sequence.getTracks.toVector.map(makeNoteSequence) | |
println(noteFunc ++ "\n\n" ++ ns.flatten.sorted.map(_.show(spt)).mkString(" &\n")) | |
} | |
type Instant = Long | |
type Duration = Long | |
type Pitch = Int | |
object Event { | |
final case class On(at: Instant, pitch: Pitch) extends Event | |
final case class Off(at: Instant, pitch: Pitch) extends Event | |
} | |
trait Event { | |
def at: Instant | |
def pitch: Pitch | |
} | |
final case class Note(start: Instant, duration: Duration, pitch: Pitch) { | |
def show(spt: Double): String = { | |
s"note ${start * spt} ${hertz(pitch)} ${duration * spt}" | |
} | |
} | |
implicit val noteOrdering: Ordering[Note] = (x: Note, y: Note) => (x.start - y.start).toInt | |
def notes(events: Vector[Event]): Vector[Note] = | |
events | |
.sortBy(_.at) | |
.foldLeft(State.empty)(_ ingest _) | |
.notes | |
private object State { | |
val empty = State(Map.empty, Vector.empty) | |
} | |
private final case class State(pressed: Map[Pitch, Instant], notes: Vector[Note]) { | |
def ingest(event: Event): State = | |
event match { | |
case Event.On(at, pitch) => | |
// ignore duplicate note ons | |
if (pressed contains pitch) this | |
else copy(pressed = pressed + (pitch -> at)) | |
case Event.Off(at, pitch) => | |
pressed get pitch match { | |
// ignore missing note ons | |
case None => this | |
case Some(start) => copy(pressed = pressed - pitch, notes = notes :+ Note(start, at - start, pitch)) | |
} | |
} | |
} | |
def makeNoteSequence(t: Track): Vector[Note] = { | |
val onOffMap = makeNoteStateMap(t) | |
notes(onOffMap) | |
} | |
def makeNoteStateMap(t: Track): Vector[Event] = { | |
(0 until t.size()).toVector.map(t.get).map(event => { | |
val evs = event.getTick | |
event.getMessage match { | |
case sm: ShortMessage => | |
val key = sm.getData1 | |
sm.getCommand match { | |
case `noteOn` => Some(Event.On(evs, key)) | |
case `noteOff` => Some(Event.Off(evs, key)) | |
case _ => None | |
} | |
case _ => None | |
} | |
}).filter(_.isDefined).map(_.get) | |
} | |
def hertz(key: Int): Double = { | |
Math.pow(2.0, (key - 69.0) / 12.0) * 440.0 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Much thanks to @ritschwumm for helping with the conversions from Events to Notes.