Skip to content

Instantly share code, notes, and snippets.

@sorenbug sorenbug/speakmid.scala
Last active Feb 9, 2019

Embed
What would you like to do?
Script that generates a set of speaker-test commands from a MIDI
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
}
}
@sorenbug

This comment has been minimized.

Copy link
Owner Author

commented Feb 9, 2019

Much thanks to @ritschwumm for helping with the conversions from Events to Notes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.