Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions workspace/w13_music_proj/src/main/scala/music/Chord.scala
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
package music

import scala.math.floorMod
import scala.util.Random

case class Chord(ps: Vector[Pitch]):
assert(ps.nonEmpty, "Chord pitch sequence is empty")

val pitchClasses: Vector[Int] = ps.map(_.pitchClass).toVector
val pitchClasses: Vector[Int] = ps.map(_.pitchClass).toVector

def apply(i: Int): Pitch = ps(i)

def intervals(root: Pitch = ps(0)): Vector[Int] = ps.map(_.nbr - root.nbr)

def relativePitchClasses(root: Pitch = ps(0)): Vector[Int] =
intervals(root).map(i => (i%12 + 12) % 12).distinct.sorted
def simpleIntervals(root: Pitch = ps(0)): Vector[Int] =
intervals(root).map(i => floorMod(i, 12)).distinct.sorted

def name(root: Pitch = ps(0)): String = relativePitchClasses(root) match
def name(root: Pitch = ps(0)): String = simpleIntervals(root) match
case Vector(0, 4, 7) => root.pitchClassName
case Vector(0, 3, 7) => root.pitchClassName + "m"
case Vector(0, 4, 7, 10) => root.pitchClassName + "7"
case _ => root.pitchClassName + intervals(root).mkString("[",",","]")
case _ => root.pitchClassName + intervals(root).mkString("[", ",", "]")

override def toString = ps.map(_.name).mkString("Chord(",",",")")
override def toString = ps.map(_.name).mkString("Chord(", ",", ")")

object Chord:
def apply(xs: String*): Chord = Chord(xs.map(Pitch.apply).toVector)

def random(pitchNumbers: Seq[Int] = (60 to 72), n: Int = 3): Chord =
val shuffled = scala.util.Random.shuffle(pitchNumbers).toVector
def random(pitchNumbers: Seq[Int] = 60 to 72, n: Int = 3): Chord =
val shuffled = Random.shuffle(pitchNumbers).toVector
Chord(shuffled.take(n).map(Pitch.apply))
13 changes: 6 additions & 7 deletions workspace/w13_music_proj/src/main/scala/music/ChordPlayer.scala
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package music

object ChordPlayer:

case class Strike(
velocity: Int = 50, // hur hårt anslag i Range(0, 128)
duration: Long = 500, // hur länge i millisekunder
spread: Long = 50, // millisekunder mellan tonerna
after: Long = 0 // millisekunder innan första tonen
velocity: Int = 50, // hur hårt anslag i Range(0, 128)
duration: Long = 500, // hur länge i millisekunder
spread: Long = 50, // millisekunder mellan tonerna
after: Long = 0 // millisekunder innan första tonen
)

def play(chord: Chord, strike: Strike = Strike(), channel: Int = 0): Unit =
strike match
strike match
case Strike(v, d, s, a) => ???

6 changes: 4 additions & 2 deletions workspace/w13_music_proj/src/main/scala/music/Main.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package music

import scala.io.StdIn

object Main:
val (helloMsg, exitMsg) = ("*** Welcome to music!", "Goodbye music!")
def readLine(): String = scala.io.StdIn.readLine("music> ")
def readLine(): String = StdIn.readLine("music> ")

def main(args: Array[String]): Unit =
println(helloMsg)
Synth.playBlocking()
Command.loopUntilExit(readLine _)
Command.loopUntilExit(readLine)
26 changes: 14 additions & 12 deletions workspace/w13_music_proj/src/main/scala/music/Pitch.scala
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
package music

case class Pitch(nbr: Int): //Tonhöjd
import scala.util.Try

case class Pitch(nbr: Int): // Tonhöjd
assert((0 to 127) contains nbr, s"Error: nbr $nbr outside (0 to 127)")
def pitchClass: Int = nbr % 12
def pitchClass: Int = nbr % 12
def pitchClassName: String = Pitch.pitchClassNames(pitchClass)
def name: String = s"$pitchClassName$octave"
def octave: Int = nbr / 12
def +(offset: Int): Pitch = Pitch(nbr + offset)
override def toString = s"Pitch($name)"
def name: String = s"$pitchClassName$octave"
def octave: Int = nbr / 12 - 1
def +(offset: Int): Pitch = Pitch(nbr + offset)
override def toString = s"Pitch($name)"

