aboutsummaryrefslogblamecommitdiffstats
path: root/src/main.rs
blob: 9b9c12fa92f17d5a9450c36c93083727d650f04f (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

















                                                                        
                














                                       


                                                                                                   

                                                  

                                                             






























































                                                                          










                                                                           

                                                                                                    

                        





































































                                                                                                 
                          


                                                   
                           


                                                 

                         


                                             
                                    


                                                          
                               


                                                     
                                   


                                                         
                             


                                                 
                        


                                             
                        


                                           
                                


                                                   
                              


                                                 
                                 


                                                    
                              


                                                 
                             


                                                
                         







                                                                                  










                                                                
 


















                                                                                                                       


                                             

               
 





                                                                          
















                                                                             







                                                                                   


























































                                                                                        
/* cbconvert -- A program to parse and convert Cubase projects
 * Copyright (C) 2020  Harald Eilertsen
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

use nom::{
    branch::alt,
    bytes::complete::*,
    multi::{count, length_data, many0},
    number::complete::*,
    sequence::tuple,
    Finish,
    IResult,
};
use std::{
    convert::From,
    error::Error,
    path::Path,
};

/// Parser for length prefixed strings.
///
/// Cubase uses a string format where the length of the string is given as a 32 bit big endian word
/// before the actual string data. The string may also be zero terminated, with the zero term char
/// included in the length. In that case, we trim off the ending zero bytes.
fn cmstring(data: &[u8]) -> IResult<&[u8], &str> {
    let (r, s) = length_data(be_u32)(data)?;
    let end = s.iter().rposition(|b| b != &0u8).unwrap() + 1;
    Ok((r, std::str::from_utf8(&s[0..end]).unwrap()))
}

#[derive(Debug, PartialEq)]
enum NodeType {
    Container,
    Leaf,
}

impl From<u32> for NodeType {
    fn from(v: u32) -> NodeType {
        match v {
            0xffffffff => NodeType::Leaf,
            0xfffffffe => NodeType::Container,
            _ => panic!(format!("Unexpected node type: {:#x} found.", v)),
        }
    }
}

#[derive(Debug)]
enum NodeValue<'a> {
    Unknown,
    //List(Vec<Node<'a>>),
    Raw(Vec<u32>),
    Arrangement(PArrangement),
    InstrumentTrackEvent(MInstrumentTrackEvent<'a>),

    MidiAfterTouch(u32),
    MidiController(u32),
    MidiNote(u32),
    MidiPart(MMidiPart<'a>),
    MidiPartEvent(Vec<u8>),
    MidiPitchBend(u32),
    MidiPolyPressure(u32),
    MidiProgramChange(u32),
    MidiSysex(Vec<u8>),

    SignatureTrackEvent(MSignatureTrackEvent<'a>),
    TempoTrackEvent(&'a [u8]),
    Version(PAppVersion<'a>),
}

#[derive(Debug)]
struct Node<'a> {
    nodetype: NodeType,
    nodeclass: &'a str,
    num: u16,
    value: Box<NodeValue<'a>>,
}

/// A vector of 32bit words preceeded by a 16 bit count.
fn counted_vec(data: &[u8]) -> IResult<&[u8], Vec<u32>> {
    let (r, len) = be_u16(data)?;
    count(be_u32, len as usize)(&r)
}

/// A byte vector prefixed with a 32 bit count.
fn bytevec<'a>(data: &'a[u8]) -> IResult<&'a [u8], &'a [u8]> {
    length_data(be_u32)(&data)
}

/// Version information about the app that created the file.
#[derive(Debug)]
struct PAppVersion<'a> {
    appname: &'a str,
    appversion: &'a str,
    appdate: &'a str,
    num2: u32,
    apparch: &'a str,
    num3: u32,
    appencoding: &'a str,
    applocale: &'a str,
}

fn p_appversion<'a>(data: &'a [u8]) -> IResult<&'a [u8], PAppVersion<'a>> {
    let (r, (appname, appversion, appdate, num2, apparch, num3, appencoding, applocale)) =
        tuple((cmstring, cmstring, cmstring, be_u32, cmstring, be_u32, cmstring, cmstring))(&data)?;

    Ok((r, PAppVersion {
        appname,
        appversion,
        appdate,
        num2,
        apparch,
        num3,
        appencoding,
        applocale,
    }))
}

#[derive(Debug)]
struct PArrangement {
    num1: u16,
    num2: u16,
}

fn p_arrangement(data: &[u8]) -> IResult<&[u8], PArrangement> {
    let (r, (num1, num2)) = tuple((be_u16, be_u16))(data)?;
    Ok((r, PArrangement { num1, num2 }))
}

#[derive(Debug)]
struct MInstrumentTrackEvent<'a> {
    num16_3: Vec<u16>,
    num32_8: Vec<u32>,
    name: &'a str,
    num32: u32,
}

fn m_instrument_track_event<'a>(data: &'a [u8]) -> IResult<&'a [u8], MInstrumentTrackEvent<'a>> {
    let (r, (num16_3, num32_8, name, num32)) =
        tuple((count(be_u16, 3), count(be_u32, 8), cmstring, be_u32))(&data)?;
    Ok((r, MInstrumentTrackEvent {
        num16_3,
        num32_8,
        name,
        num32,
    }))
}

fn m_tempo_track_event<'a>(data: &'a [u8]) -> IResult<&'a [u8], &'a [u8]> {
    bytevec(&data)
}

#[derive(Debug)]
struct MSignatureTrackEvent<'a> {
    bv: &'a [u8],
    num32: u32,
}

fn m_signature_track_event<'a>(data: &'a [u8]) -> IResult<&'a [u8], MSignatureTrackEvent<'a>> {
    let (r, (bv, num32)) = tuple((bytevec, be_u32))(&data)?;
    Ok((r, MSignatureTrackEvent { bv, num32 }))
}

#[derive(Debug)]
struct MMidiPart<'a> {
    num32: u32,
    name: &'a str,
    data: Vec<u8>,
}

fn m_midi_part<'a>(data: &'a [u8]) -> IResult<&'a [u8], MMidiPart<'a>> {
    let (r, (num32, name, data)) = tuple((be_u32, cmstring, count(be_u8, 17)))(&data)?;
    Ok((r, MMidiPart { num32, name, data }))
}

fn node_value<'a>(class: &str, data: &'a [u8]) -> IResult<&'a [u8], NodeValue<'a>> {
    match class {
        "PAppVersion" => {
            let (r, appver) = p_appversion(&data)?;
            Ok((r, NodeValue::Version(appver)))
        },
        "PArrangement" => {
            let (r, arr) = p_arrangement(&data)?;
            Ok((r, NodeValue::Arrangement(arr)))
        },
        "MRoot" |
        "MTrackList" => {
            let (r, v) = counted_vec(&data)?;
            Ok((r, NodeValue::Raw(v)))
        },
        "MInstrumentTrackEvent" => {
            let (r, e) = m_instrument_track_event(&data)?;
            Ok((r, NodeValue::InstrumentTrackEvent(e)))
        },
        "MTempoTrackEvent" => {
            let (r, e) = m_tempo_track_event(&data)?;
            Ok((r, NodeValue::TempoTrackEvent(e)))
        },
        "MSignatureTrackEvent" => {
            let (r, e) = m_signature_track_event(&data)?;
            Ok((r, NodeValue::SignatureTrackEvent(e)))
        },
        "MMidiPartEvent" => {
            let (r, v) = count(be_u8, 30)(data)?;
            Ok((r, NodeValue::MidiPartEvent(v)))
        },
        "MMidiPart" => {
            let (r, v) = m_midi_part(&data)?;
            Ok((r, NodeValue::MidiPart(v)))
        },
        "MMidiNote" => {
            let (r, v) = be_u32(data)?;
            Ok((r, NodeValue::MidiNote(v)))
        },
        "MMidiPolyPressure" => {
            let (r, v) = be_u32(data)?;
            Ok((r, NodeValue::MidiPolyPressure(v)))
        },
        "MMidiAfterTouch" => {
            let (r, v) = be_u32(data)?;
            Ok((r, NodeValue::MidiAfterTouch(v)))
        },
        "MMidiProgramChange" => {
            let (r, v) = be_u32(data)?;
            Ok((r, NodeValue::MidiProgramChange(v)))
        },
        "MMidiController" => {
            let (r, v) = be_u32(data)?;
            Ok((r, NodeValue::MidiController(v)))
        },
        "MMidiPitchBend" => {
            let (r, v) = be_u32(data)?;
            Ok((r, NodeValue::MidiPitchBend(v)))
        },
        "MMidiSysex" => {
            let sep = vec![0xff, 0xff, 0xff, 0xff];
            let (r, v) = take_until(sep.as_slice())(data)?;
            Ok((r, NodeValue::MidiSysex(v.to_vec())))
        },
        &_ => Ok((&[], NodeValue::Unknown)) //"Unknown node class \"{}\".", class)
    }
}

fn fourcc<'a>(data: &'a [u8]) -> IResult<&'a [u8], &'a str> {
    let (rest, tag) = take(4usize)(data)?;
    Ok((rest, std::str::from_utf8(tag).unwrap()))
}

fn root_chunk<'a>(data: &'a [u8]) -> IResult<&'a [u8], ()> {
    let (rest, (key, val)) = tuple((cmstring, cmstring))(data)?;
    println!("    {}: {}", key, val);
    Ok((rest, ()))
}


/**
 * A container node does not contain any data of it's own, but contains
 * one or more sub elements. These can be either other containers, or
 * object/leaf nodes.
 */
fn container_node<'a>(data: &'a [u8]) -> IResult<&'a [u8], ()> {
    let (r, (_, name, num)) = tuple((tag(b"\xff\xff\xff\xfe"), cmstring, be_u16))(data)?;
    println!("     Container: {}, {}", name, num);
    Ok((r, ()))
}

