diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Cargo.lock | 113 | ||||
-rw-r--r-- | Cargo.toml | 8 | ||||
-rw-r--r-- | src/main.rs | 363 |
4 files changed, 486 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53eaa21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e2010bd --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,113 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bitvec" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "funty 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "radium 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tap 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "wyz 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cbconv" +version = "0.1.0" +dependencies = [ + "nom 6.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "funty" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "lexical-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "arrayvec 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "static_assertions 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "memchr" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "nom" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitvec 0.19.4 (registry+https://github.com/rust-lang/crates.io-index)", + "lexical-core 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "version_check 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "ryu" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "tap" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum arrayvec 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +"checksum bitvec 0.19.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81" +"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +"checksum funty 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0ba62103ce691c2fd80fbae2213dfdda9ce60804973ac6b6e97de818ea7f52c8" +"checksum lexical-core 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616" +"checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" +"checksum nom 6.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4489ccc7d668957ddf64af7cd027c081728903afa6479d35da7e99bf5728f75f" +"checksum radium 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" +"checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" +"checksum static_assertions 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +"checksum tap 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e" +"checksum version_check 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +"checksum wyz 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..513cea1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "cbconv" +version = "0.1.0" +authors = ["Harald Eilertsen <haraldei@anduin.net>"] +edition = "2018" + +[dependencies] +nom = "6.0.0" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0c51ea0 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,363 @@ +/* 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::{ + 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 is also zero terminated, with the zero term +/// char included in the length. +fn cmstring(data: &[u8]) -> IResult<&[u8], &str> { + let (r, s) = length_data(be_u32)(data)?; + Ok((r, std::str::from_utf8(s).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> { + num1: u32, + 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, (num1, appname, appversion, appdate, num2, apparch, num3, appencoding, applocale)) = + tuple((be_u32, cmstring, cmstring, cmstring, be_u32, cmstring, be_u32, cmstring, cmstring))(&data)?; + + Ok((r, PAppVersion { + num1, + 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\0" => { + let (r, appver) = p_appversion(&data)?; + Ok((r, NodeValue::Version(appver))) + }, + "PArrangement\0" => { + let (r, arr) = p_arrangement(&data)?; + Ok((r, NodeValue::Arrangement(arr))) + }, + "MRoot\0" | + "MTrackList\0" => { + let (r, v) = counted_vec(&data)?; + Ok((r, NodeValue::Raw(v))) + }, + "MInstrumentTrackEvent\0" => { + let (r, e) = m_instrument_track_event(&data)?; + Ok((r, NodeValue::InstrumentTrackEvent(e))) + }, + "MTempoTrackEvent\0" => { + let (r, e) = m_tempo_track_event(&data)?; + Ok((r, NodeValue::TempoTrackEvent(e))) + }, + "MSignatureTrackEvent\0" => { + let (r, e) = m_signature_track_event(&data)?; + Ok((r, NodeValue::SignatureTrackEvent(e))) + }, + "MMidiPartEvent\0" => { + let (r, v) = count(be_u8, 30)(data)?; + Ok((r, NodeValue::MidiPartEvent(v))) + }, + "MMidiPart\0" => { + let (r, v) = m_midi_part(&data)?; + Ok((r, NodeValue::MidiPart(v))) + }, + "MMidiNote\0" => { + let (r, v) = be_u32(data)?; + Ok((r, NodeValue::MidiNote(v))) + }, + "MMidiPolyPressure\0" => { + let (r, v) = be_u32(data)?; + Ok((r, NodeValue::MidiPolyPressure(v))) + }, + "MMidiAfterTouch\0" => { + let (r, v) = be_u32(data)?; + Ok((r, NodeValue::MidiAfterTouch(v))) + }, + "MMidiProgramChange\0" => { + let (r, v) = be_u32(data)?; + Ok((r, NodeValue::MidiProgramChange(v))) + }, + "MMidiController\0" => { + let (r, v) = be_u32(data)?; + Ok((r, NodeValue::MidiController(v))) + }, + "MMidiPitchBend\0" => { + let (r, v) = be_u32(data)?; + Ok((r, NodeValue::MidiPitchBend(v))) + }, + "MMidiSysex\0" => { + 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 nodetype<'a>(data: &'a [u8]) -> IResult<&'a [u8], NodeType> { + let (rest, nt) = be_u32(data)?; + + Ok((rest, NodeType::from(nt))) +} + +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, ())) +} + +fn arch_chunk<'a>(data: &'a [u8]) -> IResult<&'a [u8], ()> { + let (rest, (nt, t, x)) = tuple((nodetype, cmstring, be_u16))(data)?; + println!(" {:?}: {}, {}", nt, t, x); + + match nt { + NodeType::Container => { + arch_chunk(rest)?; + }, + NodeType::Leaf => { + let (_, nodeval) = node_value(t, rest)?; + println!(" {:?}", nodeval); + } + }; + + 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, ())) +} + +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(()) +} |