object Pitch:
val defaultOctave = 5 // mittenoktaven på ett pianos tangentbord
val defaultOctave = 4 // mittenoktaven på ett pianos tangentbord

val pitchClassNames: Vector[String] =
Vector("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B")

val pitchClassIndex: Map[String, Int] = pitchClassNames.zipWithIndex.toMap

def fromString(s: String): Option[Pitch] = scala.util.Try {
val (pitchClassName, octaveName) = s.partition(c => !c.isDigit)
val octave = if octaveName.nonEmpty then octaveName.toInt else 5
Pitch(pitchClassIndex(pitchClassName) + octave * 12)
def fromString(s: String): Option[Pitch] = Try {
val (pitchClassName, octaveName) = s.partition(c => c.isLetter)
val octave = if octaveName.nonEmpty then octaveName.toInt else defaultOctave
Pitch(pitchClassIndex(pitchClassName) + (octave + 1) * 12)
}.toOption

def apply(s: String): Pitch =
Expand Down
58 changes: 35 additions & 23 deletions workspace/w13_music_proj/src/main/scala/music/Synth.scala
Original file line number Diff line number Diff line change
@@ -1,31 +1,43 @@
package music

object Synth:
import javax.sound.midi._
import GMInstruments._
import javax.sound.midi.*
import GMInstruments.*

val underlying: Synthesizer =
println("Initializing javax.sound.MidiSystem ...")
println(MidiSystem.getMidiDeviceInfo().mkString(" "))
val synth = MidiSystem.getSynthesizer
synth.open
assert(synth.loadAllInstruments(synth.getDefaultSoundbank), "Loading MIDI instruments failed")
assert(
synth.loadAllInstruments(synth.getDefaultSoundbank),
"Loading MIDI instruments failed"
)
synth
resetInstruments() // assign some different instruments to channels

def midiChannel(channel: Int): MidiChannel = underlying.getChannels.apply(channel)
def midiChannel(channel: Int): MidiChannel =
underlying.getChannels.apply(channel)
def channels: Range = underlying.getChannels.indices

def instruments: Seq[Instrument] = underlying.getLoadedInstruments.toSeq

def changeInstrument(program: Int, channel: Int = 0): Unit =
val patch = instruments.find(_.getPatch.getProgram == program).map(_.getPatch)
val patch =
instruments.find(_.getPatch.getProgram == program).map(_.getPatch)
patch match
case Some(p) => midiChannel(channel).programChange(p.getBank, p.getProgram)
case Some(p) =>
midiChannel(channel).programChange(p.getBank, p.getProgram)
case None => println(s"Instrument with program number $program not found")

lazy val defaultInstruments =
Vector(AcousticGrandPiano, AcousticGuitarNylon, AcousticBass, Trumpet, Flute)
lazy val defaultInstruments =
Vector(
AcousticGrandPiano,
AcousticGuitarNylon,
AcousticBass,
Trumpet,
Flute
)

def resetInstruments(): Unit = defaultInstruments.zipWithIndex.foreach {
case (program, channel) => changeInstrument(program, channel)
Expand All @@ -44,34 +56,34 @@ object Synth:
def delay(millis: Long): Unit = Thread.sleep(millis)

def playBlocking(
noteNumbers: Seq[Int] = Vector(60),
velocity: Int = 60,
duration: Long = 300,
spread: Long = 50,
after: Long = 0,
channel: Int = 0
noteNumbers: Seq[Int] = Vector(60),
velocity: Int = 60,
duration: Long = 300,
spread: Long = 50,
after: Long = 0,
channel: Int = 0
): Unit =
delay(after)
noteNumbers.foreach{ nbr =>
noteNumbers.foreach { nbr =>
noteOn(nbr, velocity, channel)
delay(spread)
}
delay(duration)
noteNumbers.foreach(noteOff(_, channel))

def playConcurrently(
noteNumbers: Seq[Int] = Vector(60),
velocity: Int = 60,
duration: Long = 300,
spread: Long = 50,
after: Long = 0,
channel: Int = 0
noteNumbers: Seq[Int] = Vector(60),
velocity: Int = 60,
duration: Long = 300,
spread: Long = 50,
after: Long = 0,
channel: Int = 0
): Unit =
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

val _ = scala.concurrent.Future{
val _ = Future:
playBlocking(noteNumbers, velocity, duration, spread, after, channel)
}

object GMInstruments:
// These program numbers are defined as particular instruments by General MIDI.
Expand Down
21 changes: 11 additions & 10 deletions workspace/w13_music_proj/src/main/scala/music/commands.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,39 @@ abstract class Command(val str: String, val help: String):

object Command:
val all: Seq[Command] = Seq(Help, Quit, Play)
val allHelpTexts: String =
Command.all.map(c => c.str.padTo(10,' ') + c.help).mkString("\n")
val allHelpTexts: String =
Command.all.map(c => c.str.padTo(10, ' ') + c.help).mkString("\n")

def find(command: String): Option[Command] = all.find(_.str == command)

def apply(cmd: String, args: Seq[String]): String =
all.find(_.str == cmd) match
case Some(c) => c(args)
case None => s"Unknown command: $cmd\nType ? for help."
case None => s"Unknown command: $cmd\nType ? for help."

def loopUntilExit(nextLine: () => String): Unit =
val line = nextLine()
if line != null then
val result = line.split(' ').toSeq match
case Seq() => ""
case Seq() => ""
case cmd +: args => Command(cmd, args)
if result != "" then println(result)
if result != Main.exitMsg then loopUntilExit(nextLine)
else
println("\n" + Main.exitMsg)
else println("\n" + Main.exitMsg)

object Help extends Command("?", "print help"):
def apply(args: Seq[String]): String = args match
case Seq() => Command.allHelpTexts
case Seq() => Command.allHelpTexts
case Seq(cmd) => Command.find(cmd).map(_.help).getOrElse(s"Unknown: $cmd")
case _ => s"Usage: $str [cmd]"
case _ => s"Usage: $str [cmd]"

object Quit extends Command(":q", "quit this app"):
def apply(args: Seq[String]): String = args match
case Seq() => Main.exitMsg
case _ => s"Error: $args after :q not allowed"
case _ => s"Error: $args after :q not allowed"

object Play extends Command("!", "play chord TODO"):
def apply(args: Seq[String]): String = args match
case _ => Synth.playBlocking(); s"play chord TODO"
case _ =>
Synth.playBlocking()
s"play chord TODO"
20 changes: 11 additions & 9 deletions workspace/w13_music_proj/src/main/scala/music/instruments.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package music

trait StringInstrument { def toChordOpt: Option[Chord] }
trait StringInstrument:
def toChordOpt: Option[Chord]

case class Piano(isKeyDown: Set[Int]) extends StringInstrument:
def toChordOpt: Option[Chord] =
Expand All @@ -11,21 +12,22 @@ case class Piano(isKeyDown: Set[Int]) extends StringInstrument:
trait FrettedInstrument extends StringInstrument:
def nbrOfStrings: Int
def tuning: Vector[Pitch]
def grip: Vector[Int]
def toChordOpt: Option[Chord] =
val notes =
for i <- grip.indices if grip(i) >= 0
def grip: Vector[Int]
def toChordOpt: Option[Chord] =
val notes =
for i <- grip.indices if grip(i) >= 0
yield tuning(i) + grip(i)
if notes.nonEmpty then Some(Chord(notes.toVector)) else None

case class Guitar(pos: (Int,Int,Int,Int,Int,Int)) extends FrettedInstrument:
case class Guitar(pos: (Int, Int, Int, Int, Int, Int))
extends FrettedInstrument:
val grip = Vector(pos._1, pos._2, pos._3, pos._4, pos._5, pos._6)
val nbrOfStrings = 6
val tuning =
"E3 A3 D4 G4 B4 E5".split(' ').map(Pitch.apply).toVector
"E2 A2 D3 G3 B3 E4".split(' ').map(Pitch.apply).toVector

case class Ukulele(pos: (Int,Int,Int,Int)) extends FrettedInstrument:
case class Ukulele(pos: (Int, Int, Int, Int)) extends FrettedInstrument:
val grip = Vector(pos._1, pos._2, pos._3, pos._4)
val nbrOfStrings = 4
val tuning =
"A5 D5 F#5 B5".split(' ').map(Pitch.apply).toVector
"A4 D4 F#4 B4".split(' ').map(Pitch.apply).toVector