commit 63991ae469347164cee1cb1f8b1261976322d5be Author: kevin Date: Thu Jul 30 10:26:58 2020 -0400 Initial commit 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 0000000..b748ea2 Binary files /dev/null and b/doc/score_follow_0.png differ 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 +*/