Skip to content

feat: implement Produce Sound functionality in Wave Generator#3123

Merged
marcnause merged 2 commits intofossasia:flutterfrom
rahul31124:wave_gen_audio
Mar 9, 2026
Merged

feat: implement Produce Sound functionality in Wave Generator#3123
marcnause merged 2 commits intofossasia:flutterfrom
rahul31124:wave_gen_audio

Conversation

@rahul31124
Copy link
Contributor

@rahul31124 rahul31124 commented Mar 9, 2026

Fixes #3101

Changes

Previously, the "Produce Sound" feature was missing from the Wave Generator. It has now been implemented using mp_audio_stream package . The following functions were added in wave_generator_state_provider.dart to handle audio generation.

Functions

toggleSound()
Controls the audio playback state. It updates the isPlayingSound flag.

_startAudioStream()
Initializes the audio using an AudioStream .

_fillAudioBuffer()
Generates the waveform samples. It calculates the amplitude for each of the samples based on frequency and duty cycle, using trigonometric calculations for Sine and Triangular waves and conditional logic (High& Low) for PWM.

_stopAudioStream() & dispose()
Handles cleanup. _stopAudioStream shuts down the audio hardware, while dispose ensures audio stops immediately if the user leaves the screen, preventing memory leaks and unnecessary battery usage.

UI Update

The wave_generator_screen.dart file was also updated because the onPressed method for the Produce Sound button was missing. It has now been connected to the audio functionality.

Screenshots / Recordings

Produce_Sound.mp4

Checklist:

  • No hard coding: I have used values from constants.dart or localization files instead of hard-coded values.
  • No end of file edits: No modifications done at end of resource files.
  • Code reformatting: I have formatted the code using dart format or the IDE formatter.
  • Code analysis: My code passes checks run in flutter analyze and tests run in flutter test.

Summary by Sourcery

Add audio playback support to the Wave Generator so it can produce and stop sound based on the configured waveform.

New Features:

  • Introduce audio streaming in the wave generator using mp_audio_stream to synthesize sound from the selected waveform, frequency, and duty cycle.
  • Expose a toggleSound control in the WaveGeneratorStateProvider to start and stop audio output and track playback state.
  • Update the Wave Generator UI button to control sound playback and reflect playing state with dynamic label and styling.

Enhancements:

  • Add localized "Stop Sound" string across all supported languages for the updated Wave Generator control.

Build:

  • Declare mp_audio_stream as a new dependency in pubspec.yaml.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Mar 9, 2026

Reviewer's Guide

Implements the missing "Produce Sound" feature in the Wave Generator by integrating the mp_audio_stream package for real-time audio synthesis, wiring it to the UI toggle button, and adding localization for the new stop-sound label across all supported languages.

Sequence diagram for Produce Sound toggle and audio streaming

sequenceDiagram
  actor User
  participant WaveGeneratorScreen
  participant WaveGeneratorStateProvider
  participant AudioStream

  User->>WaveGeneratorScreen: tapProduceSoundButton
  WaveGeneratorScreen->>WaveGeneratorStateProvider: toggleSound()
  alt isPlayingSound is false
    WaveGeneratorStateProvider->>WaveGeneratorStateProvider: set isPlayingSound true
    WaveGeneratorStateProvider->>AudioStream: getAudioStream()
    WaveGeneratorStateProvider->>AudioStream: init(bufferMilliSec, channels, sampleRate)
    WaveGeneratorStateProvider->>AudioStream: resume()
    loop prefillBuffers
      WaveGeneratorStateProvider->>WaveGeneratorStateProvider: _fillAudioBuffer(buffer, bufferSize)
      WaveGeneratorStateProvider->>AudioStream: push(buffer)
    end
    loop while isPlayingSound
      WaveGeneratorStateProvider->>WaveGeneratorStateProvider: _fillAudioBuffer(buffer, bufferSize)
      WaveGeneratorStateProvider->>AudioStream: push(buffer)
    end
  else isPlayingSound is true
    WaveGeneratorStateProvider->>WaveGeneratorStateProvider: set isPlayingSound false
    WaveGeneratorStateProvider->>WaveGeneratorStateProvider: _stopAudioStream()
    WaveGeneratorStateProvider->>AudioStream: uninit()
  end
  WaveGeneratorStateProvider-->>WaveGeneratorScreen: notifyListeners()
  WaveGeneratorScreen-->>User: updateButtonLabelAndStyle
