Search code examples
rubywindowscygwinlameflac

ruby - IO.popen not working lame stdin and stdout encoding


I've been working with pipes and IO.popen specifically in Ruby and have come across a problem that I can't figure out. I am trying to write binary data from the flac process to the lame process into a file. The code structure I am using is below.

# file paths
file = Pathname.new('example.flac').realpath
dest = Pathname.new('example.mp3')

# execute the process and return the IO object
wav = IO.popen("flac --decode --stdout \"#{file}\"", 'rb')
lame = IO.popen("lame -V0 --vbr-new - -", 'r+b')

# write output from wav to the lame IO object
lame << wav.read

# close pipe for writing (should indicate to the
# process that input for stdin is finished).
lame.close_write

# open up destiniation file and write from lame stdout
dest.open('wb'){|out|
    out << lame.read
}

# close all pipes
wav.close
lame.close

However, it doesn't work. After flac has run, the script hangs and lame remains idle (no processor usage at all). No errors or exceptions occur.

I am using cygwin on Windows 7, with the cygwin ruby package (1.9.3p429 (2013-05-15) [i386-cygwin]).

I must be doing something wrong, any help is much appreciated. Thanks!

EXTRA #1

I am wanting to pipe in and out the binary data from the lame process because I am trying to create a platform independent (ruby support limited of course) to transcode audio files, and the Windows binary of lame only supports Windows' path names, and not cygwin's.

EDIT #1

I read in some places (I did not save the URLs, I'll try looking for them in my browser history) that IO.popen has known issues with blocking processes in Windows and that this could be the case.

I have played around with other libraries including Ruby's Open3.popen3 and Open4, however following a very similar code structure to the one above, the lame process still hangs and remains unresponsive.

EDIT #2

I found this article which talked about the limitations of of Windows's cmd.exe and how it prevents the use of streamed data from files to stdin.

I refactored my code to look like shown below to test this out, and as it turns out, lame freezes on stdin write. If I removed (comment out) that line, the lame process executes (with an 'unsupported audio format' warning). Perhaps what the article said could explain my problem here.

# file paths
file = Pathname.new('example.flac').realpath
dest = Pathname.new('example.mp3')

# some local variables
read_wav = nil
read_lame = nil

# the flac process, which exits succesfully
IO.popen("flac --decode --stdout \"#{file}\"", 'rb'){|wav|
    until wav.eof do
        read_wav = wav.read
    end
}

# the lame process, which fails
IO.popen("lame -V0 --vbr-new --verbose - -", 'r+b'){|lame|
    lame << read_wav # if I comment out this, the process exits, instead of hanging
    lame.close_write
    until lame.eof do
        read_lame << lame.read
    end
}

EDIT #3

I found this stackoverflow which (in the first answer) mentioned that cygwin pipe implementation is unreliable. This could perhaps not actually be related to Windows (at least not directly) but instead to cygwin and its emulation. I have instead opted to use the following code, based upon icy's answer, which works!

flac = "flac --decode --stdout \"#{file}\""
lame = "lame -V0 --vbr-new --verbose - \"#{dest}\""

system(flac + ' | ' + lame)

Solution

  • Did you try the pipe | character?
    Tested this on windows with ruby installer

    require 'open3'
    
    command = 'dir /B | sort /R'  # a windows example command
    Open3.popen3(command) {|stdin, stdout, stderr, wait_thr|
      pid = wait_thr.pid
      puts stdout.read  #<a list of files in cwd in reverse order>
    }
    

    Other ways: Ruby pipes: How do I tie the output of two subprocesses together?

    EDIT: using IO::pipe

    require 'open3'
    
    command1 = 'dir /B'
    command2 = 'sort /R'
    
    reader,writer = IO.pipe
    Open3.popen3(command1) {|stdin, stdout, stderr, wait_thr|
      writer.write stdout.read
    }
    writer.close
    
    stdout, stderr, status = Open3.capture3(command2, :stdin_data => reader.read)
    reader.close
    
    puts "status: #{status}"   #pid and exit code
    puts "stderr: #{stderr}"   #use this to debug command2 errors
    puts stdout
    

    Embedding the two also appears to work, yet, as the blog you referred to said, one must wait for the first command to finish (not real-time -- test with a ping command)

    stdout2 = ''
    Open3.popen3(command1) {|stdin, stdout, stderr, wait_thr|
      stdout2, stderr2, status2 = Open3.capture3(command2, :stdin_data => stdout.read)
    }
    puts stdout2