LMMS / lmms

Cross-platform music production software
https://lmms.io
GNU General Public License v2.0
8.14k stars 1.01k forks source link

Implement Import/Export of the dawproject open format. #6909

Open consolegrl opened 1 year ago

consolegrl commented 1 year ago

Enhancement Summary

This is a note/action plan for implementing dawproject format. I've been working on the DataFile class a lot lately so this is a natural extension of that work.

Justification

Something similar was brough up in #3981

@Veratil Since BitWig and PreSonus are the only ones using it right now, it would be something to get LMMS onboard quick and maybe some more eyes on?

michaelgregorius commented 1 year ago

Proposal to use a code generator

The project offers an XSD which describes the schema of valid XML documents in the context of the project.

The XSD in turn can be used with a code generator, e.g. CodeSynthesis XSD, to generate classes from the XSD. As a result the workflow in the code is roughly the following one:

michaelgregorius commented 1 year ago

I have just played with CodeSynthesis XSD in a Docker container and unfortunately the generated code seems to introduce a compile time dependency to the library and a runtime dependency to Xerces. 🙁

This might be rather unattractive as the project already uses Qt's XML library.

Veratil commented 1 year ago

I have just played with CodeSynthesis XSD in a Docker container and unfortunately the generated code seems to introduce a compile time dependency to the library and a runtime dependency to Xerces. 🙁

This might be rather unattractive as the project already uses Qt's XML library.

We already install libxml2-utils (Ubuntu package name), which contains xmllint. I've verified that using the example project in dawproject's README will validate using xmllint --noout --schema Project.xsd test.xml, where test.xml is the example in its own file (--noout just quiets the tree output).

Veratil commented 1 year ago

So I spent some time going over the dawproject schema and seeing if I could convert our data/projects/shorties/Crunk(Demo).mmp to the dawproject format.

So far the only things I really hit were:

  1. There's no way that I can find to save base64 encoded data, so a few of our plugins won't work currently
  2. Note's documentation says channel isn't required, but the Project.xsd schema defines it as required
  3. Some elements (Channel) have a specific order of subelements (Mute, Pan, Volume for instance HAVE to be in a certain order); I really think they shouldn't need to be

I've opened up two PRs and we'll see how it goes.

So with the two issues above, I modified the Project.xsd slightly to work with my hand made XML for Crunk(Demo) (yeah, by hand... I know it was a long task to do it this way but I learned quite a bit so far).

diff --git a/Project.xsd b/Project.xsd
index 12e3696..6cb5350 100644
--- a/Project.xsd
+++ b/Project.xsd
@@ -61,6 +61,8 @@

   <xs:element name="Scene" type="scene"/>

+  <xs:element name="StringParameter" type="stringParameter"/>
+
   <xs:element name="TimeSignatureParameter" type="timeSignatureParameter"/>

   <xs:element name="TimeSignaturePoint" type="timeSignaturePoint"/>
@@ -168,6 +170,15 @@
     </xs:complexContent>
   </xs:complexType>

+  <xs:complexType name="stringParameter">
+    <xs:complexContent>
+      <xs:extension base="parameter">
+        <xs:sequence/>
+        <xs:attribute name="value" type="xs:string"/>
+      </xs:extension>
+    </xs:complexContent>
+  </xs:complexType>
+
   <xs:complexType name="integerParameter">
     <xs:complexContent>
       <xs:extension base="parameter">
@@ -327,6 +338,7 @@
                   <xs:element ref="BoolParameter"/>
                   <xs:element ref="IntegerParameter"/>
                   <xs:element ref="EnumParameter"/>
+                  <xs:element ref="StringParameter"/>
                   <xs:element ref="TimeSignatureParameter"/>
                 </xs:choice>
               </xs:sequence>
@@ -496,7 +508,7 @@
     </xs:sequence>
     <xs:attribute name="time" type="xs:string" use="required"/>
     <xs:attribute name="duration" type="xs:string" use="required"/>
