diff --git a/workspace/w13_music_proj/src/main/scala/music/Chord.scala b/workspace/w13_music_proj/src/main/scala/music/Chord.scala index b9569dc1..046d0970 100644 --- a/workspace/w13_music_proj/src/main/scala/music/Chord.scala +++ b/workspace/w13_music_proj/src/main/scala/music/Chord.scala @@ -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)) diff --git a/workspace/w13_music_proj/src/main/scala/music/ChordPlayer.scala b/workspace/w13_music_proj/src/main/scala/music/ChordPlayer.scala index 9324bd87..355e824a 100644 --- a/workspace/w13_music_proj/src/main/scala/music/ChordPlayer.scala +++ b/workspace/w13_music_proj/src/main/scala/music/ChordPlayer.scala @@ -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) => ??? - diff --git a/workspace/w13_music_proj/src/main/scala/music/Main.scala b/workspace/w13_music_proj/src/main/scala/music/Main.scala index 3dcd4081..5c91fa0e 100644 --- a/workspace/w13_music_proj/src/main/scala/music/Main.scala +++ b/workspace/w13_music_proj/src/main/scala/music/Main.scala @@ -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) diff --git a/workspace/w13_music_proj/src/main/scala/music/Pitch.scala b/workspace/w13_music_proj/src/main/scala/music/Pitch.scala index 7e0559a5..87675d36 100644 --- a/workspace/w13_music_proj/src/main/scala/music/Pitch.scala +++ b/workspace/w13_music_proj/src/main/scala/music/Pitch.scala @@ -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 = diff --git a/workspace/w13_music_proj/src/main/scala/music/Synth.scala b/workspace/w13_music_proj/src/main/scala/music/Synth.scala index 0fa4a822..dff54987 100644 --- a/workspace/w13_music_proj/src/main/scala/music/Synth.scala +++ b/workspace/w13_music_proj/src/main/scala/music/Synth.scala @@ -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) @@ -44,15 +56,15 @@ 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) } @@ -60,18 +72,18 @@ object Synth: 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. diff --git a/workspace/w13_music_proj/src/main/scala/music/commands.scala b/workspace/w13_music_proj/src/main/scala/music/commands.scala index 067b29ab..94b6c434 100644 --- a/workspace/w13_music_proj/src/main/scala/music/commands.scala +++ b/workspace/w13_music_proj/src/main/scala/music/commands.scala @@ -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" diff --git a/workspace/w13_music_proj/src/main/scala/music/instruments.scala b/workspace/w13_music_proj/src/main/scala/music/instruments.scala index e5d5ccbd..64436b72 100644 --- a/workspace/w13_music_proj/src/main/scala/music/instruments.scala +++ b/workspace/w13_music_proj/src/main/scala/music/instruments.scala @@ -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] = @@ -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