Skip to content

Instantly share code, notes, and snippets.

@dreness dreness/RunMe.command

Last active Jul 27, 2020
Embed
What would you like to do?
Attempt to repair an interrupted QuickTime Player audio recording
#!/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!'
<?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
# This helper script assumes it lives in the same folder as fix-aifc.sh,
# which will be the case if you click the "Download ZIP" button on the gist page:
# https://gist.github.com/dreness/e61fb16dcb831adaf6ff
#
# To use this:
# 1. Open the Terminal application, in /Applications/Utilities
# 2. In Finder, drag this file into the Terminal window, then press return.
# 3. You will be prompted to drag in the broken recording and press return.
echo "Drag your broken recording into this window, then press return."
read F
echo Running: fix-aifc.sh ${F}
fix-aifc.sh ${F}
echo Press return to exit
read
@dreness

This comment has been minimized.

Copy link
Owner Author

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.

Copy link

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.

Copy link
Owner Author

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

@anotheraccount1

This comment has been minimized.

Copy link

anotheraccount1 commented Sep 25, 2018

I signed up at github soley to say thank you for this code... It worked to recover an audio file that was unreadable after my osc crashed. You are an internet superhero sir. Cheers!

@dreness

This comment has been minimized.

Copy link
Owner Author

dreness commented Dec 2, 2018

I signed up at github soley to say thank you for this code... It worked to recover an audio file that was unreadable after my osc crashed. You are an internet superhero sir. Cheers!

Woot!! Data loss sucks, and losing data you can't re-create sucks the most. It's very rewarding in the rare case I can help recover somebody else's data, so thanks for saying thanks!

Of the three people who commented on the AIFC-fixing script (two here and one on youtube), two achieved complete data recovery and one partially recovered, so I guess my aim to do useful things in niche spaces is fairly well calibrated sometimes...

@NJariri

This comment has been minimized.

Copy link

NJariri commented Jul 25, 2020

Just commented on your YouTube link. You're amazing. Can't thank you enough. It worked in a very weird way. I'm basically stupid. I don't understand any of the codes above, but I made my own stupid calculations based on your demo on YouTube. It worked. I managed to retrieve my 20 minute precious recording. Thank you thank you thank you thank you

@dreness

This comment has been minimized.

Copy link
Owner Author

dreness commented Jul 25, 2020

Just commented on your YouTube link. You're amazing. Can't thank you enough. It worked in a very weird way. I'm basically stupid. I don't understand any of the codes above, but I made my own stupid calculations based on your demo on YouTube. It worked. I managed to retrieve my 20 minute precious recording. Thank you thank you thank you thank you

Yesss! Thanks for the feedback, also. It has helped me realize I need to add some additional instructions for people who aren't command-line nerds. The fact that you stumbled your way through it without running the script and still ended up saving your recording makes both of us look good :)

@NJariri

This comment has been minimized.

Copy link

NJariri commented Jul 25, 2020

Just commented on your YouTube link. You're amazing. Can't thank you enough. It worked in a very weird way. I'm basically stupid. I don't understand any of the codes above, but I made my own stupid calculations based on your demo on YouTube. It worked. I managed to retrieve my 20 minute precious recording. Thank you thank you thank you thank you

Yesss! Thanks for the feedback, also. It has helped me realize I need to add some additional instructions for people who aren't command-line nerds. The fact that you stumbled your way through it without running the script and still ended up saving your recording makes both of us look good :)

I tried a zillion ways on Youtube. I gave up. I don't remember how I stumble on your link! I'm just thrilled it worked. Well, I followed your steps and because it would take forever to select over 750,000,000 bytes, I noticed from your work that the "form" figure = total bytes - 8; and that "sound" figure = total bytes - 4088. It worked :) Of course, I first downloaded this Hex Fiend which I never knew existed ... So, THANK YOU

@dreness

This comment has been minimized.

Copy link
Owner Author

dreness commented Jul 25, 2020

