Saving tags of playing file: file in use

Aug 1, 2014 at 7:27 PM
Edited Aug 1, 2014 at 7:27 PM
Hello,

I have a rather complicated question. In my audio player, one of the requirements is to be able to save the tags of the playing song (I'm using taglib). Of course, as the song is playing, it is in use. Saving the tags fails. I'm currently using the workaround to delay saving the tags until the song has finished playing.

I was wondering however: is there a way of opening and playing a file with cscore, and not keeping it in use? (by creating a memorystream and playing from the stream for example, or another mechanism I've looked over). I'm currently using CodecFactory.Instance.GetCodec(pathToFile) to open my files.
Coordinator
Aug 1, 2014 at 9:53 PM
You could cache the source or you could just load the whole file into a memory stream by yourself and decode the memory stream. Don't know which way you prefer.
Aug 8, 2014 at 9:18 AM
Hi Filoe,

What do you mean by caching? Copying the file somewhere else and playing from there?

The memorystream idea is interesting. It is actually something I've experimented with long ago with other audio libraries. Is there a way to play a memorystream with CSCore? I've analyzed the source code but didn't find a way to do this. I'm afraid I have no idea yet how to decode the memorystream myself and provide it to CSCore.
Coordinator
Aug 8, 2014 at 5:33 PM
First of all, you can load the whole file into a cache:
var cachedSource = new CachedSoundSource(CodecFactory.Instance.GetCodec(@"Your file"));
Since the CachedSoundSource class loads the whole audio file into the memory, you can release the underlying file stream by disposing the IWaveSource instance returned by the GetCodec-method:
IWaveSource cachedSoundSource;
using(var fileSource = CodecFactory.Instance.GetCodec(@"Your file"))
{
    cachedSoundSource = new CachedSoundSource(fileSource);
}

//use your cachedSoundSource here.
But remember this is only possible with the CachedSoundSource since the CachedSoundSource does not try to access the underlying IWaveSource instance.

Since all decoders, used by CSCore, are stream-based you can of course also load the file into the memory yourself and pass the stream to the CodecFactory class. But this would make the whole thing just much more complicated.
Aug 9, 2014 at 1:06 PM
Nice, I wasn't aware there was a cachedSoundSource. I tried it and it indeed frees the file and tagging while playing from the cachedSoundSource works. But the caching action is very slow. It takes several seconds to cache the file and start playing. Is there an issue or is this the expected behaviour?
Coordinator
Aug 9, 2014 at 1:14 PM
Well. Of course it is quite slow. The problem ist simply that you have to decode the whole audio file to load it into a cache. If you really need to that, you can try to load the file as it is into the memory and decode it from the memory. You would need to modify the CodecFactory class and add the following two methods:
public IWaveSource GetCodec(Stream stream, string filename)
{
    if(String.IsNullOrEmpty(filename))
        throw new ArgumentNullException("filename");
    if(stream == null)
        throw new ArgumentNullException("stream");

    //remove the dot in front of the file extension.
    string extension = Path.GetExtension(filename).Remove(0, 1);
    foreach (var codecEntry in _codecs)
    {
        try
        {
            if (codecEntry.Value.FileExtensions.Contains(extension))
                return codecEntry.Value.GetCodecAction(stream);
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex.ToString());
        }
    }

    return Default(filename);
}

private IWaveSource Default(Stream stream)
{
    return new MediafoundationDecoder(stream);
}
You just would need to pass any readable stream and the filename of the file to the GetCodec method. The stream parameter could be a MemoryStream.
Aug 9, 2014 at 9:22 PM
I did a quick test and it works great with passing a memorystream to the modified CodecFactory for MP3 and M4A, but not for FLAC. I'm guessing the wrong decoder is being returned. I'll do more tests in the next days.
Aug 17, 2014 at 3:42 PM
Hi Filoe,

I was able to spend more time debugging this today. I know I'm using the library in a non-converntional way, but I'm stuck on this one and hope you have some pointers for me to work around this. For FLAC, passing a stream to the GetCodec function causes an exception. Here is what I do an what is happening:

Step 1: load flac file into memorystream
Public Shared Function File2MemoryStream(iPath As String) As MemoryStream

            Dim ms As New MemoryStream()
            Using fs As FileStream = File.OpenRead(iPath)
                fs.CopyTo(ms)
            End Using

            Return ms
        End Function
Step 2: pass the stream to the GetCodec function you posted above. That causes the following exception when hitting the codecEntry for flac "return codecEntry.Value.GetCodecAction(stream);":

[System.IO.EndOfStreamException] = {"Attempted to read past the end of the stream."}

I'm hitting the constructor:
public FlacFile(Stream stream)
            : this(stream, FlacPreScanMethodMode.Default)
        {
        }
Which then executes the constructor:
public FlacFile(Stream stream, FlacPreScanMethodMode scanFlag)
            : this(stream, scanFlag, null)
        {
        }
and then:
public FlacFile(Stream stream, FlacPreScanMethodMode scanFlag,
            Action<FlacPreScanFinishedEventArgs> onscanFinished)
        {
And it is ID3v2.SkipTag(stream); which causes the exception.

When I comment out the ID3v2 line, I get a little further in the constructor, but then
if (read < beginSync.Length)
                throw new EndOfStreamException("Can not read \"fLaC\" sync.");
throws the exception: Can not read \"fLaC\" sync. The "read" variable is 0 at that time.

Can I do anything to avoid this?
Thanks.
Aug 17, 2014 at 4:51 PM
It seems I fixed it. the stream.position was equal to the stream.length. So I modified the constructor a bit as follows:
public FlacFile(Stream stream, FlacPreScanMethodMode scanFlag,
            Action<FlacPreScanFinishedEventArgs> onscanFinished)
        {
            if (stream == null)
                throw new ArgumentNullException();
            if (!stream.CanRead)
                throw new ArgumentException("Stream is not readable.", "stream");

            _stream = stream;
            _stream.Position = 0; // <- __ADDED THIS__

           ...
I'm not sure this is the right way to do it though. So I'm interested if you have a more appropriate way.
Coordinator
Aug 17, 2014 at 4:57 PM
It seems like that the CopyTo method sets does not reset the position of the MemoryStreSo you can modify the FlacFile constructor but I would recomment to just modify your File2MemoryStream method:
Public Shared Function File2MemoryStream(iPath As String) As MemoryStream

    Dim ms As New MemoryStream()
    Using fs As FileStream = File.OpenRead(iPath)
        fs.CopyTo(ms)
    End Using

    ms.Position = 0
    Return ms
End Function
Aug 17, 2014 at 5:01 PM
That's fantastic! Thanks a lot.