-    <xs:attribute name="channel" type="xs:int" use="required"/>
+    <xs:attribute name="channel" type="xs:int"/>
     <xs:attribute name="key" type="xs:int" use="required"/>
     <xs:attribute name="vel" type="xs:string"/>
     <xs:attribute name="rel" type="xs:string"/>

And then here's the converted (and validated with xmllint with the changes above), please remember this may not be 100% correct or best, but it's following what I understand currently. I'm pretty sure some of the RealParameter units may not actually be linear, but I didn't feel like trying to find out. lol

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Project version="1.0">
    <Application name="LMMS" version="1.2.0"/>
    <Transport>
        <Tempo max="999" min="1" unit="bpm" value="130" id="tempo0" name="Tempo"/>
        <TimeSignature denominator="4" numerator="4" id="timesig0"/>
    </Transport>
    <Structure>
        <Track contentType="tracks" id="bbtrack">
            <Track contentType="audio" loaded="true" id="afp0" name="Kick 01">
                <Channel role="regular" audioChannels="2" solo="false" destination="mixer1">
                    <Devices>
                        <BuiltinDevice deviceRole="instrument" deviceName="audiofileprocessor" deviceVendor="LMMS">
                            <Parameters>
                                <IntegerParameter value="1" min="1" max="60" name="range"/>
                                <IntegerParameter value="0" name="pitch"/>
                                <IntegerParameter value="57" name="baseNote"/>
                                <BoolParameter value="true" name="useMasterPitch"/>
                                <RealParameter value="0" unit="decibel" min="-96.0" max="6.0" name="amp"/>
                                <RealParameter value="0" unit="normalized" min="0" max="1" name="startPoint"/>
                                <RealParameter value="0" unit="normalized" min="0" max="1" name="loopPoint"/>
                                <RealParameter value="1" unit="normalized" min="0" max="1" name="endPoint"/>
                                <BoolParameter value="false" name="reverse"/>
                                <IntegerParameter value="0" name="looped"/>
                                <BoolParameter value="false" name="stutter"/>
                                <IntegerParameter value="1" name="interp"/>
                            </Parameters>
                            <State external="false" path="drums/kick01.ogg"/>
                        </BuiltinDevice>
                    </Devices>
                    <Mute value="false"/>
                    <Pan value="0" unit="linear" min="-100" max="100"/>
                    <Volume value="-2.4" unit="decibel" min="-96.0" max="6.0"/>
                </Channel>
            </Track>
            <Track contentType="audio" loaded="true" id="afp1" name="Clap 04">
                <Channel role="regular" audioChannels="2" solo="false" destination="mixer1">
                    <Devices>
                        <BuiltinDevice deviceRole="instrument" deviceName="audiofileprocessor" deviceVendor="LMMS">
                            <Parameters>
                                <IntegerParameter value="1" min="1" max="60" name="pitchrange"/>
                                <IntegerParameter value="0" name="pitch"/>
                                <IntegerParameter value="57" name="baseNote"/>
                                <BoolParameter value="true" name="useMasterPitch"/>
                                <RealParameter value="0" unit="decibel" min="-96.0" max="6.0" name="amp"/>
                                <RealParameter value="0" unit="normalized" min="0" max="1" name="startPoint"/>
                                <RealParameter value="0" unit="normalized" min="0" max="1" name="loopPoint"/>
                                <RealParameter value="1" unit="normalized" min="0" max="1" name="endPoint"/>
                                <BoolParameter value="false" name="reverse"/>
                                <IntegerParameter value="0" name="looped"/>
                                <BoolParameter value="false" name="stutter"/>
                                <IntegerParameter value="1" name="interp"/>
                            </Parameters>
                            <State external="false" path="drums/clap04.ogg"/>
                        </BuiltinDevice>
                    </Devices>
                    <Mute value="false"/>
                    <Pan value="0" unit="linear" min="-100" max="100"/>
                    <Volume value="-1.1" unit="decibel" min="-96.0" max="6.0"/>
                </Channel>
            </Track>
        </Track>
        <Track contentType="tracks" id="song">
            <Track contentType="audio" loaded="true" id="track0" name="Drum">
                <Channel role="regular" audioChannels="2" solo="false" destination="mixer1">
                    <Mute value="false"/>
                    <Pan value="1" unit="linear" comment="This is actually unused for bbtrack"/>
                    <Volume value="1" unit="linear" comment="This is actually unused for bbtrack"/>
                </Channel>
            </Track>
            <Track contentType="audio" loaded="true" id="track1" name="Bass">
                <Channel role="regular" audioChannels="2" solo="false" destination="mixer2">
                    <Devices>
                        <BuiltinDevice deviceRole="instrument" deviceName="audiofileprocessor" deviceVendor="LMMS" id="afp2">
                            <Parameters>
                                <IntegerParameter value="1" min="1" max="60" name="pitchrange"/>
                                <IntegerParameter value="0" name="pitch"/>
                                <IntegerParameter value="57" name="baseNote"/>
                                <BoolParameter value="true" name="useMasterPitch"/>
                                <RealParameter value="0" unit="decibel" min="-96.0" max="6.0" name="amp"/>
                                <RealParameter value="0" unit="normalized" min="0" max="1" name="startPoint"/>
                                <RealParameter value="0" unit="normalized" min="0" max="1" name="loopPoint"/>
                                <RealParameter value="1" unit="normalized" min="0" max="1" name="endPoint"/>
                                <BoolParameter value="false" name="reverse"/>
                                <IntegerParameter value="0" name="looped"/>
                                <BoolParameter value="false" name="stutter"/>
                                <IntegerParameter value="1" name="interp"/>
                            </Parameters>
                            <State external="false" path="basses/bass_hard01.ogg"/>
                        </BuiltinDevice>
                    </Devices>
                    <Mute value="false"/>
                    <Pan value="0" unit="linear" min="-100" max="100"/>
                    <Volume value="0" unit="decibel" min="-96.0" max="6.0"/>
                </Channel>
            </Track>
            <Track contentType="audio" loaded="true" id="track2" name="Synth">
                <Channel role="regular" audioChannels="2" solo="false" destination="mixer3">
                    <Devices>
                        <BuiltinDevice deviceRole="instrument" deviceName="FreeBoy" deviceVendor="LMMS" id="freeboy0">
                            <Parameters>
                                <IntegerParameter value="1" min="1" max="60" name="pitchrange"/>
                                <IntegerParameter value="0" name="pitch"/>
                                <IntegerParameter value="57" name="baseNote"/>
                                <BoolParameter value="true" name="useMasterPitch"/>
                                <RealParameter value="4" unit="linear" min="0" max="7" name="st" comment="ch1SweepTime"/>
                                <BoolParameter value="0" name="sd" comment="ch1SweepDir"/>
                                <RealParameter value="0" unit="linear" min="0" max="7" name="srs" comment="ch1SweepRtShift"/>
                                <RealParameter value="2" unit="linear" min="0" max="3" name="ch1wpd" comment="ch1WavePatternDuty"/>
                                <RealParameter value="15" unit="linear" min="0" max="15" name="ch1vol" comment="ch1Volume"/>
                                <BoolParameter value="0" name="ch1vsd" comment="ch1VolSweepDir"/>
                                <RealParameter value="0" unit="linear" min="0" max="7" name="ch1ssl" comment="ch1SweepStepLength"/>
                                <RealParameter value="2" unit="linear" min="0" max="3" name="ch2wpd" comment="ch2WavePatternDuty"/>
                                <RealParameter value="15" unit="linear" min="0" max="15" name="ch2vol" comment="ch2Volume"/>
                                <BoolParameter value="0" name="ch2vsd" comment="ch2VolSweepDir"/>
                                <RealParameter value="0" unit="linear" min="0" max="7" name="ch2ssl" comment="ch2SweepStepLength"/>
                                <RealParameter value="3" unit="linear" min="0" max="3" name="ch3vol" comment="ch3Volume"/>
                                <RealParameter value="15" unit="linear" min="0" max="15" name="ch4vol" comment="ch4Volume"/>
                                <BoolParameter value="0" name="ch4vsd" comment="ch4VolSweepDir"/>
                                <RealParameter value="7" unit="linear" min="0" max="7" name="ch4ssl" comment="ch4SweepStepLength"/>
                                <BoolParameter value="0" name="srw" comment="ch4ShiftRegWidth"/>
                                <RealParameter value="7" unit="linear" min="0" max="7" name="so1vol" comment="so1Volume"/>
                                <RealParameter value="7" unit="linear" min="0" max="7" name="so2vol" comment="so2Volume"/>
                                <BoolParameter value="1" name="ch1so1" comment="ch1So1"/>
                                <BoolParameter value="1" name="ch2so1" comment="ch2So1"/>
                                <BoolParameter value="0" name="ch3so1" comment="ch3So1"/>
                                <BoolParameter value="0" name="ch4so1" comment="ch4So1"/>
                                <BoolParameter value="1" name="ch1so2" comment="ch1So2"/>
                                <BoolParameter value="1" name="ch2so2" comment="ch2So2"/>
                                <BoolParameter value="0" name="ch3so2" comment="ch3So2"/>
                                <BoolParameter value="0" name="ch4so2" comment="ch4So2"/>
                                <RealParameter value="461" unit="linear" min="-1" max="600" name="Bass"/>
                                <RealParameter value="-11" unit="linear" min="-100" max="200" name="Treble"/>
                                <StringParameter value="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" name="sampleShape"/>
                            </Parameters>
                        </BuiltinDevice>
                        <AuPlugin deviceRole="audioFX" deviceName="Plate2x2" deviceVendor="caps" loaded="true" name="ladspaeffect0">
                            <Parameters>
                                <RealParameter value="1" unit="linear" min="-1" max="1" name="W/D"/>
                                <IntegerParameter value="1" min="1" max="8000" name="Decay"/>
                                <RealParameter value="0" unit="linear" min="0" max="1" name="Gate"/>
                                <RealParameter value="0.18389" unit="linear" min="0.005" max="0.999" name="port02" comment="bandwidth"/>
                                <RealParameter value="0.5767" unit="linear" min="0" max="0.749" name="port03" comment="tail"/>
                                <RealParameter value="0.999" unit="linear" min="0.0005" max="1" name="port04" comment="damping"/>
                                <RealParameter value="0.37" unit="linear" min="0" max="1" name="port05" comment="blend"/>
                            </Parameters>
                            <Enabled value="true"/>
                        </AuPlugin>
                    </Devices>
                    <Mute value="false"/>
                    <Pan value="0" unit="linear" min="-100" max="100"/>
                    <Volume value="-1.1" unit="decibel" min="-96.0" max="6.0"/>
                </Channel>
            </Track>
        </Track>
        <Channel role="master" audioChannels="2" solo="false" id="mixer0" name="Master">
            <Mute value="false"/>
            <Volume value="100.0" unit="linear" min="0" max="200"/>
        </Channel>
        <Channel role="regular" audioChannels="2" solo="false" id="mixer1" name="Drums">
            <Mute value="false"/>
            <Sends>
                <Send type="post" destination="mixer0">
                    <Volume value="1" unit="linear" min="0" max="1"/>
                </Send>
            </Sends>
            <Volume value="100.0" unit="linear" min="0" max="200"/>
        </Channel>
        <Channel role="regular" audioChannels="2" solo="false" id="mixer2" name="Bass">
            <Mute value="false"/>
            <Sends>
                <Send type="post" destination="mixer0">
                    <Volume value="1" unit="linear" min="0" max="1"/>
                </Send>
            </Sends>
            <Volume value="100.0" unit="linear" min="0" max="200"/>
        </Channel>
        <Channel role="regular" audioChannels="2" solo="false" id="mixer3" name="Lead">
            <Mute value="false"/>
            <Sends>
                <Send type="post" destination="mixer0">
                    <Volume value="1" unit="linear" min="0" max="1"/>
                </Send>
            </Sends>
            <Volume value="100.0" unit="linear" min="0" max="200"/>
        </Channel>
    </Structure>
    <Arrangement>
        <Lanes timeUnit="beats" id="id20">
            <Lanes track="afp0" id="id21">
                <Clips>
                    <Clip time="0" duration="16">
                        <Notes>
                            <Note time="0" duration="-1" key="57" vel="1"/>
                            <Note time="2.5" duration="-1" key="57" vel="1"/>
                        </Notes>
                    </Clip>
                </Clips>
            </Lanes>
            <Lanes track="afp1" id="id23">
                <Clips>
                    <Clip time="0" duration="16">
                        <Notes>
                            <Note time="1" duration="-1" key="57" vel="1"/>
                            <Note time="3" duration="-1" key="57" vel="1"/>
                        </Notes>
                    </Clip>
                </Clips>
            </Lanes>
            <Clips track="track0" name="Drum">
                <Clip time="0" duration="16" comment="bbtrack clip"/>
                <Clip time="4" duration="16" comment="bbtrack clip"/>
            </Clips>
            <Lanes track="track1">
                <Clips name="Bass">
                    <Clip time="0" duration="16">
                        <Notes>
                            <Note time="0" duration="0.75" key="52" vel="1"/>
                            <Note time="1.5" duration="0.75" key="52" vel="1"/>
                            <Note time="3" duration="0.75" key="52" vel="1"/>
                            <Note time="4.5" duration="0.5" key="59" vel="1"/>
                            <Note time="5" duration="0.5" key="59" vel="1"/>
                            <Note time="5.75" duration="0.5" key="59" vel="1"/>
                            <Note time="6.5" duration="0.5" key="59" vel="1"/>
                            <Note time="7.5" duration="0.5" key="59" vel="1"/>
                            <Note time="8" duration="0.75" key="60" vel="1"/>
                            <Note time="9.5" duration="0.75" key="60" vel="1"/>
                            <Note time="11" duration="0.75" key="60" vel="1"/>
                            <Note time="12.5" duration="0.5" key="55" vel="1"/>
                            <Note time="13" duration="0.5" key="55" vel="1"/>
                            <Note time="13.75" duration="0.5" key="55" vel="1"/>
                            <Note time="14.5" duration="0.5" key="59" vel="1"/>
                            <Note time="15.5" duration="0.5" key="59" vel="1"/>
                        </Notes>
                    </Clip>
                    <Clip time="4" duration="16">
                        <Notes>
                            <Note time="0" duration="0.75" key="52" vel="1"/>
                            <Note time="1.5" duration="0.75" key="52" vel="1"/>
                            <Note time="3" duration="0.75" key="52" vel="1"/>
                            <Note time="4.5" duration="0.5" key="59" vel="1"/>
                            <Note time="5" duration="0.5" key="59" vel="1"/>
                            <Note time="5.75" duration="0.5" key="59" vel="1"/>
                            <Note time="6.5" duration="0.5" key="59" vel="1"/>
                            <Note time="7.5" duration="0.5" key="59" vel="1"/>
                            <Note time="8" duration="0.75" key="60" vel="1"/>
                            <Note time="9.5" duration="0.75" key="60" vel="1"/>
                            <Note time="11" duration="0.75" key="60" vel="1"/>
                            <Note time="12.5" duration="0.5" key="55" vel="1"/>
                            <Note time="13" duration="0.5" key="55" vel="1"/>
                            <Note time="13.75" duration="0.5" key="55" vel="1"/>
                            <Note time="14.5" duration="0.5" key="59" vel="1"/>
                            <Note time="15.5" duration="0.5" key="59" vel="1"/>
                        </Notes>
                    </Clip>
                </Clips>
            </Lanes>
            <Lanes track="track2">
                <Clips name="Synth">
                    <Clip time="0" duration="16">
                        <Notes>
                            <Note time="0.5" duration="0.25" key="51" vel="1"/>
                            <Note time="1.25" duration="0.25" key="58" vel="1"/>
                            <Note time="2" duration="0.25" key="51" vel="1"/>
                            <Note time="4.5" duration="0.25" key="58" vel="1"/>
                            <Note time="5.25" duration="0.25" key="50" vel="1"/>
                            <Note time="6" duration="0.25" key="58" vel="1"/>
                            <Note time="8.5" duration="0.25" key="51" vel="1"/>
                            <Note time="9.25" duration="0.25" key="58" vel="1"/>
                            <Note time="10" duration="0.25" key="51" vel="1"/>
                            <Note time="12.5" duration="0.25" key="58" vel="1"/>
                            <Note time="13.25" duration="0.25" key="50" vel="1"/>
                            <Note time="14" duration="0.25" key="58" vel="1"/>
                        </Notes>
                    </Clip>
                    <Clip time="4" duration="16">
                        <Notes>
                            <Note time="0.5" duration="0.25" key="51" vel="1"/>
                            <Note time="1.25" duration="0.25" key="58" vel="1"/>
                            <Note time="2" duration="0.25" key="51" vel="1"/>
                            <Note time="4.5" duration="0.25" key="58" vel="1"/>
                            <Note time="5.25" duration="0.25" key="50" vel="1"/>
                            <Note time="6" duration="0.25" key="58" vel="1"/>
                            <Note time="8.5" duration="0.25" key="51" vel="1"/>
                            <Note time="9.25" duration="0.25" key="58" vel="1"/>
                            <Note time="10" duration="0.25" key="51" vel="1"/>
                            <Note time="12.5" duration="0.25" key="58" vel="1"/>
                            <Note time="13.25" duration="0.25" key="50" vel="1"/>
                            <Note time="14" duration="0.25" key="58" vel="1"/>
                        </Notes>
                    </Clip>
                </Clips>
            </Lanes>
        </Lanes>
        <TempoAutomation unit="bpm" id="tempo1" name="Automation Track">
            <Target parameter="tempo0"/>
            <IntegerPoint value="131" time="0"/>
        </TempoAutomation>
    </Arrangement>
    <Scenes/>
</Project>
michaelgregorius commented 1 year ago

@Veratil, that sounds like a lot of work! 👍

So far the only things I really hit were:

1. There's no way that I can find to save base64 encoded data, so a few of our plugins won't work currently

I have only glanced over the schema but if I understand correctly the internal state of the plugins is stored under State which is of type fileReference.

<xs:element name="State" type="fileReference" minOccurs="0"/>

So the internal state is not contained in the XML itself but in one or more files which are then referenced from the XML. It would be really nice to have the option for completely self-contained XML files though.

Assuming that these files are intended to store byte streams I now wonder if this might be one of the limits of the project and the format because in that case one might run into problems of endianess, etc. in case plugins do not always save in the exact binary format on all platforms.

Now that I have written all this I also wonder what the other parameters are then intended to be used for and where's the boundary between parameters and state? 🤔

Veratil commented 1 year ago

I have only glanced over the schema but if I understand correctly the internal state of the plugins is stored under State which is of type fileReference.

I guess that could be done instead, yes. Endianness doesn't matter as long as the reader is made to read it properly. MIDI files for instance are written in big endian, and we read those fine. :)