Just commented on your YouTube link. You're amazing. Can't thank you enough. It worked in a very weird way. I'm basically stupid. I don't understand any of the codes above, but I made my own stupid calculations based on your demo on YouTube. It worked. I managed to retrieve my 20 minute precious recording. Thank you thank you thank you thank you

Yesss! Thanks for the feedback, also. It has helped me realize I need to add some additional instructions for people who aren't command-line nerds. The fact that you stumbled your way through it without running the script and still ended up saving your recording makes both of us look good :)

I tried a zillion ways on Youtube. I gave up. I don't remember how I stumble on your link! I'm just thrilled it worked. Well, I followed your steps and because it would take forever to select over 750,000,000 bytes, I noticed from your work that the "form" figure = total bytes - 8; and that "sound" figure = total bytes - 4088. It worked :) Of course, I first downloaded this Hex Fiend which I never knew existed ... So, THANK YOU

Ok, I tried to make it a little easier to run the script

  1. Click the "Download ZIP" button at the top right of this page to download a folder containing RunMe.command and fix-aifc.sh.
  2. Open the Terminal application, in /Applications/Utilities
  3. In Finder, drag RunMe.command into the Terminal window, then press return.
  4. You will be prompted to drag in the broken recording and press return.
@NJariri

This comment has been minimized.

Copy link

NJariri commented Jul 25, 2020

Just commented on your YouTube link. You're amazing. Can't thank you enough. It worked in a very weird way. I'm basically stupid. I don't understand any of the codes above, but I made my own stupid calculations based on your demo on YouTube. It worked. I managed to retrieve my 20 minute precious recording. Thank you thank you thank you thank you

Yesss! Thanks for the feedback, also. It has helped me realize I need to add some additional instructions for people who aren't command-line nerds. The fact that you stumbled your way through it without running the script and still ended up saving your recording makes both of us look good :)

I tried a zillion ways on Youtube. I gave up. I don't remember how I stumble on your link! I'm just thrilled it worked. Well, I followed your steps and because it would take forever to select over 750,000,000 bytes, I noticed from your work that the "form" figure = total bytes - 8; and that "sound" figure = total bytes - 4088. It worked :) Of course, I first downloaded this Hex Fiend which I never knew existed ... So, THANK YOU

Ok, I tried to make it a little easier to run the script

  1. Click the "Download ZIP" button at the top right of this page to download a folder containing RunMe.command and fix-aifc.sh.
  2. Open the Terminal application, in /Applications/Utilities
  3. In Finder, drag RunMe.command into the Terminal window, then press return.
  4. You will be prompted to drag in the broken recording and press return.

This is brilliant. I'll save it for the next time I lose my file :) Thank you Andre. Really. I'm still dancing around myself like a fool :)

@dreness

This comment has been minimized.

Copy link
Owner Author

dreness commented Jul 27, 2020

I noticed from your work that the "form" figure = total bytes - 8; and that "sound" figure = total bytes - 4088. It worked :)

Using the above formula probably would work for most or all aborted recordings because:

  • The AIFC file format has been around for a long time and is stable
  • There's probably not a very wide range of different (user) settings that might make the recording different enough that the 'hard coded' logic above no longer works.

I Just wanted to make sure you (and any future readers) are aware that the most correct way to fix a broken recording involves calculating the two different values that are incorrect in an aborted recording. Even if the AIFC file format or Quicktime Player doesn't change, there are still other changes that could break that logic - for example, a change in the filesystem type or CPU architecture that might come with a newer Mac. Depending on how old your Mac is, it's possible that you might experience both kinds of changes if your next Mac has an Apple CPU instead of Intel (those machines haven't started shipping yet, but soon...)

I completely understand that in the quest to save your data, you were perhaps not so interested in the conceptual aspects of how file formats work (which I thought was the main value of the video, but clearly I was wrong about that :) If your recording really is 750 MB, that's huge, and I can understand the stress at the thought of losing it. Don't forget that if you want extra practice at rescuing broken recordings, just do what I did in the video - start recording then force quit Quicktime Player.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.