Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cue sheet support #457

Open
wants to merge 9 commits into
base: devel
Choose a base branch
from
Open

Cue sheet support #457

wants to merge 9 commits into from

Conversation

sn4kebite
Copy link

Hi, these commits adds support for cue sheets. These changes are based on some changes I did last summer but didn't have time to finish.

This works by adding some intrusive changes throughout the code, especially the transcoding and playlist parts. Most importantly it passes a track parameter around to indicate which track in the cue sheet should be played. The cue sheet is then parsed and the correct track is fetched, along with the proper time offset and length. The file pointed to by the cue sheet is then decoded with ffmpeg using these time codes.

The reason I'm going with ffmpeg here is that in theory you can provide pretty much any file format with cue sheets, and while the most common is FLAC, there are also some less popular formats such as TTA and TAK which are often used (TTA is fairly common in my case). This also adds another issue, which is missing audiolength for the last track when the format is unknown to cherrymusic; the first tracks uses the start time of the next track as their end time. I guess this could be solved with some ffmpeg magic, unless it's preferable to implement this natively in TinyTag (which I've so far only had a brief look at).

As far as I can see normal playback and transcoding is not affected (works for me), and all unit tests pass (yay), so there shouldn't be any issues for regular usage. Also I'm open for suggestions for any changes or other feedback.

Thanks,
Jon

This change adds support for cuesheets by passing a track parameter
around which indicates which track in the cuesheet should be played. The
actual audio file is split with ffmpeg using the -ss and -t parameters,
which are filled by the decoder when the file extension is '.cue'.

cuesheet.py is added to parse cuesheets. This is taken from a separate
project so it's a bit messy and contains some stuff that isn't used.

Metadata is also fetched from the cuesheet per track.
@coveralls
Copy link

Coverage Status

Coverage decreased (-1.73%) when pulling ab51037 on sn4kebite:cuesheet into 8b4ad25 on devsnd:devel.

@devsnd
Copy link
Owner

devsnd commented May 22, 2014

Wow, that's really cool! You've worked through the full stack of CM to create that feature, very impressive indeed! :)

I'll look through it on the weekend so we can get it merged soon 🍦

@devsnd
Copy link
Owner

devsnd commented May 26, 2014

Hi @sn4kebite, I'm sorry, I didn't get to review your code yet!
I'm quite busy at the moment, but I hope that I'll get to it soon :)

@sn4kebite
Copy link
Author

Hi, thanks for the response. If we can solve other issues by using another approach, I guess that's the way to go. However I didn't quite get what you mean; by duration, do you mean track length, or something like an time interval? Passing the start time should also work, as the cue sheet code could easily figure out which track the given time position belongs to (the last track that starts before or at the given time). I saw that there's already some references to starttime in the code, but I haven't looked too much into that.

@devsnd
Copy link
Owner

devsnd commented May 27, 2014

Sorry, I didn't make myself too clear. What I mean is exactly what you mentioned: Using the starttime and a new attribute duration for creating the tracks that make up the cuesheet. Right now you are parsing the cuesheet inside audiotranscode, but audiotranscode actually does not need to know any detail about cuesheets.

I though it could be handled like so:

diagram svg

So audiotranscode does not need to know anything about cuesheets... well actually nobody knows about cuesheets but the file-listing. Everything after that can be handled using start and end time of a track.

@sn4kebite
Copy link
Author