/**
 * An object node contains serialized structured data.
 *
 * It has a size, as well as a payload containing the actual serialized data.
 */
fn object_node<'a>(data: &'a [u8]) -> IResult<&'a [u8], ()> {
    let (r, (_, name, num, payload)) = tuple((tag(b"\xff\xff\xff\xff"), cmstring, be_u16, length_data(be_u32)))(data)?;
    println!("       Object: {}, {}, {}", name, num, payload.len());

    let (_, nv) = node_value(name, payload)?;
    println!("         {:?}", nv);
    Ok((r, ()))
}

/**
 * ARCH chunks contains one or more structured data elements that can be
 * either container nodes or object/leaf nodes..
 */
fn arch_chunk<'a>(data: &'a [u8]) -> IResult<&'a [u8], ()> {
    let (rest, _nodes) = many0(alt((container_node, object_node)))(data)?;
    Ok((rest, ()))
}

fn chunk<'a>(data: &'a [u8]) -> IResult<&'a [u8], ()> {
    let (rest, (tag, payload)) = tuple((fourcc, length_data(be_u32)))(data)?;

    println!("  {}: {}", tag, payload.len());

    match tag {
        "ROOT" => root_chunk(payload)?,
        "ARCH" => arch_chunk(payload)?,
        &_     => (payload, ())
    };

    Ok((rest, ()))
}

