Swan's Quest, Chapter 3: The notable scroll

Description: Swift Playgrounds presents "Swan’s Quest,” an interactive adventure in four chapters for all ages. Calling all musicians! In this chapter, our Hero has found a mysterious scroll of music, and only you can help decode it. (Don’t worry if you can’t read music, our clever Lizard is standing by to assist. It’s sure to be a note-worthy experience.) By learning a little theory, and mastering time to create tones of different lengths, you just might help our Hero face the music… and move onto the next part of their quest. Swan’s Quest was created for Swift Playgrounds on iPad and Mac, combining frameworks and resources which power the educational experiences in many of our playgrounds, including Sonic Workshop, Sensor Arcade, and Augmented Reality. To learn more about building your own playgrounds, be sure to watch "Create Swift Playgrounds content for iPad and Mac". And don’t forget to stop by the Developer Forums and share your solution for our side quest.

Swan's Quest is a series of challenges where each chapter has a specific programming challenge for you that will build on the prior chapters.

Download the .playgroundbook here.

Melody

In this challenge we will play a melody with different length of tones, and we will need to implement this ourselves.

Solution

Advice from the Lizard

  • main
let notes: [Note] = [.quarter(.a4), .quarter(.b4), .quarter(.c4)]
  • music
public protocol PitchProtocol {
    
    /// The sonic frequency of this Pitch
    var frequency: Double { get }
    
}

public protocol NoteProtocol {
    associatedtype PitchType: PitchProtocol
    
    /// Play this Note through a ToneOutput
    var tone: Tone { get }
    
    /// The duration of this Note as a multiple of quarter notes, e.g., a half note would equal 2.0, an eight note
    var length: Float { get }
    
    /// Length of the smallest Note supported
    static var shortestSupportedNoteLength: Float { get }
    
    /// Subdivide into a series pitches, according to the shortest supported note
    func subdivide() -> [PitchType]
}

public enum Note : NoteProtocol {
    case quarter(Pitch)
    case half(Pitch)
    
    /// Play this Note through a ToneOutput
    public var tone: Tone {
        switch self {
        case .quarter(let pitch), .half(let pitch):
            return Tone(pitch: pitch.frequency, volume: 0.3)
        }
    }
    
    /// The duration of this Note as a multiple of quarter notes, e.g., a half note would equal 2.0, an eight note
    public var length: Float {
        switch self {
        case .quarter:
            return 1.0
        case .half:
            return 2.0
        }
    }
    
    /// Length of the smallest Note supported
    public static var shortestSupportedNoteLength: Float {
        return Note.quarter(.a4).length
    }
    
    /// Subdivide into a series pitches, according to the shortest supported note
    public func subdivide() -> [Pitch] {
        switch self {
        case .quarter(let pitch):
            return [pitch]
        case .half(let pitch):
            return [pitch, pitch]
        }
    }
}

public enum Pitch : Double, PitchProtocol {
    case c3 = 261.63
    case d3 = 293.66
    case e3 = 329.63
    case f3 = 349.23
    case g3 = 392.00
    case a4 = 440.0
    case b4 = 493.88
    case c4 = 523.25
    
    /// The sonic frequency of this Pitch
    public var frequency: Double {
        return self.rawValue
    }
}

Perdomance at Swan Hall

func performance(owner: Assessable) {
    let toneOutput = ToneOutput()
    let quarterPitches: [Pitch] = [.e3, .e3, .f3, .g3, .g3, .f3, .e3, .d3, .c3, .c3, .d3, .e3, .e3, .d3]
    let halfNotePitch: Pitch = .d3
    let notes: [Note] = quarterPitches.map{ Note.quarter($0) } + [Note.half(halfNotePitch)]
    
    var pitches: [Pitch] = notes
        .flatMap { $0.subdivide() }
        .reversed()
    
    let interval = TimeInterval(Note.shortestSupportedNoteLength * 0.5)
    Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { timer in
        guard let pitch = pitches.popLast() else {
            toneOutput.stopTones()
            timer.invalidate()
            owner.endPerformance()
            return
        }
        
        toneOutput.play(tone: Tone(pitch: pitch.frequency, volume: 0.3))
    }
    owner.endPerformance()
}

Missing anything? Corrections? Contributions are welcome 😃

Related

Written by

Federico Zanetello

Federico Zanetello

Software engineer with a strong passion for well-written code, thought-out composable architectures, automation, tests, and more.