I lost 7 days worth of captures due to misclicking the microphone mute button. Ended up with a week's worth of silent audio
173 lines
5.6 KiB
Odin
173 lines
5.6 KiB
Odin
package main
|
|
|
|
import "base:runtime"
|
|
import "base:intrinsics"
|
|
import "core:fmt"
|
|
import "core:slice"
|
|
import "core:time"
|
|
import "core:encoding/endian"
|
|
import "core:sys/posix"
|
|
import "core:os/os2"
|
|
|
|
import "shared:alsa"
|
|
|
|
DEVICE :: "default"
|
|
OUTPUT_FILE :: "capture"
|
|
FRAMES :: 128
|
|
FORMAT: alsa.pcm_format_t : .S16_LE
|
|
BITS_PER_SAMPLE :: 16
|
|
CHANNELS :: 1
|
|
SAMPLE_RATE :: 22050
|
|
|
|
WAV_HEADER_SIZE :: 44
|
|
|
|
interrupted := false
|
|
|
|
unmute_if_we_are_mute :: proc() {
|
|
handle: ^alsa.mixer_t
|
|
sid: ^alsa.mixer_selem_id_t
|
|
selem_name :: "Capture"
|
|
|
|
alsa.mixer_selem_id_malloc(&sid)
|
|
defer alsa.mixer_selem_id_free(sid)
|
|
alsa.mixer_selem_id_set_index(sid, 0)
|
|
alsa.mixer_selem_id_set_name(sid, selem_name)
|
|
|
|
if alsa.mixer_open(&handle, 0) < 0 {
|
|
panic("Failed to open mixer")
|
|
}
|
|
defer alsa.mixer_close(handle)
|
|
|
|
if alsa.mixer_attach(handle, DEVICE) < 0 {
|
|
panic("Failed to attach mixer to device")
|
|
}
|
|
|
|
if alsa.mixer_selem_register(handle, nil, nil) < 0 {
|
|
panic("Failed to register mixer element")
|
|
}
|
|
|
|
if alsa.mixer_load(handle) < 0 {
|
|
panic("Failed to load mixer")
|
|
}
|
|
|
|
// We can loop over the available elements and print their names. "Capture" seems to work tho
|
|
// fmt.println(alsa.mixer_first_elem(handle))
|
|
// for elem := alsa.mixer_first_elem(handle); elem != nil; elem = alsa.mixer_elem_next(elem) {
|
|
// name := alsa.mixer_selem_get_name(elem)
|
|
// fmt.println(name)
|
|
// }
|
|
|
|
elem := alsa.mixer_find_selem(handle, sid)
|
|
assert(elem != nil, "Mixer element is nil")
|
|
|
|
mic_enabled: i32
|
|
|
|
if alsa.mixer_selem_has_capture_switch(elem) {
|
|
assert(alsa.mixer_selem_get_capture_switch(elem, .MONO, &mic_enabled) >= 0, "get_capture_switch for mono channel failed")
|
|
} else {
|
|
panic("No capture switch found")
|
|
}
|
|
|
|
if mic_enabled == 0 {
|
|
fmt.println("I Have no Ears, and I Must Listen (Attempting to unmute microphone)")
|
|
assert(alsa.mixer_selem_set_capture_switch(elem, .MONO, 1) >= 0, "Failed to unmute capture device")
|
|
}
|
|
}
|
|
|
|
main :: proc() {
|
|
unmute_if_we_are_mute()
|
|
|
|
handle: ^alsa.pcm_t
|
|
|
|
alsa_err := alsa.pcm_open(&handle, DEVICE, .CAPTURE, .BLOCK)
|
|
if alsa_err < 0 {
|
|
fmt.eprintln("Failed to open default capture device.")
|
|
os2.exit(1)
|
|
}
|
|
defer alsa.pcm_close(handle)
|
|
|
|
now := time.now()
|
|
year, month, day := time.date(now)
|
|
hour, minute, second := time.clock_from_time(now)
|
|
|
|
out, err := os2.open(fmt.tprintf("%v_%v-%02d-%02dT%02d:%02d:%02d.wav", OUTPUT_FILE, year, int(month), day, hour, minute, second), {.Write, .Create, .Trunc}, 0o644)
|
|
if err != nil {
|
|
fmt.eprintln("Failed to open output file:", err)
|
|
os2.exit(1)
|
|
}
|
|
defer os2.close(out)
|
|
|
|
alsa_err = alsa.pcm_set_params(handle, FORMAT, .RW_INTERLEAVED, CHANNELS, SAMPLE_RATE, 1, 500000)
|
|
if alsa_err < 0 {
|
|
fmt.eprintln("Failed to set the HW params")
|
|
os2.exit(1)
|
|
}
|
|
|
|
alsa_err = alsa.pcm_prepare(handle)
|
|
if alsa_err < 0 {
|
|
fmt.eprintln("Failed to prepare audio device for capture.")
|
|
os2.exit(1)
|
|
}
|
|
|
|
samples := make([]u8, FRAMES * CHANNELS * alsa.pcm_format_width(FORMAT) / 8)
|
|
|
|
fmt.println("Capturing audio")
|
|
|
|
action: posix.sigaction_t
|
|
action.sa_handler = proc "c" (signal: posix.Signal) {
|
|
context = runtime.default_context()
|
|
fmt.println("\nCaught SIGINT. Exiting gracefully")
|
|
intrinsics.atomic_store(&interrupted, true)
|
|
}
|
|
posix.sigaction(.SIGINT, &action, nil)
|
|
|
|
four_byte_slice := make([]byte, 4)
|
|
two_byte_slice := make([]byte, 2)
|
|
defer delete(four_byte_slice)
|
|
defer delete(two_byte_slice)
|
|
|
|
os2.write(out, []byte{'R', 'I', 'F', 'F'}) // Riff file
|
|
os2.write(out, []byte{0,0,0,0}) // File size - 8 bytes. To be filled in later
|
|
os2.write(out, []byte{'W', 'A', 'V', 'E'}) // WAVE data
|
|
os2.write(out, []byte{'f', 'm', 't', ' '}) // Format chunk marker with NUL byte
|
|
endian.put_i32(four_byte_slice, .Little, 16)
|
|
os2.write(out, four_byte_slice) // Chunk size: 16 for PCM
|
|
endian.put_i16(two_byte_slice, .Little, 1)
|
|
os2.write(out, two_byte_slice) // Audio format: 1 for PCM
|
|
os2.write(out, two_byte_slice) // Number of channels
|
|
endian.put_i32(four_byte_slice, .Little, SAMPLE_RATE)
|
|
os2.write(out, four_byte_slice) // Sample rate
|
|
endian.put_i32(four_byte_slice, .Little, SAMPLE_RATE * CHANNELS * BITS_PER_SAMPLE / 8)
|
|
os2.write(out, four_byte_slice) // Byte rate
|
|
endian.put_i16(two_byte_slice, .Little, CHANNELS * BITS_PER_SAMPLE / 8)
|
|
os2.write(out, two_byte_slice) // Block align
|
|
endian.put_i16(two_byte_slice, .Little, BITS_PER_SAMPLE)
|
|
os2.write(out, two_byte_slice) // Bits per sample
|
|
os2.write(out, []byte{'d', 'a', 't', 'a'}) // Data chunk header
|
|
|
|
size_written := 0
|
|
|
|
os2.write(out, []byte{0,0,0,0}) // Length of data. To be filled in later
|
|
|
|
for !intrinsics.atomic_load(&interrupted) {
|
|
read := alsa.pcm_readi(handle, raw_data(samples), FRAMES)
|
|
if read != FRAMES {
|
|
fmt.eprintfln("Didn't read %v frames", FRAMES)
|
|
os2.exit(1)
|
|
}
|
|
|
|
n, err := os2.write(out, samples) // Write the data
|
|
if err != nil {
|
|
fmt.eprintln("Failed to write sample data to file:", err)
|
|
os2.exit(1)
|
|
}
|
|
size_written += n
|
|
}
|
|
|
|
endian.put_i32(four_byte_slice, .Little, i32(size_written + WAV_HEADER_SIZE - 8))
|
|
os2.write_at(out, four_byte_slice, 4) // Overall size - 8 bytes
|
|
|
|
endian.put_i32(four_byte_slice, .Little, i32(size_written))
|
|
os2.write_at(out, four_byte_slice, 40) // Data size
|
|
}
|