From 63991ae469347164cee1cb1f8b1261976322d5be Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 30 Jul 2020 10:26:58 -0400 Subject: [PATCH] Initial commit --- .gitignore | 36 + Makefile.am | 80 ++ README.md | 452 +++++++ build/clean.sh | 37 + build/linux/debug/build.sh | 25 + build/linux/release/build.sh | 24 + build/osx/debug/build.sh | 20 + build/osx/release/build.sh | 19 + configure.ac | 152 +++ doc/score_follow_0.png | Bin 0 -> 64164 bytes doc/xscore_gen.md | 254 ++++ m4/os_64.m4 | 8 + m4/os_type.m4 | 11 + src/cmtools/audiodev.c | 397 ++++++ src/cmtools/cmtools.c | 431 +++++++ src/cmtools/mas.c | 2232 ++++++++++++++++++++++++++++++++++ 16 files changed, 4178 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile.am create mode 100644 README.md create mode 100755 build/clean.sh create mode 100755 build/linux/debug/build.sh create mode 100755 build/linux/release/build.sh create mode 100755 build/osx/debug/build.sh create mode 100755 build/osx/release/build.sh create mode 100644 configure.ac create mode 100644 doc/score_follow_0.png create mode 100644 doc/xscore_gen.md create mode 100644 m4/os_64.m4 create mode 100644 m4/os_type.m4 create mode 100644 src/cmtools/audiodev.c create mode 100644 src/cmtools/cmtools.c create mode 100644 src/cmtools/mas.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af37e38 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# directories to ignore +libcm + +.deps +autom4te.cache +build-aux +build/linux/debug/src/ +build/linux/debug/bin +build/linux/debug/lib +build/linux/debug/include +build/linux/release/src/ +build/linux/release/bin +build/linux/release/lib +build/linux/release/include + +#Files to ignore +*~ +*.[oa] + +Makefile +aclocal.m4 +config.h.in +config.h +configure +hold.makefile +Makefile.in +config.log +config.status +libtool +stamp-h1 + +m4/libtool.m4 +m4/ltoptions.m4 +m4/ltsugar.m4 +m4/ltversion.m4 +m4/lt~obsolete.m4 diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..bf0f876 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,80 @@ +AM_CXXFLAGS = +AM_LDFLAGS = +AM_CPPFLAGS = +AM_CFLAGS = + +ACLOCAL_AMFLAGS = -I m4 # use custom macro's in ./m4 + +# if we are building and linking to a nested copy of libcm +if BUILD_LIBCM + SUBDIRS = src/libcm # causes recursive make into given sub-directories + AM_CPPFLAGS += -I$(srcdir)/src/libcm/src -I$(srcdir)/src/libcm/src/dsp -I$(srcdir)/src/libcm/src/vop -I$(srcdir)/src/libcm/src/app + AM_LDFLAGS += -Lsrc/libcm/src +endif + +# To Profile w/ gprof: +# 1) Modify configure: ./configure --disable-shared CFLAGS="-pg" +# 2) Run the program. ./foo +# 3) Run gprof /libtool --mode=execute gprof ./foo + + +# C compiler flags +# _GNU_SOURCE - turns on GNU specific extensions and gives correct prototype for double log2(double) +# -Wall turn on all warnings +# -Wno-multichar - turns off multi-character constant warnings from cmAudioFile.c + + + +AM_CPPFLAGS += -D _GNU_SOURCE -I.. +AM_CFLAGS += -Wno-multichar + + +# debug/release switches +if DEBUG + AM_CFLAGS += -g + AM_CXXFLAGS += -g +else + AM_CFLAGS += -O3 + AM_CXXFLAGS += -O3 +endif + +MYLIBS = -lpthread -lfftw3f -lfftw3 -lcm + +# Linux specific +if OS_LINUX + MYLIBS += -lsatlas -lasound +if OS_64 + AM_CFLAGS += -m64 + AM_LDFLAGS += -L/usr/lib64/atlas -L/usr/lib64 + MYLIBS += -lrt -lm +endif +endif + +if OS_OSX + AM_CPPFLAGS += -I/opt/local/include # Search macports directory for fftw headers + AM_LDFLAGS += -L/opt/local/lib # and libraries. + AM_LDFLAGS += -framework Cocoa -framework CoreAudio -framework CoreMIDI -framework Carbon -framework Accelerate +endif + +src_cmtools_cmtools_SOURCES = src/cmtools/cmtools.c +src_cmtools_cmtools_LDADD = $(MYLIBS) +bin_PROGRAMS = src/cmtools/cmtools + +src_cmtools_mas_SOURCES = src/cmtools/mas.c +src_cmtools_mas_LDADD = $(MYLIBS) +bin_PROGRAMS += src/cmtools/mas + +src_cmtools_audiodev_SOURCES = src/cmtools/audiodev.c +src_cmtools_audiodev_LDADD = $(MYLIBS) +bin_PROGRAMS += src/cmtools/audiodev + +# See: https://www.gnu.org/savannah-checkouts/gnu/automake/manual/html_node/Clean.html#Clean +# 'make distclean' sets the source tree back to it's pre-configure state +# 'distclean-local' is used by automake 'distclean' to perform customized local actions +# ${exec_prefix} is the install prefix given to 'configure' by the user. +# ${srcdir} is the directory of this Makefile and is set by autoconf. +distclean-local: + rm -rf ${exec_prefix}/src + rm -rf ${exec_prefix}/bin + rm -rf ${exec_prefix}/include + rm -rf ${exec_prefix}/lib diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd47f9f --- /dev/null +++ b/README.md @@ -0,0 +1,452 @@ +Prerequisites +============= + +fftw fftw-devel atlas atlas-devel alsa-lib alsa-lib-devel fltk fltk-devel + + + +In the 'template generation' mode the program generates a text file that contains information +of interest from the MusicXML file. We refer to the text file as the as the 'decoration' file +because it allows the score to be further decorated by adding additional information to the score. + +In the 'escore generation' mode the program outputs a data file in CSV format which contains +the score in a format which is conveniently readable by a score matching program. The program +also generates a MIDI file which allows the clarified score to be rendered with a sampler. +An additional SVG (scalable vector graphics) file is generated which shows the score in +piano roll form along with any problems that the program may have had during the conversion process. + + + +``` +cmtool --score_gen -x -d {-c } {-m } {-s } {-r report} {-b begMeasNumb} {t begTempoBPM} + + +Enumerated group: Action selector + + -S --score_gen + Run the score generation tool. + + -T --timeline_gen + Run the time line generation tool. + + -M --meas + Generate perfomance measurements. + +-x --xml_fn + Name of the input MusicXML file. + +-d --dec_fn + Name of a score decoration file. + +-c --csv_fn + Name of a CSV score file. + +-p --pgm_rsrc_fn + Name of program resource file. + +-m --midi_out_fn + Name of a MIDI file to generate as output. + +-i --midi_in_fn + Name of a MIDI file to generate as output. + +-s --svg_fn + Name of a HTML/SVG file to generate as output. + +-t --timeline_fn + Name of a timeline to generate as output. + +-r --report_fn + Name of a status file to generate as output. + +-f --debug_fl (required) + Print a report of the score following processing. + +-b --beg_meas + The first measure the to be written to the output CSV, MIDI and SVG files. + +-e --beg_bpm + Set to 0 to use the tempo from the score otherwise set to use the tempo at begMeasNumb. + +-n --svg_stand_alone_fl + Write the SVG output with an HTML wrapper. + +-z --svg_pan_zoom_fl + Include the SVG pan-zoom control (--svg_stand_alone must also be enabled) + +-h --help + Print this usage information. + +-v --version + Print version information. + +``` + + +Score Following and Timeline Marker Generator +============================================== + +Perform score following based tasks. +Generate the time line marker information used by the performance program resource file. + +``` +cmtool --score_follow -c -i -r -s -m -t +``` + +If `` is given then the a copy of `` will be created with +note velocities and sostenuto pedal events from the score. + + + + +Score match report output snippet: + +``` + Score Score Score MIDI MIDI MIDI + Bar UUID Pitch UUID Ptch Vel. +- ----- ----- ----- ----- ---- ---- +m 1 3 B3 19 B3 127 +m 1 4 D#4 20 D#4 127 +m 1 5 G#5 21 G#5 127 +m 1 14 E4 25 E4 29 +m 1 15 G#3 27 G#3 36 +m 1 16 E4 29 E4 43 +m 1 17 G#3 31 G#3 50 +``` + +TODO: Show errors +TODO: Show SVG output + + +Performance Measurement Generators +================================== + +Given a performance program resource file generate performance measurements where the internal MIDI file +is used as a substitute for a real player. + + +``` +cmtool --meas_gen -p -r +``` + + +Example ``: + +``` +{ + timeLineFn: "kc/data/round2.js" + tlPrefixPath: "projects/imag_themes/scores/gen" + scoreFn: "score.csv" + + // pppp ppp pp p mp mf f ff fff + dynRef: [ 14, 28, 42, 56, 71, 85, 99, 113,128 ] + +} +``` + +Example call with output file snippet: + +``` +cmtool --meas_gen -g ~/src/cmtools/examples/perf_meas/pgm_rsrc_round2.js -r ~/src/cmtools/examples/perf_meas/perf_meas_out.js + +{ + meas : + [ + [ "sec" "typeLabel" "val" "cost" "loc" "evt" "seq" "mark" "typeId" ] + [ "6002" "tempo" 34.730932 0.000000 59 76 0 "1" 3 ] + [ "6002" "dyn" 0.952381 0.000000 59 76 0 "1" 2 ] + [ "6002" "even" 0.600000 0.000000 59 76 0 "1" 1 ] + [ "6002" "dyn" 1.000000 0.142857 59 76 0 "2" 2 ] + ] +} + + ``` + +Column Descriptions: + +Column | Description +----------|----------------------------------------------------- +sec | Score section to which this measurement will be applied. +typeLabel | Measurement type +value | Measurement value +cost | Score follower error value (0= perfect match) +loc | Score location index where this measurement will be applied +evt | Score event index where this measurement will be applied +seq | Sequence id. +mark | Time line marker label from which the measurements were made +typeId | Measurement type id (numeric id associated with typeLabel) + +Note that the event indexes and score locations are best +seen in the score report (as generated by `cmtool --score_report`) +NOT by directly referencing the score CSV file. + + +Score Report +============ + +Generate a human readable score report from a score CSV file. + +``` +cmtool --score_report -c -r " + +``` + +Example report file snippet: + +``` +evnt CSV bar +index line loctn bar idx type pitch ETD Dynamic +----- ----- ----- --- --- ----- ----- --- ------- + 0 2 1 bar + 1 3 0 1 0 non B3 section:6001 + 2 4 0 1 1 non D#4 + 3 5 0 1 2 non G#5 + 4 6 1 1 3 ped dn + 5 7 2 1 3 ped dn + 6 8 3 1 3 non E4 + 7 9 4 1 4 non G#3 td ppp + 8 10 5 1 5 non E4 td pp + 9 11 6 1 6 non G#3 td pp + 10 12 7 1 7 non C#2 td p + 11 13 8 1 8 non C4 td mp + 12 14 9 1 9 non G#3 td mp + 13 15 10 1 10 non C#2 td mf + 14 16 11 1 11 non A#2 td f + 15 17 12 1 12 non C4 td f + 16 18 13 1 13 non A#2 td f + 17 19 14 1 14 non C#1 + + +``` + +MIDI File Reports +================= + +Generate a MIDI file report and optional SVG piano roll image." + + cmtool --midi_report -i -r {-s {--svg_stand_alone_fl} {--svg_pan_zoom_fl} } + + +Timeline Report +================= + +Generate human readable report from a time line setup file. + + cmtool --timeline_report -t -l -r + +tlPrefix is the folder where data files for this timeline are stored. + + +Score Follow Report +=================== + +``` +cmtool --score_follow -c round2.csv -i new_round2.mid -r report.txt -s report_svg.html + ``` + +SVG Description +--------------- + +- Red borders around score events that were not matched. +- Red borders around MIDI events that did not match. +- Line is drawn to MIDI events that matched to multiple score events. The lines +are drawn to all score events after the first match. + + +Audio Device Test +================= + +Real-time audio port test. + +This test also excercises the real-time audio buffer which implements most of the global audio control functions. + +``` + + +``` + +``` +-s --srate Audio system sample rate. +-z --hz Tone frequency in Hertz. +-x --ch_index Index of first channel index. +-c --ch_cnt Count of audio channels. +-b --buf_cnt Count of audio buffers. (e.g. 2=double buffering, 3=triple buffering) +-f --frames_per_buf Count of audio channels. +-i --in_dev_index Input device index as taken from the audio device report. +-o --out_dev_index Output device index as taken from the audio device report. +-r --report_flag Print a report of the score following processing. +-h --help Print this usage information. +-v --version Print version information. +-p --parms Print the arguments. +``` + +MIDI Audio Sync (MAS) +===================== + +1) Synchronize Audio to MIDI based on onset patterns: + + a. Convert MIDI to audio impulse files: + + mas -m -i -o -s + + Notes: + + * If is given then use all files in the directory as input otherwise convert a single file. + + * The files written to are audio files with impulses written at the location of note on msg's. The amplitude of the the impulse is velocity/127. + + b. Convert the onsets in audio file(s) to audio impulse + file(s). + + mas -a -i -o + -w -f -u -r + -x -t -z -e + + 1) If is given then use all files + in the directory as input otherwise convert a + single file. + 2) The onset detector uses a spectral flux based + algorithm. + See cmOnset.h/.c for an explanation of the + onset detection parameters. + + + c) Convolve impulse files created in a) and b) with a + Hann window to widen the impulse width. + + mas -c -i -o -w + + 1) If is given then use all files + in the directory as input otherwise convert a + single file. + 2) gives the width of the Hann window. + + d) Synchronize MIDI and Audio based convolved impulse + files based on their onset patterns. + + mas -y -i -o + + 1) The file has the following format: + { + ref_dir : "/home/kevin/temp/mas/midi_conv" // location of ref files + key_dir : "/home/kevin/temp/mas/onset_conv" // location of key files + hop_ms : 25 // sliding window increment + + sync_array : + [ + // ref_fn wnd_beg_secs wnd_dur_secs key_fn key_beg_secs, key_end_secs + [ "1.aif", 678, 113, "Piano 3_01.aif", 239.0, 417.0], + [ "3.aif", 524, 61, "Piano 3_06.aif", 556.0, 619.0], + ] + } + + Notes: + a. The 'window' is the section of the reference file which is compared + to the key file search area to by sliding it + in increments of 'hop_ms' samples. + + b. Set 'key_end_secs' to 0 to search to the end of the file. + + c. When one key file matches to multiple reference files the + key files sync recd should be listed consecutively. This way + the earlier searches can stop when they reach the beginning + of the next sync records search region. See sync_files(). + + Note that by setting to a non-zero value + as occurs in the multi-key-file case has a subtle effect of + changing the master-slave relationship between the reference + an key file. + + In general the reference file is the master and the key file + is the slave. When a non-zero is given however + this relationship reverses. See masCreateTimeLine() for + how this is used to assign file group id's during the + time line creation. + + 3) The has the following form. +``` + { + "sync" : + { + "refDir" : "/home/kevin/temp/mas/midi_conv" + "keyDir" : "/home/kevin/temp/mas/onset_conv" + "hopMs" : 25.000000 + + "array" : + [ + + // + // sync results for "1.aif" to "Piano 3_01.aif" + // + + { + // The following block of fields were copied from . + "refFn" : "1.aif" + "refWndBegSecs" : 678.000000 + "refWndSecs" : 113.000000 + "keyFn" : "Piano 3_01.aif" + "keyBegSecs" : 239.000000 + "keyEndSecs" : 417.000000 + + // Sync. location of the 'window' in the key file. + // Sample index into the key file which matches to the first sample + // in the reference window. + "keySyncIdx" : 25768800 // Offset into the key file of the best match. + + "syncDist" : 4184.826108 // Match distance score for the sync location. + "refSmpCnt" : 200112000 // Count of samples in the reference file. + "keySmpCnt" : 161884800 // Count of samples in the key file. + "srate" : 96000.000000 // Sample rate of the reference and key file. + }, + ] + } + } +``` +2) Create a time line from the results of a synchronization. A time line is a data structure + (See cmTimeLine.h/.c) which maintains a time based ordering of Audio files, MIDI files, + and arbitrary markers. + + mas -g -i -o -R -K -M -A + +| The output file produced as a result of a previous MIDI <-> Audio synchronization. +| +| Location of the reference files (MIDI) used for the synchronization. +| File extension used by the reference files. +| Locate of the key files (Audio) used for the synchronization. +| File extension used by the key files. + + a. The time line 'trackId' assigned to each time line object is based on the files + 'groupId'. A common group id is given to sets of files which are + locked in time relative to one another. For example + if file B and C are synced to master file A and + file D is synced to file E which is synced to master + file F. Then files A,B,C will be given one group + id and files D,E and F will be given another group id. + (See masCreateTimeLine()). + + b. The time line object 'offset' values gives the offset in samples where the object + begins relative to other objects in the group. Note that the master object in the + group may not begin at offset 0 if there are slave objects which start before it. + + + + +TODO: +===== + +* replace round2.csv time with the times in the full fragment MIDI file + +* update timeline lite to allow for a synchronized audio file + +* change the build setup to default to getting libcm from the system +and allow an option to build it from src/libcm + +* for all svg output create a standalone flag that wraps the output in HTML +and another option to load pan-zoom. + + +* MIDI report output example and description + +* Timeline report output example and description + + diff --git a/build/clean.sh b/build/clean.sh new file mode 100755 index 0000000..a9eceb8 --- /dev/null +++ b/build/clean.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# +# Run 'make distclean' to clean many of the temporary make files. +# then use this script run from cm/build to clean the remaining files +# + + + +function clean_dir { + + make -C $1 uninstall + make -C $1 distclean + + rm -f $1/bin/kc.app/Contents/MacOS/kc + + +} + + + +clean_dir linux/debug +clean_dir linux/release +clean_dir osx/debug +clean_dir osx/release + +rm -rf osx/debug/a.out.dSYM + +# delete everything created by 'autoreconf'. +rm -rf ../build-aux +rm -rf ../autom4te.cache +rm -f ../config.h.in ../config.h.in~ ../configure ../libtool.m4 +rm -f ../Makefile.in ../aclocal.m4 +rm -f ../m4/libtool.m4 ../m4/ltoptions.m4 ../m4/ltsugar.m4 ../m4/ltversion.m4 ../m4/lt~obsolete.m4 + + + + diff --git a/build/linux/debug/build.sh b/build/linux/debug/build.sh new file mode 100755 index 0000000..2bd6942 --- /dev/null +++ b/build/linux/debug/build.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +curdir=`pwd` + +cd ../../.. +autoreconf --force --install + +cd ${curdir} + +# To Profile w/ gprof: +# 1) Modify configure: ./configure --disable-shared CFLAGS="-pg" +# 2) Run the program. ./foo +# 3) Run gprof /libtool --mode=execute gprof ./foo + +../../../configure --prefix=${curdir} \ + --enable-debug --enable-build_libcm \ + CFLAGS="-g -Wall" \ + CXXFLAGS="-g -Wall" \ + LIBS= + +# CPPFLAGS="-I/home/kevin/src/libcm/build/linux/debug/include " \ +# LDFLAGS="-L/home/kevin/src/libcm/build/linux/debug/lib" \ + +#make +#make install diff --git a/build/linux/release/build.sh b/build/linux/release/build.sh new file mode 100755 index 0000000..768adea --- /dev/null +++ b/build/linux/release/build.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +curdir=`pwd` + +cd ../../.. +autoreconf --force --install + +cd ${curdir} + +# To Profile w/ gprof: +# 1) Modify configure: ./configure --disable-shared CFLAGS="-pg" +# 2) Run the program. ./foo +# 3) Run gprof /libtool --mode=execute gprof ./foo + +../../../configure --prefix=${curdir} \ +CFLAGS="-Wall" \ +CXXFLAGS="-Wall" \ +CPPFLAGS= \ +LDFLAGS= \ +LIBS= + + +#make +#make install diff --git a/build/osx/debug/build.sh b/build/osx/debug/build.sh new file mode 100755 index 0000000..d53c3ad --- /dev/null +++ b/build/osx/debug/build.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +curdir=`pwd` + +cd ../../.. +autoreconf --force --install + +cd ${curdir} + +../../../configure --prefix=${curdir} \ +--enable-debug \ +CFLAGS="-g -Wall" \ +CXXFLAGS="-g -Wall" \ +CPPFLAGS= \ +LDFLAGS= \ +LIBS= + + +#make +#make install \ No newline at end of file diff --git a/build/osx/release/build.sh b/build/osx/release/build.sh new file mode 100755 index 0000000..d1e4027 --- /dev/null +++ b/build/osx/release/build.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +curdir=`pwd` + +cd ../../.. +autoreconf --force --install + +cd ${curdir} + +../../../configure --prefix=${curdir} \ +CFLAGS="-Wall" \ +CXXFLAGS="-Wall" \ +CPPFLAGS= \ +LDFLAGS= \ +LIBS= + + +#make +#make install \ No newline at end of file diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..b205997 --- /dev/null +++ b/configure.ac @@ -0,0 +1,152 @@ +# +# Use "autoreconf --force --install" to update depedent files after changing +# this configure.ac or any of the Makefile.am files. +# + +AC_INIT([cmtools],[1.0],[kl@currawongproject.org]) +AC_CONFIG_AUX_DIR([build-aux]) # put aux files in build-aux +AM_INIT_AUTOMAKE([1.9 -Wall foreign subdir-objects]) # subdir-objects needed for non-recursive make +AC_CONFIG_SRCDIR([src/cmtools/cmtools.c]) +AC_CONFIG_HEADERS([config.h]) +AC_CONFIG_MACRO_DIR([m4]) + +AM_PROG_AR + +LT_INIT + +# Check for programs +AC_PROG_CC +AC_PROG_CXX +# AC_PROG_RANLIB # required for static librarires + +AM_PROG_CC_C_O + +# Checks for libraries. +# AC_CHECK_LIB([cairo],[cairo_debug_reset_static_data],[AC_MSG_RESULT([The 'cairo' library was found.])],[AC_MSG_ERROR([The 'cairo' library was not found.])]) +#TODO: add more library checks + +# Checks for header files. +AC_CHECK_HEADERS([arpa/inet.h fcntl.h float.h limits.h mach/mach.h netinet/in.h stdlib.h string.h sys/ioctl.h sys/socket.h sys/time.h termios.h unistd.h]) + +# Checks for typedefs, structures, and compiler characteristics. +AC_HEADER_STDBOOL +AC_C_INLINE +AC_TYPE_OFF_T +AC_TYPE_SSIZE_T +AC_TYPE_UINT64_T + +# Checks for library functions. +AC_FUNC_ERROR_AT_LINE +AC_FUNC_FORK +AC_FUNC_FSEEKO +AC_FUNC_MALLOC +AC_FUNC_REALLOC +AC_FUNC_STRTOD +AC_CHECK_FUNCS([clock_gettime floor memmove memset mkdir pow rint select socket sqrt strcasecmp strchr strcspn strerror strspn strstr strtol]) + + +# The following is a custom macro in ./m4/os_type.m4 +# be sure to also set "ACLOCAL_AMFLAGS = -I m4" in ./Makefile.am +# Defines the config.h variable OS_LINUX or OS_OSX +AX_FUNC_OS_TYPE + +AX_FUNC_OS_64 + +# ac_cv_os_type is set by AX_FUNC_OS_TYPE +AM_CONDITIONAL([OS_LINUX],[test x"${ax_cv_os_type}" = xLinux]) +AM_CONDITIONAL([OS_OSX],[test x"${ax_cv_os_type}" = xDarwin]) +echo "OS='${ax_cv_os_type}'" + +AM_CONDITIONAL([OS_64],[test x"${ax_cv_os_64}" == xx86_64]) +echo "ptr width='${ax_cv_os_64}'" + +# check if a request has been made to build libcm +AC_ARG_ENABLE([build_libcm], + [ --enable-build_libcm libcm is included in the local source tree], + [case "${enableval}" in + yes) build_libcm=true ;; + no) build_libcm=false ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-build_libcm]) ;; + esac],[build_libcm=false]) + +echo "build_libcm=${build_libcm}" + +# check if a nested copy of libcm exists in /src/libcm +AC_CHECK_FILE([${srcdir}/src/libcm/src/cmGlobal.h],[local_libcm=true],[local_libcm=false]) +echo "local_libcm=${local_libcm}" + +# set BUILD_LIBCM if a libcm build request was set and a nested copy of libcm exists +AM_CONDITIONAL([BUILD_LIBCM], [test x$build_libcm = xtrue -a x$local_libcm = xtrue ]) + +AC_ARG_ENABLE([debug], + [ --enable-debug Turn on debugging], + [case "${enableval}" in + yes) debug=true ;; + no) debug=false ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-debug]) ;; + esac],[debug=false]) + +echo "debug=${debug}" + +AM_CONDITIONAL([DEBUG], [test x$debug = xtrue]) + +if test x$debug = xfalse; then +AC_DEFINE([NDEBUG], 1,[Debugging off.]) +fi + +AC_ARG_ENABLE([vectop], + [ --enable-vectop Turn on use of Lapack and Atlas vector/matrix operations. ], + [case "${enableval}" in + yes) vectop=true ;; + no) vectop=false ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-vectop]) ;; + esac],[vectop=true]) + +echo "vectop=${vectop}" + +# if --enable-vectop then #define CM_VECTOP = 1 in config.h otherwise CM_VECTOP is undefined. +if test x"$vectop" = xtrue; then +AC_DEFINE([CM_VECTOP], 1,[Use Lapack and Atlas.]) +fi + + +AC_ARG_ENABLE([memalign], + [ --enable-memalign Turn on memory alignment on dynamic memory allocations. ], + [case "${enableval}" in + yes) memalign=true ;; + no) memalign=false ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-memalign]) ;; + esac],[memalign=true]) + +echo "memalign=${memalign}" + +# if --enable-vectop then #define CM_MEMALIGN = 1 in config.h otherwise CM_MEMALIGN is undefined. +if test x"$memalign" = xtrue; then +AC_DEFINE([CM_MEMALIGN], 1,[Turn on dynamic memory alignment.]) +fi + +AC_ARG_ENABLE([sonicart], + [ --enable-sonicart Enable use of Sonic Arts proprietary code. ], + [case "${enableval}" in + yes) sonicart=true ;; + no) sonicart=false ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-sonicart]) ;; + esac],[sonicart=false]) + +echo "sonicart=${sonicart}" + +# if --enable-sonicart then #define CM_SONICART = 1 in config.h otherwise CM_SONICART is undefined. +if test x"$sonicart" = xtrue; then +AC_DEFINE([CM_SONICART], 1,[Include Sonic Arts proprietry code.]) +fi + +AM_CONDITIONAL([INC_SONICART], [test x$sonicart = xtrue]) + +AC_CONFIG_FILES([ Makefile ]) + +# if local nested libcm then do recursive configure into subdirs +if test x$build_libcm = xtrue -a x$local_libcm = xtrue; then +AC_CONFIG_SUBDIRS([src/libcm]) +fi + +AC_OUTPUT diff --git a/doc/score_follow_0.png b/doc/score_follow_0.png new file mode 100644 index 0000000000000000000000000000000000000000..b748ea22313244b362831ba0a278492d5ac8fcc2 GIT binary patch literal 64164 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 za�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 z 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 Nu4Q<>=qHD3{{tM=@R9%k literal 0 HcmV?d00001 diff --git a/doc/xscore_gen.md b/doc/xscore_gen.md new file mode 100644 index 0000000..2032cc1 --- /dev/null +++ b/doc/xscore_gen.md @@ -0,0 +1,254 @@ +xscore_gen +========== + +*xscore_gen* parses MusicXML score files and generates a text file +which allows the score to be clarified and additional information to +be added. This is the first step in creating a 'machine readable +score' based on the 'human readable score'. + +This step is necessary because we need a way to efficiently +append additional information to the score which cannot be entered +directly by the score editing program (e.g. Sibelius, Dorico, Finale). +In practice we use Sibelius 6 as our primary score editor. + +Likewise there are certain limitations to the generated MusicXML +which need to be worked around. The primary problem being that +dynamic markings are not tied to specific notes. This is important +for purposes of score analysis as well as audio rendering. + +The overall approach to adding this addtional information +is as follows: + +1. Add as much auxilliary information as possible from within Sibelius. +This entails using colored note heads, and carefully placed +text strings. + +2. Generate the MusicXML file using the [Dolet 6 Sibelius +plug-in](https://www.musicxml.com/). The resulting MusicXML file is +run through *xscore_gen* and parsed to find any invalid structures +such as damper up events not preceeded by damper down events, or tied +notes with no ending note. These problems are cleared by careful +re-editing of the score within Sibelius until all the problematic +structures are fixed. + +3. As a side effect of step 2 a template 'decoration' file is generated. +This text file has all the relavant 'machine score' information +from the XML score as a time tagged list. In this step *decoration* information is manually +added by entering codes at the end of each line. The codes +are cryptic but they are also succinct and allow for relatively +painless editing. + +4. Once the addition information is entered *xscore_gen* is +run again to generate three output files: + +- machine score as a CSV file +- MIDI file suitable for audio rendering +- SVG piano roll file + +As with step 2 this step may need to be iterated several times +to clear syntactic errors in the decoration data. + +5. Generate the time line marker information to be used with the performance program: + +Generate the time line marker information, into `temp/time_line_temp.txt` like this: + +`cmtest -F` + +This calls `cmMidiScoreFollowMain()` in app\cmMidiScoreFollow.c. + +Then paste `temp\time_line_temp.txt` into kc/src/kc/data/round1.js. + + + +Preparing the score +------------------- + +Note color is used to assign notes to groups. +These groups may later be used to indicate +certain processes which will be performed +on these notes during performance. + +There are currently three defined groups +'even','dynamics' and 'tempo'. + + +### Score Coloring Chart: + +Description Number Color +------------------- -------- ------------------------- +Even #0000FF blue +Tempo #00FF00 green +Dyn #FF0000 red +Tempo + Even #00FFFF green + blue (turquoise) +Dyn + Even #FF00FF red + blue +Dyn + Tempo #FF7F00 red + green (brown) +Tempo + Even + Dyn #996633 purple + +Decrement color by one (i.e. 0xFE) to indicate the last note in a group +of measured notes. Note that a decremented color stops all active measures +not just the measurement associated with the decremented color. + + +Preparing the Music XML File +---------------------------- + +*xscore_gen* is know to work with the MusicXML files produced by +the [Dolet 6 Sibelius plug-in] + +After generating the file it is necessary to do some +minor pre-processing before submitting it to *xscore_gen* + +iconv -f UTF-16 -t UTF-8 -o score-utf16.xml score-utf8.xml + + +Create the decoration file +-------------------------- + +``` +cmtools --score_gen -x myscore.xml -d mydec.txt +``` + +Here's a snippet of a typical 'decoration' file. + +``` +Part:P1 + 1 : div:768 beat:4 beat-type:4 (3072) + idx voc loc tick durtn rval flags + --- --- ----- ------- ----- ---- --- --------------- + 0 0 2 0 0 0.0 |-------------- + 1 0 0 0 54 4.0 --------------- 54 bpm + 2 1 0 0 3072 1.0 -R------------- + 3 5 0 0 2304 2.0 -R-.----------- + 4 0 0 996 0 0.0 --------V------ + 5 0 0 1920 0 0.0 --------^------ + 6 5 0 2304 341 8.0 -R------------- + 7 0 0 2643 0 0.0 --------V------ + 8 5 0 2645 85 32.0 -R------------- + 9 5 3 2730 85 32.0 F 5 --------------* + 10 5 4 2815 85 32.0 Ab2 --------------* + 11 5 5 2900 85 32.0 C 3 --------------* + 12 5 6 2985 87 32.0 F 6 --------------* + 13 1 0 3072 768 4.0 -R------------- + 14 5 0 3072 768 4.0 -R------------- 3840 +``` + +### Decoration file format + +Column | Description +-------|----------------------------- +idx | event index +voc | voice index +tick | MIDI tick +durtn | duration in MIDI ticks +rval | rythmic value +pitch | scientific pitch +flags | event attributes + +### Event attribute flags: + +Event attribute symbols used in the decoration file: + +Desc | Flag | +----------|------|----------------------------------------- +Bar | | | Beginning of a measure +Rest | R | Rest event +Grace | G | Grace note event +Dot | . | note is dotted +Chord | C | note is part of a chord +Even | e | note is part of an 'even' group +Dyn | d | note is part of a 'dynamics' group +Tempo | t | note is part of a 'tempo' group +DampDn | V | damper down event +DampUp | ^ | damper up event +DampUpDn | X | damper up/down event +SostDn | { | sostenuto down event +Section | S | section boundary +SostUp | } | sostenuto up event +Heel | H | heel event +Tie Begin | T | begin of a tied note +Tie End | _ | end of a tied note +Onset | * | note onset + + + +Decoration Sytax: +------------------ + +! Assign dynamics +!() - less uncertain dynamic mark +! - begin of dynamic fork (See note below regarding dynamic forks) +!! - end of dynamic fork +~ Insert or remove event (See pedal marks below.) +@ Move event to a new time position +% Flag note as a grace note +%% -last note in grace note sequence +$ Assign a note a new pitch + + + b (begin grace) + a (add grace and end grace) + s (subtract grace and end grace) + g (grace note) + A (after first) + N (soon after first) + +Note: The first non-grace note in a grace note sequence is marked with a %b. +The last non-grace note in a grace note sequence is marked with a %s or %a. + +Where: %s = steal time from the note marked with %b. + %a = insert time prior to the note marked with %a. + + The last (by row number) note (grace or non-grace) in the the sequence +is marked with %%# where # is replaced with a,b,s,or g. + +It is only necessary to mark the tick number of grace notes in order +to give the time sequence of the notes. A single grace note therefore does +not require an explict tick mark notation (i.e. @####) + + +Insert/delete Event Marks: +----------------------------------- + d (sostenuto down - just after note onset) + u ( " up - just before this event) + x ( " up just before this event and down just after it) + D (damper pedal down - after this event) + U (damper pedal up - before this event) + _ (set tie end flag) + & (skip this event) + +Dynamic Marks: +-------------------------- + s (silent note) + pppp- + pppp + pppp+ + ppp- + ppp + ppp+ + pp- + pp + pp+ + p- + p + p+ + mp- + mp + mp+ + mf- + mf + mf+ + f- + f + f+ + ff + ff+ + fff + +Note: Dynamic Forks: +-------------------- +Use upper case dynamic letters to indicate forks in the dynamics +which should be filled automatically. Note that all notes +in the voice assigned to the first note in the fork will be +included in the dynamic change. To exclude a note from the +fork assign it a lower case mark. + diff --git a/m4/os_64.m4 b/m4/os_64.m4 new file mode 100644 index 0000000..f84a4a1 --- /dev/null +++ b/m4/os_64.m4 @@ -0,0 +1,8 @@ +AC_DEFUN([AX_FUNC_OS_64], +[AC_CACHE_CHECK([operating system address width], +[ax_cv_os_64], +[ax_cv_os_64=`uname -m`]) +if test x"$ax_cv_os_64" = xx86_64; then +AC_DEFINE([OS_64], 1,[Operating system is 64 bits.]) +fi +]) # AX_FUNC_OS_TYPE diff --git a/m4/os_type.m4 b/m4/os_type.m4 new file mode 100644 index 0000000..9b2b86e --- /dev/null +++ b/m4/os_type.m4 @@ -0,0 +1,11 @@ +AC_DEFUN([AX_FUNC_OS_TYPE], +[AC_CACHE_CHECK([operating system type], +[ax_cv_os_type], +[ax_cv_os_type=`uname`]) +if test x"$ax_cv_os_type" = xLinux; then +AC_DEFINE([OS_LINUX], 1,[Operating system is Linux.]) +fi +if test x"$ax_cv_os_type" = xDarwin; then +AC_DEFINE([OS_OSX], 1,[Operating system is Darwin.]) +fi]) # AX_FUNC_OS_TYPE + diff --git a/src/cmtools/audiodev.c b/src/cmtools/audiodev.c new file mode 100644 index 0000000..f7b8325 --- /dev/null +++ b/src/cmtools/audiodev.c @@ -0,0 +1,397 @@ +#include "cmPrefix.h" +#include "cmGlobal.h" +#include "cmRpt.h" +#include "cmErr.h" +#include "cmCtx.h" +#include "cmMem.h" +#include "cmMallocDebug.h" +#include "cmLinkedHeap.h" +#include "cmFileSys.h" +#include "cmText.h" + +#include "cmPgmOpts.h" + +#include "cmTime.h" +#include "cmAudioPort.h" +#include "cmApBuf.h" // only needed for cmApBufTest(). +#include "cmAudioPortFile.h" +#include "cmAudioAggDev.h" +#include "cmAudioNrtDev.h" + +#include "cmFloatTypes.h" +#include "cmAudioFile.h" +#include "cmFile.h" + +enum +{ + kOkAdRC = cmOkRC, + kAudioPortFailAdRC, + kAudioPortFileFailAdRC, + kAudioPortNrtFailAdRC, + kAudioBufFailAdRC +}; + +const cmChar_t* poBegHelpStr = + "audiodev Test the real-time audio ports" + "\n" + "audiodev -i -o \n" + "\n" + "All arguments are optional. The default input and output device index is 0.\n" + "\n"; + +const cmChar_t* poEndHelpStr = ""; + +/// [cmAudioPortExample] + +// See cmApPortTest() below for the main point of entry. + +// Data structure used to hold the parameters for cpApPortTest() +// and the user defined data record passed to the host from the +// audio port callback functions. +typedef struct +{ + unsigned bufCnt; // 2=double buffering 3=triple buffering + unsigned chIdx; // first test channel + unsigned chCnt; // count of channels to test + unsigned framesPerCycle; // DSP frames per cycle + unsigned bufFrmCnt; // count of DSP frames used by the audio buffer (bufCnt * framesPerCycle) + unsigned bufSmpCnt; // count of samples used by the audio buffer (chCnt * bufFrmCnt) + unsigned inDevIdx; // input device index + unsigned outDevIdx; // output device index + double srate; // audio sample rate + unsigned meterMs; // audio meter buffer length + + // param's and state for cmApSynthSine() + unsigned phase; // sine synth phase + double frqHz; // sine synth frequency in Hz + + // buffer and state for cmApCopyIn/Out() + cmApSample_t* buf; // buf[bufSmpCnt] - circular interleaved audio buffer + unsigned bufInIdx; // next input buffer index + unsigned bufOutIdx; // next output buffer index + unsigned bufFullCnt; // count of full samples + + // debugging log data arrays + unsigned logCnt; // count of elements in log[] and ilong[] + char* log; // log[logCnt] + unsigned* ilog; // ilog[logCnt] + unsigned logIdx; // current log index + + unsigned cbCnt; // count the callback +} cmApPortTestRecd; + +unsigned _cmGlobalInDevIdx = 0; +unsigned _cmGlobalOutDevIdx = 0; +unsigned _cmGlobalCbCnt = 0; + +#define aSrate 48000 +#define aFrmN aSrate*10 +#define aChN 2 +#define abufN aFrmN*aChN + +cmApSample_t abuf[ abufN ]; +unsigned abufi = 0; + +void _abuf_copy_in( cmApAudioPacket_t* pktArray, unsigned pktN ) +{ + unsigned i,j,k; + for(i=0; irpt ); +} + +void _abuf_write_csv_file(cmCtx_t* ctx ) +{ + cmFileH_t fH = cmFileNullHandle; + unsigned i = 0,j; + cmFileOpen( &fH, "/home/kevin/temp/temp.csv", kWriteFileFl, &ctx->rpt ); + + for(i=0; irpt; + + cmApSample_t buf[r->bufSmpCnt]; + char log[r->logCnt]; + unsigned ilog[r->logCnt]; + + r->buf = buf; + r->log = log; + r->ilog = ilog; + r->cbCnt = 0; + + _cmGlobalInDevIdx = r->inDevIdx; + _cmGlobalOutDevIdx= r->outDevIdx; + + cmRptPrintf(rpt,"%s in:%i out:%i chidx:%i chs:%i bufs=%i frm=%i rate=%f\n",runFl?"exec":"rpt",r->inDevIdx,r->outDevIdx,r->chIdx,r->chCnt,r->bufCnt,r->framesPerCycle,r->srate); + + if( cmApFileAllocate(rpt) != kOkApRC ) + { + rc = cmErrMsg(&ctx->err, kAudioPortFileFailAdRC,"Audio port file allocation failed."); + goto errLabel; + } + + // allocate the non-real-time port + if( cmApNrtAllocate(rpt) != kOkApRC ) + { + rc = cmErrMsg(&ctx->err, kAudioPortNrtFailAdRC,"Non-real-time system allocation failed."); + goto errLabel; + } + + // initialize the audio device interface + if( cmApInitialize(rpt) != kOkApRC ) + { + rc = cmErrMsg(&ctx->err, kAudioPortFailAdRC,"Port initialize failed.\n"); + goto errLabel; + } + + // report the current audio device configuration + for(i=0; imeterMs ); + + // setup the buffer for the output device + cmApBufSetup( r->outDevIdx, r->srate, r->framesPerCycle, r->bufCnt, cmApDeviceChannelCount(r->outDevIdx,true), r->framesPerCycle, cmApDeviceChannelCount(r->outDevIdx,false), r->framesPerCycle, srateMult ); + + // setup the buffer for the input device + if( r->inDevIdx != r->outDevIdx ) + cmApBufSetup( r->inDevIdx, r->srate, r->framesPerCycle, r->bufCnt, cmApDeviceChannelCount(r->inDevIdx,true), r->framesPerCycle, cmApDeviceChannelCount(r->inDevIdx,false), r->framesPerCycle, srateMult ); + + + // setup an output device + if(cmApDeviceSetup(r->outDevIdx,r->srate,r->framesPerCycle,_cmApPortCb2,&r) != kOkApRC ) + rc = cmErrMsg(&ctx->err,kAudioPortFailAdRC,"Out audio device setup failed.\n"); + else + // setup an input device + if( cmApDeviceSetup(r->inDevIdx,r->srate,r->framesPerCycle,_cmApPortCb2,&r) != kOkApRC ) + rc = cmErrMsg(&ctx->err,kAudioPortFailAdRC,"In audio device setup failed.\n"); + else + // start the input device + if( cmApDeviceStart(r->inDevIdx) != kOkApRC ) + rc = cmErrMsg(&ctx->err,kAudioPortFailAdRC,"In audio device start failed.\n"); + else + // start the output device + if( cmApDeviceStart(r->outDevIdx) != kOkApRC ) + rc = cmErrMsg(&ctx->err, kAudioPortFailAdRC,"Out audio device start failed.\n"); + else + cmRptPrintf(rpt,"Started..."); + + cmApBufEnableChannel(r->inDevIdx, -1, kEnableApFl | kInApFl ); + cmApBufEnableMeter( r->inDevIdx, -1, kEnableApFl | kInApFl ); + + cmApBufEnableChannel(r->outDevIdx, -1, kEnableApFl | kOutApFl ); + cmApBufEnableMeter( r->outDevIdx, -1, kEnableApFl | kOutApFl ); + + cmRptPrintf(rpt,"q=quit O/o=output tone, I/i=input tone P/p=pass s=buf report\n"); + char c; + while((c=getchar()) != 'q') + { + //cmApAlsaDeviceRtReport(rpt,r->outDevIdx); + + switch(c) + { + case 'i': + case 'I': + cmApBufEnableTone(r->inDevIdx,-1,kInApFl | (c=='I'?kEnableApFl:0)); + break; + + case 'o': + case 'O': + cmApBufEnableTone(r->outDevIdx,-1,kOutApFl | (c=='O'?kEnableApFl:0)); + break; + + case 'p': + case 'P': + cmApBufEnablePass(r->outDevIdx,-1,kOutApFl | (c=='P'?kEnableApFl:0)); + break; + + case 's': + cmApBufReport(rpt); + cmRptPrintf(rpt,"CB:%i\n",_cmGlobalCbCnt); + break; + } + + } + + // stop the input device + if( cmApDeviceIsStarted(r->inDevIdx) ) + if( cmApDeviceStop(r->inDevIdx) != kOkApRC ) + cmRptPrintf(rpt,"In device stop failed.\n"); + + // stop the output device + if( cmApDeviceIsStarted(r->outDevIdx) ) + if( cmApDeviceStop(r->outDevIdx) != kOkApRC ) + cmRptPrintf(rpt,"Out device stop failed.\n"); + } + + errLabel: + + // release any resources held by the audio port interface + if( cmApFinalize() != kOkApRC ) + rc = cmErrMsg(&ctx->err,kAudioPortFailAdRC,"Finalize failed.\n"); + + cmApBufFinalize(); + + cmApNrtFree(); + cmApFileFree(); + + // report the count of audio buffer callbacks + cmRptPrintf(rpt,"cb count:%i\n", r->cbCnt ); + //for(i=0; i<_logCnt; ++i) + // cmRptPrintf(rpt,"%c(%i)",_log[i],_ilog[i]); + //cmRptPrintf(rpt,"\n"); + + + return rc; +} + +void print( void* arg, const char* text ) +{ + printf("%s",text); +} + +int main( int argc, char* argv[] ) +{ + enum + { + kSratePoId = kBasePoId, + kHzPoId, + kChIdxPoId, + kChCntPoId, + kBufCntPoId, + kFrmCntPoId, + kFrmsPerBufPoId, + kInDevIdxPoId, + kOutDevIdxPoId, + kReportFlagPoId + }; + + cmRC_t rc = cmOkRC; + bool memDebugFl = cmDEBUG_FL; + unsigned memGuardByteCnt = memDebugFl ? 8 : 0; + unsigned memAlignByteCnt = 16; + unsigned memFlags = memDebugFl ? kTrackMmFl | kDeferFreeMmFl | kFillUninitMmFl : 0; + cmPgmOptH_t poH = cmPgmOptNullHandle; + const cmChar_t* appTitle = "audiodev"; + unsigned reportFl = 0; + cmCtx_t ctx; + cmApPortTestRecd r; + memset(&r,0,sizeof(r)); + r.meterMs = 50; + r.logCnt = 100; + + memset(abuf,0,sizeof(cmApSample_t)*abufN); + + cmCtxSetup(&ctx,appTitle,print,print,NULL,memGuardByteCnt,memAlignByteCnt,memFlags); + + cmMdInitialize( memGuardByteCnt, memAlignByteCnt, memFlags, &ctx.rpt ); + + cmFsInitialize( &ctx, appTitle ); + + cmTsInitialize(&ctx ); + + cmPgmOptInitialize(&ctx, &poH, poBegHelpStr, poEndHelpStr ); + + cmPgmOptInstallDbl( poH, kSratePoId, 's', "srate", 0, 48000, &r.srate, 1, + "Audio system sample rate." ); + + cmPgmOptInstallDbl( poH, kHzPoId, 'z', "hz", 0, 1000, &r.frqHz, 1, + "Tone frequency in Hertz." ); + + cmPgmOptInstallUInt( poH, kChIdxPoId, 'x', "ch_index", 0, 0, &r.chIdx, 1, + "Index of first channel index." ); + + cmPgmOptInstallUInt( poH, kChCntPoId, 'c', "ch_cnt", 0, 2, &r.chCnt, 1, + "Count of audio channels." ); + + cmPgmOptInstallUInt( poH, kBufCntPoId, 'b', "buf_cnt", 0, 3, &r.bufCnt, 1, + "Count of audio buffers. (e.g. 2=double buffering, 3=triple buffering)" ); + + cmPgmOptInstallUInt( poH, kFrmsPerBufPoId, 'f', "frames_per_buf",0, 512, &r.framesPerCycle, 1, + "Count of audio channels." ); + + cmPgmOptInstallUInt( poH, kInDevIdxPoId, 'i', "in_dev_index",0, 0, &r.inDevIdx, 1, + "Input device index as taken from the audio device report." ); + + cmPgmOptInstallUInt( poH, kOutDevIdxPoId, 'o', "out_dev_index",0, 0, &r.outDevIdx, 1, + "Output device index as taken from the audio device report." ); + + cmPgmOptInstallFlag( poH, kReportFlagPoId, 'r', "report_flag", 0, 1, &reportFl, 1, + "Print an audio device report." ); + + // parse the command line arguments + if( cmPgmOptParse(poH, argc, argv ) == kOkPoRC ) + { + // handle the built-in arg's (e.g. -v,-p,-h) + // (returns false if only built-in options were selected) + if( cmPgmOptHandleBuiltInActions(poH, &ctx.rpt ) == false ) + goto errLabel; + + rc = audio_port_test( &ctx, &r, !reportFl ); + } + + errLabel: + _abuf_write_audio_file(&ctx); + cmPgmOptFinalize(&poH); + cmTsFinalize(); + cmFsFinalize(); + cmMdReport( kIgnoreNormalMmFl ); + cmMdFinalize(); + + return rc; +} diff --git a/src/cmtools/cmtools.c b/src/cmtools/cmtools.c new file mode 100644 index 0000000..19c1420 --- /dev/null +++ b/src/cmtools/cmtools.c @@ -0,0 +1,431 @@ +#include "cmPrefix.h" +#include "cmGlobal.h" +#include "cmRpt.h" +#include "cmErr.h" +#include "cmCtx.h" +#include "cmMem.h" +#include "cmMallocDebug.h" +#include "cmLinkedHeap.h" +#include "cmFileSys.h" +#include "cmText.h" + +#include "cmPgmOpts.h" + +#include "cmXScore.h" +#include "cmMidiScoreFollow.h" +#include "cmScoreProc.h" + + +#include "cmSymTbl.h" +#include "cmTime.h" +#include "cmMidi.h" +#include "cmScore.h" + +#include "cmMidiFile.h" + +#include "cmFloatTypes.h" +#include "cmAudioFile.h" +#include "cmTimeLine.h" + +enum +{ + kOkCtRC = cmOkRC, + kNoActionIdSelectedCtRC, + kMissingRequiredFileNameCtRC, + kScoreGenFailedCtRC, + kScoreFollowFailedCtRC, + kMidiFileRptFailedCtRC, + kTimeLineRptFailedCtRC, + kAudioFileRptFailedCtRC +}; + + +const cmChar_t poEndHelpStr[] = ""; +const cmChar_t poBegHelpStr[] = + "xscore_proc Music XML to electronic score generator\n" + "\n" + "USAGE:\n" + "\n" + "Parse an XML score file and decoration file to produce a score file in CSV format.\n" + "\n" + "cmtool --score_gen -x -d {-c } {-s } {-r report} {-b begMeasNumb} {t begTempoBPM}\n" + "\n" + "Notes:\n" + "1. If does not exist then a decoration template file will be generated based on the MusicXML file. \n" + "2. Along with the CSV score file MIDI and HTML/SVG files will also be produced based on the contents of the MusicXML and decoration file.\n" + "3. See README.md for a detailed description of the how to edit the decoration file.\n" + "\n" + "\n" + "Use the score follower to generate a timeline configuration file.\n" + "\n" + "cmtool --timeline_gen -c -i -r -s {-m } {-t timelineOutFn} \n" + "\n" + "Measure some perforamance attributes:\n" + "\n" + "cmtool --meas_gen -g -r \n" + "\n" + "Generate a score file report\n" + "\n" + "cmtool --score_report -c -r \n" + "\n" + "Generate a MIDI file report and optional SVG piano roll image\n" + "\n" + "cmtool --midi_report -i -r {-s }\n" + "\n" + "Generate a timeline report\n" + "\n" + "cmtool --timeline_report -t -r \n" + "\n" + "Generate an audio file report\n" + "\n" + "cmtool --audiofile_report -a -r \n" + "\n"; + + +void print( void* arg, const char* text ) +{ + printf("%s",text); +} + +bool verify_file_exists( cmCtx_t* ctx, const cmChar_t* fn, const cmChar_t* msg ) +{ + if( fn == NULL || cmFsIsFile(fn)==false ) + return cmErrMsg(&ctx->err,kMissingRequiredFileNameCtRC,"The required file <%s> does not exist.",msg); + + return kOkCtRC; +} + +bool verify_non_null_filename( cmCtx_t* ctx, const cmChar_t* fn, const cmChar_t* msg ) +{ + if( fn == NULL ) + return cmErrMsg(&ctx->err,kMissingRequiredFileNameCtRC,"The required file name <%s> is blank.",msg); + + return kOkCtRC; +} + +cmRC_t score_gen( cmCtx_t* ctx, const cmChar_t* xmlFn, const cmChar_t* decFn, const cmChar_t* csvOutFn, const cmChar_t* midiOutFn, const cmChar_t* svgOutFn, unsigned reportFl, int begMeasNumb, int begTempoBPM, bool svgStandAloneFl, bool svgPanZoomFl ) +{ + cmRC_t rc; + if((rc = verify_file_exists(ctx,xmlFn,"XML file")) != kOkCtRC ) + return rc; + + if( cmXScoreTest( ctx, xmlFn, decFn, csvOutFn, midiOutFn, svgOutFn, reportFl, begMeasNumb, begTempoBPM, svgStandAloneFl, svgPanZoomFl ) != kOkXsRC ) + return cmErrMsg(&ctx->err,kScoreGenFailedCtRC,"score_gen failed."); + + return kOkCtRC; +} + +cmRC_t score_follow( cmCtx_t* ctx, const cmChar_t* csvScoreFn, const cmChar_t* midiInFn, const cmChar_t* matchRptOutFn, const cmChar_t* matchSvgOutFn, const cmChar_t* midiOutFn, const cmChar_t* timelineFn ) +{ + cmRC_t rc; + + if((rc = verify_file_exists(ctx,csvScoreFn,"Score CSV file")) != kOkCtRC ) + return rc; + + if((rc = verify_file_exists(ctx,midiInFn,"MIDI input file")) != kOkCtRC ) + return rc; + + //if((rc = verify_file_exists(ctx,matchRptOutFn,"Match report file")) != kOkCtRC ) + // return rc; + + //if((rc = verify_file_exists(ctx,matchSvgOutFn,"Match HTML/SVG file")) != kOkCtRC ) + // return rc; + + if(cmMidiScoreFollowMain(ctx, csvScoreFn, midiInFn, matchRptOutFn, matchSvgOutFn, midiOutFn, timelineFn) != kOkMsfRC ) + return cmErrMsg(&ctx->err,kScoreFollowFailedCtRC,"score_follow failed."); + + return kOkCtRC; +} + +cmRC_t meas_gen( cmCtx_t* ctx, const cmChar_t* pgmRsrcFn, const cmChar_t* outFn ) +{ + cmRC_t rc; + + if((rc = verify_file_exists(ctx,pgmRsrcFn,"Program resource file")) != kOkCtRC ) + return rc; + + if((rc = verify_non_null_filename( ctx,outFn,"Measurements output file.")) != kOkCtRC ) + return rc; + + return cmScoreProc(ctx, "meas", pgmRsrcFn, outFn ); +} + +cmRC_t score_report( cmCtx_t* ctx, const cmChar_t* csvScoreFn, const cmChar_t* rptFn ) +{ + cmRC_t rc; + + if((rc = verify_file_exists(ctx,csvScoreFn,"Score CSV file")) != kOkCtRC ) + return rc; + + cmScoreReport(ctx,csvScoreFn,rptFn); + + return rc; +} + + +cmRC_t midi_file_report( cmCtx_t* ctx, const cmChar_t* midiFn, const cmChar_t* rptFn, const cmChar_t* svgFn, bool standAloneFl, bool panZoomFl ) +{ + cmRC_t rc ; + + if((rc = verify_file_exists(ctx,midiFn,"MIDI file")) != kOkCtRC ) + return rc; + + if((rc = cmMidiFileReport(ctx, midiFn, rptFn )) != kOkMfRC ) + return cmErrMsg(&ctx->err,kMidiFileRptFailedCtRC,"MIDI file report generation failed."); + + if( svgFn != NULL ) + if((rc = cmMidiFileGenSvgFile(ctx, midiFn, svgFn, "midi_file_svg.css", standAloneFl, panZoomFl )) != kOkMfRC ) + return cmErrMsg(&ctx->err,kMidiFileRptFailedCtRC,"MIDI file SVG output generation failed."); + + return kOkCtRC; +} + +cmRC_t timeline_report( cmCtx_t* ctx, const cmChar_t* timelineFn, const cmChar_t* tlPrefixPath, const cmChar_t* rptFn ) +{ + cmRC_t rc ; + + if((rc = verify_file_exists(ctx,timelineFn,"Timeline file")) != kOkCtRC ) + return rc; + + if((rc = cmTimeLineReport( ctx, timelineFn, tlPrefixPath, rptFn )) != kOkTlRC ) + return cmErrMsg(&ctx->err,kTimeLineRptFailedCtRC,"The timeline file report failed."); + + return rc; +} + +cmRC_t audio_file_report( cmCtx_t* ctx, const cmChar_t* audioFn, const cmChar_t* rptFn ) +{ + cmRC_t rc; + + if((rc = verify_file_exists(ctx,audioFn,"Audio file")) != kOkCtRC ) + return rc; + + if((rc = cmAudioFileReportInfo( ctx, audioFn, rptFn )) != kOkTlRC ) + return cmErrMsg(&ctx->err,kAudioFileRptFailedCtRC,"The audio file report failed."); + + return rc; +} + +cmRC_t midi_trim(cmCtx_t* ctx, const cmChar_t* midiInFn, unsigned begMidiUId, unsigned endMidiUId, const cmChar_t* midiOutFn) +{ + cmRC_t rc; + + if((rc = verify_file_exists(ctx,midiInFn,"MIDI file")) != kOkCtRC ) + return rc; + // kNoteTerminateFl | kPedalTerminateFl + return cmMidiFileTrimFn(ctx, midiInFn, begMidiUId, endMidiUId, 0, midiOutFn ); +} + + +int main( int argc, char* argv[] ) +{ + cmRC_t rc = cmOkRC; + enum + { + kInvalidPoId = kBasePoId, + kActionPoId, + kXmlFileNamePoId, + kDecorateFileNamePoId, + kCsvOutFileNamePoId, + kPgmRsrcFileNamePoId, + kMidiOutFileNamePoId, + kMidiInFileNamePoId, + kSvgOutFileNamePoId, + kStatusOutFileNamePoId, + kTimelineFileNamePoId, + kTimelinePrefixPoId, + kAudioFileNamePoId, + kReportFlagPoId, + kSvgStandAloneFlPoId, + kSvgPanZoomFlPoId, + kBegMeasPoId, + kBegBpmPoId, + kBegMidiUidPoId, + kEndMidiUidPoId + }; + + enum { + kNoSelId, + kScoreGenSelId, + kScoreFollowSelId, + kMeasGenSelId, + kScoreReportSelId, + kMidiReportSelId, + kTimelineReportSelId, + kAudioReportSelId, + kMidiTrimSelId + }; + + + // initialize the heap check library + bool memDebugFl = 0; //cmDEBUG_FL; + unsigned memGuardByteCnt = memDebugFl ? 8 : 0; + unsigned memAlignByteCnt = 16; + unsigned memFlags = memDebugFl ? kTrackMmFl | kDeferFreeMmFl | kFillUninitMmFl : 0; + cmPgmOptH_t poH = cmPgmOptNullHandle; + const cmChar_t* appTitle = "cmtools"; + cmCtx_t ctx; + const cmChar_t* xmlFn = NULL; + const cmChar_t* decFn = NULL; + const cmChar_t* pgmRsrcFn = NULL; + const cmChar_t* csvScoreFn = NULL; + const cmChar_t* midiOutFn = NULL; + const cmChar_t* midiInFn = NULL; + const cmChar_t* audioFn = NULL; + const cmChar_t* svgOutFn = NULL; + const cmChar_t* timelineFn = NULL; + const cmChar_t* timelinePrefix = NULL; + const cmChar_t* rptFn = NULL; + unsigned reportFl = 0; + unsigned svgStandAloneFl = 1; + unsigned svgPanZoomFl = 1; + int begMeasNumb = 0; + int begTempoBPM = 60; + unsigned begMidiUId = cmInvalidId; + unsigned endMidiUId = cmInvalidId; + unsigned actionSelId = kNoSelId; + + cmCtxSetup(&ctx,appTitle,print,print,NULL,memGuardByteCnt,memAlignByteCnt,memFlags); + + cmMdInitialize( memGuardByteCnt, memAlignByteCnt, memFlags, &ctx.rpt ); + + cmFsInitialize( &ctx, appTitle); + + cmTsInitialize(&ctx ); + + cmPgmOptInitialize(&ctx, &poH, poBegHelpStr, poEndHelpStr ); + + cmPgmOptInstallEnum( poH, kActionPoId, 'S', "score_gen", 0, kScoreGenSelId, kNoSelId, &actionSelId, 1, + "Run the score generation tool.","Action selector"); + + cmPgmOptInstallEnum( poH, kActionPoId, 'F', "score_follow", 0, kScoreFollowSelId, kNoSelId, &actionSelId, 1, + "Run the time line marker generation tool.",NULL); + + cmPgmOptInstallEnum( poH, kActionPoId, 'M', "meas_gen", 0, kMeasGenSelId, kNoSelId, &actionSelId, 1, + "Generate perfomance measurements.",NULL); + + cmPgmOptInstallEnum( poH, kActionPoId, 'R', "score_report", 0, kScoreReportSelId, kNoSelId, &actionSelId, 1, + "Generate a score file report.",NULL); + + cmPgmOptInstallEnum( poH, kActionPoId, 'I', "midi_report", 0, kMidiReportSelId, kNoSelId, &actionSelId, 1, + "Generate a MIDI file report and optional SVG piano roll output.",NULL); + + cmPgmOptInstallEnum( poH, kActionPoId, 'E', "timeline_report", 0, kTimelineReportSelId, kNoSelId, &actionSelId, 1, + "Generate a timeline report.",NULL); + + cmPgmOptInstallEnum( poH, kActionPoId, 'A', "audio_report", 0, kAudioReportSelId, kNoSelId, &actionSelId, 1, + "Generate an audio file report.",NULL); + + cmPgmOptInstallEnum( poH, kActionPoId, 'T', "midi_trim", 0, kMidiTrimSelId, kNoSelId, &actionSelId, 1, + "Trim a MIDI file to create a shortened version.",NULL); + + cmPgmOptInstallStr( poH, kXmlFileNamePoId, 'x', "muisic_xml_fn",0, NULL, &xmlFn, 1, + "Name of the input MusicXML file."); + + cmPgmOptInstallStr( poH, kDecorateFileNamePoId, 'd', "dec_fn", 0, NULL, &decFn, 1, + "Name of a score decoration file."); + + cmPgmOptInstallStr( poH, kCsvOutFileNamePoId, 'c', "score_csv_fn",0, NULL, &csvScoreFn, 1, + "Name of a CSV score file."); + + cmPgmOptInstallStr( poH, kPgmRsrcFileNamePoId, 'g', "pgm_rsrc_fn", 0, NULL, &pgmRsrcFn, 1, + "Name of program resource file."); + + cmPgmOptInstallStr( poH, kMidiOutFileNamePoId, 'm', "midi_out_fn", 0, NULL, &midiOutFn, 1, + "Name of a MIDI file to generate as output."); + + cmPgmOptInstallStr( poH, kMidiInFileNamePoId, 'i', "midi_in_fn", 0, NULL, &midiInFn, 1, + "Name of a MIDI file to generate as output."); + + cmPgmOptInstallStr( poH, kSvgOutFileNamePoId, 's', "svg_fn", 0, NULL, &svgOutFn, 1, + "Name of a HTML/SVG file to generate as output."); + + cmPgmOptInstallStr( poH, kTimelineFileNamePoId, 't', "timeline_fn", 0, NULL, &timelineFn, 1, + "Name of a timeline to generate as output."); + + cmPgmOptInstallStr( poH, kTimelinePrefixPoId, 'l', "tl_prefix", 0, NULL, &timelinePrefix,1, + "Timeline data path prefix."); + + cmPgmOptInstallStr( poH, kAudioFileNamePoId, 'a', "audio_fn", 0, NULL, &audioFn, 1, + "Audio file name."); + + cmPgmOptInstallStr( poH, kStatusOutFileNamePoId,'r', "report_fn", 0, NULL, &rptFn, 1, + "Name of a status file to generate as output."); + + cmPgmOptInstallFlag( poH, kReportFlagPoId, 'f', "debug_fl", 0, 1, &reportFl, 1, + "Print a report of the score following processing." ); + + cmPgmOptInstallInt( poH, kBegMeasPoId, 'b', "beg_meas", 0, 1, &begMeasNumb, 1, + "The first measure the to be written to the output CSV, MIDI and SVG files." ); + + cmPgmOptInstallInt( poH, kBegBpmPoId, 'e', "beg_bpm", 0, 0, &begTempoBPM, 1, + "Set to 0 to use the tempo from the score otherwise set to use the tempo at begMeasNumb." ); + + cmPgmOptInstallFlag( poH, kSvgStandAloneFlPoId, 'n', "svg_stand_alone_fl",0, 1, &svgStandAloneFl, 1, + "Write the SVG file as a stand alone HTML file. Enabled by default." ); + + cmPgmOptInstallFlag( poH, kSvgPanZoomFlPoId, 'z', "svg_pan_zoom_fl", 0, 1, &svgPanZoomFl, 1, + "Include the pan-zoom control. Enabled by default." ); + + cmPgmOptInstallUInt( poH, kBegMidiUidPoId, 'w', "beg_midi_uid", 0, 1, &begMidiUId, 1, + "Begin MIDI msg. uuid." ); + + cmPgmOptInstallUInt( poH, kEndMidiUidPoId, 'y', "end_midi_uid", 0, 1, &endMidiUId, 1, + "End MIDI msg. uuid." ); + + // parse the command line arguments + if( cmPgmOptParse(poH, argc, argv ) == kOkPoRC ) + { + // handle the built-in arg's (e.g. -v,-p,-h) + // (returns false if only built-in options were selected) + if( cmPgmOptHandleBuiltInActions(poH, &ctx.rpt ) == false ) + goto errLabel; + + switch( actionSelId ) + { + case kScoreGenSelId: + rc = score_gen( &ctx, xmlFn, decFn, csvScoreFn, midiOutFn, svgOutFn, reportFl, begMeasNumb, begTempoBPM, svgStandAloneFl, svgPanZoomFl ); + break; + + case kScoreFollowSelId: + rc = score_follow( &ctx, csvScoreFn, midiInFn, rptFn, svgOutFn, midiOutFn, timelineFn ); + break; + + case kMeasGenSelId: + rc = meas_gen(&ctx, pgmRsrcFn, rptFn); + break; + + case kScoreReportSelId: + rc = score_report(&ctx, csvScoreFn, rptFn ); + break; + + case kMidiReportSelId: + rc = midi_file_report(&ctx, midiInFn, rptFn, svgOutFn, svgStandAloneFl, svgPanZoomFl ); + break; + + case kTimelineReportSelId: + rc = timeline_report(&ctx, timelineFn, timelinePrefix, rptFn ); + break; + + case kAudioReportSelId: + rc = audio_file_report(&ctx, audioFn, rptFn ); + break; + + case kMidiTrimSelId: + rc = midi_trim(&ctx, midiInFn, begMidiUId, endMidiUId, midiOutFn); + break; + + default: + rc = cmErrMsg(&ctx.err, kNoActionIdSelectedCtRC,"No action selector was selected."); + + } + } + + errLabel: + cmPgmOptFinalize(&poH); + cmTsFinalize(); + cmFsFinalize(); + cmMdReport( kIgnoreNormalMmFl ); + cmMdFinalize(); + return rc; +} diff --git a/src/cmtools/mas.c b/src/cmtools/mas.c new file mode 100644 index 0000000..6589859 --- /dev/null +++ b/src/cmtools/mas.c @@ -0,0 +1,2232 @@ +#include "cmPrefix.h" +#include "cmGlobal.h" +#include "cmFloatTypes.h" +#include "cmComplexTypes.h" +#include "cmRpt.h" +#include "cmErr.h" +#include "cmCtx.h" +#include "cmMem.h" +#include "cmMallocDebug.h" +#include "cmLinkedHeap.h" +#include "cmSymTbl.h" +#include "cmFile.h" +#include "cmFileSys.h" +#include "cmTime.h" +#include "cmMidi.h" +#include "cmAudioFile.h" +#include "cmMidiFile.h" +#include "cmJson.h" +#include "cmText.h" + +#include "cmProcObj.h" +#include "cmProcTemplateMain.h" +#include "cmVectOpsTemplateMain.h" +#include "cmProc.h" +#include "cmProc2.h" + +#include "cmTimeLine.h" +#include "cmOnset.h" +#include "cmPgmOpts.h" +#include "cmScore.h" + +typedef cmRC_t masRC_t; + +enum +{ + kOkMasRC = cmOkRC, + kFailMasRC, + kJsonFailMasRC, + kParamErrMasRC, + kTimeLineFailMasRC +}; + +enum +{ + kMidiToAudioSelId, + kAudioOnsetSelId, + kConvolveSelId, + kSyncSelId, + kGenTimeLineSelId, + kLoadMarkersSelId, + kTestStubSelId +}; + + +typedef struct +{ + const cmChar_t* input; + const cmChar_t* output; + unsigned selId; + double wndMs; + double srate; + cmOnsetCfg_t onsetCfg; + const cmChar_t* refDir; + const cmChar_t* keyDir; + const cmChar_t* refExt; + const cmChar_t* keyExt; + const cmChar_t* markFn; + const cmChar_t* prefixPath; +} masPgmArgs_t; + +typedef struct +{ + const char* refFn; + double refWndBegSecs; // location of the ref window in the midi file + double refWndSecs; // length of the ref window + const char* keyFn; + double keyBegSecs; // offset into audio file of first sliding window + double keyEndSecs; // offset into audio file of the last sliding window + unsigned keySyncIdx; // index into audio file of best matched sliding window + double syncDist; // distance (matching score) to the ref window of the best matched sliding window + unsigned refSmpCnt; // count of samples in the midi file + unsigned keySmpCnt; // count of samples in the audio file + double srate; // sample rate of audio and midi file +} syncRecd_t; +// Notes: +// audioBegSecs +// != 0 - audio file is locked to midi file +// == 0 - midi file is locked to audio file + +typedef struct +{ + cmJsonH_t jsH; + syncRecd_t* syncArray; + unsigned syncArrayCnt; + const cmChar_t* refDir; + const cmChar_t* keyDir; + double hopMs; +} syncCtx_t; + +enum +{ + kMidiFl = 0x01, + kAudioFl= 0x02, + kLabelCharCnt = 31 +}; + + +// Notes: +// 1) Master files will have refPtr==NULL. +// 2) refSmpIdx and keySmpIdx are only valid for slave files. +// 3) A common group id is given to sets of files which are +// locked in time relative to one another. For example +// if file B and C are synced to master file A and +// file D is synced to file E which is synced to master +// file F. Then files A,B,C will be given one group +// id and files D,E and F will be given another group id. +// +typedef struct file_str +{ + unsigned flags; // see kXXXFl + const char* fn; // file name of this file + const char* fullFn; // path and name + unsigned refIdx; // index into file array of recd pointed to by refPtr + struct file_str* refPtr; // ptr to the file that this file is positioned relative to (set to NULL for master files) + int refSmpIdx; // index into the reference file that is synced to keySmpIdx + int keySmpIdx; // index into this file which is synced to refSmpIdx + int absSmpIdx; // abs smp idx of sync location + int absBegSmpIdx; // file beg smp idx - the earliest file in the group is set to 0. + int smpCnt; // file duration + double srate; // file sample rate + unsigned groupId; // every file belongs to a group - a group is a set of files referencing a common master + + char label[ kLabelCharCnt+1 ]; + +} fileRecd_t; + + +void print( void* p, const cmChar_t* text) +{ + if( text != NULL ) + { + printf("%s",text); + fflush(stdout); + } +} + +masRC_t midiStringSearch( cmCtx_t* ctx, const cmChar_t* srcDir, cmMidiByte_t* x, unsigned xn ) +{ + cmFileSysDirEntry_t* dep = NULL; + unsigned dirEntryCnt = 0; + unsigned i,j,k; + masRC_t rc = kOkMasRC; + unsigned totalNoteCnt = 0; + + typedef struct + { + cmMidiByte_t pitch; + unsigned micros; + } note_t; + + assert( xn > 0 ); + + note_t wnd[ xn ]; + + // iterate the source directory + if( (dep = cmFsDirEntries( srcDir, kFileFsFl | kFullPathFsFl, &dirEntryCnt )) == NULL ) + return cmErrMsg(&ctx->err,kFailMasRC,"Unable to iterate the source directory '%s'.",srcDir); + + // for each file in the source directory + for(i=0; ierr,kFailMasRC,"The MIDI file '%s' could not be opened.",dep[i].name); + goto errLabel; + } + + cmRptPrintf(ctx->err.rpt,"%3i of %3i %s ",i,dirEntryCnt,dep[i].name); + + unsigned msgCnt = cmMidiFileMsgCount(mfH); // get the count of messages in the MIDI file + const cmMidiTrackMsg_t** msgPtrPtr = cmMidiFileMsgArray(mfH); // get a ptr to the base of the the MIDI msg array + //cmMidiFileTickToMicros(mfH); // convert the MIDI msg time base from ticks to microseconds + + // empty the window + for(j=0; jdtick; + + if( mp->status == kNoteOnMdId ) + { + ++noteCnt; + + // shift the window to the left + for(j=0; ju.chMsgPtr->d0; + wnd[ xn-1 ].micros = micros; + + // compare the window to the search string + for(j=0; jerr.rpt,"\n %5i %5.1f ", i, /* minuites */ (double)micros/60000000.0 ); + } + + } + + totalNoteCnt += noteCnt; + + cmRptPrintf(ctx->err.rpt,"%i %i \n",noteCnt,totalNoteCnt); + + // close the midi file + cmMidiFileClose(&mfH); + + } + + errLabel: + cmFsDirFreeEntries(dep); + + return rc; +} + + +// Generate an audio file containing impulses at the location of each note-on message. +masRC_t midiToAudio( cmCtx_t* ctx, const cmChar_t* midiFn, const cmChar_t* audioFn, double srate ) +{ + cmMidiFileH_t mfH = cmMidiFileNullHandle; + unsigned sampleBits = 16; + unsigned chCnt = 1; + + unsigned msgCnt; + const cmMidiTrackMsg_t** msgPtrPtr; + masRC_t rc = kFailMasRC; + cmRC_t afRC = kOkAfRC; + cmAudioFileH_t afH = cmNullAudioFileH; + unsigned bufSmpCnt = 1024; + cmSample_t buf[ bufSmpCnt ]; + cmSample_t *bufPtr = buf; + unsigned noteOnCnt = 0; + + // open the MIDI file + if( cmMidiFileOpen(ctx,&mfH,midiFn) != kOkMfRC ) + return kFailMasRC; + + // force the first event to occur one quarter note into the file + cmMidiFileSetDelay(mfH, cmMidiFileTicksPerQN(mfH) ); + + double mfDurSecs = cmMidiFileDurSecs(mfH); + cmRptPrintf(&ctx->rpt,"Secs:%f \n",mfDurSecs); + + msgCnt = cmMidiFileMsgCount(mfH); // get the count of messages in the MIDI file + msgPtrPtr = cmMidiFileMsgArray(mfH); // get a ptr to the base of the the MIDI msg array + //cmMidiFileTickToMicros(mfH); // convert the MIDI msg time base from ticks to microseconds + + if( msgCnt == 0 ) + { + rc = kOkMasRC; + goto errLabel; + } + + // create the output audio file + if( cmAudioFileIsValid( afH = cmAudioFileNewCreate(audioFn, srate, sampleBits, chCnt, &afRC, &ctx->rpt))==false ) + { + cmErrMsg(&ctx->err,kFailMasRC,"The attempt to create the audio file '%s' failed.",audioFn); + goto errLabel; + } + + unsigned msgIdx = 0; + unsigned msgSmpIdx = floor( msgPtrPtr[msgIdx]->dtick * srate / 1000000.0); + unsigned begSmpIdx = 0; + + do + { + + // zero the audio buffer + cmVOS_Zero(buf,bufSmpCnt); + + // for each msg which falls inside the current buffer + while( begSmpIdx <= msgSmpIdx && msgSmpIdx < begSmpIdx + bufSmpCnt ) + { + + // put an impulse representing this note-on msg in the buffer + if( msgPtrPtr[msgIdx]->status == kNoteOnMdId ) + { + buf[ msgSmpIdx - begSmpIdx ] = (cmSample_t)msgPtrPtr[msgIdx]->u.chMsgPtr->d1 / 127; + ++noteOnCnt; + } + + // advance to the next msg + ++msgIdx; + + if( msgIdx == msgCnt ) + break; + + // update the current msg time + msgSmpIdx += floor( msgPtrPtr[msgIdx]->dtick * srate / 1000000.0); + } + + // write the audio buffer + if( cmAudioFileWriteFloat(afH, bufSmpCnt, chCnt, &bufPtr ) != kOkAfRC ) + { + cmErrMsg(&ctx->err,kFailMasRC,"Audio file write failed on '%s'.",audioFn); + goto errLabel; + } + + // advance the buffer position + begSmpIdx += bufSmpCnt; + + }while(msgIdx < msgCnt); + + /* + // for each MIDI msg + for(i=0; idtick; + + // if this is a note on msg + if( mp->status == kNoteOnMdId && absUSecs > shiftUSecs ) + { + // convert the msg time to samples + unsigned smpIdx = floor((absUSecs - shiftUSecs) * srate / 1000000.0); + + assert(smpIdxu.chMsgPtr->d1 / 127; + } + } + + cmSample_t** bufPtrPtr = &sV; + if( cmAudioFileWriteFileSample(audioFn,srate,sampleBits,smpCnt,chCnt,bufPtrPtr,&ctx->rpt) != kOkAfRC ) + goto errLabel; + */ + + rc = kOkMasRC; + + cmRptPrintf(&ctx->rpt,"Note-on count:%i\n",noteOnCnt); + + errLabel: + + //cmMemFree(sV); + + if( cmAudioFileIsValid(afH) ) + cmAudioFileDelete(&afH); + + // close the midi file + cmMidiFileClose(&mfH); + + return rc; + +} + +masRC_t filter( cmCtx_t* ctx, const cmChar_t* inAudioFn, const cmChar_t* outAudioFn, double wndMs, double feedbackCoeff ) +{ + cmAudioFileH_t iafH = cmNullAudioFileH; + cmAudioFileH_t oafH = cmNullAudioFileH; + masRC_t rc = kFailMasRC; + cmAudioFileInfo_t afInfo; + cmRC_t afRC; + double prog = 0.1; + unsigned progIdx = 0; + + // open the input audio file + if( cmAudioFileIsValid( iafH = cmAudioFileNewOpen(inAudioFn,&afInfo,&afRC, &ctx->rpt ))==false) + return kFailMasRC; + + // create the output audio file + if( cmAudioFileIsValid( oafH = cmAudioFileNewCreate(outAudioFn,afInfo.srate,afInfo.bits,1,&afRC,&ctx->rpt)) == false ) + goto errLabel; + else + { + unsigned wndSmpCnt = floor(afInfo.srate * wndMs / 1000); + unsigned procSmpCnt = wndSmpCnt; + cmSample_t procBuf[procSmpCnt]; + unsigned actFrmCnt; + cmReal_t a[] = {-feedbackCoeff }; + cmReal_t b0 = 1.0; + cmReal_t b[] = {0,}; + cmReal_t d[] = {0,0}; + + do + { + unsigned chIdx = 0; + unsigned chCnt = 1; + + cmSample_t* procBufPtr = procBuf; + + actFrmCnt = 0; + + // read the next procSmpCnt samples from the input file into procBuf[] + cmAudioFileReadSample(iafH, procSmpCnt, chIdx, chCnt, &procBufPtr, &actFrmCnt ); + + if( actFrmCnt > 0 ) + { + + cmSample_t y[actFrmCnt]; + cmSample_t* yp = y; + cmVOS_Filter( y, actFrmCnt, procBuf, actFrmCnt, b0, b, a, d, 1 ); + + // write the output audio file + if( cmAudioFileWriteSample(oafH, actFrmCnt, chCnt, &yp ) != kOkAfRC ) + goto errLabel; + } + + progIdx += actFrmCnt; + + if( progIdx > prog * afInfo.frameCnt ) + { + cmRptPrintf(&ctx->rpt,"%i ",(int)round(prog*10)); + prog += 0.1; + } + + }while(actFrmCnt==procSmpCnt); + + cmRptPrint(&ctx->rpt,"\n"); + } + + + rc = kOkMasRC; + + errLabel: + + if( cmAudioFileIsValid(iafH) ) + cmAudioFileDelete(&iafH); + + if( cmAudioFileIsValid(oafH) ) + cmAudioFileDelete(&oafH); + + return rc; +} + + +masRC_t convolve( cmCtx_t* ctx, const cmChar_t* inAudioFn, const cmChar_t* outAudioFn, double wndMs ) +{ + cmAudioFileH_t iafH = cmNullAudioFileH; + cmAudioFileH_t oafH = cmNullAudioFileH; + cmCtx* ctxp = NULL; + cmConvolve* cnvp = NULL; + masRC_t rc = kFailMasRC; + cmAudioFileInfo_t afInfo; + cmRC_t afRC; + double prog = 0.1; + unsigned progIdx = 0; + + // open the input audio file + if( cmAudioFileIsValid( iafH = cmAudioFileNewOpen(inAudioFn,&afInfo,&afRC, &ctx->rpt ))==false) + return kFailMasRC; + + // create the output audio file + if( cmAudioFileIsValid( oafH = cmAudioFileNewCreate(outAudioFn,afInfo.srate,afInfo.bits,1,&afRC,&ctx->rpt)) == false ) + goto errLabel; + else + { + unsigned wndSmpCnt = floor(afInfo.srate * wndMs / 1000); + unsigned procSmpCnt = wndSmpCnt; + cmSample_t wnd[wndSmpCnt]; + cmSample_t procBuf[procSmpCnt]; + unsigned actFrmCnt; + + cmVOS_Hann(wnd,wndSmpCnt); + //cmVOS_DivVS(wnd,wndSmpCnt, fl ? 384 : 2); + cmVOS_DivVS(wnd,wndSmpCnt, 4); + + ctxp = cmCtxAlloc(NULL,&ctx->rpt,cmLHeapNullHandle,cmSymTblNullHandle); // alloc a cmCtx object + cnvp = cmConvolveAlloc(ctxp,NULL,wnd,wndSmpCnt,procSmpCnt); // alloc a convolver object + + do + { + unsigned chIdx = 0; + unsigned chCnt = 1; + + cmSample_t* procBufPtr = procBuf; + + actFrmCnt = 0; + + // read the next procSmpCnt samples from the input file into procBuf[] + cmAudioFileReadSample(iafH, procSmpCnt, chIdx, chCnt, &procBufPtr, &actFrmCnt ); + + if( actFrmCnt > 0 ) + { + // convolve the audio signal with the Gaussian window + cmConvolveExec(cnvp,procBuf,actFrmCnt); + + //cmVOS_AddVV( cnvp->outV, cnvp->outN, procBufPtr ); + + // write the output audio file + if( cmAudioFileWriteSample(oafH, cnvp->outN, chCnt, &cnvp->outV ) != kOkAfRC ) + goto errLabel; + } + + progIdx += actFrmCnt; + + if( progIdx > prog * afInfo.frameCnt ) + { + cmRptPrintf(&ctx->rpt,"%i ",(int)round(prog*10)); + prog += 0.1; + } + + }while(actFrmCnt==procSmpCnt); + + cmRptPrint(&ctx->rpt,"\n"); + } + + + rc = kOkMasRC; + + errLabel: + cmCtxFree(&ctxp); + cmConvolveFree(&cnvp); + + if( cmAudioFileIsValid(iafH) ) + cmAudioFileDelete(&iafH); + + if( cmAudioFileIsValid(oafH) ) + cmAudioFileDelete(&oafH); + + return rc; +} + +masRC_t audioToOnset( cmCtx_t* ctx, const cmChar_t* ifn, const cmChar_t* ofn, const cmOnsetCfg_t* cfg ) +{ + masRC_t rc = kOkMasRC; + cmOnH_t onH = cmOnsetNullHandle; + cmFileSysPathPart_t* ofsp = NULL; + const cmChar_t* tfn = NULL; + + // parse the output file name + if((ofsp = cmFsPathParts(ofn)) == NULL ) + { + rc = cmErrMsg(&ctx->err,kFailMasRC,"Onset detector output file name '%s' could not be parsed.",cmStringNullGuard(ofn)); + goto errLabel; + } + + // verify the output audio file does not use the 'txt' extension + if(strcmp(ofsp->extStr,"txt") == 0 ) + { + rc = cmErrMsg(&ctx->err,kFailMasRC,"The onset detector output audio file name cannot use the file name extension 'txt' because it will class with the output text file name."); + goto errLabel; + } + + // generate the output text file name by setting the output audio file name to '.txt'. + if((tfn = cmFsMakeFn(ofsp->dirStr,ofsp->fnStr,"txt",NULL)) == NULL ) + { + rc = cmErrMsg(&ctx->err,kFailMasRC,"Onset detector output file name generation failed on %s.",cmStringNullGuard(ifn)); + goto errLabel; + } + + // initialize the onset detection API + if( cmOnsetInitialize(ctx,&onH) != kOkOnRC ) + { + rc = cmErrMsg(&ctx->err,kFailMasRC,"The onset detector initialization failed on %s.",cmStringNullGuard(ifn)); + goto errLabel; + } + + // run the onset detector + if( cmOnsetProc( onH, cfg, ifn ) != kOkOnRC ) + { + rc = cmErrMsg(&ctx->err,kFailMasRC,"The onset detector execution failed on %s.",cmStringNullGuard(ifn)); + goto errLabel; + } + + // store the results of the onset detection + if( cmOnsetWrite( onH, ofn, tfn) != kOkOnRC ) + { + rc = cmErrMsg(&ctx->err,kFailMasRC,"The onset detector write result failed on %s.",cmStringNullGuard(ifn)); + goto errLabel; + } + + errLabel: + // finalize the onset detector API + if( cmOnsetFinalize(&onH) != kOkOnRC ) + rc = cmErrMsg(&ctx->err,kFailMasRC,"The onset detector finalization failed on %s.",cmStringNullGuard(ifn)); + + cmFsFreeFn(tfn); + cmFsFreePathParts(ofsp); + + return rc; +} + +typedef struct +{ + const char* fn; + unsigned startSmpIdx; + unsigned durSmpCnt; +} audioFileRecd_t; + +int compareAudioFileRecds( const void* p0, const void* p1 ) +{ return strcmp(((const audioFileRecd_t*)p0)->fn,((const audioFileRecd_t*)p1)->fn); } + +// Print out information on all audio files in a directory. +masRC_t audioFileStartTimes( cmCtx_t* ctx, const char* dirStr ) +{ + cmFileSysDirEntry_t* dep = NULL; + unsigned dirEntryCnt = 0; + unsigned i,n; + masRC_t rc = kOkMasRC; + + if( (dep = cmFsDirEntries( dirStr, kFileFsFl | kFullPathFsFl, &dirEntryCnt )) == NULL ) + return kFailMasRC; + else + { + audioFileRecd_t afArray[ dirEntryCnt ]; + + memset(afArray,0,sizeof(afArray)); + + for(i=0,n=0; irpt ) == kOkAfRC ) + { + + afArray[n].fn = dep[i].name; + afArray[n].durSmpCnt = afInfo.frameCnt; + afArray[n].startSmpIdx = afInfo.bextRecd.timeRefLow; + ++n; + } + + } + + qsort(afArray,n,sizeof(audioFileRecd_t),compareAudioFileRecds); + + for(i=0; ierr,kFailMasRC,"Attempt to make directory the directory '%s' failed.",dstDir); + + // iterate the source directory + if( (dep = cmFsDirEntries( srcDir, kFileFsFl | kFullPathFsFl, &dirEntryCnt )) == NULL ) + return cmErrMsg(&ctx->err,kFailMasRC,"Unable to iterate the source directory '%s'.",srcDir); + else + { + // for each file in the source directory + for(i=0; ifnStr, "aif", NULL ); + + cmRptPrintf(&ctx->rpt,"Source File:%s\n", dep[i].name); + + switch( sel ) + { + case kMidiToAudioSelId: + // convert the MIDI to an audio impulse file + if( midiToAudio(ctx, dep[i].name, dstFn, srate ) != kOkMasRC ) + cmErrMsg(&ctx->err,kFailMasRC,"MIDI to audio failed."); + break; + + case kConvolveSelId: + // convolve impulse audio file with Hann window + if( convolve(ctx,dep[i].name, dstFn, wndMs ) != kOkMasRC ) + cmErrMsg(&ctx->err,kFailMasRC,"Convolution failed."); + break; + + case kAudioOnsetSelId: + if( audioToOnset(ctx,dep[i].name, dstFn, onsetCfgPtr ) ) + cmErrMsg(&ctx->err,kFailMasRC,"Audio to onset failed."); + break; + } + + cmFsFreeFn(dstFn); + + cmFsFreePathParts(pp); + } + + cmFsDirFreeEntries(dep); + } + + return rc; +} + + +// b0 = base of window to compare. +// b0[i] = location of sample in b0[] to compare to b1[0]. +// b1[n] = reference window +double distance( const cmSample_t* b0, const cmSample_t* b1, unsigned n, double maxDist ) +{ + double sum = 0; + const cmSample_t* ep = b1 + n; + + while(b1 < ep && sum < maxDist ) + { + sum += ((*b0)-(*b1)) * ((*b0)-(*b1)); + ++b0; + ++b1; + } + return sum; +} + + +// write a syncCtx_t record as a JSON file +masRC_t write_sync_json( cmCtx_t* ctx, const syncCtx_t* scp, const cmChar_t* outJsFn ) +{ + masRC_t rc = kOkMasRC; + unsigned i; + cmJsonH_t jsH = cmJsonNullHandle; + cmJsonNode_t* jnp; + + // create a JSON tree + if( cmJsonInitialize(&jsH,ctx) != kOkJsRC ) + { + rc = cmErrMsg(&ctx->err,kJsonFailMasRC,"JSON output handle initialization failed on '%s'.",cmStringNullGuard(outJsFn)); + goto errLabel; + } + + // create an outer container object + if((jnp = cmJsonCreateObject(jsH,NULL)) == NULL ) + goto errLabel; + + // create the 'sync' object + if((jnp = cmJsonInsertPairObject(jsH,jnp,"sync")) == NULL ) + goto errLabel; + + if( cmJsonInsertPairs(jsH,jnp, + "refDir",kStringTId,scp->refDir, + "keyDir",kStringTId,scp->keyDir, + "hopMs", kRealTId, scp->hopMs, + NULL) != kOkJsRC ) + { + goto errLabel; + } + + if((jnp = cmJsonInsertPairArray(jsH,jnp,"array")) == NULL ) + goto errLabel; + + for(i=0; isyncArrayCnt; ++i) + { + const syncRecd_t* s = scp->syncArray + i; + + if( cmJsonCreateFilledObject(jsH,jnp, + "refFn", kStringTId, s->refFn, + "refWndBegSecs",kRealTId, s->refWndBegSecs, + "refWndSecs", kRealTId, s->refWndSecs, + "keyFn", kStringTId, s->keyFn, + "keyBegSecs", kRealTId, s->keyBegSecs, + "keyEndSecs", kRealTId, s->keyEndSecs, + "keySyncIdx", kIntTId, s->keySyncIdx, + "syncDist", kRealTId, s->syncDist, + "refSmpCnt", kIntTId, s->refSmpCnt, + "keySmpCnt", kIntTId, s->keySmpCnt, + "srate", kRealTId, s->srate, + NULL) == NULL ) + { + goto errLabel; + } + } + + errLabel: + if( cmJsonErrorCode(jsH) != kOkJsRC ) + rc = cmErrMsg(&ctx->err,kJsonFailMasRC,"JSON tree construction failed on '%s'.",cmStringNullGuard(outJsFn)); + else + { + if( cmJsonWrite(jsH,cmJsonRoot(jsH),outJsFn) != kOkJsRC ) + rc = cmErrMsg(&ctx->err,kJsonFailMasRC,"JSON write failed on '%s.",cmStringNullGuard(outJsFn)); + } + + if( cmJsonFinalize(&jsH) != kOkJsRC ) + rc = cmErrMsg(&ctx->err,kJsonFailMasRC,"JSON output finalization failed on '%s'.",cmStringNullGuard(outJsFn)); + + + return rc; +} + +masRC_t _masJsonFieldNotFoundError( cmCtx_t* c, const char* msg, const char* errLabelPtr, const char* cfgFn ) +{ + masRC_t rc; + + if( errLabelPtr != NULL ) + rc = cmErrMsg( &c->err, kJsonFailMasRC, "Cfg. %s field not found:'%s' in file:'%s'.",msg,cmStringNullGuard(errLabelPtr),cmStringNullGuard(cfgFn)); + else + rc = cmErrMsg( &c->err, kJsonFailMasRC, "Cfg. %s parse failed '%s'.",msg,cmStringNullGuard(cfgFn) ); + + return rc; +} + +// Initialize a syncCtx_t record from a JSON file. +masRC_t read_sync_json( cmCtx_t* ctx, syncCtx_t* scp, const cmChar_t* jsFn ) +{ + masRC_t rc = kOkMasRC; + cmJsonNode_t* jnp; + const cmChar_t* errLabelPtr = NULL; + unsigned i; + + // if the JSON tree already exists then finalize it + if( cmJsonFinalize(&scp->jsH) != kOkJsRC ) + return cmErrMsg(&ctx->err,kJsonFailMasRC,"JSON object finalization failed."); + + // initialize a JSON tree from a file + if( cmJsonInitializeFromFile(&scp->jsH, jsFn, ctx ) != kOkJsRC ) + { + rc = cmErrMsg(&ctx->err,kJsonFailMasRC,"Initializatoin from JSON file failed on '%s'.",cmStringNullGuard(jsFn)); + goto errLabel; + } + + // find the 'sync' object + if((jnp = cmJsonFindValue(scp->jsH,"sync",cmJsonRoot(scp->jsH),kObjectTId)) == NULL ) + { + rc = cmErrMsg(&ctx->err,kJsonFailMasRC,"This JSON file does not have a 'sync' object."); + goto errLabel; + } + + // read the 'sync' object header + if( cmJsonMemberValues( jnp, &errLabelPtr, + "refDir", kStringTId, &scp->refDir, + "keyDir", kStringTId, &scp->keyDir, + "hopMs", kRealTId, &scp->hopMs, + "array", kArrayTId, &jnp, + NULL ) != kOkJsRC ) + { + rc = _masJsonFieldNotFoundError(ctx, "sync", errLabelPtr, jsFn ); + goto errLabel; + } + + // allocate the array to hold the sync array records + if((scp->syncArrayCnt = cmJsonChildCount(jnp)) > 0 ) + scp->syncArray = cmMemResizeZ(syncRecd_t,scp->syncArray,scp->syncArrayCnt); + + // read each sync recd + for(i=0; isyncArrayCnt; ++i) + { + const cmJsonNode_t* cnp = cmJsonArrayElementC(jnp,i); + syncRecd_t* s = scp->syncArray + i; + + if( cmJsonMemberValues(cnp, &errLabelPtr, + "refFn", kStringTId, &s->refFn, + "refWndBegSecs",kRealTId, &s->refWndBegSecs, + "refWndSecs", kRealTId, &s->refWndSecs, + "keyFn", kStringTId, &s->keyFn, + "keyBegSecs", kRealTId, &s->keyBegSecs, + "keyEndSecs", kRealTId, &s->keyEndSecs, + "keySyncIdx", kIntTId, &s->keySyncIdx, + "syncDist", kRealTId, &s->syncDist, + "refSmpCnt", kIntTId, &s->refSmpCnt, + "keySmpCnt", kIntTId, &s->keySmpCnt, + "srate", kRealTId, &s->srate, + NULL) != kOkJsRC ) + { + rc = _masJsonFieldNotFoundError(ctx, "sync record", errLabelPtr, jsFn ); + goto errLabel; + } + } + + errLabel: + + if( rc != kOkMasRC ) + { + if( cmJsonFinalize(&scp->jsH) != kOkJsRC ) + rc = cmErrMsg(&ctx->err,kJsonFailMasRC,"JSON finalization failed."); + + cmMemPtrFree(&scp->syncArray); + } + return rc; +} + +// Form a reference window from file 0 at refBegMs:refBegMs + wndMs. +// Compare each wndMs window in file 1 to this window and +// record the closest match. +// Notes: +// fn0 = midi file +// fn1 = audio file +masRC_t slide_match( cmCtx_t* ctx, const cmChar_t* fn0, const cmChar_t* fn1, syncRecd_t* s, unsigned hopMs, unsigned keyEndMs ) +{ + masRC_t rc = kOkMasRC; + cmAudioFileInfo_t afInfo0; + cmAudioFileInfo_t afInfo1; + cmRC_t afRC; + unsigned wndMs = s->refWndSecs * 1000; + unsigned refBegMs = s->refWndBegSecs * 1000; + unsigned keyBegMs = s->keyBegSecs * 1000; + cmAudioFileH_t af0H = cmNullAudioFileH; + cmAudioFileH_t af1H = cmNullAudioFileH; + cmSample_t *buf0 = NULL; + cmSample_t *buf1 = NULL; + unsigned minSmpIdx = cmInvalidIdx; + double minDist = DBL_MAX; + + if( cmAudioFileIsValid( af0H = cmAudioFileNewOpen(fn0,&afInfo0,&afRC, &ctx->rpt ))==false) + return cmErrMsg(&ctx->err,kFailMasRC,"The ref. audio file could not be opened.",cmStringNullGuard(fn0)); + + if( cmAudioFileIsValid( af1H = cmAudioFileNewOpen(fn1,&afInfo1,&afRC, &ctx->rpt ))==false) + { + rc = cmErrMsg(&ctx->err,kFailMasRC,"The key audio file could not be opened.",cmStringNullGuard(fn1)); + goto errLabel; + } + + assert( afInfo0.srate == afInfo1.srate ); + + unsigned chCnt = 1; + unsigned chIdx = 0; + unsigned actFrmCnt = 0; + unsigned wndSmpCnt = floor(wndMs * afInfo0.srate / 1000); + unsigned hopSmpCnt = floor(hopMs * afInfo0.srate / 1000); + unsigned smpIdx = 0; + double progIdx = 0.01; + unsigned keyBegSmpIdx = floor(keyBegMs * afInfo1.srate / 1000); + unsigned keyEndSmpIdx = floor(keyEndMs * afInfo1.srate / 1000); + unsigned hopCnt = keyEndSmpIdx==0 ? afInfo1.frameCnt / hopSmpCnt : (keyEndSmpIdx-keyBegSmpIdx) / hopSmpCnt; + + // make wndSmpCnt an even multiple of hopSmpCnt + wndSmpCnt = (wndSmpCnt/hopSmpCnt) * hopSmpCnt; + + if( refBegMs != 0 ) + smpIdx = floor(refBegMs * afInfo0.srate / 1000); + else + { + if( afInfo0.frameCnt >= wndSmpCnt ) + smpIdx = floor(afInfo0.frameCnt / 2 - wndSmpCnt/2); + else + { + wndSmpCnt = afInfo0.frameCnt; + smpIdx = 0; + } + } + + printf("wnd:%i hop:%i cnt:%i ref:%i\n",wndSmpCnt,hopSmpCnt,hopCnt,smpIdx); + + + // seek to the location of the reference window + if( cmAudioFileSeek( af0H, smpIdx ) != kOkAfRC ) + { + rc = cmErrMsg(&ctx->err,kFailMasRC,"File seek failed while moving to ref. window in '%s'.",cmStringNullGuard(fn0)); + goto errLabel; + } + + // take the center of file 1 as the key window + if( cmAudioFileSeek( af1H, keyBegSmpIdx ) != kOkAfRC ) + { + rc = cmErrMsg(&ctx->err,kFailMasRC,"File seek failed while moving to search begin location in '%s'.",cmStringNullGuard(fn1)); + goto errLabel; + } + + // allocate the window buffers + buf0 = cmMemAllocZ(cmSample_t,wndSmpCnt); // reference window + buf1 = cmMemAllocZ(cmSample_t,wndSmpCnt); // sliding window + + cmSample_t* bp0 = buf0; + cmSample_t* bp1 = buf1; + + // fill the reference window - the other buffer will be compared to this widow + if( cmAudioFileReadSample(af0H, wndSmpCnt, chIdx, chCnt, &bp0, &actFrmCnt ) != kOkAfRC ) + { + rc = cmErrMsg(&ctx->err,kFailMasRC,"Audio file read failed while reading the ref. window in '%s'.",cmStringNullGuard(fn0)); + goto errLabel; + } + + // fill all except the last hopSmpCnt samples in the sliding window + if( cmAudioFileReadSample(af1H, wndSmpCnt-hopSmpCnt, chIdx, chCnt, &bp1, &actFrmCnt ) != kOkAfRC ) + { + rc = cmErrMsg(&ctx->err,kFailMasRC,"Audio file read failed while making the first search area read in '%s'.",cmStringNullGuard(fn1)); + goto errLabel; + } + + smpIdx = keyBegSmpIdx; + bp1 = buf1 + (wndSmpCnt - hopSmpCnt); + minSmpIdx = smpIdx; + + unsigned i = 0; + + do + { + // read the new samples into the last hopSmpCnt ele's of the sliding buffer + if( cmAudioFileReadSample(af1H, hopSmpCnt, chIdx, chCnt, &bp1, &actFrmCnt ) != kOkAfRC ) + break; + + // compare the sliding window to the ref. window + double dist = distance(buf1,buf0,wndSmpCnt,minDist+1); + + // record the min dist + if( dist < minDist ) + { + //printf("%i %f %f %f\n",minSmpIdx,minDist,dist,minDist-dist); + minSmpIdx = smpIdx; + minDist = dist; + } + + smpIdx += hopSmpCnt; + + // shift off the expired samples + memmove(buf1, buf1 + hopSmpCnt, (wndSmpCnt-hopSmpCnt)*sizeof(cmSample_t)); + + ++i; + + if( i > progIdx*hopCnt ) + { + printf("%i ",(int)(round(progIdx*100))); + fflush(stdout); + progIdx += 0.01; + } + + + }while(irpt,cmLHeapNullHandle,cmSymTblNullHandle); // alloc a cmCtx object + cmBinMtxFile_t* bf0p = cmBinMtxFileAlloc(ctxp,NULL,"/home/kevin/temp/bf0.bin"); + cmBinMtxFile_t* bf1p = cmBinMtxFileAlloc(ctxp,NULL,"/home/kevin/temp/bf1.bin"); + + if( cmAudioFileSeek( af1H, minSmpIdx ) != kOkAfRC ) + goto errLabel; + + bp1 = buf1; + if( cmAudioFileReadSample(af1H, wndSmpCnt, chIdx, chCnt, &bp1, &actFrmCnt ) != kOkAfRC ) + goto errLabel; + + + cmBinMtxFileExecS(bf1p,buf1,wndSmpCnt); + cmBinMtxFileExecS(bf0p,buf0,wndSmpCnt); + cmBinMtxFileFree(&bf0p); + cmBinMtxFileFree(&bf1p); + cmCtxFree(&ctxp); + } + + cmMemPtrFree(&buf0); + cmMemPtrFree(&buf1); + cmAudioFileDelete(&af0H); + cmAudioFileDelete(&af1H); + + s->syncDist = minDist; + s->keySyncIdx = minSmpIdx; + s->refSmpCnt = afInfo0.frameCnt; + s->keySmpCnt = afInfo1.frameCnt; + s->srate = afInfo1.srate; + return rc; +} + + +// +// { +// sync_array: +// { +// { } +// } +// } +masRC_t parse_sync_cfg_file( cmCtx_t* c, const cmChar_t* fn, syncCtx_t* scp ) +{ + masRC_t rc = kOkMasRC; + cmJsonNode_t* arr = NULL; + const char* errLabelPtr = NULL; + unsigned i,j; + + if( cmJsonInitializeFromFile( &scp->jsH, fn, c ) != kOkJsRC ) + { + rc = cmErrMsg(&c->err,kJsonFailMasRC,"JSON file open failed on '%s'.",cmStringNullGuard(fn)); + goto errLabel; + } + + if( cmJsonMemberValues( cmJsonRoot(scp->jsH), &errLabelPtr, + "ref_dir", kStringTId, &scp->refDir, + "key_dir", kStringTId, &scp->keyDir, + "hop_ms", kRealTId, &scp->hopMs, + "sync_array", kArrayTId, &arr, + NULL ) != kOkJsRC ) + { + rc = _masJsonFieldNotFoundError(c, "header", errLabelPtr, fn ); + goto errLabel; + } + + if((scp->syncArrayCnt = cmJsonChildCount(arr)) == 0 ) + goto errLabel; + + scp->syncArray = cmMemAllocZ(syncRecd_t,scp->syncArrayCnt); + + for(i=0; ierr,kJsonFailMasRC,"A 'sync_array' element record at index %i is not a 6 element array in '%s'.",i,fn); + goto errLabel; + } + + for(j=0; jerr,kJsonFailMasRC,"The 'sync_array' element record contains too many fields on record index %i in '%s'.",i,fn); + goto errLabel; + } + } + + if( jsRC != kOkJsRC ) + { + rc = cmErrMsg(&c->err,kJsonFailMasRC,"The 'sync_array' element record at index %i at field index %i in '%s'.",i,j,fn); + goto errLabel; + } + } + + scp->syncArray[i].refFn = refFn; + scp->syncArray[i].refWndBegSecs = wndBegSecs; + scp->syncArray[i].refWndSecs = wndDurSecs; + scp->syncArray[i].keyFn = keyFn; + scp->syncArray[i].keyBegSecs = keyBegSecs; + scp->syncArray[i].keyEndSecs = keyEndSecs; + + //printf("beg:%f dur:%f ref:%s key:%s key beg:%f\n",wndBegSecs,wndDurSecs,refFn,keyFn,keyBegSecs); + } + + errLabel: + + if( rc != kOkMasRC ) + { + cmJsonFinalize(&scp->jsH); + cmMemPtrFree(&scp->syncArray); + } + + return rc; +} + + + +unsigned findFile( const char* fn, unsigned flags, fileRecd_t* array, unsigned fcnt ) +{ + unsigned j; + + for(j=0; jrefPtr == NULL ) + return 0; + + // if the reference is a master then f->refSmpIdx is also f->absSmpIdx + if( f->refPtr->refPtr == NULL ) + return f->refSmpIdx; + + // this file has a master - recurse + int v = calcAbsSmpIdx( f->refPtr ); + + // absSmpIdx is the absSmpIdx of the reference plus the difference to this sync point + // Note that both f->refSmpIdx and f->refPtr->keySmpIdx are both relative to the file pointed to by f->refPtr + return v + (f->refSmpIdx - f->refPtr->keySmpIdx); +} + +// Write an array of fileRecd_t[] (which was created from the output of sync_files()) to +// a JSON file which can be read by cmTimeLineReadJson(). +masRC_t masWriteJsonTimeLine( + cmCtx_t* ctx, + double srate, + fileRecd_t* fileArray, + unsigned fcnt, + const char* outFn ) +{ + masRC_t rc = kJsonFailMasRC; + unsigned i; + cmJsonH_t jsH = cmJsonNullHandle; + cmJsonNode_t* jnp; + + // create JSON tree + if( cmJsonInitialize(&jsH, ctx ) != kOkJsRC ) + { + cmErrMsg(&ctx->err,kJsonFailMasRC,"JSON time_line output tree initialization failed."); + goto errLabel; + } + + // create JSON root object + if((jnp = cmJsonCreateObject(jsH,NULL )) == NULL ) + { + cmErrMsg(&ctx->err,kJsonFailMasRC,"JSON time_line output tree root object create failed."); + goto errLabel; + } + + // create the 'time_line' object + if((jnp = cmJsonInsertPairObject(jsH,jnp,"time_line")) == NULL ) + goto errLabel; + + if( cmJsonInsertPairs(jsH,jnp, + "srate",kRealTId,srate, + NULL) != kOkJsRC ) + { + goto errLabel; + } + + if((jnp = cmJsonInsertPairArray(jsH,jnp,"objArray")) == NULL ) + goto errLabel; + + + for(i=0; iflags,kAudioFl) ? "af" : "mf"; + const cmChar_t* refLabel = f->refPtr == NULL ? "" : f->refPtr->label; + //int childOffset = f->refPtr == NULL ? 0 : f->absBegSmpIdx - f->refPtr->absBegSmpIdx; + + if( cmJsonCreateFilledObject(jsH,jnp, + "label",kStringTId,f->label, + "type", kStringTId,typeLabel, + "ref", kStringTId,refLabel, + "offset",kIntTId,f->absBegSmpIdx, + "smpCnt",kIntTId,f->smpCnt, + "trackId",kIntTId,f->groupId, + "textStr",kStringTId,f->fullFn, + NULL) == NULL ) + { + goto errLabel; + } + } + + if( cmJsonWrite(jsH,cmJsonRoot(jsH),outFn) != kOkJsRC ) + goto errLabel; + + rc = kOkMasRC; + errLabel: + + if( cmJsonFinalize(&jsH) != kOkJsRC || rc == kJsonFailMasRC ) + { + rc = cmErrMsg(&ctx->err,rc,"JSON fail while creating time_line file."); + } + + return rc; +} + +const cmChar_t* _masGenTlFileName( const cmChar_t* dir, const cmChar_t* fn, const cmChar_t* ext ) +{ + cmFileSysPathPart_t* pp = cmFsPathParts(fn); + + if( pp == NULL ) + return cmFsMakeFn(dir,fn,NULL,NULL); + + fn = cmFsMakeFn(dir,pp->fnStr,ext==NULL?pp->extStr:ext,NULL); + + cmFsFreePathParts(pp); + + return fn; +} + +enum +{ + kSequenceGroupsMasFl = 0x01, + kMakeOneGroupMasFl = 0x02 +}; + +// +// Make adjustments to fileArray[]. +// +// If kSequenceGroupsMasFl is set then adjust the groups to be sequential in time by +// separating them with 'secsBetweenGroups'. +// +// If kMakeOneGroupMasFl is set then the time line object track id is set to 0 for all objects. +// +void masProcFileArray( + fileRecd_t* fileArray, + unsigned fcnt, + unsigned smpsBetweenGroups, + unsigned flags + ) +{ + unsigned groupCnt = 0; + unsigned groupId = cmInvalidId; + unsigned i,j; + + // determine the count of groups + for(i=0; i maxEndSmpIdx ) + maxEndSmpIdx = fileArray[j].absBegSmpIdx + fileArray[j].smpCnt; + + if( fileArray[j].refPtr == NULL ) + fileArray[j].absBegSmpIdx = offsetSmpCnt; + + } + + offsetSmpCnt += maxEndSmpIdx + smpsBetweenGroups; + } + } + + // merge all groups into one group + if( cmIsFlag(flags,kMakeOneGroupMasFl ) ) + { + for(j=0; jsyncArrayCnt == 0 ) + return kOkMasRC; + + masRC_t rc = kOkMasRC; + unsigned i; + unsigned gcnt = 0; + unsigned fcnt = 0; + fileRecd_t fileArray[2*scp->syncArrayCnt]; + + // fill in the file array + for(i=0; isyncArrayCnt; ++i) + { + const syncRecd_t* s = scp->syncArray + i; + //printf("beg:%f sync:%i dist:%f ref:%s key:%s \n",s->keyBegSecs,s->keySyncIdx,s->syncDist,s->refFn,s->keyFn); + + // insert the reference (master) file prior to the dependent (slave) file + const char* fn0 = s->keyBegSecs == 0 ? s->refFn : s->keyFn; + const char* fn1 = s->keyBegSecs == 0 ? s->keyFn : s->refFn; + unsigned fl0 = s->keyBegSecs == 0 ? kMidiFl : kAudioFl; + unsigned fl1 = s->keyBegSecs == 0 ? kAudioFl : kMidiFl; + unsigned sn0 = s->keyBegSecs == 0 ? s->refSmpCnt : s->keySmpCnt; + unsigned sn1 = s->keyBegSecs == 0 ? s->keySmpCnt : s->refSmpCnt; + const char* dr0 = s->keyBegSecs == 0 ? refDir : keyDir; + const char* dr1 = s->keyBegSecs == 0 ? keyDir : refDir; + const char* ex0 = s->keyBegSecs == 0 ? refExt : keyExt; + const char* ex1 = s->keyBegSecs == 0 ? keyExt : refExt; + + const char* ffn0 = _masGenTlFileName( dr0, fn0, ex0 ); + const char* ffn1 = _masGenTlFileName( dr1, fn1, ex1 ); + + fcnt = insertFile( fn0, ffn0, fl0, sn0, s->srate, fileArray, fcnt); + fcnt = insertFile( fn1, ffn1, fl1, sn1, s->srate, fileArray, fcnt); + } + + // locate the reference file in each sync recd + for(i=0; isyncArrayCnt; ++i) + { + const syncRecd_t* s = scp->syncArray + i; + unsigned mfi = findFile( s->refFn, kMidiFl, fileArray, fcnt ); + unsigned afi = findFile( s->keyFn, kAudioFl, fileArray, fcnt ); + + assert( mfi != -1 && afi != -1 ); + + fileRecd_t* mfp = fileArray + mfi; + fileRecd_t* afp = fileArray + afi; + + if( s->keyBegSecs == 0 ) + { + // lock audio to midi + afp->refIdx = mfi; + afp->refPtr = mfp; + afp->refSmpIdx = floor( s->refWndBegSecs * s->srate ); + afp->keySmpIdx = s->keySyncIdx; + } + else + { + // lock midi to audio + mfp->refIdx = afi; + mfp->refPtr = afp; + mfp->refSmpIdx = s->keySyncIdx; + mfp->keySmpIdx = floor( s->refWndBegSecs * s->srate ); + } + } + + // Calculate the absolute sample indexes and set groupId's. + // Note that this process depends on reference files being processed before their dependents + for(i=0; irefPtr == NULL ) + { + f->groupId = gcnt++;// form a new group + f->absSmpIdx = 0; // absSmpIdx is meaningless for master files becuase they do not have a sync point + f->absBegSmpIdx = 0; // the master file location is always 0 + } + else // this is a slave file + { + f->absSmpIdx = calcAbsSmpIdx(f); // calc the absolute time of the sync location + //f->absBegSmpIdx = f->absSmpIdx - f->keySmpIdx; // calc the absolute begin time of the file + f->absBegSmpIdx = f->refSmpIdx - f->keySmpIdx; + f->groupId = f->refPtr->groupId; // set the group id + } + } + + // At this point the absBegSmpIdx of the master file in each group is set to 0 + // and the absBegSmpIdx of slave files is then set relative to 0. This means that + // some slave files may have negative offsets if they start prior to the master. + // + // Set the earliest file in the group to have an absBegSmpIdx == 0 and shift all + // other files relative to this. After this process all absBegSmpIdx values will + // be positive. + // + if(0) + { + for(i=0; igroupId==i && (begSmpIdx == -1 || f->absBegSmpIdx < begSmpIdx) ) + begSmpIdx = f->absBegSmpIdx; + + } + + // subtract the earliest absolute start time from all files in groupId==i + for(j=0; jgroupId == i ) + f->absBegSmpIdx -= begSmpIdx; + } + } + } + + // fill in the text label assoc'd with each file + unsigned acnt = 0; + unsigned mcnt = 0; + + for(i=0; iflags,kAudioFl) ) + snprintf(f->label,kLabelCharCnt,"af-%i",acnt++); + else + { + if( cmIsFlag(f->flags,kMidiFl) ) + snprintf(f->label,kLabelCharCnt,"mf-%i",mcnt++); + else + { assert(0); } + } + } + + if( fcnt > 0 ) + { + cmReal_t srate = fileArray[0].srate; + unsigned smpsBetweenGroups = floor(secsBetweenGroups * srate ); + masProcFileArray(fileArray,fcnt,smpsBetweenGroups,procFlags); + + rc = masWriteJsonTimeLine(ctx,fileArray[0].srate,fileArray,fcnt,outFn); + + for(i=0; isyncArrayCnt; ++i) + { + syncRecd_t* s = scp->syncArray + i; + + // form the ref (midi) and key (audio) file names + const cmChar_t* refFn = cmFsMakeFn(scp->refDir, s->refFn, NULL, NULL); + const cmChar_t* keyFn = cmFsMakeFn(scp->keyDir, s->keyFn, NULL, NULL); + + double keyEndSecs = s->keyEndSecs; + + // if the cur key fn is the same as the next key file. Use the search start + // location (keyBegSecs) of the next sync recd as the search end + // location for this file. + if( i < scp->syncArrayCnt-1 && strcmp(s->keyFn, scp->syncArray[i+1].keyFn) == 0 ) + { + keyEndSecs = scp->syncArray[i+1].keyBegSecs; + + if( keyEndSecs < s->keyBegSecs ) + { + rc = cmErrMsg(&ctx->err,kParamErrMasRC,"The key file search area start times for for multiple sync records referencing the the same key file should increment in time."); + } + } + + masRC_t rc0; + if((rc0 = slide_match(ctx,refFn,keyFn,s,scp->hopMs,floor(keyEndSecs*1000))) != kOkMasRC) + { + cmErrMsg(&ctx->err,rc0,"Slide match failed on Ref:%s Key:%s.",cmStringNullGuard(refFn),cmStringNullGuard(keyFn)); + rc = rc0; + } + + printf("\nbeg:%f end:%f sync:%i dist:%f ref:%s key:%s \n",s->keyBegSecs,keyEndSecs,s->keySyncIdx,s->syncDist,refFn,keyFn); + + cmFsFreeFn(keyFn); + cmFsFreeFn(refFn); + } + + return rc; +} + +void masSyncCtxInit(syncCtx_t* scp) +{ + memset(scp,0,sizeof(syncCtx_t)); + scp->jsH = cmJsonNullHandle; +} + +masRC_t masSyncCtxFinalize(cmCtx_t* ctx, syncCtx_t* scp) +{ + masRC_t rc = kOkMasRC; + if( cmJsonFinalize(&scp->jsH) != kOkJsRC ) + rc = cmErrMsg(&ctx->err,kJsonFailMasRC,"syncCtx JSON finalization failed."); + + cmMemFree(scp->syncArray); + scp->syncArrayCnt = 0; + return rc; +} + +masRC_t masMidiToImpulse( cmCtx_t* ctx, const masPgmArgs_t* p ) +{ + assert(p->input!=NULL && p->output!=NULL); + + if( cmFsIsDir(p->input) ) + return fileDriver(ctx, kMidiToAudioSelId, p->input, p->output, p->srate, 0, NULL ); + + return midiToAudio(ctx, p->input, p->output, p->srate ); +} + +masRC_t masAudioToOnset( cmCtx_t* ctx, const masPgmArgs_t* p ) +{ + assert(p->input!=NULL && p->output!=NULL); + + if( cmFsIsDir(p->input) ) + return fileDriver(ctx, kAudioOnsetSelId, p->input, p->output, 0, 0, &p->onsetCfg ); + + return audioToOnset(ctx, p->input, p->output, &p->onsetCfg ); +} + +masRC_t masConvolve( cmCtx_t* ctx, const masPgmArgs_t* p ) +{ + assert(p->input!=NULL && p->output!=NULL); + + if( cmFsIsDir(p->input) ) + return fileDriver(ctx, kConvolveSelId, p->input, p->output, 0, p->wndMs, NULL ); + + return convolve(ctx, p->input, p->output, p->wndMs ); +} + +masRC_t masSync( cmCtx_t* ctx, const masPgmArgs_t* p ) +{ + masRC_t rc = kOkMasRC,rc0; + syncCtx_t sc; + + assert(p->input!=NULL && p->output!=NULL); + + masSyncCtxInit(&sc); + + if( (rc = parse_sync_cfg_file(ctx, p->input, &sc )) == kOkMasRC ) + if((rc = sync_files(ctx, &sc )) == kOkMasRC ) + rc = write_sync_json(ctx,&sc,p->output); + + rc0 = masSyncCtxFinalize(ctx,&sc); + + return rc!=kOkMasRC ? rc : rc0; +} + + +masRC_t masGenTimeLine( cmCtx_t* ctx, const masPgmArgs_t* p ) +{ + masRC_t rc,rc0; + syncCtx_t sc; + + if( p->refDir == NULL ) + return cmErrMsg(&ctx->err,kParamErrMasRC,"A directory must be provided to locate the audio and MIDI files. See the program parameter 'ref-dir'."); + + if( p->keyDir == NULL ) + return cmErrMsg(&ctx->err,kParamErrMasRC,"A directory must be provided to locate the audio and MIDI files. See the program parameter 'key-dir'."); + + assert(p->input!=NULL && p->output!=NULL); + + masSyncCtxInit(&sc); + + if((rc = read_sync_json(ctx,&sc,p->input)) != kOkMasRC ) + goto errLabel; + + // TODO: Add these as program options, also add a --dry-run option + // + unsigned procFlags = 0; //kZeroBaseTimeMasFl | kSequenceGroupsMasFl | kMakeOneGroupMasFl; + double secsBetweenGroups = 60.0; + + if((rc = masCreateTimeLine(ctx, &sc, p->output, p->refDir, p->keyDir, p->refExt, p->keyExt, secsBetweenGroups, procFlags)) != kOkMasRC ) + goto errLabel; + + errLabel: + rc0 = masSyncCtxFinalize(ctx,&sc); + + return rc!=kOkMasRC ? rc : rc0; +} + +// Given a time line file and a marker file, insert the markers in the time line and +// then write the time line to an output file. The marker file must have the following format: +//{ +// markerArray : [ +// { sect:1 beg:630.0 end:680.0 label:"Sec 3 m10"}, +// { sect:3 beg:505.1 end:512.15 label:"Sec 4 m12"}, +// { sect:4 beg:143.724490 end:158.624322 label:"Sec 6, 6a m14-16, #2 (A) slower tempo"}, +// ] +// } +// +// NOTES: +// 1) beg/end are in seconds, +// 2) 'sect' refers to the audio file number (e.g. "Piano_01.wav,Piano_03.wav,Piano_04.wav") +// +masRC_t masLoadMarkers( cmCtx_t* ctx, const masPgmArgs_t* p ) +{ + assert(p->input!=NULL); + assert(p->markFn!=NULL); + assert(p->output!=NULL); + + masRC_t rc = kOkMasRC; + const cmChar_t* tlFn = p->input; + const cmChar_t* mkFn = p->markFn; + const cmChar_t* outFn = p->output; + const cmChar_t* afFmtStr = "/home/kevin/media/audio/20110723-Kriesberg/Audio Files/Piano 3_%02.2i.wav"; + cmTlH_t tlH = cmTimeLineNullHandle; + cmJsonH_t jsH = cmJsonNullHandle; + cmJsonNode_t* anp = NULL; + + // create the time line + if( cmTimeLineInitializeFromFile(ctx, &tlH, NULL, NULL, tlFn, p->prefixPath ) != kOkTlRC ) + return cmErrMsg(&ctx->err,kTimeLineFailMasRC,"Time line created failed on '%s'.", cmStringNullGuard(tlFn)); + + // open the marker file + if( cmJsonInitializeFromFile(&jsH, mkFn, ctx ) != kOkJsRC ) + { + rc = cmErrMsg(&ctx->err,kJsonFailMasRC,"Marker file open failed on '%s'.",cmStringNullGuard(mkFn)); + goto errLabel; + } + + // locate the marker array in the marker file + if((anp = cmJsonFindValue(jsH,"markerArray",NULL,kArrayTId)) == NULL ) + { + rc = cmErrMsg(&ctx->err,kJsonFailMasRC,"The marker file is missing a 'markerArray' node in '%s'.",cmStringNullGuard(mkFn)); + goto errLabel; + } + + unsigned i; + unsigned markerCnt = cmJsonChildCount(anp); + for(i=0; ierr,kJsonFailMasRC,"The field '%s' was missing on the marker record at index %i.",errLabel,i); + else + rc = cmErrMsg(&ctx->err,kJsonFailMasRC,"An error occurred while reading the marker record at index %i.",i); + goto errLabel; + } + + cmChar_t* afFn = cmTsPrintfS(afFmtStr,sectId); + cmTlAudioFile_t* tlop; + + // find the audio file this marker refers to in the time line + if(( tlop = cmTimeLineFindAudioFile(tlH,afFn)) == NULL ) + cmErrWarnMsg(&ctx->err,kParamErrMasRC,"The audio file '%s' associated with the marker record at index %i could not be found in the time line.",cmStringNullGuard(afFn),i); + else + { + // convert the marker seconds to samples + unsigned begSmpIdx = floor(cmTimeLineSampleRate(tlH) * begSecs); + unsigned durSmpCnt = floor(cmTimeLineSampleRate(tlH) * endSecs) - begSmpIdx; + + // insert the marker into the time line + if( cmTimeLineInsert(tlH,cmTsPrintfS("Mark %i",i),kMarkerTlId,markText,begSmpIdx,durSmpCnt,tlop->obj.name, tlop->obj.seqId) != kOkTlRC ) + { + rc = cmErrMsg(&ctx->err,kTimeLineFailMasRC,"Marker record insertion failed for marker at record index %i.",i); + goto errLabel; + } + } + + } + + // write the time line as a JSON file + if( cmTimeLineWrite(tlH,outFn) != kOkTlRC ) + { + rc = cmErrMsg(&ctx->err,kTimeLineFailMasRC,"Time line write to '%s'. failed.",cmStringNullGuard(outFn)); + goto errLabel; + } + + errLabel: + cmJsonFinalize(&jsH); + cmTimeLineFinalize(&tlH); + return rc; +} + +masRC_t masTestStub( cmCtx_t* ctx, const masPgmArgs_t* p ) +{ + //return masSync(ctx,p); + masRC_t rc = kOkMasRC; + + const char* scFn = "/home/kevin/src/mas/src/data/mod0.txt"; + const char* tlFn = "/home/kevin/src/mas/src/data/tl3.js"; + const char* mdDir= "/home/kevin/media/audio/20110723-Kriesberg/midi"; + + if(0) + { + cmMidiByte_t x[] = { 37, 65, 87 }; + midiStringSearch(ctx, mdDir, x, sizeof(x)/sizeof(x[0]) ); + return rc; + } + + if(1) + { + const cmChar_t* aFn = "/Users/kevin/temp/mas/sine_96k_24bits.aif"; + double srate = 96000; + unsigned bits = 24; + double hz = 1; + double gain = 1; + double secs = 1; + cmAudioFileSine( ctx, aFn, srate, bits, hz, gain, secs ); + return rc; + } + + cmTimeLinePrintFn(ctx, tlFn, p->prefixPath, &ctx->rpt ); + return rc; + + //cmScoreSyncTimeLineTest(ctx, tlFn, scFn ); + //return rc; + + cmScoreTest(ctx,scFn); + return rc; + + cmTimeLineTest(ctx,tlFn,p->prefixPath); + return rc; + + + //const char* inFn = "/home/kevin/temp/mas/out0.bin"; + //const char* faFn = "/home/kevin/temp/mas/file0.bin"; + //const char* outFn = "/home/kevin/src/mas/src/data/file0.js"; + //const char* mdir = "/home/kevin/media/audio/20110723-Kriesberg/midi"; + //const char* adir = "/home/kevin/media/audio/20110723-Kriesberg/Audio Files"; + //createFileArray(ctx, inFn, outFn ); + //printFileArray( ctx, faFn, outFn, adir, mdir); + + return rc; +} + + + +int main( int argc, char* argv[] ) +{ + + // initialize the heap check library + bool memDebugFl = cmDEBUG_FL; + unsigned memPadByteCnt = memDebugFl ? 8 : 0; + unsigned memAlignByteCnt = 16; + unsigned memFlags = memDebugFl ? kTrackMmFl | kDeferFreeMmFl | kFillUninitMmFl : 0; + masRC_t rc = kOkMasRC; + cmPgmOptH_t poH = cmPgmOptNullHandle; + cmCtx_t ctx; + masPgmArgs_t args; + + enum + { + kInputFileSelId = kBasePoId, + kOutputFileSelId, + kExecSelId, + kWndMsSelId, + kHopFactSelId, + kAudioChIdxSelId, + kWndFrmCntSelId, + kPreWndMultSelId, + kThresholdSelId, + kMaxFreqHzSelId, + kFiltCoeffSelId, + kPreDlyMsSelId, + kMedFltWndMsSelId, + kFilterSelId, + kSmthFiltSelId, + kMedianFiltSelId, + kSrateSelId, + kRefDirSelId, + kKeyDirSelId, + kRefExtSelId, + kKeyExtSelId, + kMarkFnSelId, + kPrefixPathSelId, + }; + + const cmChar_t helpStr0[] = + { + // 1 2 3 4 5 6 7 8 + "Usage: mas -{m|a|c} -i 'input' -o 'output' \n\n" + }; + + const cmChar_t helpStr1[] = + { + // 1 2 3 4 5 6 7 8 + "If --input option specifies a directory then all files in the directory are\n" +"taken as input files. In this case the names of the output files are generated\n" +"automatically and the --ouptut option must specify a directory to receive all\n" +"the output files.\n\nIf the --input option specifies a file then the --output\n" +" option should specifiy the complete name of the output file.\n" + }; + + memset(&args,0,sizeof(args)); + + cmCtxSetup(&ctx,"Project",print,print,NULL,memPadByteCnt,memAlignByteCnt,memFlags); + cmMdInitialize( memPadByteCnt, memAlignByteCnt, memFlags, &ctx.rpt ); + cmFsInitialize( &ctx, "mas" ); + cmTsInitialize( &ctx ); + cmPgmOptInitialize(&ctx,&poH,helpStr0,helpStr1); + + + // poH numId charId wordId flags enumId default return ptr cnt help string + cmPgmOptInstallStr( poH, kInputFileSelId, 'i', "input", kReqPoFl, NULL, &args.input, 1, "Input file or directory." ); + cmPgmOptInstallStr( poH, kOutputFileSelId, 'o', "output", kReqPoFl, NULL, &args.output, 1, "Output file or directory." ); + cmPgmOptInstallEnum(poH, kExecSelId, 'm', "midi_to_impulse", kReqPoFl, kMidiToAudioSelId,cmInvalidId, &args.selId, 1, "Create an audio impulse file from a MIDI file.","Command Code" ); + cmPgmOptInstallEnum(poH, kExecSelId, 'a', "onsets", kReqPoFl, kAudioOnsetSelId, cmInvalidId, &args.selId, 1, "Create an audio impulse file from the audio event onset detector.",NULL ); + cmPgmOptInstallEnum(poH, kExecSelId, 'c', "convolve", kReqPoFl, kConvolveSelId, cmInvalidId, &args.selId, 1, "Convolve a Hann window with an audio file.",NULL ); + cmPgmOptInstallEnum(poH, kExecSelId, 'y', "sync", kReqPoFl, kSyncSelId, cmInvalidId, &args.selId, 1, "Run a synchronization process based on a JSON sync control file and generate a sync. output JSON file..",NULL); + cmPgmOptInstallEnum(poH, kExecSelId, 'g', "gen_time_line", kReqPoFl, kGenTimeLineSelId,cmInvalidId, &args.selId, 1, "Generate a time-line JSON file from a sync. output JSON file.",NULL); + cmPgmOptInstallEnum(poH, kExecSelId, 'k', "markers", kReqPoFl, kLoadMarkersSelId,cmInvalidId, &args.selId, 1, "Read markers into the time line.",NULL); + cmPgmOptInstallEnum(poH, kExecSelId, 'T', "test", kReqPoFl, kTestStubSelId, cmInvalidId, &args.selId, 1, "Run the test stub.",NULL ), + cmPgmOptInstallDbl( poH, kWndMsSelId, 'w', "wnd_ms", 0, 42.0, &args.wndMs, 1, "Analysis window look in milliseconds." ); + cmPgmOptInstallUInt(poH, kHopFactSelId, 'f', "hop_factor", 0, 4, &args.onsetCfg.hopFact, 1, "Sliding window hop factor 1=1:1 2=1:2 4=1:4 ..."); + cmPgmOptInstallUInt(poH, kAudioChIdxSelId, 'u', "ch_idx", 0, 0, &args.onsetCfg.audioChIdx, 1, "Audio channel index."); + cmPgmOptInstallUInt(poH, kWndFrmCntSelId, 'r', "wnd_frm_cnt", 0, 3, &args.onsetCfg.wndFrmCnt, 1, "Audio onset window frame count."); + cmPgmOptInstallDbl( poH, kPreWndMultSelId, 'x', "wnd_pre_mult", 0, 3, &args.onsetCfg.preWndMult, 1, "Audio onset pre-window multiplier."); + cmPgmOptInstallDbl( poH, kThresholdSelId, 't', "threshold", 0, 0.6, &args.onsetCfg.threshold, 1, "Audio onset threshold value."); + cmPgmOptInstallDbl( poH, kMaxFreqHzSelId, 'z', "max_frq_hz", 0, 20000, &args.onsetCfg.maxFrqHz, 1, "Audio onset maximum analysis frequency."); + cmPgmOptInstallDbl( poH, kFiltCoeffSelId, 'e', "filt_coeff", 0, 0.7, &args.onsetCfg.filtCoeff, 1, "Audio onset smoothing filter coefficient."); + cmPgmOptInstallDbl( poH, kPreDlyMsSelId, 'd', "pre_delay_ms", 0, 0, &args.onsetCfg.preDelayMs, 1, "Move each detected audio onset backwards in time by this amount."); + cmPgmOptInstallDbl( poH, kMedFltWndMsSelId, 'l',"med_flt_wnd_ms", 0, 50, &args.onsetCfg.medFiltWndMs, 1, "Length of the onset detection median filter. Ignored if the median filter is not used."); + cmPgmOptInstallEnum(poH, kFilterSelId, 'b', "smooth_filter", 0, kSmthFiltSelId, cmInvalidId, &args.onsetCfg.filterId, 1, "Apply a smoothing filter to the onset detection function.","Audio onset filter"); + cmPgmOptInstallEnum(poH, kFilterSelId, 'n', "median_filter", 0, kMedianFiltSelId, cmInvalidId, &args.onsetCfg.filterId, 1, "Apply a median filter to the onset detections function.", NULL ); + cmPgmOptInstallDbl( poH, kSrateSelId, 's', "sample_rate", 0, 44100, &args.srate, 1, "MIDI to impulse output sample rate."); + cmPgmOptInstallStr( poH, kRefDirSelId, 'R', "ref_dir", 0, NULL, &args.refDir, 1, "Location of the reference files. Only used with 'gen_time_line'."); + cmPgmOptInstallStr( poH, kKeyDirSelId, 'K', "key_dir", 0, NULL, &args.keyDir, 1, "Location of the key files. Only used with 'gen_time_line'."); + cmPgmOptInstallStr( poH, kRefExtSelId, 'M', "ref_ext", 0, NULL, &args.refExt, 1, "Reference file extension. Only used with 'gen_time_line'."); + cmPgmOptInstallStr( poH, kKeyExtSelId, 'A', "key_ext", 0, NULL, &args.keyExt, 1, "Key file extension. Only used with 'gen_time_line'."); + cmPgmOptInstallStr( poH, kMarkFnSelId, 'E', "mark_fn", 0, NULL, &args.markFn, 1, "Marker file name"); + cmPgmOptInstallStr( poH, kPrefixPathSelId, 'P', "prefix_path", 0, NULL, &args.prefixPath, 1, "Time Line data file prefix path"); + + + if((rc = cmPgmOptRC(poH,kOkPoRC)) != kOkPoRC ) + goto errLabel; + + if( cmPgmOptParse(poH, argc, argv ) != kOkPoRC ) + goto errLabel; + + if( cmPgmOptHandleBuiltInActions(poH,&ctx.rpt) ) + { + switch( args.selId ) + { + case kMidiToAudioSelId: + masMidiToImpulse(&ctx,&args); + break; + + case kAudioOnsetSelId: + args.onsetCfg.wndMs = args.wndMs; + switch( args.onsetCfg.filterId ) + { + case kSmthFiltSelId: args.onsetCfg.filterId = kSmoothFiltId; break; + case kMedianFiltSelId: args.onsetCfg.filterId = kMedianFiltId; break; + default: + args.onsetCfg.filterId = 0; + } + + masAudioToOnset(&ctx,&args); + break; + + case kConvolveSelId: + masConvolve(&ctx,&args); + break; + + case kSyncSelId: + masSync(&ctx,&args); + break; + + case kGenTimeLineSelId: + masGenTimeLine(&ctx,&args); + break; + + case kLoadMarkersSelId: + masLoadMarkers(&ctx,&args); + break; + + case kTestStubSelId: + masTestStub(&ctx,&args); + break; + + default: + { assert(0); } + } + } + + errLabel: + cmPgmOptFinalize(&poH); + cmTsFinalize(); + cmFsFinalize(); + cmMdReport( kIgnoreNormalMmFl ); + cmMdFinalize(); + return rc; + +} +/* +Use Cases: +1) Synchronize Audio to MIDI based on onset patterns: + + a) Convert MIDI to audio impulse files: + + mas -m -i -o -s + + Notes: + 1) If is given then use all files + in the directory as input otherwise convert a + single file. + 2) The files written to are audio files with + impulses written at the location of note on msg's. + The amplitude of the the impulse is velocity/127. + + b) Convert the onsets in audio file(s) to audio impulse + file(s). + + mas -a -i -o + -w -f -u -r + -x -t -z -e + + 1) If is given then use all files + in the directory as input otherwise convert a + single file. + 2) The onset detector uses a spectral flux based + algorithm. + See cmOnset.h/.c for an explanation of the + onset detection parameters. + + + c) Convolve impulse files created in a) and b) with a + Hann window to widen the impulse width. + + mas -c -i -o -w + + 1) If is given then use all files + in the directory as input otherwise convert a + single file. + 2) gives the width of the Hann window. + + d) Synchronize MIDI and Audio based convolved impulse + files based on their onset patterns. + + mas -y -i -o + + 1) The file has the following format: + { + ref_dir : "/home/kevin/temp/mas/midi_conv" // location of ref files + key_dir : "/home/kevin/temp/mas/onset_conv" // location of key files + hop_ms : 25 // sliding window increment + + sync_array : + [ + // ref_fn wnd_beg_secs wnd_dur_secs key_fn key_beg_secs, key_end_secs + [ "1.aif", 678, 113, "Piano 3_01.aif", 239.0, 417.0], + [ "3.aif", 524, 61, "Piano 3_06.aif", 556.0, 619.0], + ] + } + + Notes: + a. The 'window' is the section of the reference file which is compared + to the key file search area to by sliding it + in increments of 'hop_ms' samples. + + b. Set 'key_end_secs' to 0 to search to the end of the file. + + c. When one key file matches to multiple reference files the + key files sync recd should be listed consecutively. This way + the earlier searches can stop when they reach the beginning + of the next sync records search region. See sync_files(). + + Note that by setting to a non-zero value + as occurs in the multi-key-file case has a subtle effect of + changing the master-slave relationship between the reference + an key file. + + In general the reference file is the master and the key file + is the slave. When a non-zero is given however + this relationship reverses. See masCreateTimeLine() for + how this is used to assign file group id's during the + time line creation. + + 3) The has the following form. + + { + "sync" : + { + "refDir" : "/home/kevin/temp/mas/midi_conv" + "keyDir" : "/home/kevin/temp/mas/onset_conv" + "hopMs" : 25.000000 + + "array" : + [ + + // + // sync results for "1.aif" to "Piano 3_01.aif" + // + + { + // The following block of fields were copied from . + "refFn" : "1.aif" + "refWndBegSecs" : 678.000000 + "refWndSecs" : 113.000000 + "keyFn" : "Piano 3_01.aif" + "keyBegSecs" : 239.000000 + "keyEndSecs" : 417.000000 + + // Sync. location of the 'window' in the key file. + // Sample index into the key file which matches to the first sample + // in the reference window. + "keySyncIdx" : 25768800 // Offset into the key file of the best match. + + "syncDist" : 4184.826108 // Match distance score for the sync location. + "refSmpCnt" : 200112000 // Count of samples in the reference file. + "keySmpCnt" : 161884800 // Count of samples in the key file. + "srate" : 96000.000000 // Sample rate of the reference and key file. + }, + ] + } + } + +2) Create a time line from the results of a synchronization. A time line is a data structure + (See cmTimeLine.h/.c) which maintains a time based ordering of Audio files, MIDI files, + and arbitrary markers. + + mas -g -i -o -R -K -M -A + + The output file produced as a result of a previous MIDI <-> Audio synchronization. + + Location of the reference files (MIDI) used for the synchronization. + File extension used by the reference files. + Locate of the key files (Audio) used for the synchronization. + File extension used by the key files. + + 1) The time line 'trackId' assigned to each time line object is based on the files + 'groupId'. A common group id is given to sets of files which are + locked in time relative to one another. For example + if file B and C are synced to master file A and + file D is synced to file E which is synced to master + file F. Then files A,B,C will be given one group + id and files D,E and F will be given another group id. + (See masCreateTimeLine()). + + 2) The time line object 'offset' values gives the offset in samples where the object + begins relative to other objects in the group. Note that the master object in the + group may not begin at offset 0 if there are slave objects which start before it. + + + + */ + +/* MIDI File Durations (rounded to next minute) + + +1 35 678 113 01 0 +2 30 53 114 03 0 + 655 116 04 0 + 1216 102 05 0 +3 19 524 61 06 0 + 958 40 07 0 +4 15 206 54 08 0 + 797 40 09 0 +5 40 491 104 11 0 + 1712 109 12 0 + 2291 84 13 0 +6 44 786 105 13 299 + 1723 112 14 0 +7 3 99 41 15 0 +8 38 521 96 17 0 + 1703 71 18 0 +9 31 425 104 19 0 +10 2 16 19 21 0 +12 10 140 87 21 222 +13 14 377 58 21 942 +15 18 86 71 21 1975 + 593 79 22 0 +16-2 16 211 75 23 0 +17-1 8 129 38 24 0 +17-2 16 381 54 26 0 +18 22 181 98 27 0 +19 22 134 57 28 0 +20 7 68 44 29 0 +*/