From 24d8258430114c20aae7715bd29b2af7302348bd Mon Sep 17 00:00:00 2001 From: Moritz Bunkus <moritz@bunkus.org> Date: Fri, 4 Nov 2011 22:40:36 +0100 Subject: [PATCH] XSLT 2.0 stylesheets for chapters to: cue sheets; shnsplit timecodes --- ChangeLog | 6 + examples/stylesheets/chapters-to-cuesheet.xsl | 152 ++++++++++++++++++ examples/stylesheets/chapters-to-shnsplit.xsl | 137 ++++++++++++++++ 3 files changed, 295 insertions(+) create mode 100644 examples/stylesheets/chapters-to-cuesheet.xsl create mode 100644 examples/stylesheets/chapters-to-shnsplit.xsl diff --git a/ChangeLog b/ChangeLog index 172ac8175..e48ae19b4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,11 @@ 2011-11-04 Moritz Bunkus <moritz@bunkus.org> + * examples: Added XSLT 2.0 stylesheets in the + "examples/stylesheets" directory for turning Matroska chapters + into cue sheet and split points for "shntool" (useful for + situations in which you have e.g. a live recording from a concert + including chapters and want to create one audio file per song). + * mkvmerge: bug fix: Fixed reading VC1 video tracks from Matroska files that don't use VC1 start markers (0x00 0x00 0x01 ...). diff --git a/examples/stylesheets/chapters-to-cuesheet.xsl b/examples/stylesheets/chapters-to-cuesheet.xsl new file mode 100644 index 000000000..a0ec88f9f --- /dev/null +++ b/examples/stylesheets/chapters-to-cuesheet.xsl @@ -0,0 +1,152 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + +Chapters to cue sheet +===================== + +This is a XSLT 2.0 stylesheet for turning the chapter format that mkvmerge +understands into a cue sheet as used by e.g. CD recording software. + +You need a XSLT 2.0 compatible XSLT processor, e.g. Saxon-HE (see +http://saxon.sourceforge.net/). + +Usage depends on your processor. For Saxon-HE on Linux this should work: + +$ java -classpath saxon9he.jar net.sf.saxon.Transform \ + -o:output-filename.cue \ + -xsl:chapters-to-cuesheet.xsl \ + input-filename.xml + +You have to extract the chapters from a Matroska file with mkvextract first if +you want to turn the chapters included in one into a cue sheet. + +This can be useful if you want to turn an audio file contain in a Matroska +file into one file per chapter (e.g. for music videos). For this you need both +"chapters-to-cuesheet.xsl" and "chapters-to-shnsplit.xsl" as well as the +"cuetools" (see https://github.com/svend/cuetools) and "shntool" (see +http://etree.org/shnutils/shntool/) packages. + +Assumptions are (meaning: adjust these values in the commands): + +- the source file contains an audio track with track ID 2 of type FLAC, +- the band is "Perpetuum Jazzile", +- the album is "Live 2009", +- the source file name is "source.mkv". + +Here we go: + + # 1. Extract audio track from Matroska file: + $ mkvextract source.mkv tracks 2:source.flac + + # 2. Extract chapters from same file: + $ mkvextract source.mkv chapters > chapters.xml + + # 3. Generate the split points used in step 4: + $ java -classpath saxon9he.jar net.sf.saxon.Transform \ + -o:splitpoints.txt \ + -xsl:chapters-to-cuesheet.xsl \ + chapters.xml + + # 4. Split the audio file into multiple files: + $ shnsplit -o flac source.flac < splitpoints.txt + + # 5. Generate the cue sheet used in step 6: + $ java -classpath saxon9he.jar net.sf.saxon.Transform \ + -o:source.cue \ + -xsl:chapters-to-cuesheet.xsl \ + chapters.xml \ + artist="Perpetuum Jazzile" \ + album="Live 2009" + + # 6: Assign tags (artist and title): + $ cuetag source.cue split-*.flac + +Written by Moritz Bunkus <moritz@bunkus.org>. + +--> + +<xsl:stylesheet version="2.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xsl:param name="album" select="'ChangeMeAlbum'"/> + <xsl:param name="artist" select="'ChangeMeAlbumArtist'"/> + <xsl:param name="track-artist" select="if ($artist eq 'ChangeMeAlbumArtist') then 'ChangeMeTrackArtist' else $artist"/> + + <xsl:output method="text"/> + <xsl:strip-space elements="*"/> + + <xsl:template match="/Chapters"> + <xsl:apply-templates select="EditionEntry"/> + </xsl:template> + + <xsl:template name="timecode-to-ns" as="xs:integer"> + <xsl:param name="timecode" select="'0'"/> + <xsl:choose> + <xsl:when test="$timecode eq ''"> + <xsl:value-of select="0"/> + </xsl:when> + <xsl:when test="matches($timecode, '^\d+:\d+:\d+\.\d{9}$')"> + <xsl:analyze-string select="$timecode" + regex="^ (\d+) : (\d+) : (\d+) \. (\d+) $" + flags="x"> + <xsl:matching-substring> + <xsl:value-of select=" (regex-group(1) cast as xs:integer) * 3600000000000 + + (regex-group(2) cast as xs:integer) * 60000000000 + + (regex-group(3) cast as xs:integer) * 1000000000 + + (regex-group(4) cast as xs:integer)"/> + </xsl:matching-substring> + </xsl:analyze-string> + </xsl:when> + <xsl:when test="matches($timecode, '^\d+:\d+:\d+\.\d*$')"> + <xsl:call-template name="timecode-to-ns"> + <xsl:with-param name="timecode" select="concat($timecode, '0')"/> + </xsl:call-template> + </xsl:when> + <xsl:when test="matches($timecode, '^\d+:\d+:\d+$')"> + <xsl:call-template name="timecode-to-ns"> + <xsl:with-param name="timecode" select="concat($timecode, '.')"/> + </xsl:call-template> + </xsl:when> + <xsl:otherwise> + <xsl:call-template name="timecode-to-ns"> + <xsl:with-param name="timecode" select="concat('0:', $timecode)"/> + </xsl:call-template> + </xsl:otherwise> + </xsl:choose> + </xsl:template> + + <xsl:template match="EditionEntry"> + <xsl:text>PERFORMER "</xsl:text> + <xsl:value-of select="$artist"/> + <xsl:text>" +TITLE "</xsl:text> + <xsl:value-of select="$album"/> + <xsl:text>" +FILE "ChangeMe.mp3" MP3 +</xsl:text> + <xsl:for-each select="ChapterAtom"> + <xsl:variable name="timecode" as="xs:integer"> + <xsl:call-template name="timecode-to-ns"> + <xsl:with-param name="timecode" select="ChapterTimeStart"/> + </xsl:call-template> + </xsl:variable> + <xsl:text> TRACK </xsl:text> + <xsl:number format="01" value="position()"/> + <xsl:text> AUDIO + PERFORMER "</xsl:text> + <xsl:value-of select="$track-artist"/> + <xsl:text>" + TITLE "</xsl:text> + <xsl:value-of select="ChapterDisplay[1]/ChapterString"/> + <xsl:text>" + INDEX 01 </xsl:text> + <xsl:number format="01" value=" $timecode idiv 60000000000"/> + <xsl:text>:</xsl:text> + <xsl:number format="01" value="($timecode idiv 1000000000) mod 60"/> + <xsl:text>:</xsl:text> + <xsl:value-of select="($timecode mod 1000000000) * 75 idiv 1000000000"/> + <xsl:text>
</xsl:text> + </xsl:for-each> + </xsl:template> +</xsl:stylesheet> diff --git a/examples/stylesheets/chapters-to-shnsplit.xsl b/examples/stylesheets/chapters-to-shnsplit.xsl new file mode 100644 index 000000000..c348818e4 --- /dev/null +++ b/examples/stylesheets/chapters-to-shnsplit.xsl @@ -0,0 +1,137 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + +Chapters to shnsplit +==================== + +This is a XSLT 2.0 stylesheet for turning the chapter format that mkvmerge +understands into a list of timecodes for the "shnsplit" tool from the +"shntool" package (see http://etree.org/shnutils/shntool/). It allows you to +split a large audio file into smaller ones, optionally encoding/decoding in +the process. + +You need a XSLT 2.0 compatible XSLT processor, e.g. Saxon-HE (see +http://saxon.sourceforge.net/). + +Usage depends on your processor. For Saxon-HE on Linux this should work: + + $ java -classpath saxon9he.jar net.sf.saxon.Transform \ + -o:splitpoints.txt \ + -xsl:chapters-to-shnsplit.xsl \ + input-filename.xml \ + artist="Name of the artist" \ + album="Name of the album" + +You have to extract the chapters from a Matroska file with mkvextract first if +you want to turn the chapters included in one into split points. + +This can be useful if you want to turn an audio file contain in a Matroska +file into one file per chapter (e.g. for music videos). For this you need both +"chapters-to-cuesheet.xsl" and "chapters-to-shnsplit.xsl" as well as the +"cuetools" (see https://github.com/svend/cuetools) and "shntool" (see +http://etree.org/shnutils/shntool/) packages. + +Assumptions are (meaning: adjust these values in the commands): + +- the source file contains an audio track with track ID 2 of type FLAC, +- the band is "Perpetuum Jazzile", +- the album is "Live 2009", +- the source file name is "source.mkv". + +Here we go: + + # 1. Extract audio track from Matroska file: + $ mkvextract source.mkv tracks 2:source.flac + + # 2. Extract chapters from same file: + $ mkvextract source.mkv chapters > chapters.xml + + # 3. Generate the split points used in step 4: + $ java -classpath saxon9he.jar net.sf.saxon.Transform \ + -o:splitpoints.txt \ + -xsl:chapters-to-cuesheet.xsl \ + chapters.xml + + # 4. Split the audio file into multiple files: + $ shnsplit -o flac source.flac < splitpoints.txt + + # 5. Generate the cue sheet used in step 6: + $ java -classpath saxon9he.jar net.sf.saxon.Transform \ + -o:source.cue \ + -xsl:chapters-to-cuesheet.xsl \ + chapters.xml \ + artist="Perpetuum Jazzile" \ + album="Live 2009" + + # 6: Assign tags (artist and title): + $ cuetag source.cue split-*.flac + +Written by Moritz Bunkus <moritz@bunkus.org>. + +--> + +<xsl:stylesheet version="2.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xsl:output method="text"/> + <xsl:strip-space elements="*"/> + + <xsl:template match="/Chapters"> + <xsl:apply-templates select="EditionEntry"/> + </xsl:template> + + <xsl:template name="timecode-to-ns" as="xs:integer"> + <xsl:param name="timecode" select="'0'"/> + <xsl:choose> + <xsl:when test="$timecode eq ''"> + <xsl:value-of select="0"/> + </xsl:when> + <xsl:when test="matches($timecode, '^\d+:\d+:\d+\.\d{9}$')"> + <xsl:analyze-string select="$timecode" + regex="^ (\d+) : (\d+) : (\d+) \. (\d+) $" + flags="x"> + <xsl:matching-substring> + <xsl:value-of select=" (regex-group(1) cast as xs:integer) * 3600000000000 + + (regex-group(2) cast as xs:integer) * 60000000000 + + (regex-group(3) cast as xs:integer) * 1000000000 + + (regex-group(4) cast as xs:integer)"/> + </xsl:matching-substring> + </xsl:analyze-string> + </xsl:when> + <xsl:when test="matches($timecode, '^\d+:\d+:\d+\.\d*$')"> + <xsl:call-template name="timecode-to-ns"> + <xsl:with-param name="timecode" select="concat($timecode, '0')"/> + </xsl:call-template> + </xsl:when> + <xsl:when test="matches($timecode, '^\d+:\d+:\d+$')"> + <xsl:call-template name="timecode-to-ns"> + <xsl:with-param name="timecode" select="concat($timecode, '.')"/> + </xsl:call-template> + </xsl:when> + <xsl:otherwise> + <xsl:call-template name="timecode-to-ns"> + <xsl:with-param name="timecode" select="concat('0:', $timecode)"/> + </xsl:call-template> + </xsl:otherwise> + </xsl:choose> + </xsl:template> + + <xsl:template match="EditionEntry"> + <xsl:for-each select="ChapterAtom"> + <xsl:variable name="timecode" as="xs:integer"> + <xsl:call-template name="timecode-to-ns"> + <xsl:with-param name="timecode" select="ChapterTimeStart"/> + </xsl:call-template> + </xsl:variable> + <xsl:if test="$timecode > 0"> + <xsl:number format="01" value=" $timecode idiv 60000000000"/> + <xsl:text>:</xsl:text> + <xsl:number format="01" value="($timecode idiv 1000000000) mod 60"/> + <xsl:text>.</xsl:text> + <xsl:number format="001" value="($timecode mod 1000000000) idiv 1000000"/> + <xsl:text>
</xsl:text> + </xsl:if> + </xsl:for-each> + </xsl:template> +</xsl:stylesheet>