Yeah, that sounds like what I was thinking. I've been playing around a bit with the code, and the only issue I see so far is that when fetching metainfo, we also need to know which track in the cue sheet to fetch metainfo for. This could be solved by passing starttime to getsonginfo, but I guess that's not the ideal way to handle that (I'm passing track currently).

Edit: Also, we still need to pass the filepath for the cue sheet instead of the audio file itself. The files does not necessarily have the same base name (you can have eg. foo.cue and bar.flac), so you need to know the filepath to the cue sheet; the audio filepath can easily be resolved by parsing the cue sheet. I currently added this parsing back into decode, but I guess we could also put it in the trans handler or whatever seems more fitting.

Current issues:

* For file formats that CM doesn't know about, we don't know the length
  of the audio file, and thus can't fetch the length of the last track.

* We should remove the duration flag when no duration is given, this is
  currently set to 100000 seconds when duration is None (unspecified).

Other notes:

* getsonginfo() needs to know which track is being fetched, this is
  currenly checked by comparing the provided starttime parameter with
  each track. This could be either left as-is, or done differently.

* Decode.decode() must resolve the audio filepath itself, since we use
  the cue filepath to map to the proper decoder. The cue and audio files
  can also have different base names (eg. foo.cue can point to bar.flac).

* starttime must include decimals to avoid playing back the last second
  or so of the previous track, duration also does this to stay
  consistent.
@sn4kebite
Copy link
Author

Okay, so I basically just replaced track with starttime and duration. I'm currently passing starttime to getsonginfo as I don't see any other way to do that other than reintroducing track. I left a list with issues and other notes in the commit message .

@devsnd
Copy link
Owner

devsnd commented Jun 2, 2014

Yeah you're right, there is something I need to do before this can be solved elegantly: Allowing to pass meta-data directly into the filebrowser, instead of fetching it after the fact asynchronously. I didn't think of that.

If you look inside res/templates/mediabrowser-file.html you can see that there is a unloaded css-class for the file. This css-class is the reason for the CM client to load the meta-info for that track. When listing a cuesheet, we already know all the information up front. There is no need to fetch the meta-data afterwards. We can simply render it directly into the browser and leave out that unloaded class.

I've opened another issue for the one thing that's left to do to make this work: after we show the correct meta-data in the file browser, they have to be passed to the playlist. Essentially that means not to call getmetainfo twice (once for the filebrowser and once for the playlist) but instead transferring all meta-data from the filebrowser to the playlist if it is already available.

…esheet

Conflicts:
	res/dist/cherrymusic.dist.js
This allows passing of metainfo for cuesheets without calling
getsonginfo for every track. This gets rid of the starttime argument to
getsonginfo, and allows us to pass the metainfo we already have when
listing cuesheet contents without having to parse the cuesheet for
every track. getsonginfo should now be called only once for all tracks.
@coveralls
Copy link

Coverage Status

Coverage decreased (-1.72%) when pulling 73f97cd on sn4kebite:cuesheet into c305c9a on devsnd:devel.

@sn4kebite
Copy link
Author

So I never got around to finish this because of summer and stuff, but finally found the time to do so. The last commit lets listdir pass metainfo directly without the need for calling getsonginfo. The metainfo is now also passed to the playlist manager, so getsonginfo is never called for cuesheets (or potentially other media which provides metainfo when listed), and only once for single tracks (which I guess should solve #460). As a bonus the cuesheet is now parsed only once per file instead of once per track.

@coveralls
Copy link

Coverage Status

Coverage decreased (-1.72%) when pulling 7b9eb01 on sn4kebite:cuesheet into c305c9a on devsnd:devel.

@coveralls
Copy link

Coverage Status

Coverage decreased (-1.75%) when pulling 1b2354d on sn4kebite:cuesheet into c305c9a on devsnd:devel.

@devsnd
Copy link
Owner

devsnd commented Sep 1, 2014

Great, I'll try to look into it this evening 👍

@coveralls
Copy link

Coverage Status

Coverage decreased (-1.72%) when pulling c8efb83 on sn4kebite:cuesheet into c305c9a on devsnd:devel.

@devsnd
Copy link
Owner

devsnd commented Sep 19, 2014

Hi Jon, I'm sorry it toomk me so long to take a look on your code, but there are still multiple issues with it, and I didn't find enough the time for a review until lately. So let's break it up in parts:

audiotranscode

You a added a new type inside audiotranscode to handle the cue sheets, but audiotranscode should only take care of transcoding audio nothing else, just as you've noticed yourself. Additionally the cuesheet may contain all kinds of data not even supported by ffmpeg and therefore needs be handled in a different way. More on that later.

I however recognise the problem of the DURATION flag for ffmpeg. audiotranscode should be able to hadle optional flags. This however is not so urgent as you have set the duration to 100000 seconds or ~27 hours, so I guess we can live with the workaround for now.

cuesheet

Your cuesheet module has some flaws: Your Track has a member nextstart, which is only set by an external call in the metainfo.py. This makes the code very hard to maintain. It would make a lot more sense if the tracks would have a duration, which is set when first parsing the cuesheet. I understand that you cannot know the duration of the last track, but then this should not matter anyways (as long as the last track is less than ~27 hours long)

I like that you made sure that a cuesheet starting with a utf-8 BOM would be parsed correctly, but some comments would have helped to understand what's going on there.

I wanted to use your module to try out some things myself, but I didn't understand the hierarchy of CDText, Descriptors and Tracks. I'd love to see some comments on top of the module to see how it can be used, how to iterate over a set of tracks and their meta-data etc.

cherrymodel

I have to admit that I don't know how the cuesheet format is specified, but to me it looks like your usage of the cuesheet model would only support cuesheets that have a single track in it. But cuesheets may contain multiple files AFAIK.

You are making the cuesheet to a very special case, while in reality it is not. It's just a playlist. It would be a lot more future proof, if you could just add a handler that takes care of all the files ending in 'cue' which then return a MusicEntry that treats playlists like folders. This would allow for adding 'pls' or 'm3u' handlers later on, which would then make playlist imports easy.

metainfo

The metainfo.py was put in place, because we changed the ways to get meta-information for tracks to often, so it's merely a class for abstraction of different library calls and allowing unified access to the data. You have however put some of the essential logic of your cuesheet module inside it. I understand you wanted to put code that does similar things in similar places, but most of it should remain in your module or in the addCueSheet call

The general Problem

To quote what I've said before

So audiotranscode does not need to know anything about cuesheets... well actually nobody knows about cuesheets but the file-listing. Everything after that can be handled using start and end time of a track.

(emphasis added)

In fact, I'd like to handle cuesheets and playlists (pls, m3u) in the same way folders are handled. You could then browse a cuesheet and select a single track, or add all tracks without accidentally selecting any other tracks from the same folder. I'm sorry if I haven't made that clear, but this is the only elegant way to handle this problem.


I hope you understand all of my critique. I like to see when people like CM and I love it when people want to improve it. But I cannot accept patches that will make it harder to maintain the program, since we are already knee-deep in code-dept and we need to learn from our mistakes. Also I'd like to apologise again for not responding any quicker after you having made this much effort. In any case, I'd still like this feature to arrive in CM!

@devsnd
Copy link
Owner

devsnd commented Sep 19, 2014

I was just looking around and found this:

http://digitalx.org/cue-sheet/examples/#example01

It might make for a good test-suite...

@devsnd
Copy link
Owner

devsnd commented Sep 19, 2014

Hey Jon, I have created new feature branch which contains a rough first version of cuesheet integration as I'd implement it.

https://github.com/devsnd/cherrymusic/tree/feature_cuesheet

I have completely rewritten the cuesheet parser, I hope you are not mad... 👊
But this one now is really easy to use, supports multiple files per sheet and is completely self-contained.

cue = CueSheet(filename)
for track in cue.tracks:
    print('track %s starts at %s and is %s seconds long' % (
        track.filename, track.start_time_abs_sec, track.duration))

I haven't changed anything in the frontend yet: everything works now by tricking the frontend into thinking the cuesheet is a folder and listing the files inside, which works really well.

Next thing would be to integrate the changes you already made for the meta-info to be preloaded and not loaded again by the MediaBrowser, so that the titles, duration and so on are being shown correctly in the file browser. I seems you have already implemented everything needed there and I only have to cherrypick! 👍

I thought, instead of going through the transcoder first, we could instead make the frontend (the jplayer) do the work for us (skip x seconds, start next song after y seconds of playback) and care for the transcoding later.

Maybe we can work together to finish this? 💃 🍦

@sn4kebite
Copy link
Author

Hi, sorry for the late reply.

Treating the cuesheet like a directory actually sounds like a great idea. Also the cuesheet.py I added was taken from another small side-project I was working on some time ago, and badly needed some cleanup anyway.

By having the frontend do the work, do you mean just delivering the file without caring for position in the transcoder (as in, deliver the entire file)?

The cue type in the transcoder was added to deal with the track parameter and exotic filetypes. We can easily get rid of this type now that it uses starttime and duration by asking for the audiofile instead of the cue sheet, however any filetypes that aren't specifically supported won't work, and some decoders like the flac decoder needs some special attention to its arguments to work correctly (seems to need --skip +duration without hours specified to work for me). The last point here is probably moot if I understood you right in not handling times in the transcoder though.

That aside I agree with the issues. And like I said in my initial post, I'm open for any changes.

@tilboerner
Copy link
Collaborator

Great job getting the ball rolling, looks like cuesheet support is almost ready.

Treating cuesheets like directories makes the most sense, I think, because we get to keep the interface simple. It also offers the chance of keeping the cuesheet logic very localized. What's the reason for letting the client handle start and end time offsets? I'm forgetting if seeking is a problem in the transcoder. Because if it's not, and if start and duration can become transcoder arguments, the client wouldn't need to care the slightest bit about where the audio is coming from.

I agree it would be good to have some tests in place, as well as some precautions for unreadable/malformed .cue files.

@devsnd
Copy link
Owner

devsnd commented Sep 30, 2014

I'm forgetting if seeking is a problem in the transcoder. Because if it's not, and if start and duration can become transcoder arguments, the client wouldn't need to care the slightest bit about where the audio is coming from.

Essentially that's the problem right now; Because we support that many different programs to transcode and the syntax varies for all of them, it takes some time to make sure everything works as expected. It's somewhat easily solvable, but only adds problems to the issue at hand, i'd say.

By having the frontend do the work, do you mean just delivering the file without caring for position in the transcoder (as in, deliver the entire file)?

Essentially yes. I thought going for the frontend first might be a good idea to get everything running; The server supports paritial HTTP requests and most sane file formats have a good way to handle seeking (i'm looking at you mp3!), so seeking on the client-side isn't such a great overhead. It's possible to instruct the jPlayer to just seek to any part of the stream, so lets use that.

So essentially we're delivering the whole file, but jPlayer and HTTP 206 (and browser caching) take care of the rest for us for all natively supported file types. We can later than make transcoding work correctly.

The issue with the transcoding and seeking within transcoded files then essentially boils down to issue #390 and #275.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants