All files / src/web microphone-input.js

12.16% Statements 9/74
0% Branches 0/22
25% Functions 2/8
13.23% Lines 9/68

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157                        9x 9x 9x 9x 9x 9x 9x 9x               9x                                                                                                                                                                                                                                                                  
import { AnalogueSource } from "../analogue-source.js";
import { createAudioContext } from "./audio-utils.js";
 
/**
 * Provides microphone input as an analogue source for the BBC Micro's ADC
 * Used for software like MicroMike that uses the analogue port for sound input
 */
export class MicrophoneInput extends AnalogueSource {
    /**
     * Create a new MicrophoneInput
     */
    constructor() {
        super();
        this.audioContext = null;
        this.microphoneStream = null;
        this.microphoneSource = null;
        this.microphoneAnalyser = null;
        this.microphoneDataArray = null;
        this.errorCallback = null;
        this.errorMessage = null;
    }
 
    /**
     * Set error callback function
     * @param {Function} callback - Function to call with error messages
     */
    setErrorCallback(callback) {
        this.errorCallback = callback;
    }
 
    /**
     * Initialise microphone access
     * @returns {Promise<boolean>} True if initialiation was successful
     */
    async initialise() {
        console.log("MicrophoneInput: Initialising microphone input");
 
        // Create audio context if needed
        if (!this.audioContext) {
            try {
                console.log("MicrophoneInput: Creating audio context");
                this.audioContext = createAudioContext();
                if (!this.audioContext) {
                    this.errorMessage = "WebAudio is not available; could not create audio context";
                    console.error("MicrophoneInput:", this.errorMessage);
                    if (this.errorCallback) this.errorCallback(this.errorMessage);
                    return false;
                }
                console.log("MicrophoneInput: Audio context created:", this.audioContext.state);
            } catch (error) {
                console.error("MicrophoneInput: Error creating audio context:", error);
                this.errorMessage = `Could not create audio context: ${error.message}`;
                if (this.errorCallback) this.errorCallback(this.errorMessage);
                return false;
            }
        }
 
        try {
            console.log("MicrophoneInput: Requesting microphone access");
            // Request microphone access
            const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
            console.log("MicrophoneInput: Microphone access granted");
 
            // Store the stream so we can stop it if needed
            this.microphoneStream = stream;
 
            // Create analyser node
            console.log("MicrophoneInput: Creating audio analyser");
            this.microphoneAnalyser = this.audioContext.createAnalyser();
            this.microphoneAnalyser.fftSize = 1024; // Larger FFT size for better resolution
            this.microphoneAnalyser.smoothingTimeConstant = 0.2; // Less smoothing for more responsive input
 
            // Create buffer for analyser data
            this.microphoneDataArray = new Uint8Array(this.microphoneAnalyser.frequencyBinCount);
            console.log("MicrophoneInput: Created data buffer with", this.microphoneDataArray.length, "samples");
 
            // Create media stream source from microphone
            console.log("MicrophoneInput: Creating media stream source");
            this.microphoneSource = this.audioContext.createMediaStreamSource(stream);
 
            // Connect microphone to analyser
            console.log("MicrophoneInput: Connecting microphone to analyser");
            this.microphoneSource.connect(this.microphoneAnalyser);
 
            this.errorMessage = null;
            console.log("MicrophoneInput: Initialisation complete");
            return true;
        } catch (error) {
            console.error("MicrophoneInput: Error accessing microphone:", error);
            this.errorMessage = `Error accessing microphone: ${error.message}`;
            if (this.errorCallback) this.errorCallback(this.errorMessage);
            return false;
        }
    }
 
    /**
     * Get the last error message if any
     * @returns {string|null} The last error message or null
     */
    getErrorMessage() {
        return this.errorMessage;
    }
 
    /**
     * Get analog value from microphone for the specified channel
     * @param {number} _channel - The ADC channel (0-3)
     * @returns {number} A value between 0 and 0xffff
     */
    getValue(_channel) {
        if (!this.microphoneAnalyser || !this.microphoneDataArray) {
            throw new Error("MicrophoneInput: getValue called but analyser not initialised");
        }
 
        // Get time domain data (waveform)
        this.microphoneAnalyser.getByteTimeDomainData(this.microphoneDataArray);
 
        // Calculate volume as average deviation from the center (128)
        let sum = 0;
        for (let i = 0; i < this.microphoneDataArray.length; i++) {
            sum += Math.abs(this.microphoneDataArray[i] - 128);
        }
 
        const average = sum / this.microphoneDataArray.length;
 
        // Scale up the signal using a configurable scaling factor
        // This can be adjusted based on testing
        const scaleFactor = 800; // Amplify the signal
        const scaledValue = average * scaleFactor;
 
        // Map to 16-bit range (0-0xFFFF)
        return Math.min(0xffff, scaledValue) | 0;
    }
 
    /**
     * Clean up resources when no longer needed
     */
    dispose() {
        if (this.microphoneStream) {
            this.microphoneStream.getTracks().forEach((track) => track.stop());
            this.microphoneStream = null;
        }
 
        if (this.microphoneSource) {
            this.microphoneSource.disconnect();
            this.microphoneSource = null;
        }
 
        this.microphoneAnalyser = null;
        this.microphoneDataArray = null;
 
        if (this.audioContext && this.audioContext.state !== "closed") {
            this.audioContext.close().catch((err) => console.error("Error closing audio context:", err));
        }
        this.audioContext = null;
    }
}