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 }