/**
 * Parse the Cubase project file header.
 *
 * A Cubase project file is almost a standard RIFF file, with the exception of an
 * extra fourcc (NUND) following the global RIFF header. The extra (NUND) fourcc
 * is _not_ followed by the length of the chunk, but rather immediately followed by
 * the next chunk.
 */
fn file_hdr<'a>(data: &'a [u8]) -> IResult<&'a [u8], ()> {
    let (rest, (_, len, _)) = tuple((tag("RIFF"), be_u32, tag("NUND")))(data)?;
    println!("RIFF: {}, NUND", len);
    Ok((rest, ()))
}

pub struct CubaseProject;

pub fn parse_cubase_project<P>(filename: P) -> Result<CubaseProject, Box<dyn Error>>
where
    P: AsRef<Path>
{
    println!("Reading {}...", filename.as_ref().to_str().ok_or("Invalid file name")?);

    let data = std::fs::read(filename)?;
    let (rest, _) = tuple((file_hdr, many0(chunk)))(&data)
        .finish()
        .map_err(|e| format!("{:?}", e))?;

    println!("Rest: {}", rest.len());

    // let mut rest = chunk.payload;

    // while dbg!(rest.len()) > 0 {
    //     let (r, chunk) = parse_chunk(&rest).unwrap();
    //     println!("  Chunk id         : {}", chunk.tag);
    //     println!("  Chunk data size  : {}/{} bytes", chunk.len, chunk.payload.len());

    //     match chunk.tag {
    //         "NUNDROOT" | "ROOT" => {
    //             let (_r, hm) = parse_nundroot(&chunk.payload).unwrap();
    //             println!("    data      : {:?}", hm);
    //         },
    //         "ARCH" => {
    //             let (_r, n) = node(&chunk.payload).unwrap();
    //             println!("    {}: {{", n.nodeclass);
    //             println!("        {:#?}", n.value);
    //             println!("    }},");
    //         },
    //         _ => (),
    //     };

    //     rest = r;
    //     std::thread::sleep(std::time::Duration::from_millis(200));
    // }

    Ok(CubaseProject {})
}

fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    let filename = std::env::args()
        .skip(1)
        .next()
        .expect("You must give me a file to analyze");

    parse_cubase_project(filename)?;

    Ok(())
}