diff --git a/Demo/MIDITrackViewDemo.xcodeproj/project.pbxproj b/Demo/MIDITrackViewDemo.xcodeproj/project.pbxproj index 29ecc02..eef166f 100644 --- a/Demo/MIDITrackViewDemo.xcodeproj/project.pbxproj +++ b/Demo/MIDITrackViewDemo.xcodeproj/project.pbxproj @@ -7,8 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 607E99BD2B0D0D97005D1988 /* RUSH_E_FINAL.mid in Resources */ = {isa = PBXBuildFile; fileRef = 607E99BC2B0D0D97005D1988 /* RUSH_E_FINAL.mid */; }; - 607E99BE2B0D0D97005D1988 /* RUSH_E_FINAL.mid in Resources */ = {isa = PBXBuildFile; fileRef = 607E99BC2B0D0D97005D1988 /* RUSH_E_FINAL.mid */; }; 608EEBB42A16DC2B00C88590 /* Sounds in Resources */ = {isa = PBXBuildFile; fileRef = 608EEBB32A16DC2B00C88590 /* Sounds */; }; 608EEBB52A16DC2B00C88590 /* Sounds in Resources */ = {isa = PBXBuildFile; fileRef = 608EEBB32A16DC2B00C88590 /* Sounds */; }; 60A16CD72A18485400F225CB /* MIDITrackData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60A16CD62A18485400F225CB /* MIDITrackData.swift */; }; @@ -33,7 +31,6 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 607E99BC2B0D0D97005D1988 /* RUSH_E_FINAL.mid */ = {isa = PBXFileReference; lastKnownFileType = audio.midi; path = RUSH_E_FINAL.mid; sourceTree = ""; }; 608EEBB32A16DC2B00C88590 /* Sounds */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Sounds; sourceTree = ""; }; 608EEBB62A16E84400C88590 /* MIDITrackViewDemo--iOS--Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "MIDITrackViewDemo--iOS--Info.plist"; sourceTree = ""; }; 60A16CD62A18485400F225CB /* MIDITrackData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MIDITrackData.swift; sourceTree = ""; }; @@ -99,7 +96,6 @@ 608EEBB32A16DC2B00C88590 /* Sounds */, 60B0B5222A12D56900DAAAF7 /* type0Demo.mid */, 60C33E91296AA0C60066D196 /* type1Demo.mid */, - 607E99BC2B0D0D97005D1988 /* RUSH_E_FINAL.mid */, 60A16CD92A1848E000F225CB /* Conductor.swift */, 60A16CD62A18485400F225CB /* MIDITrackData.swift */, 60D3325F2915C6B300EAD0D7 /* MIDITrackViewDemo.swift */, @@ -227,7 +223,6 @@ 60D332622915C6B400EAD0D7 /* Assets.xcassets in Resources */, 60B0B5242A12D56900DAAAF7 /* type0Demo.mid in Resources */, 608EEBB52A16DC2B00C88590 /* Sounds in Resources */, - 607E99BE2B0D0D97005D1988 /* RUSH_E_FINAL.mid in Resources */, 60C33E93296AA0C60066D196 /* type1Demo.mid in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -239,7 +234,6 @@ 60D332882915CAA300EAD0D7 /* Assets.xcassets in Resources */, 60B0B5232A12D56900DAAAF7 /* type0Demo.mid in Resources */, 608EEBB42A16DC2B00C88590 /* Sounds in Resources */, - 607E99BD2B0D0D97005D1988 /* RUSH_E_FINAL.mid in Resources */, 60C33E92296AA0C60066D196 /* type1Demo.mid in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -467,7 +461,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 6CV59M265C; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "MIDITrackViewDemo--iOS--Info.plist"; @@ -499,7 +493,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 6CV59M265C; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "MIDITrackViewDemo--iOS--Info.plist"; diff --git a/Demo/Shared/Conductor.swift b/Demo/Shared/Conductor.swift index 9e4f0d5..b6ab56c 100644 --- a/Demo/Shared/Conductor.swift +++ b/Demo/Shared/Conductor.swift @@ -26,8 +26,10 @@ class Conductor { drumsData = MIDITrackData(length: 0.0, height: 0.0, noteData: []) return } + midiInstrument.enableLooping() midiInstrument.loadMIDIFile(fromURL: url) + let arpNotes = midiInstrument.tracks[1].getMIDINoteData() let arpLength = midiInstrument.tracks[1].length let bassNotes = midiInstrument.tracks[2].getMIDINoteData() @@ -36,6 +38,7 @@ class Conductor { let padLength = midiInstrument.tracks[3].length let drumNotes = midiInstrument.tracks[4].getMIDINoteData() let drumLength = midiInstrument.tracks[4].length + arpData = MIDITrackData(length: arpLength, height: 200.0, noteData: arpNotes) bassData = MIDITrackData(length: bassLength, height: 200.0, noteData: bassNotes) chordsData = MIDITrackData(length: padLength, height: 200.0, noteData: padNotes) diff --git a/Demo/Shared/MIDITrackData.swift b/Demo/Shared/MIDITrackData.swift index dd835a8..6035878 100644 --- a/Demo/Shared/MIDITrackData.swift +++ b/Demo/Shared/MIDITrackData.swift @@ -6,7 +6,7 @@ import MIDITrackView /// A class for holding the MIDINoteData to display in the MIDITrackView class MIDITrackData { - var midiNotes: [CGRect] = [] + var noteRects: [CGRect] = [] var length: CGFloat = 0.0 var height: CGFloat = 200.0 var highNote: MIDINoteNumber = 0 @@ -28,7 +28,7 @@ class MIDITrackData { for note in noteData { let noteLevel = maxHeight - CGFloat(note.noteNumber - lowNote) * noteHeight let noteRect = CGRect(x: note.position.beats, y: noteLevel, width: note.duration.beats, height: self.noteHeight) - self.midiNotes.append(noteRect) + self.noteRects.append(noteRect) } } } diff --git a/Demo/Shared/MIDITrackViewDemo.swift b/Demo/Shared/MIDITrackViewDemo.swift index 618bd25..331018d 100644 --- a/Demo/Shared/MIDITrackViewDemo.swift +++ b/Demo/Shared/MIDITrackViewDemo.swift @@ -25,10 +25,10 @@ struct MIDITrackViewDemo: View { init(conductor: Conductor) { self.conductor = conductor - self.arpModel = MIDITrackViewModel(midiNotes: conductor.arpData.midiNotes, length: conductor.arpData.length, height: conductor.arpData.height, playPos: conductor.midiInstrument.currentPosition.beats, zoomLevel: 50.0, minimumZoom: 0.01, maximumZoom: 1000.0) - self.chordsModel = MIDITrackViewModel(midiNotes: conductor.chordsData.midiNotes, length: conductor.chordsData.length, height: conductor.chordsData.height, playPos: conductor.midiInstrument.currentPosition.beats, zoomLevel: 50.0, minimumZoom: 0.01, maximumZoom: 1000.0) - self.bassModel = MIDITrackViewModel(midiNotes: conductor.bassData.midiNotes, length: conductor.bassData.length, height: conductor.bassData.height, playPos: conductor.midiInstrument.currentPosition.beats, zoomLevel: 50.0, minimumZoom: 0.01, maximumZoom: 1000.0) - self.drumsModel = MIDITrackViewModel(midiNotes: conductor.drumsData.midiNotes, length: conductor.drumsData.length, height: conductor.drumsData.height, playPos: conductor.midiInstrument.currentPosition.beats, zoomLevel: 50.0, minimumZoom: 0.01, maximumZoom: 1000.0) + self.arpModel = MIDITrackViewModel(noteRects: conductor.arpData.noteRects, length: conductor.arpData.length, height: conductor.arpData.height, playhead: conductor.midiInstrument.currentPosition.beats, zoomLevel: 50.0, minimumZoom: 0.01, maximumZoom: 1000.0) + self.chordsModel = MIDITrackViewModel(noteRects: conductor.chordsData.noteRects, length: conductor.chordsData.length, height: conductor.chordsData.height, playhead: conductor.midiInstrument.currentPosition.beats, zoomLevel: 50.0, minimumZoom: 0.01, maximumZoom: 1000.0) + self.bassModel = MIDITrackViewModel(noteRects: conductor.bassData.noteRects, length: conductor.bassData.length, height: conductor.bassData.height, playhead: conductor.midiInstrument.currentPosition.beats, zoomLevel: 50.0, minimumZoom: 0.01, maximumZoom: 1000.0) + self.drumsModel = MIDITrackViewModel(noteRects: conductor.drumsData.noteRects, length: conductor.drumsData.length, height: conductor.drumsData.height, playhead: conductor.midiInstrument.currentPosition.beats, zoomLevel: 50.0, minimumZoom: 0.01, maximumZoom: 1000.0) self.isPlaying = false self.timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect() } diff --git a/Demo/Shared/RUSH_E_FINAL.mid b/Demo/Shared/RUSH_E_FINAL.mid deleted file mode 100644 index 3b7b83b..0000000 Binary files a/Demo/Shared/RUSH_E_FINAL.mid and /dev/null differ diff --git a/MIDITrackView.playground/Contents.swift b/MIDITrackView.playground/Contents.swift index b9ea611..1139ba2 100644 --- a/MIDITrackView.playground/Contents.swift +++ b/MIDITrackView.playground/Contents.swift @@ -4,19 +4,17 @@ import SwiftUI struct MIDITrackViewDemo: View { @State var model = MIDITrackViewModel( - midiNotes: [ - MIDINote(position: 80, level: 5, length: 102, height: 5), - MIDINote(position: 103, level: 23, length: 75, height: 5), - MIDINote(position: 157, level: 36, length: 25, height: 5), - MIDINote(position: 208, level: 5, length: 102, height: 5), - MIDINote(position: 301, level: 23, length: 75, height: 5), - MIDINote(position: 376, level: 36, length: 25, height: 5), - MIDINote(position: 402, level: 5, length: 102, height: 5), - MIDINote(position: 450, level: 23, length: 75, height: 5), - MIDINote(position: 500, level: 36, length: 25, height: 5) + noteRects: [ + CGRect(x: 0.5, y: 0.0, width: 1.0, height: 10.0), + CGRect(x: 1.0, y: 20.0, width: 1.0, height: 10.0), + CGRect(x: 1.5, y: 40.0, width: 5.0, height: 10.0) ], length: 500, - height: 200 + height: 200, + playPos: 0.0, + zoomLevel: 50.0, + minimumZoom: 0.01, + maximumZoom: 1000.0 ) public var body: some View { VStack { diff --git a/Sources/MIDITrackView/Documentation.docc/Documentation.md b/Sources/MIDITrackView/Documentation.docc/Documentation.md index 0c822a9..105d361 100644 --- a/Sources/MIDITrackView/Documentation.docc/Documentation.md +++ b/Sources/MIDITrackView/Documentation.docc/Documentation.md @@ -12,12 +12,5 @@ This ``MIDITrackView`` provides a similar user interface to one seen in a Digita ### Properties - ``MIDITrackView/MIDITrackView/model`` -- ``MIDITrackView/MIDITrackView/playPos`` -- ``MIDITrackView/MIDITrackView/zoomLevel`` -- ``MIDITrackView/MIDITrackView/note`` - ``MIDITrackView/MIDITrackView/trackColor`` - ``MIDITrackView/MIDITrackView/noteColor`` -- ``MIDITrackView/MIDITrackView/playheadColor`` -- ``MIDITrackView/MIDITrackView/minimumZoom`` -- ``MIDITrackView/MIDITrackView/maximumZoom`` -- ``MIDITrackView/MIDITrackView/note`` diff --git a/Sources/MIDITrackView/Documentation.docc/Resources/demo.gif b/Sources/MIDITrackView/Documentation.docc/Resources/demo.gif index af7dda3..49895d1 100644 Binary files a/Sources/MIDITrackView/Documentation.docc/Resources/demo.gif and b/Sources/MIDITrackView/Documentation.docc/Resources/demo.gif differ diff --git a/Sources/MIDITrackView/MIDITrackView.swift b/Sources/MIDITrackView/MIDITrackView.swift index 16d1c33..eb9a297 100644 --- a/Sources/MIDITrackView/MIDITrackView.swift +++ b/Sources/MIDITrackView/MIDITrackView.swift @@ -2,6 +2,15 @@ import SwiftUI +struct MIDINotesView: Shape { + let noteRects: [CGRect] + func path(in rect: CGRect) -> Path { + var path = Path().path(in: rect) + path.addRects(noteRects) + return path + } +} + /// A view representing a MIDI Track. public struct MIDITrackView: View { /// The model for the view which contains an array of MIDI notes (as `CGRect`), the track length, and the track height. @@ -10,20 +19,24 @@ public struct MIDITrackView: View { private let trackColor: Color /// The color of the notes on the track. private let noteColor: Color - public init(model: Binding, trackColor: SwiftUI.Color = Color.primary, noteColor: SwiftUI.Color = Color.accentColor) { + public init(model: Binding, + trackColor: SwiftUI.Color = Color.primary, + noteColor: SwiftUI.Color = Color.accentColor) { _model = model self.trackColor = trackColor self.noteColor = noteColor } public var body: some View { - Canvas { context, size in - context.scaleBy(x: model.getZoomLevel(), y: 1.0) - context.fill(Rectangle().path(in: CGRect(x: 0.0, y: 0.0, width: model.getLength(), height: model.getHeight())), with: .color(trackColor)) - context.translateBy(x: -model.getPlayPos(), y: 0.0) - var notePath = Path() - notePath.addRects(model.getMIDINotes()) - context.fill(notePath, with: .color(noteColor)) + ZStack { + RoundedRectangle(cornerRadius: 10.0).fill(trackColor) + MIDINotesView(noteRects: model.getNoteRects()) + .transform(CGAffineTransformConcat( + CGAffineTransform(translationX: -model.getPlayhead(), y: 0.0), + CGAffineTransform(scaleX: model.getZoomLevel(), y: 1.0)) + ) + .fill(noteColor) } + .drawingGroup() } } diff --git a/Sources/MIDITrackView/MIDITrackViewModel.swift b/Sources/MIDITrackView/MIDITrackViewModel.swift index df69b98..a36e9db 100644 --- a/Sources/MIDITrackView/MIDITrackViewModel.swift +++ b/Sources/MIDITrackView/MIDITrackViewModel.swift @@ -3,11 +3,26 @@ import SwiftUI public struct MIDITrackViewModel { - public init(midiNotes: [CGRect], length: CGFloat, height: CGFloat, playPos: Double, zoomLevel: Double, minimumZoom: Double, maximumZoom: Double) { - self.midiNotes = midiNotes + /// The model which holds data for the MIDITrackView + /// - Parameters: + /// - noteRects: the note rectangles rendered in the view + /// - length: the length of the longest track + /// - height: the height of the MIDI track + /// - playhead: the playhead of the MIDI track + /// - zoomLevel: the zoom level of the MIDI track + /// - minimumZoom: the minimum zoom level for the MIDI track + /// - maximumZoom: the maximum zoom level for the MIDI track + public init(noteRects: [CGRect], + length: CGFloat, + height: CGFloat, + playhead: Double, + zoomLevel: Double, + minimumZoom: Double, + maximumZoom: Double) { + self.noteRects = noteRects self.length = length self.height = height - self.playPos = playPos + self.playhead = playhead self.zoomLevel = zoomLevel self.minimumZoom = minimumZoom self.maximumZoom = maximumZoom @@ -24,25 +39,23 @@ public struct MIDITrackViewModel { zoomLevel = max(min(newScale, maximumZoom), minimumZoom) } public mutating func zoomLevelGestureEnded() { lastZoomLevel = 1.0 } - public mutating func updatePlayPos(newPos: Double) { playPos = newPos } + public mutating func updatePlayPos(newPos: Double) { playhead = newPos } + /// Get the zoom level of all the tracks in the view public func getZoomLevel() -> Double { return zoomLevel } + /// Get the length of the longest track. public func getLength() -> CGFloat { return length } + /// Get the height of all the tracks in the view. public func getHeight() -> CGFloat { return height } - public func getPlayPos() -> Double { return playPos } - public func getMIDINotes() -> [CGRect] { return midiNotes } - /// The notes rendered in the view. - private let midiNotes: [CGRect] - /// The length of the longest track. + /// The view's current play position (also the position which the playhead displays) + public func getPlayhead() -> Double { return playhead } + /// Get the note rectangles rendered in the view. + public func getNoteRects() -> [CGRect] { return noteRects } + private let noteRects: [CGRect] private let length: CGFloat - /// The height of all the tracks in the view. private let height: CGFloat - /// The view's current play position (also the position which the playhead displays) - private var playPos: Double - /// The zoom level of all the tracks in the view + private var playhead: Double private var zoomLevel: Double - /// The minimum zoom level. private let minimumZoom: Double - /// The maximum zoom level. private let maximumZoom: Double private var lastZoomLevel: Double = 1.0 }