Create a gist now

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Attempt to repair an interrupted QuickTime Player audio recording
<?xml version="1.0" encoding="UTF-8"?>
<ufwb version="1.14">
<grammar name="AIFC grammar" start="id:530" author="Andre LaBranche" uti="public.aifc-audio">
<description>Grammar for AIFC files</description>
<structure name="AIFC file" id="530" length="Size + 4" alignment="0" encoding="ISO_8859-1:1987" endian="big" signed="no" valueexpression="Size">
<string name="Type ID" mustmatch="yes" id="531" fillcolor="FFD8B2" type="fixed-length" length="4" encoding="ISO_8859-1:1987">
<fixedvalues>
<fixedvalue name="TypeID" value="FORM"/>
</fixedvalues>
</string>
<number name="Size" id="532" fillcolor="D8D8FE" type="integer" length="4"/>
<string name="Form Type" mustmatch="yes" id="533" fillcolor="FFB2FE" type="fixed-length" length="4">
<fixedvalues>
<fixedvalue name="Form Type" value="AIFC"/>
<fixedvalue name="Form Type" value="AIFF"/>
</fixedvalues>
</string>
<structure name="AIFC Version" id="534" length="0" alignment="2" repeatmin="0" valueexpression="timeStamp">
<string name="Type ID" mustmatch="yes" id="535" fillcolor="FFD8B2" type="fixed-length" length="4">
<fixedvalues>
<fixedvalue name="Version" value="FVER"/>
</fixedvalues>
</string>
<number name="Size" mustmatch="yes" id="536" fillcolor="D8D8FE" type="integer" length="4">
<fixedvalues>
<fixedvalue name="Size" value="4"/>
</fixedvalues>
</number>
<number name="timeStamp" mustmatch="yes" id="537" fillcolor="FFB2FE" type="integer" length="4">
<fixedvalues>
<fixedvalue name="Version" value="2726318400"/>
</fixedvalues>
</number>
</structure>
<structure name="Common" id="539" length="Size + 8" alignment="2" valueexpression="Desc">
<string name="Type ID" mustmatch="yes" id="540" fillcolor="FFD8B2" type="fixed-length" length="4">
<fixedvalues>
<fixedvalue name="Type ID" value="COMM"/>
</fixedvalues>
</string>
<number name="Size" id="541" fillcolor="D8D8FE" type="integer" length="4"/>
<number name="NumChannels" id="542" fillcolor="B2FDFF" type="integer" length="2"/>
<number name="numSampleFrames" id="543" fillcolor="FFFAB2" type="integer" length="4"/>
<number name="sampleSize" id="544" fillcolor="D8D8FE" type="integer" length="2"/>
<structure name="SampleRate" id="545" length="10" alignment="0" repeatmin="0" valueexpression="Rate">
<number name="Rate" id="546" fillcolor="FFE4B2" type="float" length="64" lengthunit="bit"/>
</structure>
<string name="Compression" id="548" fillcolor="F3FFB2" repeatmin="0" type="fixed-length" length="4"/>
<string name="Desc" id="549" fillcolor="B2D4FF" repeatmin="0" type="pascal" length="actual"/>
</structure>
<structure name="Chan" id="551" alignment="0" repeatmin="0" fillcolor="D5D5D5">
<string name="Type ID" mustmatch="yes" id="552" fillcolor="FFD8B2" type="fixed-length" length="4">
<fixedvalues>
<fixedvalue name="Type ID" value="CHAN"/>
</fixedvalues>
</string>
<number name="Size" id="553" fillcolor="D8D8FE" type="integer" length="4"/>
<structure name="ChanData" id="554" length="Size" alignment="2"/>
</structure>
<structure name="Filler" id="557" length="0" alignment="0" repeatmin="0" fillcolor="E6E6E6" valueexpression="Size">
<string name="Type ID" mustmatch="yes" id="558" type="fixed-length" length="4">
<fixedvalues>
<fixedvalue name="Type ID" value="FLLR"/>
</fixedvalues>
</string>
<number name="Size" id="559" type="integer" length="4"/>
<structure name="Filler" id="560" length="Size" alignment="0" valueexpression="Size"/>
</structure>
<structure name="Extra" id="563" length="Size + 8" alignment="2" repeatmin="0" repeatmax="-1">
<string name="Type ID" mustmatch="yes" id="564" fillcolor="FFD8B2" type="fixed-length" length="4">
<fixedvalues>
<fixedvalue name="Type" value="AUTH"/>
<fixedvalue name="Type" value="NAME"/>
<fixedvalue name="Type" value="ANNO"/>
</fixedvalues>
</string>
<number name="Size" id="565" fillcolor="D8D8FE" type="integer" length="4"/>
<string name="Value" id="566" fillcolor="FFB2FE" type="fixed-length" length="Size"/>
</structure>
<structure name="SSND" id="568" length="Size - 8" alignment="2" repeatmin="0" repeatmax="-1" valueexpression="Size">
<string name="Type ID" mustmatch="yes" id="569" fillcolor="FFD8B2" type="fixed-length" length="4">
<fixedvalues>
<fixedvalue name="Type ID" value="SSND"/>
</fixedvalues>
</string>
<number name="Size" id="570" fillcolor="D8D8FE" type="integer" length="4"/>
<number name="Offset" id="571" fillcolor="FFB2FE" type="integer" length="4"/>
<number name="blockSize" id="572" fillcolor="D8D8FE" type="integer" length="4"/>
<structure name="Sound Data" id="573" length="prev.Size - 4" alignment="0" fillcolor="D8F9C7" valueexpression="Size"/>
</structure>
</structure>
</grammar>
</ufwb>
#!/bin/zsh
#set -e
#set -x
# dre@mac.com
#
# This script attempts to repair a partial AIFC recording generated by
# QuickTime Player. This happens when QTP is interrupted while recording.
# Look for partial files with:
# find ~/Library/Containers/com.apple.QuickTimePlayerX -name "*.aifc"
# AIFC Reference: http://muratnkonar.com/aiff/index.html
#
# NOTE! This script edits the target file in place.
unset s # file size
unset form # FORM offset
unset ssnd # SSND offset
unset a_sz # AIFC data size
unset s_sz # SSND data size
# Check command line input
if [ -z $1 ] ; then
echo 'Supply the (quoted!) path of a broken AIFC recording as the only '
echo 'argument.'
exit 1
else
if [ ! -f $1 ] ; then
echo "Can't find file $1!"
exit 1
fi
file=${1}
fi
# Examine this many bytes in the file for headers to fix
peek=10000
# Error handling
checkNull() {
if [[ $# -ne 2 ]] ; then
echo "Incorrect number of args!"
exit 1
fi
if [ -z ${1} ] ; then
echo "Function description missing!"
exit 1
fi
if [ -z ${2} ] ; then
echo "Failed to ${1} - bailing."
exit 1
else
printf "%36s : %s \n" ${1} ${2}
fi
}
checkFail() {
if [[ $# -ne 2 ]] ; then
echo "Incorrect number of args!"
exit 1
fi
if [ -z ${1} ] ; then
echo "Function description required!"
exit 1
fi
if [[ ${2} -ne 0 ]] ; then
echo "Error! Could not ${1}!"
exit 1
else
printf "%36s : ... done\n" ${1}
fi
}
# get file size
s=$(stat -f "%z" ${file})
checkNull "find file size" ${s}
#### FORM chunk
#
# get start of FORM chunk
form=$(xxd -l ${peek} -c 4 ${file} | grep "FORM")
checkFail "find FORM offset" $?
form=$(echo $form | awk -F: '{print $1}')
checkFail "capture the FORM offset" $?
form=$(printf "%d" \0x${form})
checkNull "convert FORM offset to decimal" ${form}
# offset by 4 (size of "FORM")
form=$(( $form + 4 ))
checkNull "find end of FORM header" ${form}
# calculate size of AIFC data chunk
a_sz=$(( $s - $form ))
checkNull "calculate AIFC data size" ${a_sz}
# convert the AIFC byte size from decimal to hex
b=$(printf "%08x" $a_sz) # aabbccdd
checkNull "convert AIFC size to hex" $b
# prepend each hex digit with '\x'
c=''
for (( i=0; i<${#b}; i=i+2 )); do
c=${c}$(echo -en \\x${b:$i:2})
done
# write the calculated FORM size
echo -ne "$c" | \
dd conv=notrunc of=${file} bs=1 seek=${form} count=4 &> /dev/null
checkFail "write new AIFC size header" $?
#### SSND chunk
#
# find start of SSND chunk
ssnd=$(xxd -l ${peek} -c 4 ${file} | grep "SSND")
checkFail "find SSND offset" $?
ssnd=$(echo $ssnd | awk -F: '{print $1}')
checkFail "capture the SSND offset" $?
ssnd=$(printf "%d" \0x${ssnd})
checkNull "convert SSND offset to decimal" ${ssnd}
# offset by 4 (size of "SSND")
ssnd=$(( $ssnd + 4 ))
checkNull "find end of SSND header" ${ssnd}
# calculate size of SSND data chunk
s_sz=$(( $s - $ssnd ))
checkNull "calculate SSND data size" ${s_sz}
# convert the AIFC byte size from decimal to hex
b=$(printf "%08x" $s_sz)
checkNull "convert SSND size to hex" $b
# prepend each hex digit with '\x'
c=''
for (( i=0; i<${#b}; i=i+2 )); do
c=${c}$(echo -en \\x${b:$i:2})
done
# write the calculated SSND size
echo -ne "$c" | \
dd conv=notrunc of=${file} bs=1 seek=${ssnd} count=4 &> /dev/null
checkFail "write new SSND size header" $?
echo '\nAll done!'
@dreness

This comment has been minimized.

Show comment
Hide comment
@dreness

dreness Feb 1, 2016

╭─ andre@flux ~/work/aifc_hacking
╰─ $ ~/bin/fix-aifc.sh test.aifc 
                      find file size : 1831936 
                    find FORM offset : ... done
             capture the FORM offset : ... done
      convert FORM offset to decimal : 0 
             find end of FORM header : 4 
            calculate AIFC data size : 1831932 
            convert AIFC size to hex : 001bf3fc 
          write new AIFC size header : ... done
                    find SSND offset : ... done
             capture the SSND offset : ... done
      convert SSND offset to decimal : 4080 
             find end of SSND header : 4084 
            calculate SSND data size : 1827852 
            convert SSND size to hex : 001be40c 
          write new SSND size header : ... done

All done!
Owner

dreness commented Feb 1, 2016

╭─ andre@flux ~/work/aifc_hacking
╰─ $ ~/bin/fix-aifc.sh test.aifc 
                      find file size : 1831936 
                    find FORM offset : ... done
             capture the FORM offset : ... done
      convert FORM offset to decimal : 0 
             find end of FORM header : 4 
            calculate AIFC data size : 1831932 
            convert AIFC size to hex : 001bf3fc 
          write new AIFC size header : ... done
                    find SSND offset : ... done
             capture the SSND offset : ... done
      convert SSND offset to decimal : 4080 
             find end of SSND header : 4084 
            calculate SSND data size : 1827852 
            convert SSND size to hex : 001be40c 
          write new SSND size header : ... done

All done!
@nathanp

This comment has been minimized.

Show comment
Hide comment
@nathanp

nathanp Mar 3, 2018

Thank you for this. Is there any alternate to this? Using your script I was able to recover almost half of my recording but currently cannot recover the last half.

nathanp commented Mar 3, 2018

Thank you for this. Is there any alternate to this? Using your script I was able to recover almost half of my recording but currently cannot recover the last half.

@dreness

This comment has been minimized.

Show comment
Hide comment
@dreness

dreness Mar 18, 2018

You're welcome @nathanp!

Regarding your problematic recording: are you sure the file contains the data you're trying to recover? The only way to know for sure is probably to recover it, but you can calculate the expected file size based on the bit rate of the recording and your knowledge of the expected duration. If the expected size is quite a bit larger than the actual size of the partial file, then I'm afraid the 'missing' data just isn't there to recover. Possible causes of that: the interruption happened sooner than you expected, or maybe there was recorded data in memory but not yet flushed to persistent storage when the interruption occurred (and the stuff in memory was lost).

The approach taken by this script is to assume that the recording extends to the end of the file, and to use the actual size of the file on disk (as reported by 'stat') when calculating the new values for the FORM and SSDN chunk sizes (the 'broken' aspects of an aborted recording). These new sizes are written back into the file, carefully over-writing the previous values in-place, with no re-writing of unchanged parts, no truncating, no copying, etc.

If you want to dive in and make sense the file structure and data yourself, I have some ideas that might help you get started, as the second and third parts of the screencast I did about this script: https://youtu.be/N0Ec7zMyXQ8

Owner

dreness commented Mar 18, 2018

You're welcome @nathanp!

Regarding your problematic recording: are you sure the file contains the data you're trying to recover? The only way to know for sure is probably to recover it, but you can calculate the expected file size based on the bit rate of the recording and your knowledge of the expected duration. If the expected size is quite a bit larger than the actual size of the partial file, then I'm afraid the 'missing' data just isn't there to recover. Possible causes of that: the interruption happened sooner than you expected, or maybe there was recorded data in memory but not yet flushed to persistent storage when the interruption occurred (and the stuff in memory was lost).

The approach taken by this script is to assume that the recording extends to the end of the file, and to use the actual size of the file on disk (as reported by 'stat') when calculating the new values for the FORM and SSDN chunk sizes (the 'broken' aspects of an aborted recording). These new sizes are written back into the file, carefully over-writing the previous values in-place, with no re-writing of unchanged parts, no truncating, no copying, etc.

If you want to dive in and make sense the file structure and data yourself, I have some ideas that might help you get started, as the second and third parts of the screencast I did about this script: https://youtu.be/N0Ec7zMyXQ8

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment