1 /*
2  * Carla Native Plugins
3  * Copyright (C) 2012-2021 Filipe Coelho <falktx@falktx.com>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License as
7  * published by the Free Software Foundation; either version 2 of
8  * the License, or any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * For a full copy of the GNU General Public License see the doc/GPL.txt file.
16  */
17 
18 #include "CarlaNativePrograms.hpp"
19 #include "midi-base.hpp"
20 
21 #include "water/files/FileInputStream.h"
22 #include "water/midi/MidiFile.h"
23 
24 // -----------------------------------------------------------------------
25 
26 class MidiFilePlugin : public NativePluginWithMidiPrograms<FileMIDI>,
27                        public AbstractMidiPlayer
28 {
29 public:
30     enum Parameters {
31         kParameterRepeating,
32         kParameterHostSync,
33         kParameterEnabled,
34         kParameterInfoNumTracks,
35         kParameterInfoLength,
36         kParameterInfoPosition,
37         kParameterCount
38     };
39 
MidiFilePlugin(const NativeHostDescriptor * const host)40     MidiFilePlugin(const NativeHostDescriptor* const host)
41         : NativePluginWithMidiPrograms<FileMIDI>(host, fPrograms, 0),
42           fRepeatMode(false),
43 #ifdef __MOD_DEVICES__
44           fHostSync(false),
45 #else
46           fHostSync(true),
47 #endif
48           fEnabled(true),
49           fNeedsAllNotesOff(false),
50           fWasPlayingBefore(false),
51           fLastPosition(0.0f),
52           fMidiOut(this),
53           fFileLength(0.0f),
54           fNumTracks(0.0f),
55           fInternalTransportFrame(0),
56           fMaxFrame(0),
57           fLastFrame(0),
58           fPrograms(hostGetFilePath("midi"), "*.mid;*.midi")
59     {
60     }
61 
62 protected:
63     // -------------------------------------------------------------------
64     // Plugin parameter calls
65 
getParameterCount() const66     uint32_t getParameterCount() const override
67     {
68         return kParameterCount;
69     }
70 
getParameterInfo(const uint32_t index) const71     const NativeParameter* getParameterInfo(const uint32_t index) const override
72     {
73         static NativeParameter param;
74 
75         param.scalePointCount  = 0;
76         param.scalePoints      = nullptr;
77         param.unit             = nullptr;
78         param.ranges.step      = 1.0f;
79         param.ranges.stepSmall = 1.0f;
80         param.ranges.stepLarge = 1.0f;
81         param.designation      = NATIVE_PARAMETER_DESIGNATION_NONE;
82 
83         switch (index)
84         {
85         case kParameterRepeating:
86             param.name  = "Repeat Mode";
87             param.hints = static_cast<NativeParameterHints>(NATIVE_PARAMETER_IS_AUTOMABLE|
88                                                             NATIVE_PARAMETER_IS_ENABLED|
89                                                             NATIVE_PARAMETER_IS_BOOLEAN);
90             param.ranges.def = 0.0f;
91             param.ranges.min = 0.0f;
92             param.ranges.max = 1.0f;
93             break;
94         case kParameterHostSync:
95             param.name  = "Host Sync";
96             param.hints = static_cast<NativeParameterHints>(NATIVE_PARAMETER_IS_AUTOMABLE|
97                                                             NATIVE_PARAMETER_IS_ENABLED|
98                                                             NATIVE_PARAMETER_IS_BOOLEAN);
99 #ifdef __MOD_DEVICES__
100             param.ranges.def = 0.0f;
101 #else
102             param.ranges.def = 1.0f;
103 #endif
104             param.ranges.min = 0.0f;
105             param.ranges.max = 1.0f;
106             break;
107         case kParameterEnabled:
108             param.name  = "Enabled";
109             param.hints = static_cast<NativeParameterHints>(NATIVE_PARAMETER_IS_AUTOMABLE|
110                                                             NATIVE_PARAMETER_IS_ENABLED|
111                                                             NATIVE_PARAMETER_IS_BOOLEAN|
112                                                             NATIVE_PARAMETER_USES_DESIGNATION);
113             param.ranges.def = 1.0f;
114             param.ranges.min = 0.0f;
115             param.ranges.max = 1.0f;
116             param.designation = NATIVE_PARAMETER_DESIGNATION_ENABLED;
117             break;
118         case kParameterInfoNumTracks:
119             param.name  = "Num Tracks";
120             param.hints = static_cast<NativeParameterHints>(NATIVE_PARAMETER_IS_AUTOMABLE|
121                                                             NATIVE_PARAMETER_IS_ENABLED|
122                                                             NATIVE_PARAMETER_IS_INTEGER|
123                                                             NATIVE_PARAMETER_IS_OUTPUT);
124             param.ranges.def = 0.0f;
125             param.ranges.min = 0.0f;
126             param.ranges.max = 256.0f;
127             break;
128         case kParameterInfoLength:
129             param.name  = "Length";
130             param.hints = static_cast<NativeParameterHints>(NATIVE_PARAMETER_IS_AUTOMABLE|
131                                                             NATIVE_PARAMETER_IS_ENABLED|
132                                                             NATIVE_PARAMETER_IS_OUTPUT);
133             param.ranges.def = 0.0f;
134             param.ranges.min = 0.0f;
135             param.ranges.max = (float)INT64_MAX;
136             param.unit = "s";
137             break;
138         case kParameterInfoPosition:
139             param.name  = "Position";
140             param.hints = static_cast<NativeParameterHints>(NATIVE_PARAMETER_IS_AUTOMABLE|
141                                                             NATIVE_PARAMETER_IS_ENABLED|
142                                                             NATIVE_PARAMETER_IS_OUTPUT);
143             param.ranges.def = 0.0f;
144             param.ranges.min = 0.0f;
145             param.ranges.max = 100.0f;
146             param.unit = "%";
147             break;
148         default:
149             return nullptr;
150         }
151 
152         return &param;
153     }
154 
getParameterValue(const uint32_t index) const155     float getParameterValue(const uint32_t index) const override
156     {
157         switch (index)
158         {
159         case kParameterRepeating:
160             return fRepeatMode ? 1.0f : 0.0f;
161         case kParameterHostSync:
162             return fHostSync ? 1.0f : 0.0f;
163         case kParameterEnabled:
164             return fEnabled ? 1.0f : 0.0f;
165         case kParameterInfoNumTracks:
166             return fNumTracks;
167         case kParameterInfoLength:
168             return fFileLength;
169         case kParameterInfoPosition:
170             return fLastPosition;
171         default:
172             return 0.0f;
173         }
174     }
175 
176     // -------------------------------------------------------------------
177     // Plugin state calls
178 
setParameterValue(const uint32_t index,const float value)179     void setParameterValue(const uint32_t index, const float value) override
180     {
181         const bool b = (value > 0.5f);
182 
183         switch (index)
184         {
185         case kParameterRepeating:
186             if (fRepeatMode != b)
187             {
188                 fRepeatMode = b;
189                 fNeedsAllNotesOff = true;
190             }
191             break;
192         case kParameterHostSync:
193             if (fHostSync != b)
194             {
195                 fInternalTransportFrame = 0;
196                 fHostSync = b;
197             }
198             break;
199         case kParameterEnabled:
200             if (fEnabled != b)
201             {
202                 fInternalTransportFrame = 0;
203                 fEnabled = b;
204             }
205             break;
206         default:
207             break;
208         }
209     }
210 
setCustomData(const char * const key,const char * const value)211     void setCustomData(const char* const key, const char* const value) override
212     {
213         CARLA_SAFE_ASSERT_RETURN(key != nullptr && key[0] != '\0',);
214         CARLA_SAFE_ASSERT_RETURN(value != nullptr && value[0] != '\0',);
215 
216         if (std::strcmp(key, "file") != 0)
217             return;
218 
219         invalidateNextFilename();
220         _loadMidiFile(value);
221     }
222 
223     // -------------------------------------------------------------------
224     // Plugin process calls
225 
process2(const float * const *,float **,const uint32_t frames,const NativeMidiEvent * const,const uint32_t)226     void process2(const float* const*, float**, const uint32_t frames, const NativeMidiEvent* const, const uint32_t) override
227     {
228         const uint32_t maxFrame = fMaxFrame;
229         bool playing;
230         uint64_t frame;
231 
232         if (fHostSync)
233         {
234             const NativeTimeInfo* const timePos = getTimeInfo();
235             playing = fEnabled && timePos->playing;
236             frame = timePos->frame;
237         }
238         else
239         {
240             playing = fEnabled;
241             frame = fInternalTransportFrame;
242 
243             if (playing)
244                 fInternalTransportFrame += frames;
245         }
246 
247         if (fRepeatMode && maxFrame != 0 && frame >= maxFrame)
248             frame %= maxFrame;
249 
250         if (fWasPlayingBefore != playing || frame < fLastFrame)
251         {
252             fNeedsAllNotesOff = true;
253             fWasPlayingBefore = playing;
254         }
255 
256         if (fNeedsAllNotesOff)
257         {
258             NativeMidiEvent midiEvent;
259 
260             midiEvent.port    = 0;
261             midiEvent.time    = 0;
262             midiEvent.data[0] = 0;
263             midiEvent.data[1] = MIDI_CONTROL_ALL_NOTES_OFF;
264             midiEvent.data[2] = 0;
265             midiEvent.data[3] = 0;
266             midiEvent.size    = 3;
267 
268             for (int channel=MAX_MIDI_CHANNELS; --channel >= 0;)
269             {
270                 midiEvent.data[0] = uint8_t(MIDI_STATUS_CONTROL_CHANGE | (channel & MIDI_CHANNEL_BIT));
271                 NativePluginClass::writeMidiEvent(&midiEvent);
272             }
273 
274             fNeedsAllNotesOff = false;
275         }
276 
277         if (fWasPlayingBefore)
278             if (! fMidiOut.play(static_cast<uint32_t>(frame), frames))
279                 fNeedsAllNotesOff = true;
280 
281         fLastFrame = frame;
282 
283         if (frame < maxFrame)
284             fLastPosition = static_cast<float>(frame) / static_cast<float>(maxFrame) * 100.0f;
285         else
286             fLastPosition = 100.0f;
287     }
288 
289     // -------------------------------------------------------------------
290     // Plugin UI calls
291 
uiShow(const bool show)292     void uiShow(const bool show) override
293     {
294         if (! show)
295             return;
296 
297         if (const char* const filename = uiOpenFile(false, "Open MIDI File", "MIDI Files (*.mid *.midi);;"))
298             uiCustomDataChanged("file", filename);
299 
300         uiClosed();
301     }
302 
303     // -------------------------------------------------------------------
304     // Plugin state calls
305 
getState() const306     char* getState() const override
307     {
308         return fMidiOut.getState();
309     }
310 
setState(const char * const data)311     void setState(const char* const data) override
312     {
313         fMidiOut.setState(data);
314     }
315 
setStateFromFile(const char * const filename)316     void setStateFromFile(const char* const filename) override
317     {
318         _loadMidiFile(filename);
319     }
320 
321     // -------------------------------------------------------------------
322     // AbstractMidiPlayer calls
323 
writeMidiEvent(const uint8_t port,const double timePosFrame,const RawMidiEvent * const event)324     void writeMidiEvent(const uint8_t port, const double timePosFrame, const RawMidiEvent* const event) override
325     {
326         NativeMidiEvent midiEvent;
327 
328         midiEvent.port    = port;
329         midiEvent.time    = static_cast<uint32_t>(timePosFrame);
330         midiEvent.size    = event->size;
331         midiEvent.data[0] = event->data[0];
332         midiEvent.data[1] = event->data[1];
333         midiEvent.data[2] = event->data[2];
334         midiEvent.data[3] = event->data[3];
335 
336         NativePluginClass::writeMidiEvent(&midiEvent);
337     }
338 
339     // -------------------------------------------------------------------
340 
341 private:
342     bool fRepeatMode;
343     bool fHostSync;
344     bool fEnabled;
345     bool fNeedsAllNotesOff;
346     bool fWasPlayingBefore;
347     float fLastPosition;
348     MidiPattern fMidiOut;
349     float fFileLength;
350     float fNumTracks;
351     uint32_t fInternalTransportFrame;
352     uint32_t fMaxFrame;
353     uint64_t fLastFrame;
354     NativeMidiPrograms fPrograms;
355 
_loadMidiFile(const char * const filename)356     void _loadMidiFile(const char* const filename)
357     {
358         fMidiOut.clear();
359         fInternalTransportFrame = 0;
360         fFileLength = 0.0f;
361         fNumTracks = 0.0f;
362         fMaxFrame = 0;
363         fLastFrame = 0;
364         fLastPosition = 0.0f;
365 
366         using namespace water;
367 
368         const String jfilename = String(CharPointer_UTF8(filename));
369         File file(jfilename);
370 
371         if (! file.existsAsFile())
372            return;
373 
374         FileInputStream fileStream(file);
375         MidiFile        midiFile;
376 
377         if (! midiFile.readFrom(fileStream))
378             return;
379 
380         midiFile.convertTimestampTicksToSeconds();
381 
382         const double sampleRate = getSampleRate();
383         const size_t numTracks = midiFile.getNumTracks();
384 
385         for (size_t i=0; i<numTracks; ++i)
386         {
387             const MidiMessageSequence* const track(midiFile.getTrack(i));
388             CARLA_SAFE_ASSERT_CONTINUE(track != nullptr);
389 
390             for (int j=0, numEvents = track->getNumEvents(); j<numEvents; ++j)
391             {
392                 const MidiMessageSequence::MidiEventHolder* const midiEventHolder(track->getEventPointer(j));
393                 CARLA_SAFE_ASSERT_CONTINUE(midiEventHolder != nullptr);
394 
395                 const MidiMessage& midiMessage(midiEventHolder->message);
396 
397                 const int dataSize = midiMessage.getRawDataSize();
398                 if (dataSize <= 0 || dataSize > MAX_EVENT_DATA_SIZE)
399                     continue;
400 
401                 const uint8_t* const data = midiMessage.getRawData();
402                 if (! MIDI_IS_CHANNEL_MESSAGE(data[0]))
403                     continue;
404 
405                 const double time = midiMessage.getTimeStamp() * sampleRate;
406                 // const double time = track->getEventTime(i) * sampleRate;
407                 CARLA_SAFE_ASSERT_CONTINUE(time >= 0.0);
408 
409                 fMidiOut.addRaw(static_cast<uint32_t>(time + 0.5),
410                                 midiMessage.getRawData(), static_cast<uint8_t>(dataSize));
411             }
412         }
413 
414         const double lastTimeStamp = midiFile.getLastTimestamp();
415 
416         fFileLength = static_cast<float>(lastTimeStamp);
417         fNumTracks = static_cast<float>(numTracks);
418         fNeedsAllNotesOff = true;
419         fInternalTransportFrame = 0;
420         fLastFrame = 0;
421         fMaxFrame = static_cast<uint32_t>(lastTimeStamp * sampleRate + 0.5);
422     }
423 
424     PluginClassEND(MidiFilePlugin)
425     CARLA_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MidiFilePlugin)
426 };
427 
428 // -----------------------------------------------------------------------
429 
430 static const NativePluginDescriptor midifileDesc = {
431     /* category  */ NATIVE_PLUGIN_CATEGORY_UTILITY,
432     /* hints     */ static_cast<NativePluginHints>(NATIVE_PLUGIN_IS_RTSAFE
433                                                   |NATIVE_PLUGIN_HAS_UI
434                                                   |NATIVE_PLUGIN_NEEDS_UI_OPEN_SAVE
435                                                   |NATIVE_PLUGIN_REQUESTS_IDLE
436                                                   |NATIVE_PLUGIN_USES_STATE
437                                                   |NATIVE_PLUGIN_USES_TIME),
438     /* supports  */ NATIVE_PLUGIN_SUPPORTS_NOTHING,
439     /* audioIns  */ 0,
440     /* audioOuts */ 0,
441     /* midiIns   */ 0,
442     /* midiOuts  */ 1,
443     /* paramIns  */ 0,
444     /* paramOuts */ 0,
445     /* name      */ "MIDI File",
446     /* label     */ "midifile",
447     /* maker     */ "falkTX",
448     /* copyright */ "GNU GPL v2+",
449     PluginDescriptorFILL(MidiFilePlugin)
450 };
451 
452 // -----------------------------------------------------------------------
453 
454 CARLA_EXPORT
455 void carla_register_native_plugin_midifile();
456 
457 CARLA_EXPORT
carla_register_native_plugin_midifile()458 void carla_register_native_plugin_midifile()
459 {
460     carla_register_native_plugin(&midifileDesc);
461 }
462 
463 // -----------------------------------------------------------------------
464