Loading

Class diagram for WaveGeneratorStateProvider audio streaming additions

classDiagram
  class WaveGeneratorStateProvider {
    +bool isPlayingSound
    -AudioStream _audioStream
    -double _audioAngle
    +void toggleSound()
    +void dispose()
    -Future<void> _startAudioStream()
    -void _fillAudioBuffer(Float32List buffer, int bufferSize)
    -void _stopAudioStream()
  }

  class AudioStream {
    +void init(int bufferMilliSec, int channels, int sampleRate)
    +void resume()
    +void push(Float32List buffer)
    +void uninit()
  }

  WaveGeneratorStateProvider --> AudioStream : uses_for_realtime_audio_streaming
Loading

File-Level Changes

Change Details Files
Add audio streaming and waveform synthesis to WaveGeneratorStateProvider using mp_audio_stream.
  • Introduce AudioStream field, playback state flag, and phase accumulator to manage audio output lifecycle.
  • Implement toggleSound() to start/stop audio based on isPlayingSound and notify listeners.
  • Implement _startAudioStream() to configure a 44.1 kHz mono audio stream, manage a small buffer pool, and push audio buffers in a timing-aware loop while sound is enabled.
  • Implement _fillAudioBuffer() to generate sine, triangular, and PWM samples using current wave generator settings, including frequency and duty cycle, with clamped amplitude and fixed volume.
  • Implement _stopAudioStream() and override dispose() to uninitialize audio stream and prevent leaks when the provider is disposed.
  • Import dart:typed_data and mp_audio_stream, and wire getAudioStream() usage.
lib/providers/wave_generator_state_provider.dart
pubspec.yaml
Wire the UI "Produce Sound" button to the new audio toggle behavior and update styling based on playback state.
  • Make the button background color, label text, and font weight react to provider.isPlayingSound so it visually differentiates play vs stop states.
  • Connect the button onPressed callback to provider.toggleSound() instead of a no-op handler.
lib/view/wave_generator_screen.dart
Add localization support for the new "Stop Sound" label across all supported locales.
  • Declare stopSound getter in the base AppLocalizations interface.
  • Provide stopSound translations (currently "Stop Sound" placeholder) in each concrete localization class for all supported languages.
lib/l10n/app_localizations.dart
lib/l10n/app_localizations_en.dart
lib/l10n/app_localizations_de.dart
lib/l10n/app_localizations_es.dart
lib/l10n/app_localizations_fr.dart
lib/l10n/app_localizations_he.dart
lib/l10n/app_localizations_hi.dart
lib/l10n/app_localizations_id.dart
lib/l10n/app_localizations_it.dart
lib/l10n/app_localizations_ja.dart
lib/l10n/app_localizations_ml.dart
lib/l10n/app_localizations_my.dart
lib/l10n/app_localizations_nb.dart
lib/l10n/app_localizations_pt.dart
lib/l10n/app_localizations_ru.dart
lib/l10n/app_localizations_te.dart
lib/l10n/app_localizations_uk.dart
lib/l10n/app_localizations_vi.dart
lib/l10n/app_localizations_zh.dart

Assessment against linked issues

