使用AKKeyboardView连接AKRhodesPiano时如何生成MIDI文件?
Great question! I’ve tackled this exact use case before, and there are two solid approaches—one super straightforward using AudioKit’s built-in tools, and a more manual method if you need full control over the MIDI data. Let’s break them down:
The Easy Way: Use AKMIDIRecorder
AudioKit has a AKMIDIRecorder class that handles all the heavy lifting of capturing MIDI events and writing them to a file. This is by far the simplest approach since you don’t have to manage timestamps or MIDI file formatting yourself.
Step 1: Set Up the Recorder
First, initialize an AKMIDIRecorder and point it to a file path where you want to save the MIDI file:
import AudioKit class YourViewController: UIViewController { var rhodesPiano = AKRhodesPiano() var midiRecorder: AKMIDIRecorder! var midiFileURL: URL! override func viewDidLoad() { super.viewDidLoad() // Set up your AudioKit engine (assuming you already have this) AudioKit.output = rhodesPiano try? AudioKit.start() // Create a URL for the MIDI file (save to Documents directory) let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! midiFileURL = documentsDir.appendingPathComponent("rhodes_recording.mid") // Initialize the recorder do { midiRecorder = try AKMIDIRecorder() } catch { print("Failed to initialize MIDI recorder: \(error.localizedDescription)") return } } }
Step 2: Connect Keyboard Events to the Recorder
Update your AKKeyboardView callbacks to send note events to both the Rhodes piano and the MIDI recorder. This way, you hear the sound while capturing the MIDI data:
// Assuming you have an AKKeyboardView instance hooked up keyboardView.noteOn = { [weak self] noteNumber, velocity in guard let self = self else { return } // Play the note on the Rhodes self.rhodesPiano.play(noteNumber: noteNumber, velocity: velocity) // Send the Note On event to the recorder self.midiRecorder.sendEvent(AKMIDIEvent.noteOn(noteNumber: noteNumber, velocity: velocity, channel: 0)) } keyboardView.noteOff = { [weak self] noteNumber in guard let self = self else { return } // Stop the note on the Rhodes self.rhodesPiano.stop(noteNumber: noteNumber) // Send the Note Off event to the recorder self.midiRecorder.sendEvent(AKMIDIEvent.noteOff(noteNumber: noteNumber, velocity: 0, channel: 0)) }
Step 3: Start/Stop Recording
Add controls to start and stop the recording (like buttons in your UI):
@IBAction func startRecordingTapped(_ sender: UIButton) { do { try midiRecorder.startRecording(to: midiFileURL) print("Recording started!") } catch { print("Failed to start recording: \(error.localizedDescription)") } } @IBAction func stopRecordingTapped(_ sender: UIButton) { do { try midiRecorder.stopRecording() print("MIDI file saved to: \(midiFileURL.path)") } catch { print("Failed to stop recording: \(error.localizedDescription)") } }
Manual Method: Record Events & Build the MIDI File Yourself
If you need more control over the MIDI data (like editing events before saving), you can manually capture note events and construct an AKMIDIFile from scratch.
Step 1: Define a Struct to Store Events
First, create a simple struct to track each note’s details and timestamp:
struct MIDINoteEvent { let noteNumber: MIDINoteNumber let velocity: MIDIVelocity let isNoteOn: Bool let timestamp: Double // Uses AKClock's synchronized time } var recordedEvents: [MIDINoteEvent] = []
Step 2: Capture Keyboard Events
Update your keyboard callbacks to save events with timestamps from AKClock (this ensures timing matches AudioKit’s playback):
keyboardView.noteOn = { note, velocity in let event = MIDINoteEvent( noteNumber: note, velocity: velocity, isNoteOn: true, timestamp: AKClock.currentTime ) recordedEvents.append(event) self.rhodesPiano.play(noteNumber: note, velocity: velocity) } keyboardView.noteOff = { note in let event = MIDINoteEvent( noteNumber: note, velocity: 0, isNoteOn: false, timestamp: AKClock.currentTime ) recordedEvents.append(event) self.rhodesPiano.stop(noteNumber: note) }
Step 3: Generate & Save the MIDI File
Use AKMIDIFile to convert your recorded events into a valid MIDI file. You’ll need to convert timestamps to MIDI ticks (based on your project’s tempo):
func generateAndSaveMIDIFile() throws { guard !recordedEvents.isEmpty else { throw NSError(domain: "MIDIError", code: 0, userInfo: [NSLocalizedDescriptionKey: "No recorded events to save"]) } // Configure MIDI file settings (standard 480 ticks per quarter note, 120 BPM) let ticksPerQuarterNote: UInt16 = 480 let tempo = 120.0 let secondsPerTick = 60.0 / (tempo * Double(ticksPerQuarterNote)) // Create an empty MIDI file var midiFile = AKMIDIFile() // Calculate relative timestamps from the first event let startTime = recordedEvents.first!.timestamp var currentTick: UInt32 = 0 for event in recordedEvents { let relativeTime = event.timestamp - startTime let eventTick = UInt32(relativeTime / secondsPerTick) let deltaTick = eventTick - currentTick if event.isNoteOn { try midiFile.addNoteOn( noteNumber: event.noteNumber, velocity: event.velocity, channel: 0, delta: deltaTick ) } else { try midiFile.addNoteOff( noteNumber: event.noteNumber, velocity: event.velocity, channel: 0, delta: deltaTick ) } currentTick = eventTick } // Add an end-of-track marker try midiFile.addEndOfTrack(delta: 0) // Save the file let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let saveURL = documentsDir.appendingPathComponent("custom_rhodes_recording.mid") try midiFile.write(to: saveURL) print("Custom MIDI file saved to: \(saveURL.path)") }
Key Notes for Both Methods
- MIDI Channels: The examples use channel 0 (which maps to MIDI channel 1 in most DAWs). Adjust this if you need to record on a different channel.
- Tempo: If your project uses a non-120 BPM tempo, update the
tempovalue in the manual method to match yourAKClock’s tempo. - AudioKit Version: These examples work with AudioKit 5+. If you’re on an older version, some class names or methods might differ.
内容的提问来源于stack exchange,提问作者c0rruptbytes

