jstrait / wavefile

A Ruby gem for reading and writing sound files in Wave format (*.wav)
https://wavefilegem.com
MIT License
209 stars 24 forks source link

WaveFile::Reader doesn't work with pipe IO #36

Open DimitriosLisenko opened 3 years ago

DimitriosLisenko commented 3 years ago

Hello!

I was trying to use this gem to record a wav file to STDOUT and then feed it to WaveFile::Reader to analyze in realtime. If I recall correctly, it was something along the lines of

IO.popen("rec -c 1 -t wav - 2>/dev/null") do |stdout|
  WaveFile::Reader.new(stdout) do |reader|
    # do stuff
  end
end

However, this results in the following exception:

.../wavefile-1.1.1/lib/wavefile/chunk_readers/riff_reader.rb:31:in `pos': Illegal seek (Errno::ESPIPE)
    from .../wavefile-1.1.1/lib/wavefile/chunk_readers/riff_reader.rb:31:in `read_until_data_chunk'
    from .../wavefile-1.1.1/lib/wavefile/chunk_readers/riff_reader.rb:10:in `initialize'
    from .../wavefile-1.1.1/lib/wavefile/reader.rb:45:in `new'
    from .../wavefile-1.1.1/lib/wavefile/reader.rb:45:in `initialize'
    ...

which is because it's trying to seek on the IO, which is an invalid operation on a pipe.

I did also try record to a file instead of STDOUT and calling WaveFile::Reader on that, but because the file size kept increasing, that also resulted in something going wrong (I think because WaveFile::Reader#read_until_data_chunk tries to read until the end of the file). Also that means that the file keeps growing while the recording is ongoing, which is not ideal.

I ended up calling IO.popen("rec -c 1 -t s32 - 2>/dev/null") which outputs the audio amplitudes as signed 32-bit integers, and analyzing that directly.

The reason I'm opening this ticket is because I'm wondering whether analyzing an audio recording in realtime is an aim of this project, and if so, whether it's a planned feature or something that can be done now and I just missed something?

Thank you!

jstrait commented 3 years ago

Thanks for opening this issue!

Like you pointed out, WaveFile::Reader currently isn't able to read a .wav file using ~a non-seekable IO~ an IO instance that doesn't support pos. I think this functionality worked correctly in v1.0.1, but was accidentally broken by changes in v1.1.0. I made a proof of concept, similar to your first example, that reads an existing .wav file using popen. It seems to work with v1.0.1, but with v1.1.0 and v1.1.1 it gives the Illegal seek (Errno::ESPIPE) error you mentioned above.

require "wavefile"
puts WaveFile::VERSION

sample_frame_count = 0

IO.popen("cat some_file.wav") do |stdout|
  WaveFile::Reader.new(stdout).each_buffer do |buffer|
    sample_frame_count += buffer.samples.length
  end
end

puts "#{sample_frame_count} sample frames read"

Does your original script work when using v1.0.1?

As for whether analyzing an audio recording in realtime is an aim of this project, I would say the gem is agnostic. If sample data is contained in a *.wav file this gem should let you read it out, but what you do with the sample data is an outside concern. When you say realtime, do you mean for the sample data to be streamed continuously while it is being recorded, or do you mean for the entire sample data to be written all at once when recording stops?

One reason I ask is that .wav files don't seem ideal for streaming audio while it is being recorded. The problem is that the beginning of a .wav file includes fields that indicate "chunk" sizes, which are needed to know how much sample data is in the file, and where it is located. If a program records audio and continuously appends it to a .wav file, once recording stops it will have to go back to the beginning of the file and rewrite the correct chunk sizes. However, by this time the beginning of the file has already been streamed. This is more an issue with the .wav file format itself, rather than this gem per se. How does rec handle this?

(To be clear the issue isn't streaming in general, but streaming a *.wav file that is changing while it is being streamed).

Regardless of whether it solves your ultimate problem, I think it makes sense to fix the bug mentioned above - thanks for pointing it out!

jstrait commented 3 years ago

After looking more closely at this, the bug mentioned above more specifically occurs when using an IO instance that doesn't support pos, rather than when using a non-seekable IO per se. (I made a minor update to the comment above to reflect this).

WaveFile::ChunkReaders::RiffReader internally uses IO.pos to determine if there are any chunks that occur after the data chunk. If this is instead accomplished by manually keeping track of how many bytes have been read/are remaining it should be in theory possible to support using IO instances that don't support pos.

However, while that should be a fix for some .wav files, there will still probably be an issue with using IO.popen (and other non-seekable IO instances) if a .wav file has any chunks following the data chunk. RiffReader currently uses seek to read these chunk(s), and (if necessary) to seek back to the start of the data chunk, and this will cause an error. I think it makes sense to treat that as a separate bug, since fixing the original IO.pos-related bug will still represent an improvement, and a workaround for the seek issue would require a separate type of fix. (Although a 100% perfect solution is likely not possible due to how *.wav files store their data).