Issue Objective Addressed Explanation
#3101 Implement backend audio generation so that the Wave Generator can produce an audible sound.
#3101 Connect the 'Produce Sound' button in the Wave Generator screen to the audio playback logic so tapping it starts/stops sound.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In _startAudioStream, the long-running while (isPlayingSound) loop assumes _audioStream stays non-null; if dispose() is called while sound is playing, _stopAudioStream() sets _audioStream to null without changing isPlayingSound, so consider also clearing isPlayingSound (or having _stopAudioStream do it) and/or checking _audioStream for null in the loop to avoid pushing to a disposed stream.
  • It would be safer to bail out early in _startAudioStream if getAudioStream() returns null or init() fails, and to guard all _audioStream!.push calls with a null check so that audio backend failures don’t result in runtime exceptions.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_startAudioStream`, the long-running `while (isPlayingSound)` loop assumes `_audioStream` stays non-null; if `dispose()` is called while sound is playing, `_stopAudioStream()` sets `_audioStream` to null without changing `isPlayingSound`, so consider also clearing `isPlayingSound` (or having `_stopAudioStream` do it) and/or checking `_audioStream` for null in the loop to avoid pushing to a disposed stream.
- It would be safer to bail out early in `_startAudioStream` if `getAudioStream()` returns null or `init()` fails, and to guard all `_audioStream!.push` calls with a null check so that audio backend failures don’t result in runtime exceptions.

## Individual Comments

### Comment 1
<location path="lib/providers/wave_generator_state_provider.dart" line_range="108-117" />
<code_context>
+    notifyListeners();
+  }
+
+  Future<void> _startAudioStream() async {
+    _audioStream = getAudioStream();
+
+    _audioStream!.init(bufferMilliSec: 1000, channels: 1, sampleRate: 44100);
+
+    _audioStream!.resume();
+
+    await Future.delayed(const Duration(milliseconds: 100));
+
+    _audioAngle = 0.0;
+
+    final int bufferSize = 4096;
+
+    final double bufferDurationMs = (bufferSize / 44100.0) * 1000.0;
+
+    final List<Float32List> bufferPool =
+        List.generate(5, (_) => Float32List(bufferSize));
+
+    int poolIndex = 0;
+
+    double generatedAudioMs = 0.0;
+
+    Stopwatch stopwatch = Stopwatch()..start();
+
+    for (int i = 0; i < 3; i++) {
+      final buffer = bufferPool[poolIndex];
+
+      poolIndex = (poolIndex + 1) % bufferPool.length;
+
+      _fillAudioBuffer(buffer, bufferSize);
+
+      _audioStream!.push(buffer);
+
+      generatedAudioMs += bufferDurationMs;
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard `_audioStream!.push` against a null/teardown state to avoid crashes when stop/dispose races with the loop.

Because `_stopAudioStream` can run while this loop is active and set `_audioStream` to null, `_audioStream!.push(buffer)` can throw. Either cache a local `final stream = _audioStream;` after initialization and always use that, or add a null check (e.g., `if (_audioStream == null) break;`) before pushing and exit the loop safely.
</issue_to_address>

### Comment 2
<location path="lib/providers/wave_generator_state_provider.dart" line_range="227-231" />
<code_context>

   @override
   String get pin_res_desc => 'Resistance Measurement Pin';
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Align dispose semantics so that the playback flag and loop terminate when the provider is disposed.

In `dispose()`, `_stopAudioStream()` is called but `isPlayingSound` is not updated. This can leave the UI and the `_startAudioStream` loop out of sync (loop condition stays true, UI still shows playing). Please ensure `isPlayingSound` is set to `false` when disposing so both the loop and UI state stop consistently.

Suggested implementation:

```
  @override
  void dispose() {
    // Ensure playback state and control loop terminate when the provider is disposed
    isPlayingSound = false;
    _stopAudioStream();
    super.dispose();
  }

```

If the actual `dispose()` implementation in your file differs from the one in the SEARCH block (e.g., additional cleanup logic or a different order of calls), you should still:
1. Insert `isPlayingSound = false;` at the start of `dispose()` (before `_stopAudioStream()` or any loop depending on the flag).
2. Ensure there is a single source of truth for the playback state (i.e., no other flags controlling the `_startAudioStream` loop without being reset in `dispose()`).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 9, 2026

Build Status

Build successful. APKs to test: https://github.com/fossasia/pslab-app/actions/runs/22868054938/artifacts/5835634823.

Screenshots

Android Screenshots
iPhone Screenshots
iPad Screenshots

Copy link
Contributor

@marcnause marcnause left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thumbs up! 😀

@marcnause marcnause enabled auto-merge (squash) March 9, 2026 18:15
@marcnause marcnause merged commit d8e10d8 into fossasia:flutter Mar 9, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wave Generator: Produce Sound does not work

2 participants